├── .gitignore
├── LICENSE
├── README.md
├── doc
└── images
│ ├── nodegraph-pyqt.PNG
│ └── nodegraph-pyqt_perf.PNG
├── main.py
├── main_perftest.py
├── nodegraph
├── __init__.py
├── bitmap
│ ├── arrow_cross.png
│ ├── arrow_minus.png
│ └── arrow_plus.png
├── constant.py
├── edge.py
├── node.py
├── polygons.py
├── rubberband.py
├── scene.py
└── view.py
├── notes.todo
└── thirdparty
├── Qt.py
└── __init__.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 | {one line to give the program's name and a brief idea of what it does.}
635 | Copyright (C) {year} {name of author}
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | {project} Copyright (C) {year} {fullname}
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nodegraph-pyqt
2 | A generic visual directed acyclic graph implementation powered by PyQT/PySide (and networkx).
3 |
4 | The main purpose of this project is to provide a full-featured DAG GUI with an underlying network model aimed to describe any logic or arbitrary set of processes through a directed graph.
5 |
6 | This is not ready for production and may evolve in many directions without any prior warnings.
7 |
8 | # Screenshots
9 |
10 | Current simple test scene:
11 |
12 | 
13 |
14 | Performance test scene:
15 |
16 | 
17 |
18 | # Requirements
19 |
20 | * Python 2.6+
21 | * PyQt4/Pyside/PyQt5/Pyside2
22 | * Networkx (_TO BE IMPLEMENTED_)
23 |
24 | # Third party libraries
25 |
26 | Following libraries are used to various degree in this project:
27 | * [mottosso/Qt.py](https://github.com/mottosso/Qt.py)
28 |
29 | Libraries have been kept as is and are included in the repository when possible.
30 |
31 | # Reference
32 |
33 | Here a short list of similar project you may be interested by:
34 | * [cb109/qtnodes](https://github.com/cb109/qtnodes)
35 | * [LeGoffLoic/Nodz](https://github.com/LeGoffLoic/Nodz)
36 |
--------------------------------------------------------------------------------
/doc/images/nodegraph-pyqt.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dsideb/nodegraph-pyqt/e084668c2b81e5f28c5cd867acf647b84a7334ea/doc/images/nodegraph-pyqt.PNG
--------------------------------------------------------------------------------
/doc/images/nodegraph-pyqt_perf.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dsideb/nodegraph-pyqt/e084668c2b81e5f28c5cd867acf647b84a7334ea/doc/images/nodegraph-pyqt_perf.PNG
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # import os
4 | import sys
5 |
6 | # import networkx
7 | from thirdparty.Qt import QtWidgets
8 |
9 | from nodegraph.scene import Scene
10 | from nodegraph.view import View
11 |
12 |
13 | class NodeGraphDialog(QtWidgets.QMainWindow):
14 |
15 | """
16 | Handles top level dialog of Node grap
17 |
18 | """
19 |
20 | def __init__(self, parent=None):
21 | QtWidgets.QMainWindow.__init__(self, parent)
22 | self.parent = parent or self
23 |
24 | self.nodegraph = NodeGraphWidget("main", parent=self.parent)
25 | self.setCentralWidget(self.nodegraph)
26 | self.resize(800, 600)
27 | self.setWindowTitle("Node graph -")
28 |
29 | # center = self.nodegraph.graph_view.sceneRect().center()
30 | cam = self.nodegraph.graph_scene.create_node("camera")
31 | cam.setPos(-200, -150)
32 | model = self.nodegraph.graph_scene.create_node(
33 | "combine",
34 | inputs=["mesh1", "mesh2", "camera"])
35 | model.setPos(150, 0)
36 | edge = self.nodegraph.graph_scene.create_edge(
37 | cam._output,
38 | model._inputs[0])
39 |
40 | test = self.nodegraph.graph_scene.create_node("test")
41 | test.setPos(-400, 150)
42 | egde = self.nodegraph.graph_scene.create_edge(
43 | test._output,
44 | model._inputs[1])
45 |
46 |
47 | class NodeGraphWidget(QtWidgets.QWidget):
48 |
49 | """
50 | Handles node graph view
51 |
52 | """
53 |
54 | def __init__(self, name, parent=None):
55 | QtWidgets.QWidget.__init__(self, parent)
56 | self.name = name
57 | self.parent = parent
58 |
59 | self.graph_scene = Scene(parent=self.parent,
60 | nodegraph_widget=self)
61 | self.graph_view = View(self.graph_scene, parent=self.parent)
62 | self.horizontal_layout = QtWidgets.QHBoxLayout(self)
63 | self.horizontal_layout.addWidget(self.graph_view)
64 |
65 |
66 | if __name__ == "__main__":
67 | app = QtWidgets.QApplication([])
68 | dialog = NodeGraphDialog()
69 | dialog.show()
70 |
71 | sys.exit(app.exec_())
72 |
--------------------------------------------------------------------------------
/main_perftest.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # import os
4 | import sys
5 | import random
6 |
7 | # import networkx
8 | from nodegraph.scene import Scene
9 | from nodegraph.view import View
10 |
11 | from Qt import QtWidgets
12 |
13 |
14 | class NodeGraphDialog(QtWidgets.QMainWindow):
15 |
16 | """
17 | Handles top level dialog of Node grap
18 |
19 | """
20 |
21 | def __init__(self, parent=None):
22 | QtWidgets.QMainWindow.__init__(self, parent)
23 | self.parent = parent or self
24 |
25 | self.nodegraph = NodeGraphWidget("main", parent=self.parent)
26 | self.setCentralWidget(self.nodegraph)
27 | self.resize(800, 600)
28 | self.setWindowTitle("Node graph -")
29 |
30 | # center = self.nodegraph.graph_view.sceneRect().center()
31 | for i in range(0, 30):
32 | prev_node = None
33 | for j in range(0, 40):
34 | node = self.nodegraph.graph_scene.create_node(
35 | "random%d%d%d" % (i, j, random.randint(1, 1000)),
36 | inputs=["in", "add"])
37 | node.setPos(j * 350, i * 350)
38 | if prev_node:
39 | edge = self.nodegraph.graph_scene.create_edge(
40 | prev_node._output, node._inputs[0])
41 | prev_node = node
42 |
43 |
44 | class NodeGraphWidget(QtWidgets.QWidget):
45 |
46 | """
47 | Handles node graph view
48 |
49 | """
50 |
51 | def __init__(self, name, parent=None):
52 | QtWidgets.QWidget.__init__(self, parent)
53 | self.name = name
54 | self.parent = parent
55 |
56 | self.graph_scene = Scene(parent=self.parent,
57 | nodegraph_widget=self)
58 | self.graph_view = View(self.graph_scene, parent=self.parent)
59 | self.horizontal_layout = QtWidgets.QHBoxLayout(self)
60 | self.horizontal_layout.addWidget(self.graph_view)
61 |
62 |
63 | if __name__ == "__main__":
64 | app = QtWidgets.QApplication([])
65 | dialog = NodeGraphDialog()
66 | dialog.show()
67 |
68 | sys.exit(app.exec_())
69 |
--------------------------------------------------------------------------------
/nodegraph/__init__.py:
--------------------------------------------------------------------------------
1 | import sys as _sys
2 | import os as _os
3 |
4 | # Add third party as top level modules
5 | _sys.path.append(_os.path.join(
6 | _os.path.dirname(_os.path.dirname(_os.path.realpath(__file__))),
7 | "thirdparty"))
8 |
--------------------------------------------------------------------------------
/nodegraph/bitmap/arrow_cross.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dsideb/nodegraph-pyqt/e084668c2b81e5f28c5cd867acf647b84a7334ea/nodegraph/bitmap/arrow_cross.png
--------------------------------------------------------------------------------
/nodegraph/bitmap/arrow_minus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dsideb/nodegraph-pyqt/e084668c2b81e5f28c5cd867acf647b84a7334ea/nodegraph/bitmap/arrow_minus.png
--------------------------------------------------------------------------------
/nodegraph/bitmap/arrow_plus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dsideb/nodegraph-pyqt/e084668c2b81e5f28c5cd867acf647b84a7334ea/nodegraph/bitmap/arrow_plus.png
--------------------------------------------------------------------------------
/nodegraph/constant.py:
--------------------------------------------------------------------------------
1 | # =============================================================================
2 | # Nodegraph-pyqt
3 | #
4 | # Everyone is permitted to copy and distribute verbatim copies of this
5 | # document, but changing it is not allowed without permissions.
6 | #
7 | # For any questions, please contact: dsideb@gmail.com
8 | #
9 | # GNU LESSER GENERAL PUBLIC LICENSE (Version 3, 29 June 2007)
10 | # =============================================================================
11 |
12 | """
13 | Constants used in Node graph
14 |
15 | """
16 |
17 | SCENE_WIDTH = 8000000
18 | SCENE_HEIGHT = 4000000
19 |
20 | DEBUG = False
21 |
22 | NODES_COLOR = {
23 | "read": {"base_color": [100, 200, 100]},
24 | "camera": {"base_color": [100, 100, 200]},
25 | "merge": {"base_color": [200, 100, 100]}
26 | }
27 |
--------------------------------------------------------------------------------
/nodegraph/edge.py:
--------------------------------------------------------------------------------
1 | # =============================================================================
2 | # Nodegraph-pyqt
3 | #
4 | # Everyone is permitted to copy and distribute verbatim copies of this
5 | # document, but changing it is not allowed without permissions.
6 | #
7 | # For any questions, please contact: dsideb@gmail.com
8 | #
9 | # GNU LESSER GENERAL PUBLIC LICENSE (Version 3, 29 June 2007)
10 | # =============================================================================
11 |
12 | """
13 | Edge definition including:
14 |
15 | * Edge
16 | * InteractiveEdge
17 |
18 | """
19 | import sha
20 | from Qt import QtCore, QtGui, QtWidgets
21 |
22 | from constant import DEBUG
23 | from polygons import ARROW_STANDARD, ARROW_SLIM
24 | from .node import NodeSlot
25 |
26 |
27 | class Edge(QtWidgets.QGraphicsItem):
28 |
29 | """
30 | Node Edge base class that displays a directed line between two slots
31 | of a source and target node
32 |
33 | """
34 |
35 | ARROW_STANDARD = 1
36 | ARROW_SLIM = 2
37 |
38 | def __init__(self, source_slot, target_slot, scene, outline=2, arrow=None):
39 | """Creates an instance of this class
40 |
41 | :param source: Source slot (should be a output one)
42 | :type source: :class:`nodegraph.node.NodeSlot`
43 |
44 | :param target: Target slot (should be an input one)
45 | :type target: :cLass:`nodegraph.node.NodeSlot`
46 |
47 | :param scene: GraphicsScene that holds the source and target nodes
48 | :type scene: :class:`nodegraph.scene.Scene`
49 |
50 | :param outline: Width of the edge and arrow outline
51 | :type outline: int
52 |
53 | :param arrow: Define type of arrow. By default, no arrow is drawn
54 | :type arrow: int
55 |
56 | :returns: An instance of this class
57 | :rtype: :class:`nodegraph.edge.Edge`
58 |
59 | """
60 | QtWidgets.QGraphicsItem.__init__(self, parent=None, scene=scene)
61 |
62 | self._source_slot = source_slot
63 | self._target_slot = target_slot
64 | self._outline = outline
65 | self._arrow = arrow
66 | self._lod = 1
67 | self._hash = ("%s.%s >> %s.%s" %
68 | (source_slot.parent._name, source_slot._name,
69 | target_slot.parent._name, target_slot._name))
70 | self._shape = None
71 | self._line = None
72 |
73 | # Set tooltip
74 | self.setToolTip(self._hash)
75 |
76 | # Hash the hash
77 | self._hash = sha.sha(self._hash).hexdigest()
78 |
79 | # Reference hash in nodes slot
80 | source_slot.add_edge(self._hash)
81 | target_slot.add_edge(self._hash)
82 |
83 | # Settings
84 | self.setFlags(QtWidgets.QGraphicsItem.ItemIsSelectable)
85 | self.setAcceptHoverEvents(True)
86 | self.setZValue(-10)
87 |
88 | # Update position, line and path
89 | self._update()
90 |
91 | @property
92 | def hash(self):
93 | """Return the unique hash of this edge
94 |
95 | """
96 | return self._hash
97 |
98 | def _update_line(self):
99 | """Resolve start and end point from current source and target position
100 |
101 | :returns: A Qt line object
102 | :rtype: :class:`QtCore.QLineF`
103 |
104 | """
105 | start = QtCore.QPointF(0, 0)
106 | end = self._target_slot.center - self._source_slot.center
107 |
108 | self._line = QtCore.QLineF(start, end)
109 |
110 | def _update_path(self):
111 | """Build path which drives shape and bounding box
112 |
113 | :returns: A Qt path object
114 | :rtype: :class:`QtGui.QPainterPath`
115 |
116 | """
117 | # Update path
118 | width = (1 / self._lod if self._outline * self._lod < 1
119 | else self._outline)
120 | norm = self._line.unitVector().normalVector()
121 | norm = width * 3 * QtCore.QPointF(norm.x2() - norm.x1(),
122 | norm.y2() - norm.y1())
123 |
124 | self._shape = QtGui.QPainterPath()
125 | poly = QtGui.QPolygonF([self._line.p1() - norm,
126 | self._line.p1() + norm,
127 | self._line.p2() + norm,
128 | self._line.p2() - norm])
129 | self._shape.addPolygon(poly)
130 | self._shape.closeSubpath()
131 |
132 | def _update_position(self):
133 | """Update position to match center of source slot
134 |
135 | """
136 | self.setPos(self._source_slot.center)
137 |
138 | def _update(self):
139 | """Update internal properties
140 |
141 | """
142 | # Update position
143 | self._update_position()
144 |
145 | # Update line
146 | self._update_line()
147 |
148 | # Update path
149 | self._update_path()
150 |
151 | def update(self):
152 | """Re-implement update of QtGraphicsItem
153 |
154 | """
155 | # Update start, end, path and position
156 | self._update()
157 |
158 | QtWidgets.QGraphicsLineItem.update(self)
159 |
160 | def shape(self):
161 | """Re-implement shape method
162 | Return a QPainterPath that represents the bounding shape
163 |
164 | """
165 | return self._shape
166 |
167 | def boundingRect(self):
168 | """Re-implement bounding box method
169 |
170 | """
171 | # Update node
172 | # self._update()
173 |
174 | # Infer bounding box from shape
175 | return self._shape.controlPointRect()
176 |
177 | def paint(self, painter, option, widget=None):
178 | """Re-implement paint method
179 |
180 | """
181 | # Update level of detail
182 | self._lod = option.levelOfDetailFromTransform(painter.worldTransform())
183 |
184 | # Update brush
185 | palette = (self.scene().palette() if self.scene()
186 | else option.palette)
187 | brush = palette.text()
188 | if option.state & QtWidgets.QStyle.State_Selected:
189 | brush = palette.highlight()
190 | elif option.state & QtWidgets.QStyle.State_MouseOver:
191 | color = brush.color().darker(250)
192 | brush.setColor(color)
193 |
194 | # Update unit width
195 | width = (1 / self._lod if self._outline * self._lod < 1
196 | else self._outline)
197 |
198 | # Draw line
199 | painter.setPen(QtGui.QPen(brush, width))
200 | painter.drawLine(self._line)
201 |
202 | # Draw arrow if needed
203 | if self._arrow and self._lod > 0.15:
204 | # Construct arrow
205 | matrix = QtGui.QTransform()
206 | matrix.rotate(-self._line.angle())
207 | matrix.scale(width, width)
208 |
209 | if self._arrow & self.ARROW_STANDARD:
210 | poly = matrix.map(ARROW_STANDARD)
211 | elif self._arrow & self.ARROW_SLIM:
212 | poly = matrix.map(ARROW_SLIM)
213 |
214 | v = self._line.unitVector()
215 | v = (self._line.length() / 2) * QtCore.QPointF(v.x2() - v.x1(),
216 | v.y2() - v.y1())
217 | poly.translate(self._line.x1(), self._line.y1())
218 | poly.translate(v.x(), v.y())
219 |
220 | painter.setPen(QtCore.Qt.NoPen)
221 | painter.setBrush(brush)
222 | painter.drawPolygon(poly)
223 |
224 | # Draw debug
225 | if DEBUG:
226 | painter.setBrush(QtGui.QBrush())
227 | painter.setPen(QtGui.QColor(255, 0, 0))
228 | painter.drawPath(self.shape())
229 |
230 | painter.setPen(QtGui.QColor(0, 255, 0))
231 | painter.drawRect(self.boundingRect())
232 |
233 | return
234 |
235 | def mouseMoveEvent(self, event):
236 | """Re-implements mouse move event to avoid unecessaries signals
237 |
238 | """
239 | return
240 |
241 | def refresh(self, source_slot=None, target_slot=None):
242 | """Update start/end position if provided and force
243 | redraw
244 |
245 | :param source_slot: Source slot (output or input)
246 | :type source_slot: :class:`nodegraph.node.NodeSlot`
247 |
248 | :param target_slot: Source slot (output or input)
249 | :type target_slot: :class:`nodegraph.node.NodeSlot`
250 |
251 | """
252 | if source_slot:
253 | self._source_slot = source_slot
254 | if target_slot:
255 | self._target_slot = target_slot
256 | self.prepareGeometryChange()
257 | self.update()
258 |
259 | def refresh_position(self):
260 | """Updates start position
261 |
262 | """
263 | self._update_position()
264 |
265 | def is_connected_to(self, nodes):
266 | """For a given list of nodes, check if edge is connected (bo)
267 | node(s)
268 |
269 | :param nodes: node names
270 | :type nodes: list
271 |
272 | """
273 | if (self._source_slot.parent.name in nodes and
274 | self._target_slot.parent.name in nodes):
275 | return True
276 | else:
277 | return False
278 |
279 |
280 | class InteractiveEdge(Edge):
281 |
282 | """Draw an edge where one one the end point is the currrent mouse pos
283 |
284 | """
285 |
286 | def __init__(self, source_slot, mouse_pos, scene, outline=2, arrow=None):
287 | """Creates an instance of this class
288 |
289 | :param source: Source slot (should be a output one)
290 | :type source: :class:`nodegraph.node.NodeSlot`
291 |
292 | :param mouse_pos: Current scene position for mouse
293 | :type mouse: :class:`QtCore.QPointF`
294 |
295 | :param scene: GraphicsScene that holds the source and target nodes
296 | :type scene: :class:`nodegraph.scene.Scene`
297 |
298 | :param outline: Width of the edge and arrow outline
299 | :type outline: int
300 |
301 | :param arrow: Define type of arrow. By default, no arrow is drawn
302 | :type arrow: int
303 |
304 | :returns: An instance of this class
305 | :rtype: :class:`nodegraph.edge.InteractiveEdge`
306 |
307 | """
308 | QtWidgets.QGraphicsItem.__init__(self, parent=None, scene=scene)
309 |
310 | self._source_slot = source_slot
311 | self._mouse_pos = mouse_pos
312 | self._outline = outline
313 | self._arrow = arrow
314 | self._lod = 1
315 | self._shape = None
316 | self._line = None
317 |
318 | self.setZValue(-10)
319 |
320 | # Update line
321 | self._update()
322 |
323 | def _update_line(self):
324 | """Re-implement function that updates edge line definition
325 |
326 | """
327 | start = QtCore.QPoint(0, 0)
328 | if self._source_slot.family & NodeSlot.OUTPUT:
329 | end = self._mouse_pos - self._source_slot.center
330 | else:
331 | end = self._source_slot.center - self._mouse_pos
332 |
333 | self._line = QtCore.QLineF(start, end)
334 |
335 | def _update_position(self):
336 | """Re-implement function that updates internal container
337 |
338 | """
339 | if self._source_slot.family & NodeSlot.OUTPUT:
340 | position = self._source_slot.center
341 | else:
342 | position = self._mouse_pos
343 |
344 | # Update position
345 | self.setPos(position)
346 |
347 | def refresh(self, mouse_pos, source_slot=None):
348 | """Updates start/end position and force redraw
349 |
350 | :param mouse_pos: Scene position of the mouse
351 | :type mouse_pos: :class:`QtCore.QPointF`
352 |
353 | :param source_slot: Source slot (output or input)
354 | :type source_slot: :class:`nodegraph.node.NodeSlot`
355 |
356 | """
357 | self._mouse_pos = mouse_pos
358 | if source_slot:
359 | self._source_slot = source_slot
360 | self.prepareGeometryChange()
361 | self.update()
362 |
--------------------------------------------------------------------------------
/nodegraph/node.py:
--------------------------------------------------------------------------------
1 | # =============================================================================
2 | # Nodegraph-pyqt
3 | #
4 | # Everyone is permitted to copy and distribute verbatim copies of this
5 | # document, but changing it is not allowed without permissions.
6 | #
7 | # For any questions, please contact: dsideb@gmail.com
8 | #
9 | # GNU LESSER GENERAL PUBLIC LICENSE (Version 3, 29 June 2007)
10 | # =============================================================================
11 |
12 | """
13 | Base node definition including:
14 |
15 | * Node
16 | * NodeSlot
17 |
18 | """
19 | # import sha
20 | from Qt import QtCore, QtGui, QtWidgets
21 |
22 | from constant import DEBUG
23 |
24 |
25 | class Node(QtWidgets.QGraphicsItem):
26 |
27 | """
28 | Base class for node graphic item
29 |
30 | As much as possible, everything is drawn in the node paint function for
31 | performance reasons
32 |
33 | """
34 |
35 | def __init__(self, name, scene, inputs=["in"], parent=None):
36 | """Create an instance of this class
37 |
38 | """
39 | QtWidgets.QGraphicsItem.__init__(self, parent=parent, scene=scene)
40 | self._name = name
41 | self._width = 160
42 | self._height = 130
43 | self._outline = 6
44 | self._slot_radius = 10
45 | self._label_height = 34
46 | self._bbox = None # cache container
47 | self._round_slot = None
48 | self._rect_slot = None
49 | self._hover_slot = False
50 | self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable |
51 | QtWidgets.QGraphicsItem.ItemIsSelectable)
52 |
53 | self.setAcceptHoverEvents(False)
54 |
55 | # Build output slot
56 | self._output = NodeSlot("out", self, family=NodeSlot.OUTPUT)
57 |
58 | # Build input slots
59 | self._inputs = []
60 | for slot_name in inputs:
61 | aninput = NodeSlot(slot_name, self)
62 | self._inputs.append(aninput)
63 |
64 | # Update internal containers
65 | self._update()
66 |
67 | @property
68 | def name(self):
69 | """Return the family of the slot
70 |
71 | """
72 | return self._name
73 |
74 | @property
75 | def edges(self):
76 | """Return all hashes of connected edges
77 |
78 | """
79 | outputs = self._output.edge
80 | inputs = list(set([e for i in self._inputs for e in i.edge]))
81 | return set(outputs + inputs)
82 |
83 | def _update(self):
84 | """Update slots internal properties
85 |
86 | """
87 | slot_height = self._slot_radius * 2 + self._outline
88 | base_y = self._height / 2 + self._label_height / 2 + self._outline / 2
89 |
90 | # Update base slot bounding box
91 | self._draw_slot = QtCore.QRectF(0,
92 | 0,
93 | self._slot_radius * 2,
94 | self._slot_radius * 2)
95 | # Update output
96 | init_y = base_y - slot_height / 2
97 | self._output.rect = QtCore.QRectF(self._draw_slot).translated(
98 | self._width - self._slot_radius, init_y)
99 |
100 | # Update inputs
101 | init_y = base_y - slot_height * len(self._inputs) / 2
102 | for i, _input in enumerate(self._inputs):
103 | self._inputs[i].rect = QtCore.QRectF(self._draw_slot).translated(
104 | -self._slot_radius, init_y + slot_height * i)
105 |
106 | # Update bounding box
107 | self._bbox = QtCore.QRectF(
108 | -self._outline / 2 - self._slot_radius,
109 | -self._outline / 2,
110 | self._width + self._outline + self._slot_radius * 2,
111 | self._height + self._outline)
112 |
113 | def _update_hover_slot(self, slot):
114 | if slot == self._hover_slot:
115 | # No change
116 | return
117 |
118 | self._hover_slot = slot
119 |
120 | self.update()
121 |
122 | def boundingRect(self):
123 | """Return a QRect that represents the bounding box of the node.
124 | Here that sould be the bounding box of the primary shape of the node.
125 |
126 | """
127 | return self._bbox
128 |
129 | def paint(self, painter, option, widget=None):
130 | """Re-implement paint method
131 |
132 | """
133 | # print("Redraw %s" % self._name)
134 | lod = option.levelOfDetailFromTransform(painter.worldTransform())
135 |
136 | # Resolve fill, text and outlines brush
137 | fill_brush = self.scene().palette().button()
138 | text_brush = self.scene().palette().text()
139 | if option.state & QtWidgets.QStyle.State_Selected:
140 | fill_brush = self.scene().palette().highlight()
141 | text_brush = self.scene().palette().highlightedText()
142 |
143 | # Set brush and pen, then start drawing
144 | painter.setBrush(self.scene().palette().buttonText())
145 | painter.setPen(QtGui.QPen(fill_brush, self._outline))
146 |
147 | # Draw primary shape
148 | painter.drawRect(0, 0, self._width, self._height)
149 |
150 | # Draw label background
151 | # TODO: Color should be based on node type
152 | painter.setBrush(QtGui.QColor(90, 90, 140))
153 | painter.setPen(QtCore.Qt.NoPen)
154 | label_rect = QtCore.QRectF(self._outline / 2,
155 | self._outline / 2,
156 | self._width - self._outline,
157 | self._label_height - self._outline / 2)
158 | painter.drawRect(label_rect)
159 |
160 | # Draw text
161 | if lod >= 0.4:
162 | font = QtGui.QFont("Arial", 14)
163 | font.setStyleStrategy(QtGui.QFont.ForceOutline)
164 | painter.setFont(font)
165 | painter.setPen(QtGui.QPen(text_brush, 1))
166 | painter.scale(1, 1)
167 | painter.drawText(label_rect, QtCore.Qt.AlignCenter, self._name)
168 |
169 | # Draw slots
170 | if lod >= 0.15:
171 | # Should be driven by slot type
172 | hover_color = QtGui.QColor(90, 90, 140)
173 | hover_normal = self.scene().palette().text()
174 | self.setAcceptHoverEvents(True)
175 | painter.setBrush(hover_normal)
176 | painter.setPen(QtGui.QPen(fill_brush, self._outline))
177 |
178 | if lod >= 0.35:
179 | # Draw output (Ellipse)
180 | if self._hover_slot == self._output:
181 | # Hover color should be driven by slot type
182 | painter.setBrush(hover_color)
183 | painter.drawEllipse(self._output._rect)
184 |
185 | # Draw input (Ellipse)
186 | for aninput in self._inputs:
187 | if self._hover_slot == aninput:
188 | painter.setBrush(hover_color)
189 | else:
190 | painter.setBrush(hover_normal)
191 | painter.drawEllipse(aninput.rect)
192 | else:
193 | # Draw output (Rectangle)
194 | if self._hover_slot == self._output:
195 | painter.setBrush(hover_color)
196 | painter.drawRect(self._output._rect)
197 |
198 | # Drae input (Rectangle)
199 | for aninput in self._inputs:
200 | if self._hover_slot == aninput:
201 | painter.setBrush(hover_color)
202 | else:
203 | painter.setBrush(hover_normal)
204 | painter.drawRect(aninput.rect)
205 | else:
206 | self.setAcceptHoverEvents(False)
207 |
208 | # Draw slot labels
209 | if lod >= 0.5:
210 | font = QtGui.QFont("Arial", 11)
211 | font.setStyleStrategy(QtGui.QFont.ForceOutline)
212 | painter.setFont(font)
213 | painter.setPen(QtGui.QPen(self.scene().palette().text(), 1))
214 |
215 | width = self._width / 2 - self._slot_radius - self._outline
216 | height = self._slot_radius * 2
217 |
218 | # Output
219 | alignment = QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight
220 | rect = QtCore.QRectF(self._width / 2,
221 | self._output._rect.top(),
222 | width,
223 | height)
224 | painter.drawText(rect, alignment, "out")
225 | # painter.setBrush(QtCore.Qt.NoBrush)
226 | # painter.drawRect(rect)
227 |
228 | # Input
229 | alignment = QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft
230 | for aninput in self._inputs:
231 | rect = QtCore.QRectF(self._slot_radius + self._outline,
232 | aninput._rect.top(),
233 | width,
234 | height)
235 | painter.drawText(rect, alignment, aninput.name)
236 | # painter.setBrush(QtCore.Qt.NoBrush)
237 | # painter.drawRect(rect)
238 |
239 | # Draw debug
240 | if DEBUG:
241 | painter.setBrush(QtGui.QBrush())
242 | painter.setPen(QtGui.QColor(255, 0, 0))
243 | painter.drawRect(self.boundingRect())
244 |
245 | return
246 |
247 | def hoverMoveEvent(self, event):
248 | """Re-implement Mouse hover move event
249 |
250 | :param event: Hover move event
251 | :type event: :class:`QtWidgets.QMouseEvent`
252 |
253 | """
254 | # print("NODE %s hover move" % self._name)
255 | his = [i for i in self._inputs if i._rect.contains(event.pos())]
256 | if self._output._rect.contains(event.pos()):
257 | self._update_hover_slot(self._output)
258 | elif his:
259 | self._update_hover_slot(his[0])
260 | else:
261 | self._update_hover_slot(False)
262 |
263 | # Call normal behavior
264 | QtWidgets.QGraphicsItem.hoverMoveEvent(self, event)
265 |
266 | return
267 |
268 | def hoverLeaveEvent(self, event):
269 | """Re-implement Mouse hover move event
270 |
271 | :param event: Hover move event
272 | :type event: :class:`QtWidgets.QMouseEvent`
273 |
274 | """
275 | self._update_hover_slot(False)
276 |
277 | # Call normal behavior
278 | QtWidgets.QGraphicsItem.hoverLeaveEvent(self, event)
279 |
280 | def mousePressEvent(self, event):
281 | """Re-implement mousePressEvent from base class
282 |
283 | :param event: Mouse event
284 | :type event: :class:`QtWidgets.QMouseEvent`
285 |
286 | """
287 | print("MOUSE PRESS NODE!")
288 |
289 | buttons = event.buttons()
290 | # modifiers = event.modifiers()
291 |
292 | if buttons == QtCore.Qt.LeftButton:
293 | if self._output._rect.contains(event.pos()):
294 | mouse_pos = self.mapToScene(event.pos())
295 | self.scene().start_interactive_edge(self._output, mouse_pos)
296 | event.accept()
297 | return
298 | for aninput in self._inputs:
299 | if aninput._rect.contains(event.pos()):
300 | mouse_pos = self.mapToScene(event.pos())
301 | self.scene().start_interactive_edge(aninput, mouse_pos)
302 | event.accept()
303 | return
304 |
305 | QtWidgets.QGraphicsItem.mousePressEvent(self, event)
306 |
307 | # def mouseReleaseEvent(self, event):
308 | # """Re-implement mouseReleaseEvent from base class
309 |
310 | # :param event: Mouse event
311 | # :type event: :class:`QtWidgets.QGraphicsSceneMouseEvent`
312 |
313 | # """
314 | # buttons = event.button()
315 |
316 | # if buttons == QtCore.Qt.LeftButton:
317 | # # if self._output._rect.contains(event.pos()):
318 | # print(self._output._rect.contains(event.pos()))
319 | # print("DROP")
320 | # print(event.pos())
321 | # print(self._output._rect)
322 |
323 | # QtWidgets.QGraphicsItem.mouseReleaseEvent(self, event)
324 |
325 | def mouseMoveEvent(self, event):
326 | """Re-implement mouseMoveEvent from base class
327 |
328 | :param event: Mouse event
329 | :type event: :class:`QtWidgets.QMouseEvent`
330 |
331 | """
332 | buttons = event.buttons()
333 | # modifiers = event.modifiers()
334 |
335 | print("%s : mouse move event. Hover slot: %s" %(self._name, self._hover_slot))
336 |
337 | if buttons == QtCore.Qt.LeftButton:
338 | if self.scene().is_interactive_edge:
339 | # Edge creation mode
340 |
341 | # print("Node Name: %s, pos: %s" % (self._name, event.pos()))
342 | event.accept()
343 | return
344 |
345 | QtWidgets.QGraphicsItem.mouseMoveEvent(self, event)
346 |
347 | def refresh(self, refresh_edges=True):
348 | """Refreh node
349 |
350 | :param refresh_edges: If true, also connected edge
351 | :type refresh_edges: bool
352 |
353 | """
354 | self.prepareGeometryChange()
355 | self._update()
356 | if refresh_edges and self.edges:
357 | for ahash in self.edges:
358 | self.scene().edges_by_hash[ahash].refresh()
359 | self.update()
360 |
361 |
362 | class NodeSlot(object):
363 |
364 | """
365 | Base class for edge slot
366 |
367 | """
368 |
369 | INPUT = 0
370 | OUTPUT = 1
371 |
372 | def __init__(self, name, parent, family=None):
373 | """Instance this class
374 |
375 | """
376 | # QtWidgets.QGraphicsItem.__init__(self, parent=parent, scene=scene)
377 | self._name = name
378 | self.parent = parent
379 | self._family = family or self.INPUT
380 | self._rect = None
381 | self._edge = set()
382 |
383 | @property
384 | def name(self):
385 | """Return the family of the slot
386 |
387 | """
388 | return self._name
389 |
390 | @property
391 | def family(self):
392 | """Return the family of the slot
393 |
394 | """
395 | return self._family
396 |
397 | @property
398 | def rect(self):
399 | """Return bounding box of slot
400 |
401 | """
402 | return self._rect
403 |
404 | @rect.setter
405 | def rect(self, value):
406 | """ Set property rect
407 |
408 | :param value: Rectangle defintion of the slot
409 | :type value: class::`QtCore.QRectF`
410 |
411 | """
412 | self._rect = value
413 |
414 | @property
415 | def center(self):
416 | """Return center point of the slot in scene coordinates
417 |
418 | """
419 | return self.parent.mapToScene(self._rect.center())
420 |
421 | @property
422 | def edge(self):
423 | """Return hash id of connedcted edge or None
424 |
425 | :rtype: list
426 |
427 | """
428 | return list(self._edge)
429 |
430 | @edge.setter
431 | def edge(self, value):
432 | """Set property edge (replace)
433 |
434 | :type value: str or list
435 |
436 | """
437 | self._edge = set(value if isinstance(value, list) else [value])
438 |
439 | def add_edge(self, value):
440 | """Add edge hash(es) to set
441 |
442 | :type value: str or list
443 |
444 | """
445 | self._edge |= set(value if isinstance(value, list) else [value])
446 |
447 | def remove_edge(self, value):
448 | """Remove edge hash(es) from set
449 |
450 | :type value: str or list
451 |
452 | """
453 | self._edge -= set(value if isinstance(value, list) else [value])
454 |
--------------------------------------------------------------------------------
/nodegraph/polygons.py:
--------------------------------------------------------------------------------
1 | # =============================================================================
2 | # Nodegraph-pyqt
3 | #
4 | # Everyone is permitted to copy and distribute verbatim copies of this
5 | # document, but changing it is not allowed without permissions.
6 | #
7 | # For any questions, please contact: dsideb@gmail.com
8 | #
9 | # GNU LESSER GENERAL PUBLIC LICENSE (Version 3, 29 June 2007)
10 | # =============================================================================
11 |
12 | """
13 | Polygons used in Node graph
14 |
15 | """
16 |
17 | from Qt import QtCore, QtGui
18 |
19 |
20 | height = 4
21 | width = height * 3 / 4
22 | thick = height / 2
23 |
24 | ARROW_SLIM = QtGui.QPolygonF([QtCore.QPointF(thick / 2, 0),
25 | QtCore.QPointF(- thick, - width),
26 | QtCore.QPointF(- thick * 2, - width),
27 | QtCore.QPointF(- thick / 2, 0),
28 | QtCore.QPointF(- thick * 2, width),
29 | QtCore.QPointF(- thick, width)])
30 |
31 | ARROW_STANDARD = QtGui.QPolygonF([QtCore.QPointF(height, 0),
32 | QtCore.QPointF(- height, - width),
33 | QtCore.QPointF(- height, width),
34 | QtCore.QPointF(height, 0)])
35 |
--------------------------------------------------------------------------------
/nodegraph/rubberband.py:
--------------------------------------------------------------------------------
1 | # =============================================================================
2 | # Nodegraph-pyqt
3 | #
4 | # Everyone is permitted to copy and distribute verbatim copies of this
5 | # document, but changing it is not allowed without permissions.
6 | #
7 | # For any questions, please contact: dsideb@gmail.com
8 | #
9 | # GNU LESSER GENERAL PUBLIC LICENSE (Version 3, 29 June 2007)
10 | # =============================================================================
11 |
12 | """
13 | Custom rubber band selection aimed at being more efficient than the
14 | default one with a large numbers of items
15 |
16 | """
17 |
18 | from Qt import QtCore, QtGui, QtWidgets
19 | # from .node import Node
20 | # from .edge import Edge
21 |
22 | # from constant import DEBUG
23 |
24 |
25 | class RubberBand(QtWidgets.QGraphicsItem):
26 |
27 | """
28 | Draw outline of a rectangle (as a shape)
29 |
30 | """
31 |
32 | REPLACE_SELECTION = 1
33 | ADD_SELECTION = 2
34 | MINUS_SELECTION = 4
35 | TOGGLE_SELECTION = 8
36 |
37 | def __init__(self, init_pos, scene, outline=2):
38 | """Creates an instance of this class
39 |
40 | :param init_pos: Point of origin of the rubber band
41 | :type init_pos: :class:`QtCore.QPointF`
42 |
43 | :param scene: GraphicsScene that holds the source and target nodes
44 | :type scene: :class:`nodegraph.scene.Scene`
45 |
46 | :param outline: Width of the edge and arrow outline
47 | :type outline: int
48 |
49 | :returns: An instance of this class
50 | :rtype: :class:`nodegraph.rubberband.RubberBand`
51 |
52 | """
53 | QtWidgets.QGraphicsItem.__init__(self, parent=None, scene=scene)
54 |
55 | self._source_pos = init_pos
56 | self._mouse_pos = init_pos
57 | self._outline = outline
58 | self._shape = None
59 | self._selection_mode = self.REPLACE_SELECTION
60 |
61 | # Settings
62 | self.setZValue(10)
63 |
64 | # Update
65 | self._update()
66 |
67 | def _update(self):
68 | """Update internal properties
69 |
70 | """
71 | # Update path
72 | self._shape = QtGui.QPainterPath()
73 | poly = QtGui.QPolygonF([
74 | self._source_pos,
75 | QtCore.QPointF(self._mouse_pos.x(), self._source_pos.y()),
76 | self._mouse_pos,
77 | QtCore.QPoint(self._source_pos.x(), self._mouse_pos.y())
78 | ])
79 | self._shape.addPolygon(poly)
80 | self._shape.closeSubpath()
81 |
82 | def update(self):
83 | """Re-implement update of QtGraphicsItem
84 |
85 | """
86 | # Update internal containers
87 | self._update()
88 |
89 | QtWidgets.QGraphicsLineItem.update(self)
90 |
91 | def shape(self):
92 | """Re-implement shape method
93 | Return a QPainterPath that represents the bounding shape
94 |
95 | """
96 | return self._shape
97 |
98 | def boundingRect(self):
99 | """Re-implement bounding box method
100 |
101 | """
102 | # Infer bounding box from shape
103 | return self._shape.controlPointRect()
104 |
105 | def paint(self, painter, option, widget=None):
106 | """Re-implement paint method
107 |
108 | """
109 | # Define pen
110 | palette = (self.scene().palette() if self.scene()
111 | else option.palette)
112 | pen = QtGui.QPen()
113 | pen.setBrush(palette.highlight())
114 | pen.setCosmetic(True)
115 | pen.setWidth(self._outline)
116 | pen.setStyle(QtCore.Qt.DashLine)
117 |
118 | # Draw Shape
119 | painter.setPen(pen)
120 | # painter.drawPath(self._shape)
121 |
122 | color = palette.highlight().color()
123 | color.setAlphaF(0.2)
124 | painter.setBrush(QtGui.QColor(color))
125 | painter.drawRect(self.shape().controlPointRect())
126 |
127 | return
128 |
129 | def refresh(self, mouse_pos=None, init_pos=None):
130 | """Update corner of rubber band defined by mouse pos
131 |
132 | :param mouse_pos: Scene position of the mouse
133 | :type mouse_pos: :class:`QtCore.QPointF`
134 |
135 | """
136 | if mouse_pos:
137 | self._mouse_pos = mouse_pos
138 | if init_pos:
139 | self._source_pos = init_pos
140 |
141 | # self.scene().setSelectionArea(self.shape(),
142 | # QtCore.Qt.ContainsItemBoundingRect)
143 | self.prepareGeometryChange()
144 | self.update()
145 |
146 | def update_scene_selection(self, operation=None, intersect=None):
147 | """Update scene selection from the current rubber band bounding box
148 |
149 | :param operation: Replace, add or remove from the current selection
150 | :type operation: int
151 |
152 | :param intersect:
153 | Specify how items are selected, by default the item bounding box
154 | must be fully contained
155 | :type intersect: :class:`QtCore.Qt.ItemSelectionMode`
156 |
157 | """
158 | operation = operation or self.REPLACE_SELECTION
159 | intersect = intersect or QtCore.Qt.ContainsItemBoundingRect
160 |
161 | if operation == self.ADD_SELECTION:
162 | current_selection = self.scene().selectedItems()
163 | self.scene().setSelectionArea(self.shape(), intersect)
164 |
165 | for item in current_selection:
166 | item.setSelected(True)
167 |
168 | elif operation == self.MINUS_SELECTION:
169 | items = self.scene().items(self.shape(), intersect)
170 |
171 | for item in items:
172 | item.setSelected(False)
173 |
174 | elif operation == self.TOGGLE_SELECTION:
175 | items = self.scene().items(self.shape(), intersect)
176 |
177 | for item in items:
178 | if item.isSelected():
179 | item.setSelected(False)
180 | else:
181 | item.setSelected(True)
182 | else:
183 | self.scene().setSelectionArea(self.shape(), intersect)
184 |
--------------------------------------------------------------------------------
/nodegraph/scene.py:
--------------------------------------------------------------------------------
1 | # =============================================================================
2 | # Nodegraph-pyqt
3 | #
4 | # Everyone is permitted to copy and distribute verbatim copies of this
5 | # document, but changing it is not allowed without permissions.
6 | #
7 | # For any questions, please contact: dsideb@gmail.com
8 | #
9 | # GNU LESSER GENERAL PUBLIC LICENSE (Version 3, 29 June 2007)
10 | # =============================================================================
11 |
12 | """Node graph scene manager based on QGraphicsScene
13 |
14 | """
15 | from Qt import QtCore, QtGui, QtWidgets
16 |
17 | from .node import Node, NodeSlot
18 | from .edge import Edge, InteractiveEdge
19 | from .rubberband import RubberBand
20 |
21 | from .constant import SCENE_WIDTH, SCENE_HEIGHT
22 |
23 |
24 | class Scene(QtWidgets.QGraphicsScene):
25 |
26 | """
27 | Provides custom implementation of QGraphicsScene
28 |
29 | """
30 |
31 | def __init__(self, parent=None, nodegraph_widget=None):
32 | """Create an instance of this class
33 |
34 | """
35 | QtWidgets.QGraphicsScene.__init__(self, parent)
36 | self.parent = parent
37 | self._nodegraph_widget = nodegraph_widget
38 | self._nodes = []
39 | self._edges_by_hash = {}
40 | self._is_interactive_edge = False
41 | self._is_refresh_edges = False
42 | self._interactive_edge = None
43 | self._refresh_edges = {}
44 | self._rubber_band = None
45 |
46 | # Registars
47 | self._is_rubber_band = False
48 | self._is_shift_key = False
49 | self._is_ctrl_key = False
50 | self._is_alt_key = False
51 | self._is_left_mouse = False
52 | self._is_mid_mouse = False
53 | self._is_right_mouse = False
54 |
55 | # Redefine palette
56 | self.setBackgroundBrush(QtGui.QColor(60, 60, 60))
57 | palette = self.palette()
58 | palette.setColor(QtGui.QPalette.Text, QtGui.QColor(210, 210, 210))
59 | palette.setColor(QtGui.QPalette.HighlightedText,
60 | QtGui.QColor(255, 255, 255))
61 | palette.setColor(QtGui.QPalette.BrightText,
62 | QtGui.QColor(80, 180, 255))
63 | palette.setColor(QtGui.QPalette.Button, QtGui.QColor(5, 5, 5))
64 | palette.setColor(QtGui.QPalette.ButtonText, QtGui.QColor(20, 20, 20))
65 | self.setPalette(palette)
66 |
67 | self.selectionChanged.connect(self._onSelectionChanged)
68 |
69 | @property
70 | def nodes(self):
71 | """Return all nodes
72 |
73 | """
74 | return self._nodes
75 |
76 | @property
77 | def is_interactive_edge(self):
78 | """Return status of interactive edge mode
79 |
80 | """
81 | return self._is_interactive_edge
82 |
83 | @property
84 | def edges_by_hash(self):
85 | """Return a list of edges as hash
86 |
87 | """
88 | return self._edges_by_hash
89 |
90 | def create_node(self, name, inputs=["in"], parent=None):
91 | """Create a new node
92 |
93 | """
94 | node = Node(name, self, inputs=inputs, parent=parent)
95 | self._nodes.append(node)
96 | return node
97 |
98 | def create_edge(self, source, target):
99 | """Create a new edge
100 |
101 | """
102 | edge = Edge(source, target, self, arrow=Edge.ARROW_STANDARD)
103 | self._edges_by_hash[edge.hash] = edge
104 | return edge
105 |
106 | def start_interactive_edge(self, source_slot, mouse_pos):
107 | """Create an edge between source slot and mouse position
108 |
109 | """
110 | self._is_interactive_edge = True
111 | if not self._interactive_edge:
112 | # Create interactive edge
113 | self._interactive_edge = InteractiveEdge(
114 | source_slot,
115 | mouse_pos,
116 | scene=self,
117 | arrow=Edge.ARROW_STANDARD)
118 | else:
119 | # Re-use existing interactive edge
120 | self._interactive_edge.refresh(mouse_pos, source_slot)
121 |
122 | def stop_interactive_edge(self, connect_to=None):
123 | """Hide the interactive and create an edge between the source slot
124 | and the slot given by connect_to
125 |
126 | """
127 | self._is_interactive_edge = False
128 | if connect_to:
129 | eh = self._edges_by_hash # shortcut
130 | source = self._interactive_edge._source_slot
131 |
132 | found = True
133 | if isinstance(connect_to, Node):
134 | found = False
135 | # Try to find most likely slot
136 | if source.family == NodeSlot.OUTPUT:
137 | for slot in connect_to._inputs:
138 | li = [h for h in eh if eh[h]._source_slot == slot or
139 | eh[h]._target_slot == slot]
140 | if not li:
141 | connect_to = slot
142 | found = True
143 | break
144 | else:
145 | connect_to = connect_to._output
146 | found = True
147 |
148 | # Resolve direction
149 | target = connect_to
150 |
151 | if source.family == NodeSlot.OUTPUT:
152 | source = connect_to
153 | target = self._interactive_edge._source_slot
154 |
155 | # Validate the connection
156 | if (found and
157 | source.family != target.family and
158 | source.parent != target.parent and
159 | not [h for h in eh if eh[h]._target_slot == source]):
160 |
161 | # TO DO: Check new edge isn't creating a loop, i.e that the
162 | # source node opposite slot(s) are(n't) connected to the target
163 | # node
164 | if source.family == NodeSlot.OUTPUT:
165 | for aninput in source.parent._inputs:
166 | pass
167 | else:
168 | output = source.parent._output
169 |
170 | # print("Create edge from %s to %s" %
171 | # (source._name, target._name))
172 | edge = self.create_edge(target, source)
173 | else:
174 | # TO DO: Send info to status bar
175 | pass
176 |
177 | # Delete item (to be sure it's not taken into account by any function
178 | # including but not limited to fitInView)
179 | self.removeItem(self._interactive_edge)
180 | self._interactive_edge = None
181 |
182 | def start_rubber_band(self, init_pos):
183 | """Create/Enable custom rubber band
184 |
185 | :param init_pos: Top left corner of the custom rubber band
186 | :type init_pos: :class:`QtCore.QPosF`
187 |
188 | """
189 | self._is_rubber_band = True
190 | if not self._rubber_band:
191 | # Create custom rubber band
192 | self._rubber_band = RubberBand(init_pos, scene=self)
193 | else:
194 | # Re-use existing rubber band
195 | self._rubber_band.refresh(mouse_pos=init_pos, init_pos=init_pos)
196 |
197 | def stop_rubber_band(self, intersect=None):
198 | """Hide the custom rubber band and if it contains node/edges select
199 | them
200 |
201 | """
202 | self._is_rubber_band = False
203 |
204 | # Select nodes and edges inside the rubber band
205 | if self._is_shift_key and self._is_ctrl_key:
206 | self._rubber_band.update_scene_selection(
207 | self._rubber_band.TOGGLE_SELECTION)
208 | elif self._is_shift_key:
209 | self._rubber_band.update_scene_selection(
210 | self._rubber_band.ADD_SELECTION)
211 | elif self._is_ctrl_key:
212 | self._rubber_band.update_scene_selection(
213 | self._rubber_band.MINUS_SELECTION)
214 | else:
215 | self._rubber_band.update_scene_selection(intersect)
216 |
217 | self.removeItem(self._rubber_band)
218 | self._rubber_band = None
219 |
220 | def delete_selected(self):
221 | """Delete selected nodes and edges
222 |
223 | """
224 | nodes = []
225 | edges = []
226 | for i in self.selectedItems():
227 | if isinstance(i, Node):
228 | nodes.append(i)
229 | if isinstance(i, Edge):
230 | edges.append(i)
231 |
232 | print("Node(s) to delete: %s" % [n._name for n in nodes])
233 | print("Edge(s) to delete: %r" % edges)
234 | for node in self.selectedItems():
235 | # TODO: Collect all edges for deletion or reconnection
236 | pass
237 | # Delete node(s)
238 | # self.removeItem(node)
239 | # index = self._nodes.index(node)
240 | # self._nodes.pop(index)
241 |
242 | def mousePressEvent(self, event):
243 | """Re-implements mouse press event
244 |
245 | :param event: Mouse event
246 | :type event: :class:`QtWidgets.QMouseEvent`
247 |
248 | """
249 | print("MOUSE PRESS SCENE!")
250 | if not self._is_interactive_edge:
251 |
252 | if not self.items(event.scenePos()):
253 | self.start_rubber_band(event.scenePos())
254 |
255 | if self._is_shift_key or self._is_ctrl_key:
256 | event.accept()
257 | return
258 | else:
259 | if self._is_shift_key or self._is_ctrl_key:
260 | # Mouse is above scene items and single click with modfiers
261 | event.accept()
262 |
263 | if self._is_shift_key and self._is_ctrl_key:
264 | for item in self.items(event.scenePos()):
265 | item.setSelected(not item.isSelected())
266 | elif self._is_shift_key:
267 | for item in self.items(event.scenePos()):
268 | item.setSelected(True)
269 | elif self._is_ctrl_key:
270 | for item in self.items(event.scenePos()):
271 | item.setSelected(False)
272 |
273 | return
274 | else:
275 | # Items under mouse during edge creation, We may have to start an
276 | # interactive edge
277 | pass
278 |
279 | QtWidgets.QGraphicsScene.mousePressEvent(self, event)
280 |
281 | def mouseMoveEvent(self, event):
282 | """Re-implements mouse move event
283 |
284 | :param event: Mouse event
285 | :type event: :class:`QtWidgets.QMouseEvent`
286 |
287 | """
288 | buttons = event.buttons()
289 |
290 | if buttons == QtCore.Qt.LeftButton:
291 |
292 | QtWidgets.QGraphicsScene.mouseMoveEvent(self, event)
293 |
294 | # Edge creation mode?
295 | if self._is_interactive_edge:
296 | self._interactive_edge.refresh(event.scenePos())
297 | # Selection mode?
298 | elif self._is_rubber_band:
299 | self._rubber_band.refresh(event.scenePos())
300 | elif self.selectedItems():
301 | if not self._is_refresh_edges:
302 | self._is_refresh_edges = True
303 | self._refresh_edges = self._get_refresh_edges()
304 | for ahash in self._refresh_edges["move"]:
305 | self._edges_by_hash[ahash].refresh_position()
306 | for ahash in self._refresh_edges["refresh"]:
307 | self._edges_by_hash[ahash].refresh()
308 | else:
309 | return QtWidgets.QGraphicsScene.mouseMoveEvent(self, event)
310 |
311 | def mouseReleaseEvent(self, event):
312 | """Re-implements mouse release event
313 |
314 | :param event: Mouse event
315 | :type event: :class:`QtWidgets.QMouseEvent`
316 |
317 | """
318 | # buttons = event.buttons()
319 | connect_to = None
320 |
321 | # Edge creation mode?
322 | if self._is_interactive_edge:
323 | slot = None
324 | node = None
325 | for item in self.items(event.scenePos()):
326 | if isinstance(item, Node):
327 | node = item
328 | slot = node._hover_slot
329 | break
330 | connect_to = slot if slot else node
331 |
332 | self.stop_interactive_edge(connect_to=connect_to)
333 |
334 | # Edge refresh mode?
335 | if self._is_refresh_edges:
336 | self._is_refresh_edges = False
337 | self._refresh_edges = []
338 |
339 | # Rubber band mode?
340 | if self._is_rubber_band:
341 | self.stop_rubber_band()
342 |
343 | QtWidgets.QGraphicsScene.mouseReleaseEvent(self, event)
344 |
345 | def mouseDoubleClickEvent(self, event):
346 | """Re-implements doube click event
347 |
348 | :param event: Mouse event
349 | :type event: :class:`QtWidgets.QMouseEvent`
350 |
351 | """
352 | selected = self.items(event.scenePos())
353 |
354 | if len(selected) == 1:
355 | print("Edit Node %s" % selected[0]._name)
356 |
357 | def _onSelectionChanged(self):
358 | """Re-inplements selection changed event
359 |
360 | """
361 | if self._is_refresh_edges:
362 | self._refresh_edges = self._get_refresh_edges()
363 |
364 | def _get_refresh_edges(self):
365 | """Return all edges of selected items
366 |
367 | """
368 | edges = set()
369 | nodes = set()
370 | edges_to_move = []
371 | edges_to_refresh = []
372 |
373 | for item in self.selectedItems():
374 | if isinstance(item, Node):
375 | edges |= item.edges
376 | nodes.add(item.name)
377 |
378 | # Distinghish edges where both ends are selected from the rest
379 | for edge in edges:
380 | if self._edges_by_hash[edge].is_connected_to(nodes):
381 | edges_to_move.append(edge)
382 | else:
383 | edges_to_refresh.append(edge)
384 |
385 | r = {"move": edges_to_move, "refresh": edges_to_refresh}
386 | # print("move: %r\nrefresh: %r" % (edges_to_move, edges_to_refresh))
387 | return r
388 |
389 | def get_nodes_bbox(self, visible_only=True):
390 | """Return bounding box of all nodes in scene
391 |
392 | ..todo :
393 | This function could be refactored
394 |
395 | :param visible_only: If true, only evaluate visible NodeSlot
396 | :type visible_only: bool
397 |
398 | :returns: A bounding rectangle
399 | :rtype: :class:`QtCore.QrectF`
400 |
401 | """
402 | if not self._nodes:
403 | return QtCore.QRectF()
404 |
405 | min_x = SCENE_WIDTH / 2
406 | min_y = SCENE_HEIGHT / 2
407 | max_x = - min_x
408 | max_y = - min_y
409 | min_x_node = None
410 | min_y_node = None
411 | max_x_node = None
412 | max_y_node = None
413 |
414 | for node in self._nodes:
415 | if visible_only and not node.isVisible():
416 | continue
417 |
418 | if node.x() < min_x:
419 | min_x = node.x()
420 | min_x_node = node
421 | if node.y() < min_y:
422 | min_y = node.y()
423 | min_y_node = node
424 | if node.x() > max_x:
425 | max_x = node.x()
426 | max_x_node = node
427 | if node.y() > max_y:
428 | max_y = node.y()
429 | max_y_node = node
430 |
431 | top_left = QtCore.QPointF(
432 | min_x + min_x_node.boundingRect().topLeft().x(),
433 | min_y + min_y_node.boundingRect().topLeft().y())
434 | bottom_right = QtCore.QPointF(
435 | max_x + max_x_node.boundingRect().bottomRight().x(),
436 | max_y + max_y_node.boundingRect().bottomRight().y())
437 | return QtCore.QRectF(top_left, bottom_right)
438 |
--------------------------------------------------------------------------------
/nodegraph/view.py:
--------------------------------------------------------------------------------
1 | # =============================================================================
2 | # Nodegraph-pyqt
3 | #
4 | # Everyone is permitted to copy and distribute verbatim copies of this
5 | # document, but changing it is not allowed without permissions.
6 | #
7 | # For any questions, please contact: dsideb@gmail.com
8 | #
9 | # GNU LESSER GENERAL PUBLIC LICENSE (Version 3, 29 June 2007)
10 | # =============================================================================
11 |
12 | """Node graph scene manager based on QGraphicsScene
13 |
14 | """
15 | import os
16 | import random
17 |
18 | from Qt import QtCore, QtGui, QtWidgets
19 | # from . import QtOpenGL
20 |
21 | from .node import Node
22 | from .constant import SCENE_WIDTH, SCENE_HEIGHT
23 |
24 | RESOURCES = os.path.dirname(os.path.realpath(__file__))
25 |
26 |
27 | class View(QtWidgets.QGraphicsView):
28 |
29 | """
30 | Provides custom implementation of QGraphicsView
31 |
32 | """
33 |
34 | def __init__(self, scene, parent=None):
35 | """Create an instance of this class
36 |
37 | :param scene: Scene reference
38 | :type scene: :class:`nodegraph.nodegraphScene.Scene`
39 |
40 | :param parent: Parent widget
41 | :type parent: mixed
42 |
43 | :returns: An instance of this class
44 | :rtype: :class:`nodegraph.nodegraphView.View`
45 |
46 | """
47 | QtWidgets.QGraphicsView.__init__(self, scene, parent)
48 | self._last_mouse_pos = QtCore.QPoint(0, 0)
49 | self._width = SCENE_WIDTH
50 | self._height = SCENE_HEIGHT
51 | self._scale = 1.0
52 | self._is_view_initialised = False
53 | self._is_pan = False
54 | self._is_zoom = False
55 |
56 | # Custom mouse cursors
57 | img = QtGui.QPixmap(
58 | os.path.join(RESOURCES, "bitmap", "arrow_plus.png"))
59 | self.arrow_plus_cursor = QtGui.QCursor(img, hotX=0, hotY=0)
60 | img = QtGui.QPixmap(
61 | os.path.join(RESOURCES, "bitmap", "arrow_minus.png"))
62 | self.arrow_minus_cursor = QtGui.QCursor(img, hotX=0, hotY=0)
63 | img = QtGui.QPixmap(
64 | os.path.join(RESOURCES, "bitmap", "arrow_cross.png"))
65 | self.arrow_cross_cursor = QtGui.QCursor(img, hotX=0, hotY=0)
66 |
67 | # Set scene
68 | self.setScene(scene)
69 |
70 | # Set scene rectangle
71 | self.scene().setSceneRect(
72 | QtCore.QRectF(-self._width / 2, -self._height / 2,
73 | self._width, self._height))
74 |
75 | # Enable OpenGL
76 | # GL_format = QtOpenGL.QGLFormat(QtOpenGL.QGL.SampleBuffers)
77 | # viewport = QtOpenGL.QGLWidget(GL_format)
78 | # self.setViewport(viewport)
79 |
80 | # Settings
81 | self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
82 | self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
83 | self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter)
84 | self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
85 | self.setRenderHint(QtGui.QPainter.Antialiasing)
86 | # self.setRenderHint(QtGui.QPainter.TextAntialiasing)
87 | # self.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
88 | self.setViewportUpdateMode(
89 | QtWidgets.QGraphicsView.BoundingRectViewportUpdate)
90 | # self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)
91 | self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
92 | self.setRubberBandSelectionMode(QtCore.Qt.ContainsItemBoundingRect)
93 | # self.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
94 | # QtWidgets.QSizePolicy.Expanding)
95 |
96 | # Init scene
97 | self.setInteractive(True)
98 |
99 | def fit_view(self, selected=False, padding=50):
100 | """Set view transform in order to fit all/selected nodes in scene.
101 |
102 | :param selected: If enabled, fit only selected nodes
103 | :type selected: bool
104 |
105 | :param padding: Add padding around the target rectangle
106 | :type padding: int
107 |
108 | """
109 | # Resolve rectangle we want to zoom to
110 | selection = self.scene().selectedItems()
111 | if selected and selection:
112 | scene_rect = self._get_selection_bbox(selection)
113 | else:
114 | scene_rect = self.scene().itemsBoundingRect()
115 | # scene_rect = self.scene().get_nodes_bbox()
116 |
117 | # Add a bit of padding
118 | scene_rect.adjust(-padding, -padding, padding, padding)
119 |
120 | # Compare ratio, find resulting scale
121 | # view_ratio = float(self.size().width()) / float(self.size().height())
122 | # fit_ratio = scene_rect.width() / scene_rect.height()
123 | x_ratio = scene_rect.width() / float(self.size().width())
124 | y_ratio = scene_rect.height() / float(self.size().height())
125 | new_scale = 1 / max(x_ratio, y_ratio)
126 |
127 | if new_scale >= 1:
128 | # Maximum zoom limit reached.
129 | # Let's translate to center of rect with reset scale
130 | self._scale = 1
131 | self.resetTransform()
132 | self.centerOn(scene_rect.center())
133 | elif new_scale < 0.1:
134 | # Minimum zoom limit reached.
135 | # Let's translate to center of rect and set zoom to limit
136 | if (self._scale) != 0.1:
137 | self._scale = 1
138 | self.resetTransform()
139 | self.scale_view(0.1)
140 | self.centerOn(scene_rect.center())
141 | else:
142 | # Fit to rectangle while keeping aspect ratio
143 | self._scale = new_scale
144 | print("Fit en view")
145 | self.fitInView(scene_rect, QtCore.Qt.KeepAspectRatio)
146 |
147 | def translate_view(self, offset):
148 | """Translate view by the given offset
149 |
150 | :param offset: Translate the view
151 | :type offset: :class:`QtCore.QPointF`
152 |
153 | """
154 | self.setInteractive(False)
155 | self.translate(offset.x(), offset.y())
156 | self.setInteractive(True)
157 |
158 | def scale_view(self, scale_factor, limits=True):
159 | """Scale the view with upper and lower limits if True
160 |
161 | :param scale_factor:
162 | :type scale_factor: number
163 |
164 | :param limits: If true, will limits scene scale
165 | :type limits: bool
166 |
167 | """
168 | new_scale = self._scale * scale_factor
169 | if limits and (new_scale >= 1.0 or new_scale < 0.1):
170 | # Respecting scaling limits
171 | if new_scale >= 1.0:
172 | self._scale = 1
173 | self.resetTransform()
174 | return False
175 | elif new_scale < 0.1:
176 | scale_factor = new_scale = 0.1
177 | self.resetTransform()
178 |
179 | # Update global scale
180 | self._scale = new_scale
181 | self.setInteractive(False)
182 | self.scale(scale_factor, scale_factor)
183 | self.setInteractive(True)
184 | return True
185 |
186 | def keyPressEvent(self, event):
187 | """Re-implement keyPressEvent from base class
188 |
189 | :param event: Key event
190 | :type event: :class:`QtWidgets.QKeyEvent`
191 |
192 | """
193 | modifiers = event.modifiers()
194 |
195 | if modifiers & QtCore.Qt.AltModifier:
196 | print("P# ALT ON")
197 | self.scene()._is_alt_key = True
198 | self._is_pan = True
199 | self.setRenderHint(QtGui.QPainter.Antialiasing, False)
200 | self.setCursor(QtCore.Qt.OpenHandCursor)
201 |
202 | if modifiers & QtCore.Qt.ControlModifier:
203 | print("P# CTRL ON")
204 | self.scene()._is_ctrl_key = True
205 |
206 | if not self._is_pan:
207 | self.setCursor(self.arrow_minus_cursor)
208 |
209 | if modifiers & QtCore.Qt.ShiftModifier:
210 | print("P# SHIFT ON")
211 | self.scene()._is_shift_key = True
212 |
213 | if not self._is_pan:
214 | if not self.scene()._is_ctrl_key:
215 | self.setCursor(self.arrow_plus_cursor)
216 | else:
217 | self.setCursor(self.arrow_cross_cursor)
218 |
219 | if event.key() in [QtCore.Qt.Key_Delete, QtCore.Qt.Key_Backspace]:
220 | self.scene().delete_selected()
221 |
222 | # TODO: Document these!
223 | if event.text() in ['-', '_']:
224 | self.scale_view(0.9)
225 | if event.text() in ['+', '=']:
226 | self.scale_view(1.1)
227 | if event.text() in ["f"]:
228 | self.fit_view(selected=True)
229 | if event.text() in ["a"]:
230 | self.fit_view(selected=False)
231 | # if event.text() in ['t']:
232 | # items = self.scene().selectedItems()
233 | # for item in items:
234 | # item.setSelected(False)
235 | if event.text() in ['c']:
236 | n = self.scene().create_node("random%d"
237 | % random.randint(1, 1000000),
238 | inputs=["in", "in1", "in2"])
239 | n.setPos(self.mapToScene(self._last_mouse_pos) -
240 | n.boundingRect().center())
241 |
242 | if event.text() in ['o']:
243 | for node in self.scene().selectedItems():
244 | if isinstance(node, Node):
245 | node._height -= 10
246 | node.refresh()
247 | if event.text() in ['p']:
248 | for node in self.scene().selectedItems():
249 | if isinstance(node, Node):
250 | node._height += 10
251 | node.refresh()
252 | if event.text() in ['s']:
253 | print(self._scale)
254 | else:
255 | return QtWidgets.QGraphicsView.keyPressEvent(self, event)
256 |
257 | def keyReleaseEvent(self, event):
258 | """Re-implement keyReleaseEvent from base class
259 |
260 | :param event: Key event
261 | :type event: :class:`QtWidgets.QKeyEvent`
262 |
263 | """
264 | modifiers = event.modifiers()
265 |
266 | if not modifiers & QtCore.Qt.ControlModifier:
267 | print("R### CTRL OFF")
268 | self.scene()._is_ctrl_key = False
269 |
270 | if not modifiers & QtCore.Qt.ShiftModifier:
271 | print("R### SHIFT OFF")
272 | self.scene()._is_shift_key = False
273 |
274 | if not modifiers & QtCore.Qt.AltModifier:
275 | print("R### ALT OFF")
276 | self.scene()._is_alt_key = False
277 |
278 | if not self.scene()._is_mid_mouse:
279 | self._is_pan = False
280 | self.setRenderHint(QtGui.QPainter.Antialiasing, True)
281 | self.setCursor(QtCore.Qt.ArrowCursor)
282 |
283 | if self.scene()._is_shift_key:
284 | self.setCursor(self.arrow_plus_cursor)
285 | elif self.scene()._is_ctrl_key:
286 | self.setCursor(self.arrow_minus_cursor)
287 | elif not self._is_pan:
288 | self.setCursor(QtCore.Qt.ArrowCursor)
289 |
290 | return QtWidgets.QGraphicsView.keyReleaseEvent(self, event)
291 |
292 | def mousePressEvent(self, event):
293 | """Re-implement mousePressEvent from base class
294 |
295 | :param event: Mouse event
296 | :type event: :class:`QtWidgets.QMouseEvent`
297 |
298 | """
299 | # print("MOUSE PRESS VIEW!")
300 | scene = self.scene() # alias
301 |
302 | # Update registars
303 | self._last_mouse_pos = event.pos()
304 | if event.button() == QtCore.Qt.LeftButton:
305 | scene._is_left_mouse = True
306 | elif event.button() == QtCore.Qt.MidButton:
307 | scene._is_mid_mouse = True
308 | elif event.button() == QtCore.Qt.RightButton:
309 | scene._is_right_mouse = True
310 |
311 | # Update mode
312 | if scene._is_left_mouse and scene._is_alt_key:
313 | self._is_pan = True
314 | self.setCursor(QtCore.Qt.OpenHandCursor)
315 | elif scene._is_mid_mouse:
316 | self._is_pan = True
317 | else:
318 | return QtWidgets.QGraphicsView.mousePressEvent(self, event)
319 |
320 | def mouseMoveEvent(self, event):
321 | """Re-implement mouseMoveEvent from base class
322 |
323 | :param event: Mouse event
324 | :type event: :class:`QtWidgets.QMouseEvent`
325 |
326 | """
327 | if self._is_pan:
328 | delta = (self.mapToScene(self._last_mouse_pos) -
329 | self.mapToScene(event.pos()))
330 | self.setCursor(QtCore.Qt.ClosedHandCursor)
331 | self.translate_view(delta)
332 | self._last_mouse_pos = event.pos()
333 | else:
334 | self._last_mouse_pos = event.pos()
335 | QtWidgets.QGraphicsView.mouseMoveEvent(self, event)
336 |
337 | def mouseReleaseEvent(self, event):
338 | """Re-implement mouseReleaseEvent from base class
339 |
340 | :param event: Mouse event
341 | :type event: :class:`QtWidgets.QMouseEvent`
342 |
343 | """
344 | # print("MOUSE RELEASE")
345 | scene = self.scene() # alias
346 |
347 | # Update registars
348 | self._last_mouse_pos = event.pos()
349 | if event.button() == QtCore.Qt.LeftButton:
350 | scene._is_left_mouse = False
351 | elif event.button() == QtCore.Qt.MidButton:
352 | scene._is_mid_mouse = False
353 | elif event.button() == QtCore.Qt.RightButton:
354 | scene._is_right_mouse = False
355 |
356 | # Update mode
357 | if self._is_pan:
358 | self._is_pan = False
359 |
360 | # Update mouse icon
361 | if scene._is_alt_key:
362 | self.setCursor(QtCore.Qt.OpenHandCursor)
363 | elif scene._is_shift_key and scene._is_ctrl_key:
364 | self.setCursor(self.arrow_cross_cursor)
365 | elif scene._is_shift_key:
366 | self.setCursor(self.arrow_plus_cursor)
367 | elif scene._is_ctrl_key:
368 | self.setCursor(self.arrow_minus_cursor)
369 | else:
370 | self.setCursor(QtCore.Qt.ArrowCursor)
371 |
372 | QtWidgets.QGraphicsView.mouseReleaseEvent(self, event)
373 |
374 | def wheelEvent(self, event):
375 | """Re-implement wheelEvent from base class
376 |
377 | :param event: Wheel event
378 | :type event: :class:`QtWidgets.QWheelEvent`
379 |
380 | """
381 | # print("WHEELLLLL")
382 | delta = event.delta()
383 | # p = event.pos()
384 |
385 | scale_factor = pow(1.25, delta / 240.0)
386 | self.scale_view(scale_factor)
387 | event.accept()
388 |
389 | def showEvent(self, event):
390 | """Re-implent showEvent from base class
391 |
392 | :param event: Show event
393 | :type event: :class:`QtWidgets.QShowEvent`
394 |
395 | """
396 | if not self._is_view_initialised:
397 | self._is_view_initialised = True
398 | self.fit_view()
399 | QtWidgets.QGraphicsView.showEvent(self, event)
400 |
401 | def focusOutEvent(self, event):
402 | """Re-implement focusOutEvent from the base class
403 |
404 | Prevent pan mode to stay on when loosing focus to anything outside
405 | of view
406 |
407 | :param event: Focus event
408 | :type event: :class:`QtWidgets.QFocusEvent`
409 |
410 | """
411 | print("Mouse out!")
412 | # Stop dragging mode if needed
413 | self.scene()._is_alt_key = False
414 | self._is_pan = False
415 | self.setRenderHint(QtGui.QPainter.Antialiasing, True)
416 | self.setCursor(QtCore.Qt.ArrowCursor)
417 |
418 | QtWidgets.QGraphicsView.focusOutEvent(self, event)
419 |
420 | # def focusInEvent(self, event):
421 | # """Re-implement focusInEvent from the base class
422 |
423 | # :param event: Focus event
424 | # :type event: :class:`QtWidgets.QFocusEvent`
425 |
426 | # """
427 | # print("Mouse in!")
428 | # modifiers = QtWidgets.QApplication.keyboardModifiers()
429 |
430 | # if modifiers & QtCore.Qt.AltModifier:
431 | # print("P# ALT ON")
432 | # self.scene()._is_alt_key = True
433 | # self._is_pan = True
434 | # self.setRenderHint(QtGui.QPainter.Antialiasing, False)
435 | # self.setCursor(QtCore.Qt.OpenHandCursor)
436 |
437 | # QtWidgets.QGraphicsView.focusOutEvent(self, event)
438 |
439 | def _get_selection_bbox(self, selection):
440 | """For a given selection of node return the bounding box
441 |
442 | :param selection: List of graphics item
443 | :type selection: List
444 |
445 | :returns: A Qt rectangle
446 | :rtype: :class:`QtCore.QRectF`
447 |
448 | """
449 | top_left = QtCore.QPointF(self._width, self._height)
450 | bottom_right = QtCore.QPointF(- self._width, - self._height)
451 |
452 | for node in [s for s in selection if isinstance(s, Node)]:
453 | bbox = node.boundingRect()
454 | top_left.setX(min(node.x() + bbox.left(), top_left.x()))
455 | top_left.setY(min(node.y() + bbox.top(), top_left.y()))
456 | bottom_right.setX(max(node.x() + bbox.right(),
457 | bottom_right.x()))
458 | bottom_right.setY(max(node.y() + bbox.bottom(),
459 | bottom_right.y()))
460 | return QtCore.QRectF(top_left, bottom_right)
461 |
--------------------------------------------------------------------------------
/notes.todo:
--------------------------------------------------------------------------------
1 | Nodes:
2 |
3 | Design:
4 | * Undefined number of inputs
5 | * Single Output
6 | * Inputs are on left side of the node
7 | * Output is on the righ side of the node
8 | * Natural layout flow is left to right
9 | * Name of a node is unique
10 |
11 | To do:
12 | ✔ Optimize redraw by caching as much as possible any computation @done (17-11-22 09:04)
13 | ☐ Handle undefined numbers of inputs
14 | ☐ Create a disabled state
15 | ☐ Handles deletion (with edges gracefully reconnecting)
16 |
17 | Edges:
18 |
19 | Design:
20 | * An edge is defined but its parent output and its named input
21 | * Edge name is hashed from its parents
22 | * A edge is only valid if it doesn't create a loop
23 |
24 | To do:
25 | ✔ Create edge interactively @done (17-10-04 09:01)
26 | ✔ Adjust arrow size (lod bug) and implement dynamic scale @done (17-10-04 09:06)
27 | ✔ Optimize redraw by caching as much as possible any computation @done (17-11-22 09:04)
28 | ✔ Optimize redraw by updating line through callbacks @done (17-10-28 13:14)
29 | ☐ Handles deletion
30 |
31 | Nodegraph view:
32 |
33 | Design:
34 | * Zoom limits (no more than 1:1, no less than 1:10)
35 | * Bounds (no infinite canvas)
36 |
37 | To do:
38 | ✔ Limit fit all unzoom @done (17-10-05 08:53)
39 | ✔ Write a custom select rubber band (default one evaluate everything on each frame) @done (17-11-22 09:04)
40 | ✔ Add/Remove selection with rubber band @done (17-11-25 23:23)
41 | ✔ Re-implement toggle selection when ctrl+click on node @done (17-12-08 09:15)
42 | ☐ Add a node creation widget (tab)
43 | ✔ Re-implement pan to be always available (midle-click, alt) @done (17-12-08 19:05)
44 | ✔ Switch to custom cursor when shift/ctrl on selection @done (17-12-18 11:08)
45 | ✔ Support toggle selection (shift+ctrl) @done (17-12-18 11:15)
46 | ☐ refactor keypress and modifiers handler
47 |
48 | Performances issues:
49 |
50 | * OpenGL viewport don't accept partial redraw, hence redraw has to happen on the whole viewport regardless of what need to be redrawn.
51 | * Using regular viewport allows for partial redraw but then edges are not redrawn well -> prepareGeometryChange() signal wasn't emitted before update...
52 | * Hierarchy of graphicsItem contained by a Node (NodeSlot, NodeSlotLabel) is forcing QT to multiply draw calls (3 instead of 1). KILLING rubber band selection refresh.
53 | * Knobs have to be set visible (or not) on every redraw.
54 | * Selection rubber band still too slow. Custom one that is only an outline and evaluate selection on mouse button release?
55 | * Moving a lot of nodes with edges (+400) shows limit of edges refresh loop. Either find a way to only change the position of the edge (as you would do with a regular QGraphicsItem) or, even better, look into bitmap caching of all nodes and edges that moves uniformly (only redrawing edges connected to non-selected nodes)
56 |
57 |
58 | Performance influenced design:
59 |
60 | * Using partial redraw (no OpenGL viewport) but limiting complexity by forcing to bounding rectangle.
61 | * Node Slot is part of the Node QGraphicsItem (i.e. included in Node paint call).
62 | * Edges are QGraphicsItem which are refreshed either completely (redefining line and shape) or only by adjusting their position.
63 | * Nodes have many level of detail based on zoom level.
64 | * Custom rubber band that only refresh selection when releasing mouse button.
65 |
66 | Events:
67 |
68 | * First event goes to View, then Scene, then from highest item in the stack to lowest under the mouse cursor.
69 | * If not calling ancestor, stop propagation.
70 |
71 | TESTS:
72 |
73 | * Check zoom limits
74 | * Check left click selection (node, edge)
75 | * Check Lasso selection (nodes, edges)
76 | * Check add/remove from selection shortcut, with single left click selection then lasso selection.
77 | * Check selected nodes can be me moved
78 | * Check creation of a node
79 | * Check Creation of a edge (from a input to an output and opposite)
80 | * Check Pan shortcut (alone, then combined with other actions)
81 | * Check Creation of an edge (auto-drop to destination node)
82 | * Check Deletion of Nodes (no connections)
83 | * Check Deletion of Edges.
84 | * Check Deletions of a connected node (automatic re-connection)
85 | * Check saving a graph
86 | * check loading a graph
87 |
88 | BUGS:
89 |
90 | ✔ Hidden Rubberband and interactive edge are taken into account for fitInView @done (18-05-20 12:20)
91 | ☐ Can't connect anymore to specific input (always use the connect_to_next_available)
92 | ☐ When trying to connect on a input slot already connected, replace connection.
93 |
94 | Next thing to do:
95 | Move node mousePresseEvent input/output detection to public function that requires a point in scene coordinates
96 | Move start_interactive_edge from node.py to scene.py in MousePressEvent.
97 | In scene.py, mouseReleaseEvent, use utility function to detect if drop in slot.
98 | Remove complex/useless self._hover_slot (which is not updated anyway when in interactive_edge mode)
99 |
--------------------------------------------------------------------------------
/thirdparty/Qt.py:
--------------------------------------------------------------------------------
1 | """Minimal Python 2 & 3 shim around all Qt bindings
2 |
3 | DOCUMENTATION
4 | Qt.py was born in the film and visual effects industry to address
5 | the growing need for the development of software capable of running
6 | with more than one flavour of the Qt bindings for Python - PySide,
7 | PySide2, PyQt4 and PyQt5.
8 |
9 | 1. Build for one, run with all
10 | 2. Explicit is better than implicit
11 | 3. Support co-existence
12 |
13 | Default resolution order:
14 | - PySide2
15 | - PyQt5
16 | - PySide
17 | - PyQt4
18 |
19 | Usage:
20 | >> import sys
21 | >> from Qt import QtWidgets
22 | >> app = QtWidgets.QApplication(sys.argv)
23 | >> button = QtWidgets.QPushButton("Hello World")
24 | >> button.show()
25 | >> app.exec_()
26 |
27 | All members of PySide2 are mapped from other bindings, should they exist.
28 | If no equivalent member exist, it is excluded from Qt.py and inaccessible.
29 | The idea is to highlight members that exist across all supported binding,
30 | and guarantee that code that runs on one binding runs on all others.
31 |
32 | For more details, visit https://github.com/mottosso/Qt.py
33 |
34 | LICENSE
35 |
36 | See end of file for license (MIT, BSD) information.
37 |
38 | """
39 |
40 | import os
41 | import sys
42 | import types
43 | import shutil
44 |
45 |
46 | __version__ = "1.2.0.b2"
47 |
48 | # Enable support for `from Qt import *`
49 | __all__ = []
50 |
51 | # Flags from environment variables
52 | QT_VERBOSE = bool(os.getenv("QT_VERBOSE"))
53 | QT_PREFERRED_BINDING = os.getenv("QT_PREFERRED_BINDING", "")
54 | QT_SIP_API_HINT = os.getenv("QT_SIP_API_HINT")
55 |
56 | # Reference to Qt.py
57 | Qt = sys.modules[__name__]
58 | Qt.QtCompat = types.ModuleType("QtCompat")
59 |
60 | try:
61 | long
62 | except NameError:
63 | # Python 3 compatibility
64 | long = int
65 |
66 |
67 | """Common members of all bindings
68 |
69 | This is where each member of Qt.py is explicitly defined.
70 | It is based on a "lowest common denominator" of all bindings;
71 | including members found in each of the 4 bindings.
72 |
73 | The "_common_members" dictionary is generated using the
74 | build_membership.sh script.
75 |
76 | """
77 |
78 | _common_members = {
79 | "QtCore": [
80 | "QAbstractAnimation",
81 | "QAbstractEventDispatcher",
82 | "QAbstractItemModel",
83 | "QAbstractListModel",
84 | "QAbstractState",
85 | "QAbstractTableModel",
86 | "QAbstractTransition",
87 | "QAnimationGroup",
88 | "QBasicTimer",
89 | "QBitArray",
90 | "QBuffer",
91 | "QByteArray",
92 | "QByteArrayMatcher",
93 | "QChildEvent",
94 | "QCoreApplication",
95 | "QCryptographicHash",
96 | "QDataStream",
97 | "QDate",
98 | "QDateTime",
99 | "QDir",
100 | "QDirIterator",
101 | "QDynamicPropertyChangeEvent",
102 | "QEasingCurve",
103 | "QElapsedTimer",
104 | "QEvent",
105 | "QEventLoop",
106 | "QEventTransition",
107 | "QFile",
108 | "QFileInfo",
109 | "QFileSystemWatcher",
110 | "QFinalState",
111 | "QGenericArgument",
112 | "QGenericReturnArgument",
113 | "QHistoryState",
114 | "QItemSelectionRange",
115 | "QIODevice",
116 | "QLibraryInfo",
117 | "QLine",
118 | "QLineF",
119 | "QLocale",
120 | "QMargins",
121 | "QMetaClassInfo",
122 | "QMetaEnum",
123 | "QMetaMethod",
124 | "QMetaObject",
125 | "QMetaProperty",
126 | "QMimeData",
127 | "QModelIndex",
128 | "QMutex",
129 | "QMutexLocker",
130 | "QObject",
131 | "QParallelAnimationGroup",
132 | "QPauseAnimation",
133 | "QPersistentModelIndex",
134 | "QPluginLoader",
135 | "QPoint",
136 | "QPointF",
137 | "QProcess",
138 | "QProcessEnvironment",
139 | "QPropertyAnimation",
140 | "QReadLocker",
141 | "QReadWriteLock",
142 | "QRect",
143 | "QRectF",
144 | "QRegExp",
145 | "QResource",
146 | "QRunnable",
147 | "QSemaphore",
148 | "QSequentialAnimationGroup",
149 | "QSettings",
150 | "QSignalMapper",
151 | "QSignalTransition",
152 | "QSize",
153 | "QSizeF",
154 | "QSocketNotifier",
155 | "QState",
156 | "QStateMachine",
157 | "QSysInfo",
158 | "QSystemSemaphore",
159 | "QT_TRANSLATE_NOOP",
160 | "QT_TR_NOOP",
161 | "QT_TR_NOOP_UTF8",
162 | "QTemporaryFile",
163 | "QTextBoundaryFinder",
164 | "QTextCodec",
165 | "QTextDecoder",
166 | "QTextEncoder",
167 | "QTextStream",
168 | "QTextStreamManipulator",
169 | "QThread",
170 | "QThreadPool",
171 | "QTime",
172 | "QTimeLine",
173 | "QTimer",
174 | "QTimerEvent",
175 | "QTranslator",
176 | "QUrl",
177 | "QVariantAnimation",
178 | "QWaitCondition",
179 | "QWriteLocker",
180 | "QXmlStreamAttribute",
181 | "QXmlStreamAttributes",
182 | "QXmlStreamEntityDeclaration",
183 | "QXmlStreamEntityResolver",
184 | "QXmlStreamNamespaceDeclaration",
185 | "QXmlStreamNotationDeclaration",
186 | "QXmlStreamReader",
187 | "QXmlStreamWriter",
188 | "Qt",
189 | "QtCriticalMsg",
190 | "QtDebugMsg",
191 | "QtFatalMsg",
192 | "QtMsgType",
193 | "QtSystemMsg",
194 | "QtWarningMsg",
195 | "qAbs",
196 | "qAddPostRoutine",
197 | "qChecksum",
198 | "qCritical",
199 | "qDebug",
200 | "qFatal",
201 | "qFuzzyCompare",
202 | "qIsFinite",
203 | "qIsInf",
204 | "qIsNaN",
205 | "qIsNull",
206 | "qRegisterResourceData",
207 | "qUnregisterResourceData",
208 | "qVersion",
209 | "qWarning",
210 | "qrand",
211 | "qsrand"
212 | ],
213 | "QtGui": [
214 | "QAbstractTextDocumentLayout",
215 | "QActionEvent",
216 | "QBitmap",
217 | "QBrush",
218 | "QClipboard",
219 | "QCloseEvent",
220 | "QColor",
221 | "QConicalGradient",
222 | "QContextMenuEvent",
223 | "QCursor",
224 | "QDesktopServices",
225 | "QDoubleValidator",
226 | "QDrag",
227 | "QDragEnterEvent",
228 | "QDragLeaveEvent",
229 | "QDragMoveEvent",
230 | "QDropEvent",
231 | "QFileOpenEvent",
232 | "QFocusEvent",
233 | "QFont",
234 | "QFontDatabase",
235 | "QFontInfo",
236 | "QFontMetrics",
237 | "QFontMetricsF",
238 | "QGradient",
239 | "QHelpEvent",
240 | "QHideEvent",
241 | "QHoverEvent",
242 | "QIcon",
243 | "QIconDragEvent",
244 | "QIconEngine",
245 | "QImage",
246 | "QImageIOHandler",
247 | "QImageReader",
248 | "QImageWriter",
249 | "QInputEvent",
250 | "QInputMethodEvent",
251 | "QIntValidator",
252 | "QKeyEvent",
253 | "QKeySequence",
254 | "QLinearGradient",
255 | "QMatrix2x2",
256 | "QMatrix2x3",
257 | "QMatrix2x4",
258 | "QMatrix3x2",
259 | "QMatrix3x3",
260 | "QMatrix3x4",
261 | "QMatrix4x2",
262 | "QMatrix4x3",
263 | "QMatrix4x4",
264 | "QMouseEvent",
265 | "QMoveEvent",
266 | "QMovie",
267 | "QPaintDevice",
268 | "QPaintEngine",
269 | "QPaintEngineState",
270 | "QPaintEvent",
271 | "QPainter",
272 | "QPainterPath",
273 | "QPainterPathStroker",
274 | "QPalette",
275 | "QPen",
276 | "QPicture",
277 | "QPictureIO",
278 | "QPixmap",
279 | "QPixmapCache",
280 | "QPolygon",
281 | "QPolygonF",
282 | "QQuaternion",
283 | "QRadialGradient",
284 | "QRegExpValidator",
285 | "QRegion",
286 | "QResizeEvent",
287 | "QSessionManager",
288 | "QShortcutEvent",
289 | "QShowEvent",
290 | "QStandardItem",
291 | "QStandardItemModel",
292 | "QStatusTipEvent",
293 | "QSyntaxHighlighter",
294 | "QTabletEvent",
295 | "QTextBlock",
296 | "QTextBlockFormat",
297 | "QTextBlockGroup",
298 | "QTextBlockUserData",
299 | "QTextCharFormat",
300 | "QTextCursor",
301 | "QTextDocument",
302 | "QTextDocumentFragment",
303 | "QTextFormat",
304 | "QTextFragment",
305 | "QTextFrame",
306 | "QTextFrameFormat",
307 | "QTextImageFormat",
308 | "QTextInlineObject",
309 | "QTextItem",
310 | "QTextLayout",
311 | "QTextLength",
312 | "QTextLine",
313 | "QTextList",
314 | "QTextListFormat",
315 | "QTextObject",
316 | "QTextObjectInterface",
317 | "QTextOption",
318 | "QTextTable",
319 | "QTextTableCell",
320 | "QTextTableCellFormat",
321 | "QTextTableFormat",
322 | "QTouchEvent",
323 | "QTransform",
324 | "QValidator",
325 | "QVector2D",
326 | "QVector3D",
327 | "QVector4D",
328 | "QWhatsThisClickedEvent",
329 | "QWheelEvent",
330 | "QWindowStateChangeEvent",
331 | "qAlpha",
332 | "qBlue",
333 | "qGray",
334 | "qGreen",
335 | "qIsGray",
336 | "qRed",
337 | "qRgb",
338 | "qRgba"
339 | ],
340 | "QtHelp": [
341 | "QHelpContentItem",
342 | "QHelpContentModel",
343 | "QHelpContentWidget",
344 | "QHelpEngine",
345 | "QHelpEngineCore",
346 | "QHelpIndexModel",
347 | "QHelpIndexWidget",
348 | "QHelpSearchEngine",
349 | "QHelpSearchQuery",
350 | "QHelpSearchQueryWidget",
351 | "QHelpSearchResultWidget"
352 | ],
353 | "QtMultimedia": [
354 | "QAbstractVideoBuffer",
355 | "QAbstractVideoSurface",
356 | "QAudio",
357 | "QAudioDeviceInfo",
358 | "QAudioFormat",
359 | "QAudioInput",
360 | "QAudioOutput",
361 | "QVideoFrame",
362 | "QVideoSurfaceFormat"
363 | ],
364 | "QtNetwork": [
365 | "QAbstractNetworkCache",
366 | "QAbstractSocket",
367 | "QAuthenticator",
368 | "QHostAddress",
369 | "QHostInfo",
370 | "QLocalServer",
371 | "QLocalSocket",
372 | "QNetworkAccessManager",
373 | "QNetworkAddressEntry",
374 | "QNetworkCacheMetaData",
375 | "QNetworkConfiguration",
376 | "QNetworkConfigurationManager",
377 | "QNetworkCookie",
378 | "QNetworkCookieJar",
379 | "QNetworkDiskCache",
380 | "QNetworkInterface",
381 | "QNetworkProxy",
382 | "QNetworkProxyFactory",
383 | "QNetworkProxyQuery",
384 | "QNetworkReply",
385 | "QNetworkRequest",
386 | "QNetworkSession",
387 | "QSsl",
388 | "QTcpServer",
389 | "QTcpSocket",
390 | "QUdpSocket"
391 | ],
392 | "QtOpenGL": [
393 | "QGL",
394 | "QGLContext",
395 | "QGLFormat",
396 | "QGLWidget"
397 | ],
398 | "QtPrintSupport": [
399 | "QAbstractPrintDialog",
400 | "QPageSetupDialog",
401 | "QPrintDialog",
402 | "QPrintEngine",
403 | "QPrintPreviewDialog",
404 | "QPrintPreviewWidget",
405 | "QPrinter",
406 | "QPrinterInfo"
407 | ],
408 | "QtSql": [
409 | "QSql",
410 | "QSqlDatabase",
411 | "QSqlDriver",
412 | "QSqlDriverCreatorBase",
413 | "QSqlError",
414 | "QSqlField",
415 | "QSqlIndex",
416 | "QSqlQuery",
417 | "QSqlQueryModel",
418 | "QSqlRecord",
419 | "QSqlRelation",
420 | "QSqlRelationalDelegate",
421 | "QSqlRelationalTableModel",
422 | "QSqlResult",
423 | "QSqlTableModel"
424 | ],
425 | "QtSvg": [
426 | "QGraphicsSvgItem",
427 | "QSvgGenerator",
428 | "QSvgRenderer",
429 | "QSvgWidget"
430 | ],
431 | "QtTest": [
432 | "QTest"
433 | ],
434 | "QtWidgets": [
435 | "QAbstractButton",
436 | "QAbstractGraphicsShapeItem",
437 | "QAbstractItemDelegate",
438 | "QAbstractItemView",
439 | "QAbstractScrollArea",
440 | "QAbstractSlider",
441 | "QAbstractSpinBox",
442 | "QAction",
443 | "QActionGroup",
444 | "QApplication",
445 | "QBoxLayout",
446 | "QButtonGroup",
447 | "QCalendarWidget",
448 | "QCheckBox",
449 | "QColorDialog",
450 | "QColumnView",
451 | "QComboBox",
452 | "QCommandLinkButton",
453 | "QCommonStyle",
454 | "QCompleter",
455 | "QDataWidgetMapper",
456 | "QDateEdit",
457 | "QDateTimeEdit",
458 | "QDesktopWidget",
459 | "QDial",
460 | "QDialog",
461 | "QDialogButtonBox",
462 | "QDirModel",
463 | "QDockWidget",
464 | "QDoubleSpinBox",
465 | "QErrorMessage",
466 | "QFileDialog",
467 | "QFileIconProvider",
468 | "QFileSystemModel",
469 | "QFocusFrame",
470 | "QFontComboBox",
471 | "QFontDialog",
472 | "QFormLayout",
473 | "QFrame",
474 | "QGesture",
475 | "QGestureEvent",
476 | "QGestureRecognizer",
477 | "QGraphicsAnchor",
478 | "QGraphicsAnchorLayout",
479 | "QGraphicsBlurEffect",
480 | "QGraphicsColorizeEffect",
481 | "QGraphicsDropShadowEffect",
482 | "QGraphicsEffect",
483 | "QGraphicsEllipseItem",
484 | "QGraphicsGridLayout",
485 | "QGraphicsItem",
486 | "QGraphicsItemGroup",
487 | "QGraphicsLayout",
488 | "QGraphicsLayoutItem",
489 | "QGraphicsLineItem",
490 | "QGraphicsLinearLayout",
491 | "QGraphicsObject",
492 | "QGraphicsOpacityEffect",
493 | "QGraphicsPathItem",
494 | "QGraphicsPixmapItem",
495 | "QGraphicsPolygonItem",
496 | "QGraphicsProxyWidget",
497 | "QGraphicsRectItem",
498 | "QGraphicsRotation",
499 | "QGraphicsScale",
500 | "QGraphicsScene",
501 | "QGraphicsSceneContextMenuEvent",
502 | "QGraphicsSceneDragDropEvent",
503 | "QGraphicsSceneEvent",
504 | "QGraphicsSceneHelpEvent",
505 | "QGraphicsSceneHoverEvent",
506 | "QGraphicsSceneMouseEvent",
507 | "QGraphicsSceneMoveEvent",
508 | "QGraphicsSceneResizeEvent",
509 | "QGraphicsSceneWheelEvent",
510 | "QGraphicsSimpleTextItem",
511 | "QGraphicsTextItem",
512 | "QGraphicsTransform",
513 | "QGraphicsView",
514 | "QGraphicsWidget",
515 | "QGridLayout",
516 | "QGroupBox",
517 | "QHBoxLayout",
518 | "QHeaderView",
519 | "QInputDialog",
520 | "QItemDelegate",
521 | "QItemEditorCreatorBase",
522 | "QItemEditorFactory",
523 | "QKeyEventTransition",
524 | "QLCDNumber",
525 | "QLabel",
526 | "QLayout",
527 | "QLayoutItem",
528 | "QLineEdit",
529 | "QListView",
530 | "QListWidget",
531 | "QListWidgetItem",
532 | "QMainWindow",
533 | "QMdiArea",
534 | "QMdiSubWindow",
535 | "QMenu",
536 | "QMenuBar",
537 | "QMessageBox",
538 | "QMouseEventTransition",
539 | "QPanGesture",
540 | "QPinchGesture",
541 | "QPlainTextDocumentLayout",
542 | "QPlainTextEdit",
543 | "QProgressBar",
544 | "QProgressDialog",
545 | "QPushButton",
546 | "QRadioButton",
547 | "QRubberBand",
548 | "QScrollArea",
549 | "QScrollBar",
550 | "QShortcut",
551 | "QSizeGrip",
552 | "QSizePolicy",
553 | "QSlider",
554 | "QSpacerItem",
555 | "QSpinBox",
556 | "QSplashScreen",
557 | "QSplitter",
558 | "QSplitterHandle",
559 | "QStackedLayout",
560 | "QStackedWidget",
561 | "QStatusBar",
562 | "QStyle",
563 | "QStyleFactory",
564 | "QStyleHintReturn",
565 | "QStyleHintReturnMask",
566 | "QStyleHintReturnVariant",
567 | "QStyleOption",
568 | "QStyleOptionButton",
569 | "QStyleOptionComboBox",
570 | "QStyleOptionComplex",
571 | "QStyleOptionDockWidget",
572 | "QStyleOptionFocusRect",
573 | "QStyleOptionFrame",
574 | "QStyleOptionGraphicsItem",
575 | "QStyleOptionGroupBox",
576 | "QStyleOptionHeader",
577 | "QStyleOptionMenuItem",
578 | "QStyleOptionProgressBar",
579 | "QStyleOptionRubberBand",
580 | "QStyleOptionSizeGrip",
581 | "QStyleOptionSlider",
582 | "QStyleOptionSpinBox",
583 | "QStyleOptionTab",
584 | "QStyleOptionTabBarBase",
585 | "QStyleOptionTabWidgetFrame",
586 | "QStyleOptionTitleBar",
587 | "QStyleOptionToolBar",
588 | "QStyleOptionToolBox",
589 | "QStyleOptionToolButton",
590 | "QStyleOptionViewItem",
591 | "QStylePainter",
592 | "QStyledItemDelegate",
593 | "QSwipeGesture",
594 | "QSystemTrayIcon",
595 | "QTabBar",
596 | "QTabWidget",
597 | "QTableView",
598 | "QTableWidget",
599 | "QTableWidgetItem",
600 | "QTableWidgetSelectionRange",
601 | "QTapAndHoldGesture",
602 | "QTapGesture",
603 | "QTextBrowser",
604 | "QTextEdit",
605 | "QTimeEdit",
606 | "QToolBar",
607 | "QToolBox",
608 | "QToolButton",
609 | "QToolTip",
610 | "QTreeView",
611 | "QTreeWidget",
612 | "QTreeWidgetItem",
613 | "QTreeWidgetItemIterator",
614 | "QUndoCommand",
615 | "QUndoGroup",
616 | "QUndoStack",
617 | "QUndoView",
618 | "QVBoxLayout",
619 | "QWhatsThis",
620 | "QWidget",
621 | "QWidgetAction",
622 | "QWidgetItem",
623 | "QWizard",
624 | "QWizardPage"
625 | ],
626 | "QtX11Extras": [
627 | "QX11Info"
628 | ],
629 | "QtXml": [
630 | "QDomAttr",
631 | "QDomCDATASection",
632 | "QDomCharacterData",
633 | "QDomComment",
634 | "QDomDocument",
635 | "QDomDocumentFragment",
636 | "QDomDocumentType",
637 | "QDomElement",
638 | "QDomEntity",
639 | "QDomEntityReference",
640 | "QDomImplementation",
641 | "QDomNamedNodeMap",
642 | "QDomNode",
643 | "QDomNodeList",
644 | "QDomNotation",
645 | "QDomProcessingInstruction",
646 | "QDomText",
647 | "QXmlAttributes",
648 | "QXmlContentHandler",
649 | "QXmlDTDHandler",
650 | "QXmlDeclHandler",
651 | "QXmlDefaultHandler",
652 | "QXmlEntityResolver",
653 | "QXmlErrorHandler",
654 | "QXmlInputSource",
655 | "QXmlLexicalHandler",
656 | "QXmlLocator",
657 | "QXmlNamespaceSupport",
658 | "QXmlParseException",
659 | "QXmlReader",
660 | "QXmlSimpleReader"
661 | ],
662 | "QtXmlPatterns": [
663 | "QAbstractMessageHandler",
664 | "QAbstractUriResolver",
665 | "QAbstractXmlNodeModel",
666 | "QAbstractXmlReceiver",
667 | "QSourceLocation",
668 | "QXmlFormatter",
669 | "QXmlItem",
670 | "QXmlName",
671 | "QXmlNamePool",
672 | "QXmlNodeModelIndex",
673 | "QXmlQuery",
674 | "QXmlResultItems",
675 | "QXmlSchema",
676 | "QXmlSchemaValidator",
677 | "QXmlSerializer"
678 | ]
679 | }
680 |
681 |
682 | def _qInstallMessageHandler(handler):
683 | """Install a message handler that works in all bindings
684 |
685 | Args:
686 | handler: A function that takes 3 arguments, or None
687 | """
688 | def messageOutputHandler(*args):
689 | # In Qt4 bindings, message handlers are passed 2 arguments
690 | # In Qt5 bindings, message handlers are passed 3 arguments
691 | # The first argument is a QtMsgType
692 | # The last argument is the message to be printed
693 | # The Middle argument (if passed) is a QMessageLogContext
694 | if len(args) == 3:
695 | msgType, logContext, msg = args
696 | elif len(args) == 2:
697 | msgType, msg = args
698 | logContext = None
699 | else:
700 | raise TypeError(
701 | "handler expected 2 or 3 arguments, got {0}".format(len(args)))
702 |
703 | if isinstance(msg, bytes):
704 | # In python 3, some bindings pass a bytestring, which cannot be
705 | # used elsewhere. Decoding a python 2 or 3 bytestring object will
706 | # consistently return a unicode object.
707 | msg = msg.decode()
708 |
709 | handler(msgType, logContext, msg)
710 |
711 | passObject = messageOutputHandler if handler else handler
712 | if Qt.IsPySide or Qt.IsPyQt4:
713 | return Qt._QtCore.qInstallMsgHandler(passObject)
714 | elif Qt.IsPySide2 or Qt.IsPyQt5:
715 | return Qt._QtCore.qInstallMessageHandler(passObject)
716 |
717 |
718 | def _getcpppointer(object):
719 | if hasattr(Qt, "_shiboken2"):
720 | return getattr(Qt, "_shiboken2").getCppPointer(object)[0]
721 | elif hasattr(Qt, "_shiboken"):
722 | return getattr(Qt, "_shiboken").getCppPointer(object)[0]
723 | elif hasattr(Qt, "_sip"):
724 | return getattr(Qt, "_sip").unwrapinstance(object)
725 | raise AttributeError("'module' has no attribute 'getCppPointer'")
726 |
727 |
728 | def _wrapinstance(ptr, base=None):
729 | """Enable implicit cast of pointer to most suitable class
730 |
731 | This behaviour is available in sip per default.
732 |
733 | Based on http://nathanhorne.com/pyqtpyside-wrap-instance
734 |
735 | Usage:
736 | This mechanism kicks in under these circumstances.
737 | 1. Qt.py is using PySide 1 or 2.
738 | 2. A `base` argument is not provided.
739 |
740 | See :func:`QtCompat.wrapInstance()`
741 |
742 | Arguments:
743 | ptr (long): Pointer to QObject in memory
744 | base (QObject, optional): Base class to wrap with. Defaults to QObject,
745 | which should handle anything.
746 |
747 | """
748 |
749 | assert isinstance(ptr, long), "Argument 'ptr' must be of type "
750 | assert (base is None) or issubclass(base, Qt.QtCore.QObject), (
751 | "Argument 'base' must be of type ")
752 |
753 | if Qt.IsPyQt4 or Qt.IsPyQt5:
754 | func = getattr(Qt, "_sip").wrapinstance
755 | elif Qt.IsPySide2:
756 | func = getattr(Qt, "_shiboken2").wrapInstance
757 | elif Qt.IsPySide:
758 | func = getattr(Qt, "_shiboken").wrapInstance
759 | else:
760 | raise AttributeError("'module' has no attribute 'wrapInstance'")
761 |
762 | if base is None:
763 | q_object = func(long(ptr), Qt.QtCore.QObject)
764 | meta_object = q_object.metaObject()
765 | class_name = meta_object.className()
766 | super_class_name = meta_object.superClass().className()
767 |
768 | if hasattr(Qt.QtWidgets, class_name):
769 | base = getattr(Qt.QtWidgets, class_name)
770 |
771 | elif hasattr(Qt.QtWidgets, super_class_name):
772 | base = getattr(Qt.QtWidgets, super_class_name)
773 |
774 | else:
775 | base = Qt.QtCore.QObject
776 |
777 | return func(long(ptr), base)
778 |
779 |
780 | def _translate(context, sourceText, *args):
781 | # In Qt4 bindings, translate can be passed 2 or 3 arguments
782 | # In Qt5 bindings, translate can be passed 2 arguments
783 | # The first argument is disambiguation[str]
784 | # The last argument is n[int]
785 | # The middle argument can be encoding[QtCore.QCoreApplication.Encoding]
786 | if len(args) == 3:
787 | disambiguation, encoding, n = args
788 | elif len(args) == 2:
789 | disambiguation, n = args
790 | encoding = None
791 | else:
792 | raise TypeError(
793 | "Expected 4 or 5 arguments, got {0}.".format(len(args)+2))
794 |
795 | if hasattr(Qt.QtCore, "QCoreApplication"):
796 | app = getattr(Qt.QtCore, "QCoreApplication")
797 | else:
798 | raise NotImplementedError(
799 | "Missing QCoreApplication implementation for {binding}".format(
800 | binding=Qt.__binding__,
801 | )
802 | )
803 | if Qt.__binding__ in ("PySide2", "PyQt5"):
804 | sanitized_args = [context, sourceText, disambiguation, n]
805 | else:
806 | sanitized_args = [
807 | context,
808 | sourceText,
809 | disambiguation,
810 | encoding or app.CodecForTr,
811 | n
812 | ]
813 | return app.translate(*sanitized_args)
814 |
815 |
816 | def _loadUi(uifile, baseinstance=None):
817 | """Dynamically load a user interface from the given `uifile`
818 |
819 | This function calls `uic.loadUi` if using PyQt bindings,
820 | else it implements a comparable binding for PySide.
821 |
822 | Documentation:
823 | http://pyqt.sourceforge.net/Docs/PyQt5/designer.html#PyQt5.uic.loadUi
824 |
825 | Arguments:
826 | uifile (str): Absolute path to Qt Designer file.
827 | baseinstance (QWidget): Instantiated QWidget or subclass thereof
828 |
829 | Return:
830 | baseinstance if `baseinstance` is not `None`. Otherwise
831 | return the newly created instance of the user interface.
832 |
833 | """
834 | if hasattr(Qt, "_uic"):
835 | return Qt._uic.loadUi(uifile, baseinstance)
836 |
837 | elif hasattr(Qt, "_QtUiTools"):
838 | # Implement `PyQt5.uic.loadUi` for PySide(2)
839 |
840 | class _UiLoader(Qt._QtUiTools.QUiLoader):
841 | """Create the user interface in a base instance.
842 |
843 | Unlike `Qt._QtUiTools.QUiLoader` itself this class does not
844 | create a new instance of the top-level widget, but creates the user
845 | interface in an existing instance of the top-level class if needed.
846 |
847 | This mimics the behaviour of `PyQt5.uic.loadUi`.
848 |
849 | """
850 |
851 | def __init__(self, baseinstance):
852 | super(_UiLoader, self).__init__(baseinstance)
853 | self.baseinstance = baseinstance
854 |
855 | def load(self, uifile, *args, **kwargs):
856 | from xml.etree.ElementTree import ElementTree
857 |
858 | # For whatever reason, if this doesn't happen then
859 | # reading an invalid or non-existing .ui file throws
860 | # a RuntimeError.
861 | etree = ElementTree()
862 | etree.parse(uifile)
863 |
864 | widget = Qt._QtUiTools.QUiLoader.load(
865 | self, uifile, *args, **kwargs)
866 |
867 | # Workaround for PySide 1.0.9, see issue #208
868 | widget.parentWidget()
869 |
870 | return widget
871 |
872 | def createWidget(self, class_name, parent=None, name=""):
873 | """Called for each widget defined in ui file
874 |
875 | Overridden here to populate `baseinstance` instead.
876 |
877 | """
878 |
879 | if parent is None and self.baseinstance:
880 | # Supposed to create the top-level widget,
881 | # return the base instance instead
882 | return self.baseinstance
883 |
884 | # For some reason, Line is not in the list of available
885 | # widgets, but works fine, so we have to special case it here.
886 | if class_name in self.availableWidgets() + ["Line"]:
887 | # Create a new widget for child widgets
888 | widget = Qt._QtUiTools.QUiLoader.createWidget(self,
889 | class_name,
890 | parent,
891 | name)
892 |
893 | else:
894 | raise Exception("Custom widget '%s' not supported"
895 | % class_name)
896 |
897 | if self.baseinstance:
898 | # Set an attribute for the new child widget on the base
899 | # instance, just like PyQt5.uic.loadUi does.
900 | setattr(self.baseinstance, name, widget)
901 |
902 | return widget
903 |
904 | widget = _UiLoader(baseinstance).load(uifile)
905 | Qt.QtCore.QMetaObject.connectSlotsByName(widget)
906 |
907 | return widget
908 |
909 | else:
910 | raise NotImplementedError("No implementation available for loadUi")
911 |
912 |
913 | """Misplaced members
914 |
915 | These members from the original submodule are misplaced relative PySide2
916 |
917 | """
918 | _misplaced_members = {
919 | "PySide2": {
920 | "QtGui.QStringListModel": "QtCore.QStringListModel",
921 | "QtCore.Property": "QtCore.Property",
922 | "QtCore.Signal": "QtCore.Signal",
923 | "QtCore.Slot": "QtCore.Slot",
924 | "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
925 | "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
926 | "QtCore.QItemSelection": "QtCore.QItemSelection",
927 | "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel",
928 | "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange",
929 | "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi],
930 | "shiboken2.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance],
931 | "shiboken2.getCppPointer": ["QtCompat.getCppPointer", _getcpppointer],
932 | "QtWidgets.qApp": "QtWidgets.QApplication.instance()",
933 | "QtCore.QCoreApplication.translate": [
934 | "QtCompat.translate", _translate
935 | ],
936 | "QtWidgets.QApplication.translate": [
937 | "QtCompat.translate", _translate
938 | ],
939 | "QtCore.qInstallMessageHandler": [
940 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler
941 | ],
942 | },
943 | "PyQt5": {
944 | "QtCore.pyqtProperty": "QtCore.Property",
945 | "QtCore.pyqtSignal": "QtCore.Signal",
946 | "QtCore.pyqtSlot": "QtCore.Slot",
947 | "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
948 | "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
949 | "QtCore.QStringListModel": "QtCore.QStringListModel",
950 | "QtCore.QItemSelection": "QtCore.QItemSelection",
951 | "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel",
952 | "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange",
953 | "uic.loadUi": ["QtCompat.loadUi", _loadUi],
954 | "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance],
955 | "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer],
956 | "QtWidgets.qApp": "QtWidgets.QApplication.instance()",
957 | "QtCore.QCoreApplication.translate": [
958 | "QtCompat.translate", _translate
959 | ],
960 | "QtWidgets.QApplication.translate": [
961 | "QtCompat.translate", _translate
962 | ],
963 | "QtCore.qInstallMessageHandler": [
964 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler
965 | ],
966 | },
967 | "PySide": {
968 | "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
969 | "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
970 | "QtGui.QStringListModel": "QtCore.QStringListModel",
971 | "QtGui.QItemSelection": "QtCore.QItemSelection",
972 | "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel",
973 | "QtCore.Property": "QtCore.Property",
974 | "QtCore.Signal": "QtCore.Signal",
975 | "QtCore.Slot": "QtCore.Slot",
976 | "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange",
977 | "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog",
978 | "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog",
979 | "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog",
980 | "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine",
981 | "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog",
982 | "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget",
983 | "QtGui.QPrinter": "QtPrintSupport.QPrinter",
984 | "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo",
985 | "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi],
986 | "shiboken.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance],
987 | "shiboken.unwrapInstance": ["QtCompat.getCppPointer", _getcpppointer],
988 | "QtGui.qApp": "QtWidgets.QApplication.instance()",
989 | "QtCore.QCoreApplication.translate": [
990 | "QtCompat.translate", _translate
991 | ],
992 | "QtGui.QApplication.translate": [
993 | "QtCompat.translate", _translate
994 | ],
995 | "QtCore.qInstallMsgHandler": [
996 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler
997 | ],
998 | },
999 | "PyQt4": {
1000 | "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
1001 | "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
1002 | "QtGui.QItemSelection": "QtCore.QItemSelection",
1003 | "QtGui.QStringListModel": "QtCore.QStringListModel",
1004 | "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel",
1005 | "QtCore.pyqtProperty": "QtCore.Property",
1006 | "QtCore.pyqtSignal": "QtCore.Signal",
1007 | "QtCore.pyqtSlot": "QtCore.Slot",
1008 | "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange",
1009 | "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog",
1010 | "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog",
1011 | "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog",
1012 | "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine",
1013 | "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog",
1014 | "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget",
1015 | "QtGui.QPrinter": "QtPrintSupport.QPrinter",
1016 | "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo",
1017 | # "QtCore.pyqtSignature": "QtCore.Slot",
1018 | "uic.loadUi": ["QtCompat.loadUi", _loadUi],
1019 | "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance],
1020 | "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer],
1021 | "QtCore.QString": "str",
1022 | "QtGui.qApp": "QtWidgets.QApplication.instance()",
1023 | "QtCore.QCoreApplication.translate": [
1024 | "QtCompat.translate", _translate
1025 | ],
1026 | "QtGui.QApplication.translate": [
1027 | "QtCompat.translate", _translate
1028 | ],
1029 | "QtCore.qInstallMsgHandler": [
1030 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler
1031 | ],
1032 | }
1033 | }
1034 |
1035 | """ Compatibility Members
1036 |
1037 | This dictionary is used to build Qt.QtCompat objects that provide a consistent
1038 | interface for obsolete members, and differences in binding return values.
1039 |
1040 | {
1041 | "binding": {
1042 | "classname": {
1043 | "targetname": "binding_namespace",
1044 | }
1045 | }
1046 | }
1047 | """
1048 | _compatibility_members = {
1049 | "PySide2": {
1050 | "QWidget": {
1051 | "grab": "QtWidgets.QWidget.grab",
1052 | },
1053 | "QHeaderView": {
1054 | "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable",
1055 | "setSectionsClickable":
1056 | "QtWidgets.QHeaderView.setSectionsClickable",
1057 | "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode",
1058 | "setSectionResizeMode":
1059 | "QtWidgets.QHeaderView.setSectionResizeMode",
1060 | "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable",
1061 | "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable",
1062 | },
1063 | "QFileDialog": {
1064 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
1065 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
1066 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
1067 | },
1068 | },
1069 | "PyQt5": {
1070 | "QWidget": {
1071 | "grab": "QtWidgets.QWidget.grab",
1072 | },
1073 | "QHeaderView": {
1074 | "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable",
1075 | "setSectionsClickable":
1076 | "QtWidgets.QHeaderView.setSectionsClickable",
1077 | "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode",
1078 | "setSectionResizeMode":
1079 | "QtWidgets.QHeaderView.setSectionResizeMode",
1080 | "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable",
1081 | "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable",
1082 | },
1083 | "QFileDialog": {
1084 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
1085 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
1086 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
1087 | },
1088 | },
1089 | "PySide": {
1090 | "QWidget": {
1091 | "grab": "QtWidgets.QPixmap.grabWidget",
1092 | },
1093 | "QHeaderView": {
1094 | "sectionsClickable": "QtWidgets.QHeaderView.isClickable",
1095 | "setSectionsClickable": "QtWidgets.QHeaderView.setClickable",
1096 | "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode",
1097 | "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode",
1098 | "sectionsMovable": "QtWidgets.QHeaderView.isMovable",
1099 | "setSectionsMovable": "QtWidgets.QHeaderView.setMovable",
1100 | },
1101 | "QFileDialog": {
1102 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
1103 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
1104 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
1105 | },
1106 | },
1107 | "PyQt4": {
1108 | "QWidget": {
1109 | "grab": "QtWidgets.QPixmap.grabWidget",
1110 | },
1111 | "QHeaderView": {
1112 | "sectionsClickable": "QtWidgets.QHeaderView.isClickable",
1113 | "setSectionsClickable": "QtWidgets.QHeaderView.setClickable",
1114 | "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode",
1115 | "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode",
1116 | "sectionsMovable": "QtWidgets.QHeaderView.isMovable",
1117 | "setSectionsMovable": "QtWidgets.QHeaderView.setMovable",
1118 | },
1119 | "QFileDialog": {
1120 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
1121 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
1122 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
1123 | },
1124 | },
1125 | }
1126 |
1127 |
1128 | def _apply_site_config():
1129 | try:
1130 | import QtSiteConfig
1131 | except ImportError:
1132 | # If no QtSiteConfig module found, no modifications
1133 | # to _common_members are needed.
1134 | pass
1135 | else:
1136 | # Provide the ability to modify the dicts used to build Qt.py
1137 | if hasattr(QtSiteConfig, 'update_members'):
1138 | QtSiteConfig.update_members(_common_members)
1139 |
1140 | if hasattr(QtSiteConfig, 'update_misplaced_members'):
1141 | QtSiteConfig.update_misplaced_members(members=_misplaced_members)
1142 |
1143 | if hasattr(QtSiteConfig, 'update_compatibility_members'):
1144 | QtSiteConfig.update_compatibility_members(
1145 | members=_compatibility_members)
1146 |
1147 |
1148 | def _new_module(name):
1149 | return types.ModuleType(__name__ + "." + name)
1150 |
1151 |
1152 | def _import_sub_module(module, name):
1153 | """import_sub_module will mimic the function of importlib.import_module"""
1154 | module = __import__(module.__name__ + "." + name)
1155 | for level in name.split("."):
1156 | module = getattr(module, level)
1157 | return module
1158 |
1159 |
1160 | def _setup(module, extras):
1161 | """Install common submodules"""
1162 |
1163 | Qt.__binding__ = module.__name__
1164 |
1165 | for name in list(_common_members) + extras:
1166 | try:
1167 | submodule = _import_sub_module(
1168 | module, name)
1169 | except ImportError:
1170 | try:
1171 | # For extra modules like sip and shiboken that may not be
1172 | # children of the binding.
1173 | submodule = __import__(name)
1174 | except ImportError:
1175 | continue
1176 |
1177 | setattr(Qt, "_" + name, submodule)
1178 |
1179 | if name not in extras:
1180 | # Store reference to original binding,
1181 | # but don't store speciality modules
1182 | # such as uic or QtUiTools
1183 | setattr(Qt, name, _new_module(name))
1184 |
1185 |
1186 | def _reassign_misplaced_members(binding):
1187 | """Apply misplaced members from `binding` to Qt.py
1188 |
1189 | Arguments:
1190 | binding (dict): Misplaced members
1191 |
1192 | """
1193 |
1194 | for src, dst in _misplaced_members[binding].items():
1195 | dst_value = None
1196 |
1197 | src_parts = src.split(".")
1198 | src_module = src_parts[0]
1199 | src_member = None
1200 | if len(src_parts) > 1:
1201 | src_member = src_parts[1:]
1202 |
1203 | if isinstance(dst, (list, tuple)):
1204 | dst, dst_value = dst
1205 |
1206 | dst_parts = dst.split(".")
1207 | dst_module = dst_parts[0]
1208 | dst_member = None
1209 | if len(dst_parts) > 1:
1210 | dst_member = dst_parts[1]
1211 |
1212 | # Get the member we want to store in the namesapce.
1213 | if not dst_value:
1214 | try:
1215 | _part = getattr(Qt, "_" + src_module)
1216 | while src_member:
1217 | member = src_member.pop(0)
1218 | _part = getattr(_part, member)
1219 | dst_value = _part
1220 | except AttributeError:
1221 | # If the member we want to store in the namespace does not
1222 | # exist, there is no need to continue. This can happen if a
1223 | # request was made to rename a member that didn't exist, for
1224 | # example if QtWidgets isn't available on the target platform.
1225 | _log("Misplaced member has no source: {}".format(src))
1226 | continue
1227 |
1228 | try:
1229 | src_object = getattr(Qt, dst_module)
1230 | except AttributeError:
1231 | if dst_module not in _common_members:
1232 | # Only create the Qt parent module if its listed in
1233 | # _common_members. Without this check, if you remove QtCore
1234 | # from _common_members, the default _misplaced_members will add
1235 | # Qt.QtCore so it can add Signal, Slot, etc.
1236 | msg = 'Not creating missing member module "{m}" for "{c}"'
1237 | _log(msg.format(m=dst_module, c=dst_member))
1238 | continue
1239 | # If the dst is valid but the Qt parent module does not exist
1240 | # then go ahead and create a new module to contain the member.
1241 | setattr(Qt, dst_module, _new_module(dst_module))
1242 | src_object = getattr(Qt, dst_module)
1243 | # Enable direct import of the new module
1244 | sys.modules[__name__ + "." + dst_module] = src_object
1245 |
1246 | if not dst_value:
1247 | dst_value = getattr(Qt, "_" + src_module)
1248 | if src_member:
1249 | dst_value = getattr(dst_value, src_member)
1250 |
1251 | setattr(
1252 | src_object,
1253 | dst_member or dst_module,
1254 | dst_value
1255 | )
1256 |
1257 |
1258 | def _build_compatibility_members(binding, decorators=None):
1259 | """Apply `binding` to QtCompat
1260 |
1261 | Arguments:
1262 | binding (str): Top level binding in _compatibility_members.
1263 | decorators (dict, optional): Provides the ability to decorate the
1264 | original Qt methods when needed by a binding. This can be used
1265 | to change the returned value to a standard value. The key should
1266 | be the classname, the value is a dict where the keys are the
1267 | target method names, and the values are the decorator functions.
1268 |
1269 | """
1270 |
1271 | decorators = decorators or dict()
1272 |
1273 | # Allow optional site-level customization of the compatibility members.
1274 | # This method does not need to be implemented in QtSiteConfig.
1275 | try:
1276 | import QtSiteConfig
1277 | except ImportError:
1278 | pass
1279 | else:
1280 | if hasattr(QtSiteConfig, 'update_compatibility_decorators'):
1281 | QtSiteConfig.update_compatibility_decorators(binding, decorators)
1282 |
1283 | _QtCompat = type("QtCompat", (object,), {})
1284 |
1285 | for classname, bindings in _compatibility_members[binding].items():
1286 | attrs = {}
1287 | for target, binding in bindings.items():
1288 | namespaces = binding.split('.')
1289 | try:
1290 | src_object = getattr(Qt, "_" + namespaces[0])
1291 | except AttributeError as e:
1292 | _log("QtCompat: AttributeError: %s" % e)
1293 | # Skip reassignment of non-existing members.
1294 | # This can happen if a request was made to
1295 | # rename a member that didn't exist, for example
1296 | # if QtWidgets isn't available on the target platform.
1297 | continue
1298 |
1299 | # Walk down any remaining namespace getting the object assuming
1300 | # that if the first namespace exists the rest will exist.
1301 | for namespace in namespaces[1:]:
1302 | src_object = getattr(src_object, namespace)
1303 |
1304 | # decorate the Qt method if a decorator was provided.
1305 | if target in decorators.get(classname, []):
1306 | # staticmethod must be called on the decorated method to
1307 | # prevent a TypeError being raised when the decorated method
1308 | # is called.
1309 | src_object = staticmethod(
1310 | decorators[classname][target](src_object))
1311 |
1312 | attrs[target] = src_object
1313 |
1314 | # Create the QtCompat class and install it into the namespace
1315 | compat_class = type(classname, (_QtCompat,), attrs)
1316 | setattr(Qt.QtCompat, classname, compat_class)
1317 |
1318 |
1319 | def _pyside2():
1320 | """Initialise PySide2
1321 |
1322 | These functions serve to test the existence of a binding
1323 | along with set it up in such a way that it aligns with
1324 | the final step; adding members from the original binding
1325 | to Qt.py
1326 |
1327 | """
1328 |
1329 | import PySide2 as module
1330 | extras = ["QtUiTools"]
1331 | try:
1332 | try:
1333 | # Before merge of PySide and shiboken
1334 | import shiboken2
1335 | except ImportError:
1336 | # After merge of PySide and shiboken, May 2017
1337 | from PySide2 import shiboken2
1338 | extras.append("shiboken2")
1339 | except ImportError:
1340 | pass
1341 |
1342 | _setup(module, extras)
1343 | Qt.__binding_version__ = module.__version__
1344 |
1345 | if hasattr(Qt, "_shiboken2"):
1346 | Qt.QtCompat.wrapInstance = _wrapinstance
1347 | Qt.QtCompat.getCppPointer = _getcpppointer
1348 |
1349 | if hasattr(Qt, "_QtUiTools"):
1350 | Qt.QtCompat.loadUi = _loadUi
1351 |
1352 | if hasattr(Qt, "_QtCore"):
1353 | Qt.__qt_version__ = Qt._QtCore.qVersion()
1354 |
1355 | if hasattr(Qt, "_QtWidgets"):
1356 | Qt.QtCompat.setSectionResizeMode = \
1357 | Qt._QtWidgets.QHeaderView.setSectionResizeMode
1358 |
1359 | _reassign_misplaced_members("PySide2")
1360 | _build_compatibility_members("PySide2")
1361 |
1362 |
1363 | def _pyside():
1364 | """Initialise PySide"""
1365 |
1366 | import PySide as module
1367 | extras = ["QtUiTools"]
1368 | try:
1369 | try:
1370 | # Before merge of PySide and shiboken
1371 | import shiboken
1372 | except ImportError:
1373 | # After merge of PySide and shiboken, May 2017
1374 | from PySide import shiboken
1375 | extras.append("shiboken")
1376 | except ImportError:
1377 | pass
1378 |
1379 | _setup(module, extras)
1380 | Qt.__binding_version__ = module.__version__
1381 |
1382 | if hasattr(Qt, "_shiboken"):
1383 | Qt.QtCompat.wrapInstance = _wrapinstance
1384 | Qt.QtCompat.getCppPointer = _getcpppointer
1385 |
1386 | if hasattr(Qt, "_QtUiTools"):
1387 | Qt.QtCompat.loadUi = _loadUi
1388 |
1389 | if hasattr(Qt, "_QtGui"):
1390 | setattr(Qt, "QtWidgets", _new_module("QtWidgets"))
1391 | setattr(Qt, "_QtWidgets", Qt._QtGui)
1392 | if hasattr(Qt._QtGui, "QX11Info"):
1393 | setattr(Qt, "QtX11Extras", _new_module("QtX11Extras"))
1394 | Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info
1395 |
1396 | Qt.QtCompat.setSectionResizeMode = Qt._QtGui.QHeaderView.setResizeMode
1397 |
1398 | if hasattr(Qt, "_QtCore"):
1399 | Qt.__qt_version__ = Qt._QtCore.qVersion()
1400 |
1401 | _reassign_misplaced_members("PySide")
1402 | _build_compatibility_members("PySide")
1403 |
1404 |
1405 | def _pyqt5():
1406 | """Initialise PyQt5"""
1407 |
1408 | import PyQt5 as module
1409 | extras = ["uic"]
1410 | try:
1411 | import sip
1412 | extras.append(sip.__name__)
1413 | except ImportError:
1414 | sip = None
1415 |
1416 | _setup(module, extras)
1417 | if hasattr(Qt, "_sip"):
1418 | Qt.QtCompat.wrapInstance = _wrapinstance
1419 | Qt.QtCompat.getCppPointer = _getcpppointer
1420 |
1421 | if hasattr(Qt, "_uic"):
1422 | Qt.QtCompat.loadUi = _loadUi
1423 |
1424 | if hasattr(Qt, "_QtCore"):
1425 | Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR
1426 | Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR
1427 |
1428 | if hasattr(Qt, "_QtWidgets"):
1429 | Qt.QtCompat.setSectionResizeMode = \
1430 | Qt._QtWidgets.QHeaderView.setSectionResizeMode
1431 |
1432 | _reassign_misplaced_members("PyQt5")
1433 | _build_compatibility_members('PyQt5')
1434 |
1435 |
1436 | def _pyqt4():
1437 | """Initialise PyQt4"""
1438 |
1439 | import sip
1440 |
1441 | # Validation of envivornment variable. Prevents an error if
1442 | # the variable is invalid since it's just a hint.
1443 | try:
1444 | hint = int(QT_SIP_API_HINT)
1445 | except TypeError:
1446 | hint = None # Variable was None, i.e. not set.
1447 | except ValueError:
1448 | raise ImportError("QT_SIP_API_HINT=%s must be a 1 or 2")
1449 |
1450 | for api in ("QString",
1451 | "QVariant",
1452 | "QDate",
1453 | "QDateTime",
1454 | "QTextStream",
1455 | "QTime",
1456 | "QUrl"):
1457 | try:
1458 | sip.setapi(api, hint or 2)
1459 | except AttributeError:
1460 | raise ImportError("PyQt4 < 4.6 isn't supported by Qt.py")
1461 | except ValueError:
1462 | actual = sip.getapi(api)
1463 | if not hint:
1464 | raise ImportError("API version already set to %d" % actual)
1465 | else:
1466 | # Having provided a hint indicates a soft constraint, one
1467 | # that doesn't throw an exception.
1468 | sys.stderr.write(
1469 | "Warning: API '%s' has already been set to %d.\n"
1470 | % (api, actual)
1471 | )
1472 |
1473 | import PyQt4 as module
1474 | extras = ["uic"]
1475 | try:
1476 | import sip
1477 | extras.append(sip.__name__)
1478 | except ImportError:
1479 | sip = None
1480 |
1481 | _setup(module, extras)
1482 | if hasattr(Qt, "_sip"):
1483 | Qt.QtCompat.wrapInstance = _wrapinstance
1484 | Qt.QtCompat.getCppPointer = _getcpppointer
1485 |
1486 | if hasattr(Qt, "_uic"):
1487 | Qt.QtCompat.loadUi = _loadUi
1488 |
1489 | if hasattr(Qt, "_QtGui"):
1490 | setattr(Qt, "QtWidgets", _new_module("QtWidgets"))
1491 | setattr(Qt, "_QtWidgets", Qt._QtGui)
1492 | if hasattr(Qt._QtGui, "QX11Info"):
1493 | setattr(Qt, "QtX11Extras", _new_module("QtX11Extras"))
1494 | Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info
1495 |
1496 | Qt.QtCompat.setSectionResizeMode = \
1497 | Qt._QtGui.QHeaderView.setResizeMode
1498 |
1499 | if hasattr(Qt, "_QtCore"):
1500 | Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR
1501 | Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR
1502 |
1503 | _reassign_misplaced_members("PyQt4")
1504 |
1505 | # QFileDialog QtCompat decorator
1506 | def _standardizeQFileDialog(some_function):
1507 | """Decorator that makes PyQt4 return conform to other bindings"""
1508 | def wrapper(*args, **kwargs):
1509 | ret = (some_function(*args, **kwargs))
1510 |
1511 | # PyQt4 only returns the selected filename, force it to a
1512 | # standard return of the selected filename, and a empty string
1513 | # for the selected filter
1514 | return ret, ''
1515 |
1516 | wrapper.__doc__ = some_function.__doc__
1517 | wrapper.__name__ = some_function.__name__
1518 |
1519 | return wrapper
1520 |
1521 | decorators = {
1522 | "QFileDialog": {
1523 | "getOpenFileName": _standardizeQFileDialog,
1524 | "getOpenFileNames": _standardizeQFileDialog,
1525 | "getSaveFileName": _standardizeQFileDialog,
1526 | }
1527 | }
1528 | _build_compatibility_members('PyQt4', decorators)
1529 |
1530 |
1531 | def _none():
1532 | """Internal option (used in installer)"""
1533 |
1534 | Mock = type("Mock", (), {"__getattr__": lambda Qt, attr: None})
1535 |
1536 | Qt.__binding__ = "None"
1537 | Qt.__qt_version__ = "0.0.0"
1538 | Qt.__binding_version__ = "0.0.0"
1539 | Qt.QtCompat.loadUi = lambda uifile, baseinstance=None: None
1540 | Qt.QtCompat.setSectionResizeMode = lambda *args, **kwargs: None
1541 |
1542 | for submodule in _common_members.keys():
1543 | setattr(Qt, submodule, Mock())
1544 | setattr(Qt, "_" + submodule, Mock())
1545 |
1546 |
1547 | def _log(text):
1548 | if QT_VERBOSE:
1549 | sys.stdout.write(text + "\n")
1550 |
1551 |
1552 | def _convert(lines):
1553 | """Convert compiled .ui file from PySide2 to Qt.py
1554 |
1555 | Arguments:
1556 | lines (list): Each line of of .ui file
1557 |
1558 | Usage:
1559 | >> with open("myui.py") as f:
1560 | .. lines = _convert(f.readlines())
1561 |
1562 | """
1563 |
1564 | def parse(line):
1565 | line = line.replace("from PySide2 import", "from Qt import QtCompat,")
1566 | line = line.replace("QtWidgets.QApplication.translate",
1567 | "QtCompat.translate")
1568 | if "QtCore.SIGNAL" in line:
1569 | raise NotImplementedError("QtCore.SIGNAL is missing from PyQt5 "
1570 | "and so Qt.py does not support it: you "
1571 | "should avoid defining signals inside "
1572 | "your ui files.")
1573 | return line
1574 |
1575 | parsed = list()
1576 | for line in lines:
1577 | line = parse(line)
1578 | parsed.append(line)
1579 |
1580 | return parsed
1581 |
1582 |
1583 | def _cli(args):
1584 | """Qt.py command-line interface"""
1585 | import argparse
1586 |
1587 | parser = argparse.ArgumentParser()
1588 | parser.add_argument("--convert",
1589 | help="Path to compiled Python module, e.g. my_ui.py")
1590 | parser.add_argument("--compile",
1591 | help="Accept raw .ui file and compile with native "
1592 | "PySide2 compiler.")
1593 | parser.add_argument("--stdout",
1594 | help="Write to stdout instead of file",
1595 | action="store_true")
1596 | parser.add_argument("--stdin",
1597 | help="Read from stdin instead of file",
1598 | action="store_true")
1599 |
1600 | args = parser.parse_args(args)
1601 |
1602 | if args.stdout:
1603 | raise NotImplementedError("--stdout")
1604 |
1605 | if args.stdin:
1606 | raise NotImplementedError("--stdin")
1607 |
1608 | if args.compile:
1609 | raise NotImplementedError("--compile")
1610 |
1611 | if args.convert:
1612 | sys.stdout.write("#\n"
1613 | "# WARNING: --convert is an ALPHA feature.\n#\n"
1614 | "# See https://github.com/mottosso/Qt.py/pull/132\n"
1615 | "# for details.\n"
1616 | "#\n")
1617 |
1618 | #
1619 | # ------> Read
1620 | #
1621 | with open(args.convert) as f:
1622 | lines = _convert(f.readlines())
1623 |
1624 | backup = "%s_backup%s" % os.path.splitext(args.convert)
1625 | sys.stdout.write("Creating \"%s\"..\n" % backup)
1626 | shutil.copy(args.convert, backup)
1627 |
1628 | #
1629 | # <------ Write
1630 | #
1631 | with open(args.convert, "w") as f:
1632 | f.write("".join(lines))
1633 |
1634 | sys.stdout.write("Successfully converted \"%s\"\n" % args.convert)
1635 |
1636 |
1637 | def _install():
1638 | # Default order (customise order and content via QT_PREFERRED_BINDING)
1639 | default_order = ("PySide2", "PyQt5", "PySide", "PyQt4")
1640 | preferred_order = list(
1641 | b for b in QT_PREFERRED_BINDING.split(os.pathsep) if b
1642 | )
1643 |
1644 | order = preferred_order or default_order
1645 |
1646 | available = {
1647 | "PySide2": _pyside2,
1648 | "PyQt5": _pyqt5,
1649 | "PySide": _pyside,
1650 | "PyQt4": _pyqt4,
1651 | "None": _none
1652 | }
1653 |
1654 | _log("Order: '%s'" % "', '".join(order))
1655 |
1656 | # Allow site-level customization of the available modules.
1657 | _apply_site_config()
1658 |
1659 | found_binding = False
1660 | for name in order:
1661 | _log("Trying %s" % name)
1662 |
1663 | try:
1664 | available[name]()
1665 | found_binding = True
1666 | break
1667 |
1668 | except ImportError as e:
1669 | _log("ImportError: %s" % e)
1670 |
1671 | except KeyError:
1672 | _log("ImportError: Preferred binding '%s' not found." % name)
1673 |
1674 | if not found_binding:
1675 | # If not binding were found, throw this error
1676 | raise ImportError("No Qt binding were found.")
1677 |
1678 | # Install individual members
1679 | for name, members in _common_members.items():
1680 | try:
1681 | their_submodule = getattr(Qt, "_%s" % name)
1682 | except AttributeError:
1683 | continue
1684 |
1685 | our_submodule = getattr(Qt, name)
1686 |
1687 | # Enable import *
1688 | __all__.append(name)
1689 |
1690 | # Enable direct import of submodule,
1691 | # e.g. import Qt.QtCore
1692 | sys.modules[__name__ + "." + name] = our_submodule
1693 |
1694 | for member in members:
1695 | # Accept that a submodule may miss certain members.
1696 | try:
1697 | their_member = getattr(their_submodule, member)
1698 | except AttributeError:
1699 | _log("'%s.%s' was missing." % (name, member))
1700 | continue
1701 |
1702 | setattr(our_submodule, member, their_member)
1703 |
1704 | # Enable direct import of QtCompat
1705 | sys.modules['Qt.QtCompat'] = Qt.QtCompat
1706 |
1707 | # Backwards compatibility
1708 | if hasattr(Qt.QtCompat, 'loadUi'):
1709 | Qt.QtCompat.load_ui = Qt.QtCompat.loadUi
1710 |
1711 |
1712 | _install()
1713 |
1714 | # Setup Binding Enum states
1715 | Qt.IsPySide2 = Qt.__binding__ == 'PySide2'
1716 | Qt.IsPyQt5 = Qt.__binding__ == 'PyQt5'
1717 | Qt.IsPySide = Qt.__binding__ == 'PySide'
1718 | Qt.IsPyQt4 = Qt.__binding__ == 'PyQt4'
1719 |
1720 | """Augment QtCompat
1721 |
1722 | QtCompat contains wrappers and added functionality
1723 | to the original bindings, such as the CLI interface
1724 | and otherwise incompatible members between bindings,
1725 | such as `QHeaderView.setSectionResizeMode`.
1726 |
1727 | """
1728 |
1729 | Qt.QtCompat._cli = _cli
1730 | Qt.QtCompat._convert = _convert
1731 |
1732 | # Enable command-line interface
1733 | if __name__ == "__main__":
1734 | _cli(sys.argv[1:])
1735 |
1736 |
1737 | # The MIT License (MIT)
1738 | #
1739 | # Copyright (c) 2016-2017 Marcus Ottosson
1740 | #
1741 | # Permission is hereby granted, free of charge, to any person obtaining a copy
1742 | # of this software and associated documentation files (the "Software"), to deal
1743 | # in the Software without restriction, including without limitation the rights
1744 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1745 | # copies of the Software, and to permit persons to whom the Software is
1746 | # furnished to do so, subject to the following conditions:
1747 | #
1748 | # The above copyright notice and this permission notice shall be included in
1749 | # all copies or substantial portions of the Software.
1750 | #
1751 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1752 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1753 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1754 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1755 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1756 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1757 | # SOFTWARE.
1758 | #
1759 | # In PySide(2), loadUi does not exist, so we implement it
1760 | #
1761 | # `_UiLoader` is adapted from the qtpy project, which was further influenced
1762 | # by qt-helpers which was released under a 3-clause BSD license which in turn
1763 | # is based on a solution at:
1764 | #
1765 | # - https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8
1766 | #
1767 | # The License for this code is as follows:
1768 | #
1769 | # qt-helpers - a common front-end to various Qt modules
1770 | #
1771 | # Copyright (c) 2015, Chris Beaumont and Thomas Robitaille
1772 | #
1773 | # All rights reserved.
1774 | #
1775 | # Redistribution and use in source and binary forms, with or without
1776 | # modification, are permitted provided that the following conditions are
1777 | # met:
1778 | #
1779 | # * Redistributions of source code must retain the above copyright
1780 | # notice, this list of conditions and the following disclaimer.
1781 | # * Redistributions in binary form must reproduce the above copyright
1782 | # notice, this list of conditions and the following disclaimer in the
1783 | # documentation and/or other materials provided with the
1784 | # distribution.
1785 | # * Neither the name of the Glue project nor the names of its contributors
1786 | # may be used to endorse or promote products derived from this software
1787 | # without specific prior written permission.
1788 | #
1789 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
1790 | # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
1791 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
1792 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
1793 | # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
1794 | # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
1795 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
1796 | # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
1797 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
1798 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
1799 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1800 | #
1801 | # Which itself was based on the solution at
1802 | #
1803 | # https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8
1804 | #
1805 | # which was released under the MIT license:
1806 | #
1807 | # Copyright (c) 2011 Sebastian Wiesner
1808 | # Modifications by Charl Botha
1809 | #
1810 | # Permission is hereby granted, free of charge, to any person obtaining a
1811 | # copy of this software and associated documentation files
1812 | # (the "Software"),to deal in the Software without restriction,
1813 | # including without limitation
1814 | # the rights to use, copy, modify, merge, publish, distribute, sublicense,
1815 | # and/or sell copies of the Software, and to permit persons to whom the
1816 | # Software is furnished to do so, subject to the following conditions:
1817 | #
1818 | # The above copyright notice and this permission notice shall be included
1819 | # in all copies or substantial portions of the Software.
1820 | #
1821 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1822 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1823 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
1824 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
1825 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
1826 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
1827 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1828 |
--------------------------------------------------------------------------------
/thirdparty/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dsideb/nodegraph-pyqt/e084668c2b81e5f28c5cd867acf647b84a7334ea/thirdparty/__init__.py
--------------------------------------------------------------------------------