├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── question.md
└── dependabot.yml
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── docs
└── icons
│ ├── Icon.png
│ └── Icon.svg
├── index.json
└── src
├── __init__.py
├── blender_manifest.toml
├── dynamic_panels.py
├── functions.py
├── menus.py
├── operators.py
├── panels.py
├── preferences.py
├── properties.py
└── wrapper.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Report any Error or unlogical behavior.
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Version Information:**
27 | - Add-on Version [e.g. 2.1.4]
28 | - Blender Version [e.g. 3.0.1]
29 |
30 | **Additional context**
31 | Add any other context about the problem here.
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this Add-on
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen. Use Images as needed.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Ask a question about the add-on
4 | title: ''
5 | labels: question
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "github-actions" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | __pycache__/__init__.cpython-37.pyc
3 | src/Storage/
4 | *.pyc
5 | *.zip
6 | .build
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "flake8.args": [
3 | "--max-line-length=120",
4 | "--ignore=E121,E123,E126,E226,E24,E704,F722,F821,F401,W503,W504"
5 | ],
6 | "autopep8.args": [
7 | "--max-line-length",
8 | "120",
9 | "--experimental"
10 | ],
11 | "python.analysis.diagnosticSeverityOverrides": {
12 | "reportInvalidTypeForm": "none",
13 | }
14 | }
--------------------------------------------------------------------------------
/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 |
635 | Copyright (C)
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 | Copyright (C)
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 | # ScriptToButton
2 | Script To Button gives you the possibility to convert your Blender scripts into a button.
3 | The add-on saves your scripts so that they can be used in any other Blender project.
4 | You can also define properties for your script that allow you to use user input in your script.
5 |
6 | # Installation
7 | 1. Copy the url: `https://raw.githubusercontent.com/RivinHD/ScriptToButton/refs/heads/master/index.json`
8 | 2. Get Extensions → Repositories → [+] → Add Remote Repository
9 | 3. Enable `Check for Updates on Startup`
10 | 4. (Optional) Rename the name of the repository (`raw.githubusercontent.com`) to `Script To Button` to better describe it.
11 | 5. Install the extension by searching the `Available` list for `Script To Button` and click `Install`
12 |
13 | # Legacy Installation
14 | 1. Go on the left side to [Release](https://github.com/RivinHD/ScriptToButton/releases/latest) and download the latest Version `legacy_script_to_button-2.3.2.zip` at the bottem under Assets.
15 | 1. Start Blender and navigate to Edit -> Preferences -> Add-ons and click "Install"
16 | 2. Select the Add-on named "ScriptToButton.zip" or "ScriptToButton-master.zip" and import it as .zip file
17 | 3. Enable the Add-on
18 |
19 | # Usage
20 | Go to the Sidebar. A tab named "Script To Button" is now available. (Also see [Wiki](https://github.com/RivinHD/ScriptToButton/wiki))
21 |
22 | ## "Controls" Panel
23 | Here are control buttons for the Add-on located.
24 |
25 | #### Add
26 | Add a new Button to the "Buttons" Panel.
27 | When the button is pressed a popup will appear with options to name your button and to select the script from Texteditor which will be linked to the Button.
28 |
29 | #### Remove
30 | Remove the selected Button from the "Buttons" Panel.
31 | When the button is pressed a popup will appear with the options to delete the button from file and also the linked script.
32 |
33 | #### Load
34 | Give you the option to load all buttons from the Disk or from the Texteditor.
35 | When the button is pressed a popup will appear with a switch to decide where to load from.
36 | ##### Load from Disk
37 | A warning message is shown and when executed all buttons in Blender will be deleted and loaded from the disk.
38 | ##### Load from Texteditor
39 | All Texts are represented with a checkbox to decide which to load.
40 | If the Button exist it will be reloaded otherwise a popup will appear with the option to add or skip this script.
41 |
42 | #### Save
43 | (only available when Autostart is off)
44 | Save all buttons to the disk.
45 |
46 | #### Load Button
47 | Loads the selected Button into the Texteditor.
48 |
49 | #### Reload
50 | Reload the linked script of the selected button.
51 | If Autosave is active the button is also saved on the disk.
52 |
53 | #### Edit
54 | Give you the option to rename and remove properties of the selected button.
55 | When the button is pressed a popup appear with a text field to put in the new name and a list of the properties.
56 |
57 | #### Export
58 | Opens an export window to export your buttons as .py files or one .zip file.
59 | On the right side of the export window are the option to choose in which format you want to export the scripts. Under this option, all buttons are listed with a checkbox to decide which ones to export.
60 |
61 | #### Import
62 | Opens an import window to import .py files or .zip files .
63 | You can select multiple .py and .zip files to import them all at once.
64 |
65 | ## "Buttons" Panel
66 | All your buttons are displayed here
67 |
68 | ## "Properties" Panel
69 | All registered properties of the selected button are displayed here
70 |
71 |
--------------------------------------------------------------------------------
/docs/icons/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RivinHD/ScriptToButton/1f0d66afb8abe192361c4d87967c79d876f1b56e/docs/icons/Icon.png
--------------------------------------------------------------------------------
/docs/icons/Icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
177 |
--------------------------------------------------------------------------------
/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "v1",
3 | "blocklist": [],
4 | "data": [
5 | {
6 | "schema_version": "1.0.0",
7 | "id": "script_to_button",
8 | "name": "Script To Button",
9 | "tagline": "Converts scripts to buttons",
10 | "version": "2.3.2",
11 | "type": "add-on",
12 | "maintainer": "RivinHD",
13 | "license": [
14 | "SPDX:GPL-3.0-or-later"
15 | ],
16 | "blender_version_min": "4.2.0",
17 | "website": "https://github.com/RivinHD/ScriptToButton/wiki",
18 | "permissions": {
19 | "files": "Save the user defined scripts"
20 | },
21 | "tags": [
22 | "System"
23 | ],
24 | "archive_url": "https://github.com/RivinHD/ScriptToButton/releases/download/v2.3.2/script_to_button-2.3.2.zip",
25 | "archive_size": 22450,
26 | "archive_hash": "sha256:7f9271aecd5634e330ac227edc6593d686a3a6ca8550bf60f58fa47d3ab58baf"
27 | }
28 | ]
29 | }
--------------------------------------------------------------------------------
/src/__init__.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from bpy.app.handlers import persistent
3 | from . import properties, preferences, operators, functions, panels, dynamic_panels, menus
4 |
5 | bl_info = {
6 | "name": "Script To Button",
7 | "author": "RivinHD",
8 | "blender": (3, 6, 0),
9 | "version": (2, 3, 2),
10 | "location": "View3D",
11 | "category": "System",
12 | "doc_url": "https://github.com/RivinHD/ScriptToButton/wiki",
13 | "tracker_url": "https://github.com/RivinHD/ScriptToButton/issues"
14 | }
15 |
16 | keymaps = {}
17 |
18 |
19 | @persistent
20 | def load_saves(dummy=None):
21 | button_fails = functions.load(bpy.context)
22 | message = "'''"
23 | for name, fails in zip(button_fails[0], button_fails[1]):
24 | if len(fails[0]) or len(fails[1]):
25 | message += "\n %s: " % name
26 | message += functions.create_fail_message(fails)
27 | if bpy.data.texts.find('STB Fail Message') == -1:
28 | if message != "'''":
29 | bpy.data.texts.new('STB Fail Message')
30 | else:
31 | bpy.data.texts['STB Fail Message'].clear()
32 | if message != "'''":
33 | bpy.data.texts['STB Fail Message'].write("%s'''" % message)
34 | functions.NotOneStart[0] = True
35 |
36 |
37 | def register():
38 | preferences.register()
39 | properties.register()
40 | operators.register()
41 | panels.register()
42 | menus.register()
43 | bpy.app.handlers.load_post.append(load_saves)
44 |
45 | addon = bpy.context.window_manager.keyconfigs.addon
46 | if addon:
47 | km = addon.keymaps.new(name='Screen')
48 | keymaps['default'] = km
49 | items = km.keymap_items
50 | kmi = items.new("wm.call_menu", 'Y', 'PRESS', shift=True, alt=True)
51 | kmi.properties.name = "STB_MT_ButtonMenu"
52 |
53 |
54 | def unregister():
55 | preferences.unregister()
56 | properties.unregister()
57 | operators.unregister()
58 | panels.unregister()
59 | dynamic_panels.unregister()
60 | menus.unregister()
61 | bpy.app.handlers.load_post.remove(load_saves)
62 |
--------------------------------------------------------------------------------
/src/blender_manifest.toml:
--------------------------------------------------------------------------------
1 | schema_version = "1.0.0"
2 |
3 | # Example of manifest file for a Blender extension
4 | # Change the values according to your extension
5 | id = "script_to_button"
6 | version = "2.3.2"
7 | name = "Script To Button"
8 | tagline = "Converts scripts to buttons"
9 | maintainer = "RivinHD"
10 | # Supported types: "add-on", "theme"
11 | type = "add-on"
12 |
13 | # # Optional: link to documentation, support, source files, etc
14 | website = "https://github.com/RivinHD/ScriptToButton/wiki"
15 |
16 | # # Optional: tag list defined by Blender and server, see:
17 | # # https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
18 | tags = ["System"]
19 |
20 | blender_version_min = "4.2.0"
21 | # # Optional: Blender version that the extension does not support, earlier versions are supported.
22 | # # This can be omitted and defined later on the extensions platform if an issue is found.
23 | # blender_version_max = "5.1.0"
24 |
25 | # License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
26 | # https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
27 | license = [
28 | "SPDX:GPL-3.0-or-later",
29 | ]
30 | # # Optional: required by some licenses.
31 | # copyright = [
32 | # "2002-2024 Developer Name",
33 | # "1998 Company Name",
34 | # ]
35 |
36 | # # Optional: list of supported platforms. If omitted, the extension will be available in all operating systems.
37 | # platforms = ["windows-x64", "macos-arm64", "linux-x64"]
38 | # # Other supported platforms: "windows-arm64", "macos-x64"
39 |
40 | # # Optional: bundle 3rd party Python modules.
41 | # # https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html
42 | # wheels = [
43 | # "./wheels/hexdump-3.3-py3-none-any.whl",
44 | # "./wheels/jsmin-3.0.1-py3-none-any.whl",
45 | # ]
46 |
47 | # # Optional: add-ons can list which resources they will require:
48 | # # * files (for access of any filesystem operations)
49 | # # * network (for internet access)
50 | # # * clipboard (to read and/or write the system clipboard)
51 | # # * camera (to capture photos and videos)
52 | # # * microphone (to capture audio)
53 | # #
54 | # # If using network, remember to also check `bpy.app.online_access`
55 | # # https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
56 | # #
57 | # # For each permission it is important to also specify the reason why it is required.
58 | # # Keep this a single short sentence without a period (.) at the end.
59 | # # For longer explanations use the documentation or detail page.
60 |
61 | [permissions]
62 | files = "Save the user defined scripts"
63 |
64 | # # Optional: advanced build settings.
65 | # # https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
66 | [build]
67 | # These are the default build excluded patterns.
68 | # You only need to edit them if you want different options.
69 | paths_exclude_pattern = [
70 | "/Storage/"
71 | ]
72 |
--------------------------------------------------------------------------------
/src/dynamic_panels.py:
--------------------------------------------------------------------------------
1 | import typing
2 | import bpy
3 | from bpy.types import Panel, Context, Menu
4 |
5 | button_classes = {}
6 | panel_names = []
7 |
8 | ui_space_types = [
9 | 'CLIP_EDITOR', 'NODE_EDITOR', 'TEXT_EDITOR', 'SEQUENCE_EDITOR', 'NLA_EDITOR',
10 | 'DOPESHEET_EDITOR', 'VIEW_3D', 'GRAPH_EDITOR', 'IMAGE_EDITOR'
11 | ] # blender spaces with UI region
12 |
13 |
14 | def register_button_panel(name: str):
15 | unregister_register_button_panel(name, True)
16 |
17 |
18 | def unregister_button_panel(name: str):
19 | unregister_register_button_panel(name, False)
20 |
21 |
22 | def unregister_register_button_panel(name: str, register: bool):
23 | index = len(panel_names) - (not register)
24 | for space_type in ui_space_types:
25 | class STB_PT_Buttons(Panel):
26 | bl_idname = "STB_PT_Buttons_%s_%s" % (index, space_type)
27 | bl_label = ""
28 | bl_space_type = space_type
29 | bl_region_type = "UI"
30 | bl_category = "Script To Button"
31 | bl_options = {"INSTANCED"}
32 | bl_parent_id = "STB_PT_ScriptToButton_%s" % space_type
33 | bl_order = index
34 |
35 | @classmethod
36 | def poll(self, context: Context) -> bool:
37 | stb = context.scene.stb
38 | area = context.area.ui_type
39 | panel = panel_names[self.bl_order]
40 | return any((button.panel == panel and area in button.areas) for button in stb)
41 |
42 | def draw_header(self, context: Context):
43 | layout = self.layout
44 | layout.label(text=panel_names[self.bl_order])
45 |
46 | def draw(self, context):
47 | layout = self.layout
48 | area = context.area.ui_type
49 | panel = panel_names[self.bl_order]
50 | buttons = filter(
51 | lambda x: area in x.areas and x.panel == panel,
52 | context.scene.stb
53 | )
54 | for button in sorted(buttons, key=lambda x: x.name):
55 | row = layout.row(align=True)
56 | row.prop(
57 | button, 'selected',
58 | toggle=True,
59 | text="",
60 | icon='RADIOBUT_ON' if button.selected else 'RADIOBUT_OFF'
61 | )
62 | row.operator(
63 | "stb.script_button",
64 | text=button.name
65 | ).name = button.name
66 | STB_PT_Buttons.__name__ = "STB_PT_Buttons_%s_%s" % (index, space_type)
67 |
68 | global button_classes
69 | if register:
70 | button_classes[STB_PT_Buttons.__name__] = STB_PT_Buttons
71 | bpy.utils.register_class(STB_PT_Buttons)
72 | else:
73 | bpy.utils.unregister_class(button_classes[STB_PT_Buttons.__name__])
74 | del button_classes[STB_PT_Buttons.__name__]
75 |
76 | class STB_MT_Buttons(Menu):
77 | bl_idname = "STB_MT_Buttons_%s" % index
78 | bl_label = "Category"
79 | bl_order = index
80 |
81 | @classmethod
82 | def poll(self, context: Context) -> bool:
83 | stb = context.scene.stb
84 | area = context.area
85 | if area is None:
86 | return False
87 | area = context.area.ui_type
88 | panel = panel_names[self.bl_order]
89 | return any((button.panel == panel and area in button.areas) for button in stb)
90 |
91 | def draw(self, context: Context):
92 | layout = self.layout
93 | area = context.area.ui_type
94 | panel = panel_names[self.bl_order]
95 | buttons = filter(
96 | lambda x: area in x.areas and x.panel == panel,
97 | context.scene.stb
98 | )
99 | for button in sorted(buttons, key=lambda x: x.name):
100 | layout.operator(
101 | "stb.script_button",
102 | text=button.name
103 | ).name = button.name
104 | STB_MT_Buttons.__name__ = "STB_MT_Buttons_%s" % index
105 |
106 | if register:
107 | button_classes[STB_MT_Buttons.__name__] = STB_MT_Buttons
108 | bpy.utils.register_class(STB_MT_Buttons)
109 | panel_names.append(name)
110 | panel_names.sort()
111 | else:
112 | bpy.utils.unregister_class(getattr(bpy.types, STB_MT_Buttons.__name__))
113 | del button_classes[STB_MT_Buttons.__name__]
114 | panel_names.remove(name)
115 | panel_names.sort()
116 |
117 |
118 | def unregister():
119 | for cls in button_classes.values():
120 | bpy.utils.unregister_class(cls)
121 | button_classes.clear()
122 | panel_names.clear()
123 |
--------------------------------------------------------------------------------
/src/functions.py:
--------------------------------------------------------------------------------
1 | import os
2 | import bpy
3 | import zipfile
4 | import uuid
5 | from bpy.props import StringProperty, PointerProperty
6 | from bpy.types import PropertyGroup, Context, UILayout, Text, AddonPreferences, Scene
7 | from typing import TYPE_CHECKING, Union
8 | import functools
9 | from .import dynamic_panels as panels
10 | from . import __package__ as base_package
11 | from .wrapper import get_user_path
12 | if TYPE_CHECKING:
13 | from .preferences import STB_preferences
14 | from .properties import STB_button_properties
15 | else:
16 | STB_button_properties = PropertyGroup
17 | STB_preferences = AddonPreferences
18 |
19 |
20 | classes = []
21 | NotOneStart = [False]
22 | ALL_AREAS = [
23 | "3D_Viewport", "UV_Editor", "Compositor", "Video_Sequencer",
24 | "Movie_Clip_Editor", "Dope_Sheet", "Graph_Editor", "Nonlinear_Animation",
25 | "Text_Editor"
26 | ]
27 |
28 |
29 | def get_preferences(context: Context) -> STB_preferences:
30 | return context.preferences.addons[base_package].preferences
31 |
32 |
33 | def get_storage_dir():
34 | return get_user_path(base_package, "Storage", True)
35 |
36 |
37 | def save_text(active_text: Text, script_name: str) -> None:
38 | text = active_text.as_string()
39 | storage_dir = get_storage_dir()
40 | destination = os.path.join(storage_dir, "%s.py" % script_name)
41 | with open(destination, 'w', encoding='utf8') as outfile:
42 | outfile.write(text)
43 |
44 |
45 | def get_text(script_name: str) -> None:
46 | destination = os.path.join(
47 | get_storage_dir(),
48 | "%s.py" % script_name
49 | )
50 | if bpy.data.texts.find(script_name) == -1:
51 | bpy.data.texts.new(script_name)
52 | else:
53 | bpy.data.texts[script_name].clear()
54 | with open(destination, 'r', encoding='utf8') as file:
55 | bpy.data.texts[script_name].write(file.read())
56 |
57 |
58 | def get_all_saved_scripts() -> list:
59 | storage_dir = get_storage_dir()
60 | scripts = []
61 | for file in os.listdir(storage_dir):
62 | scripts.append(file.replace(".py", ""))
63 | scripts.sort()
64 | return scripts
65 |
66 |
67 | def load(context: Context) -> tuple[list, list]:
68 | scene = context.scene
69 | scene.stb.clear()
70 | STB_pref = get_preferences(context)
71 | btnFails = ([], [])
72 | scripts = get_all_saved_scripts()
73 | for script in scripts:
74 | new = scene.stb.add()
75 | new.name = script
76 | get_text(script)
77 | btnFails[0].append(script)
78 | btnFails[1].append(add_areas_and_props(
79 | new,
80 | bpy.data.texts[script].as_string()
81 | ))
82 | if not STB_pref.autoload:
83 | bpy.data.texts.remove(bpy.data.texts[script])
84 | if len(scene.stb) > 0:
85 | scene.stb[0].selected = True
86 | panel_names = set(button.panel for button in scene.stb)
87 | for panel in set(panels.panel_names).difference(panel_names):
88 | panels.unregister_button_panel(panel)
89 | for panel in panel_names.difference(panels.panel_names):
90 | panels.register_button_panel(panel)
91 | return btnFails
92 |
93 |
94 | def get_all_button_names(context: Context) -> set:
95 | return set(button.name for button in context.scene.stb)
96 |
97 |
98 | def list_to_enum_items(data: list) -> list:
99 | enum_items = []
100 | for i in range(len(data)):
101 | enum_items.append((data[i], data[i], "", "", i))
102 | return enum_items
103 |
104 |
105 | def get_panel(text: str) -> str:
106 | lines = text.splitlines()
107 | if not len(lines):
108 | return "Buttons"
109 | comments = (x.strip() for x in lines[0].split("///"))
110 | for comment in comments:
111 | if comment.startswith("#STB-Panel-"):
112 | return comment.split("-")[2]
113 | return "Buttons"
114 |
115 |
116 | def get_areas(text: str) -> list:
117 | lines = text.splitlines()
118 | if not len(lines):
119 | return []
120 | comments = (x.strip() for x in lines[0].split("///"))
121 | area_types = []
122 | for comment in comments:
123 | if comment.startswith("#STB-Area-"):
124 | area_types.append(comment.split("-")[2])
125 | if len(area_types):
126 | return area_types
127 | return ALL_AREAS
128 |
129 |
130 | AREA_PARSE_DICT = {
131 | "3D_Viewport": "VIEW_3D",
132 | "UV_Editor": "UV",
133 | "Image_Editor": "VIEW",
134 | "Compositor": "CompositorNodeTree",
135 | "Texture_Node_Editor": "TextureNodeTree",
136 | "Geomerty_Node_Editor": "GeometryNodeTree",
137 | "Shader_Editor": "ShaderNodeTree",
138 | "Video_Sequencer": "SEQUENCE_EDITOR",
139 | "Movie_Clip_Editor": "CLIP_EDITOR",
140 | "Dope_Sheet": "DOPESHEET",
141 | "Timeline": "TIMELINE",
142 | "Graph_Editor": "FCURVES",
143 | "Drivers": "DRIVERS",
144 | "Nonlinear_Animation": "NLA_EDITOR",
145 | "Text_Editor": "TEXT_EDITOR"
146 | }
147 |
148 |
149 | def area_parser(area: str) -> Union[str, bool]:
150 | return AREA_PARSE_DICT.get(area, False)
151 |
152 |
153 | def get_props(text: str) -> list:
154 | lines = text.splitlines()
155 | props = []
156 | for i in range(len(lines)):
157 | current_line = lines[i].strip()
158 | if not current_line.startswith("#STB-") or current_line.startswith("#STB-Area"):
159 | continue
160 | next_line = lines[i + 1]
161 | if next_line.startswith("#"):
162 | continue
163 | inputs = current_line.replace(" ", "").split("///")
164 | line_name = next_line.split("=")[0]
165 | value = next_line.split("=")[1].split("#")[0]
166 | for input in inputs:
167 | if not input.startswith("#STB-Input"):
168 | continue
169 | split = input.split("-")
170 | props.append({
171 | "name": line_name.strip(),
172 | "line_name": line_name,
173 | "space": split[2],
174 | "type": split[3],
175 | "sort": split[4] if len(split) > 4 else "",
176 | "line": i + 1,
177 | "value": value
178 | })
179 | return props
180 |
181 |
182 | BLENDER_TYPE_TO_PY_TYPE = {
183 | 'String': str,
184 | 'Int': int,
185 | 'Float': float,
186 | 'Bool': bool,
187 | 'Enum': (list, tuple),
188 | 'IntVector': (list, tuple),
189 | 'FloatVector': (list, tuple),
190 | 'BoolVector': (list, tuple),
191 | 'List': (list, tuple),
192 | 'Object': bpy.types.Object,
193 | }
194 | VECTOR_TYPE = {
195 | 'IntVector': int,
196 | 'FloatVector': float,
197 | 'BoolVector': bool
198 | }
199 |
200 |
201 | def add_prop(button: STB_button_properties, property) -> bool:
202 | try:
203 | value = eval(property["value"])
204 | except Exception:
205 | return False
206 |
207 | property_type = property["type"]
208 | is_type = isinstance(
209 | value,
210 | BLENDER_TYPE_TO_PY_TYPE.get(property_type, None)
211 | )
212 | if ((property_type in {'String', 'Int', 'Float', 'Bool', 'List', 'Object'} and not is_type)
213 | or
214 | (property_type == 'Enum'
215 | and not (
216 | is_type
217 | and isinstance(value[1], (list, tuple))
218 | and isinstance(value[0], str)
219 | and all(map(lambda x: isinstance(x, str), value[1])))
220 | )
221 | or
222 | (property_type in {'IntVector', 'FloatVector', 'BoolVector'}
223 | and not (
224 | is_type
225 | and all(map(lambda x: isinstance(x, VECTOR_TYPE[property_type]), value))
226 | and len(value) >= 1
227 | and len(value) <= 32)
228 | )): # Check types
229 | return False
230 |
231 | try:
232 | # Add element to the right property collection
233 | new_element = eval("button." + property_type + "Props.add()")
234 | except Exception:
235 | return False # Add to fail stack
236 |
237 | name = property["name"]
238 | new_element.name = name # parse data
239 | new_element.linename = property["line_name"]
240 | new_element.space = property["space"]
241 | new_element.line = property["line"]
242 | new_element.sort = property["sort"]
243 | if property_type == 'Enum':
244 | new_element.items.clear()
245 | for v in value[1]:
246 | item = new_element.items.add()
247 | item.name = v
248 | item.item = v
249 | try:
250 | new_element.prop = value[0]
251 | except Exception:
252 | new_element.prop = value[1][0]
253 | elif property_type in {'IntVector', 'FloatVector', 'BoolVector'}:
254 | new_element.address = create_vector_prop(
255 | len(value),
256 | ("%s_%s%s" % (
257 | button.name,
258 | name,
259 | str(new_element.line)
260 | )).replace(" ", ""),
261 | property_type,
262 | "bpy.context.scene.stb['%s'].%sProps['%s']" % (
263 | button.name,
264 | property_type,
265 | name
266 | )
267 | )
268 | exec("%s.prop = value" % new_element.address)
269 | elif property_type == 'List':
270 | new_element.prop.clear()
271 | for i in value:
272 | prop = new_element.prop.add()
273 | if not isinstance(i, (list, tuple)):
274 | if isinstance(i, (str, int, float, bool)):
275 | exec("prop." + str(type(i).__name__) + "prop = i")
276 | prop.ptype = str(type(i).__name__)
277 | else:
278 | prop.strprop = str(i)
279 | prop.ptype = 'str'
280 | continue
281 |
282 | if (isinstance(i, (list, tuple))
283 | and (isinstance(i[1], list) or isinstance(i[1], tuple))
284 | and isinstance(i[0], str)
285 | and all(map(lambda x: isinstance(x, str), i[1]))):
286 | prop.enum_prop.items.clear() # Enum
287 | prop.ptype = 'enum'
288 | for v in i[1]:
289 | item = prop.enum_prop.items.add()
290 | item.name = v
291 | item.item = v
292 | try:
293 | prop.enum_prop.prop = i[0]
294 | except Exception:
295 | prop.enum_prop.prop = i[1][0]
296 | elif (isinstance(i, {list, tuple})
297 | and all(map(lambda x: isinstance(x, bool), i))
298 | and len(i) <= 32): # BoolVector
299 | prop.boolvector_prop = create_vector_prop(
300 | len(i),
301 | ("%s_%s_list_%s" % (
302 | button.name,
303 | name,
304 | str(len(new_element.prop)))
305 | ).replace(" ", ""),
306 | "BoolVector",
307 | "bpy.context.scene.stb['%s'].ListProps['%s']" % (
308 | button.name,
309 | name
310 | )
311 | )
312 | prop.ptype = 'boolvector'
313 | exec("%s.prop = i" % prop.boolvector_prop)
314 | elif (isinstance(i, {list, tuple})
315 | and all(map(lambda x: isinstance(x, int), i))
316 | and len(i) <= 32): # IntVector
317 | prop.intvector_prop = create_vector_prop(
318 | len(i),
319 | ("%s_%s_list_%s" % (
320 | button.name,
321 | name,
322 | str(len(new_element.prop)))
323 | ).replace(" ", ""),
324 | "IntVector",
325 | "bpy.context.scene.stb['%s'].ListProps['%s']" % (
326 | button.name,
327 | name
328 | )
329 | )
330 | prop.ptype = 'intvector'
331 | exec("%s.prop = i" % prop.intvector_prop)
332 | elif (isinstance(i, {list, tuple})
333 | and all(map(lambda x: isinstance(x, float), i))
334 | and len(i) <= 32): # FloatVector
335 | prop.floatvector_prop = create_vector_prop(
336 | len(i),
337 | ("%s_%s_list_%s" % (
338 | button.name,
339 | name, str(len(new_element.prop)))
340 | ).replace(" ", ""),
341 | "FloatVector",
342 | "bpy.context.scene.stb['%s'].ListProps['%s']" % (
343 | button.name,
344 | name
345 | ),
346 | )
347 | prop.ptype = 'floatvector'
348 | exec("%s.prop = i" % prop.floatvector_prop)
349 | else:
350 | prop.strprop = str(i)
351 | prop.ptype = 'str'
352 | elif property_type == 'Object':
353 | new_element.prop = value.name
354 | else:
355 | new_element.prop = value
356 | return True
357 |
358 |
359 | def add_button(context: Context, name: str, textname: str):
360 | STB_pref = get_preferences(context)
361 | texts = bpy.data.texts
362 | text = texts[textname].as_string() # Get selected Text
363 | if STB_pref.autosave:
364 | save_text(texts[textname], name)
365 | if STB_pref.autoload:
366 | get_text(name) # do same as lower, but with File
367 | elif STB_pref.autoload:
368 | if texts.find(textname) == -1: # Create new text if not exist
369 | texts.new(textname)
370 | else:
371 | texts[textname].clear()
372 | texts[textname].write(text) # Write to Text
373 | index = context.scene.stb.find(name)
374 | if index != -1:
375 | context.scene.stb.remove(index)
376 | new = context.scene.stb.add() # Create new Instance
377 | new.name = check_for_duplicates(get_all_button_names(context), name)
378 | fails = add_areas_and_props(new, text)
379 | if new.panel not in panels.panel_names:
380 | panels.register_button_panel(new.panel)
381 | return fails
382 |
383 |
384 | def remove_button(context: Context, delete_file: bool, delete_text: bool):
385 | STB_pref = get_preferences(context)
386 | name = STB_pref.selected_button
387 | stb = context.scene.stb
388 | button = stb[name]
389 |
390 | if delete_file:
391 | os.remove(os.path.join(
392 | get_storage_dir(),
393 | "%s.py" % name
394 | ))
395 | if delete_text:
396 | if (index := bpy.data.texts.find(name)) != -1:
397 | bpy.data.texts.remove(bpy.data.texts[index])
398 | delete_vector_props(button)
399 | delete_list_prop(button)
400 | index = stb.find(STB_pref.selected_button)
401 | stb.remove(index)
402 | if index - 1 >= 0:
403 | stb[index - 1].selected = True
404 | panel_names = set(button.panel for button in stb)
405 | for panel in set(panels.panel_names).difference(panel_names):
406 | panels.unregister_button_panel(panel)
407 |
408 |
409 | def create_fail_message(fails: tuple[list, list]):
410 | message = "\n"
411 | if len(fails[0]):
412 | message += " Areas: \n"
413 | for fail in fails[0]:
414 | message += " Line: 0 #STB-Area-%s \n" % fail
415 | if len(fails[1]):
416 | message += " Properties: \n"
417 | for fail in fails[1]:
418 | message += " Line: %s #STB-Input-%s-%s %s \n" % (
419 | str(fail['line']),
420 | str(fail['space']),
421 | str(fail['type']),
422 | str(fail['value'])
423 | )
424 | return message
425 |
426 |
427 | def load_from_texteditor(op, context: Context) -> tuple[list, list]:
428 | STB_pref = get_preferences(context)
429 | btnFails = ([], [])
430 | if op.all:
431 | for txt in op.texts: # All Texts from Buttons
432 | btn_index = context.scene.stb.find(txt.txt_name)
433 | if btn_index != -1:
434 | btnFails[0].append(txt.txt_name)
435 | btnFails[1].append(reload_button_text(
436 | context.scene.stb[btn_index],
437 | bpy.data.texts[txt.txt_name].as_string(),
438 | context.scene
439 | ))
440 | if STB_pref.autosave:
441 | save_text(bpy.data.texts[txt.txt_name], txt.txt_name)
442 | else:
443 | load_add_button(txt.txt_name)
444 | return btnFails
445 |
446 | for txt in op.texts:
447 | if not txt.select: # selected Texts from Buttons
448 | continue
449 | btn_index = context.scene.stb.find(txt.txt_name)
450 | if btn_index != -1:
451 | btnFails[0].append(txt.txt_name)
452 | btnFails[1].append(reload_button_text(
453 | context.scene.stb[btn_index],
454 | bpy.data.texts[txt.txt_name].as_string(),
455 | context.scene
456 | ))
457 | if STB_pref.autosave:
458 | save_text(bpy.data.texts[txt.txt_name], txt.txt_name)
459 | else:
460 | load_add_button(txt.txt_name)
461 | return btnFails
462 |
463 |
464 | def load_add_button(name):
465 | bpy.ops.stb.addbutton(show_skip=True, name=name, text_list=name)
466 |
467 |
468 | def reload_button_text(button: STB_button_properties, text: str, scene: Scene) -> tuple[list, list]:
469 | delete_vector_props(button)
470 | delete_list_prop(button)
471 | fails = add_areas_and_props(button, text)
472 |
473 | panel_names = set(button.panel for button in scene.stb)
474 | for panel in set(panels.panel_names).difference(panel_names):
475 | panels.unregister_button_panel(panel)
476 | for panel in panel_names.difference(panels.panel_names):
477 | panels.register_button_panel(panel)
478 | return fails
479 |
480 |
481 | Property_type = {
482 | "String", "Int", "Float", "Bool", "Enum",
483 | "IntVector", "FloatVector", "BoolVector", "List", "Object"
484 | }
485 |
486 |
487 | def add_areas_and_props(button: STB_button_properties, text: str) -> tuple[list, list]:
488 | button.areas.clear() # Clear Area and Prop
489 | for prop in Property_type:
490 | getattr(button, "%sProps" % prop).clear()
491 |
492 | button.panel = get_panel(text)
493 |
494 | areas = get_areas(text) # Get Areas
495 | failed_areas = []
496 | for ele in areas: # Add Areas
497 | pars = area_parser(ele)
498 | if pars is False:
499 | failed_areas.append(ele)
500 | else:
501 | new = button.areas.add()
502 | new.name = pars
503 | new.area = pars
504 | if len(areas) == len(failed_areas): # failed to add areas
505 | for ele in ALL_AREAS:
506 | pars = area_parser(ele)
507 | new = button.areas.add()
508 | new.name = pars
509 | new.area = pars
510 |
511 | prop_list_dict = get_props(text) # Get Props
512 | failed_props = []
513 | for ele in prop_list_dict: # Add Props
514 | if not add_prop(button, ele):
515 | failed_props.append(ele)
516 | return (failed_areas, failed_props)
517 |
518 |
519 | def update_vector_property(self, context):
520 | prop = eval(self.address)
521 | update_text(
522 | prop.line,
523 | prop.linename,
524 | [ele for ele in self.prop],
525 | eval("context.scene.%s" % prop.path_from_id().split(".")[0])
526 | )
527 |
528 |
529 | def create_vector_prop(size: int, name: str, type: str, back_address: str):
530 | property_func = getattr(bpy.props, "%sProperty" % type)
531 | vec_id = uuid.uuid5(uuid.NAMESPACE_OID, name).hex
532 |
533 | class VectorProp(PropertyGroup):
534 | prop: property_func(size=size, update=update_vector_property)
535 | address: StringProperty(default=back_address)
536 | VectorProp.__name__ = "VectorProp_%s_%s" % (type, vec_id)
537 | bpy.utils.register_class(VectorProp)
538 | setattr(
539 | bpy.types.Scene,
540 | "stb_%sproperty_%s" % (type.lower(), vec_id),
541 | PointerProperty(type=VectorProp)
542 | )
543 | return "bpy.context.scene.stb_%sproperty_%s" % (type.lower(), vec_id)
544 |
545 |
546 | def unregister_vector():
547 | for cls in classes:
548 | bpy.utils.unregister_class(cls)
549 |
550 |
551 | def delete_vector_props(button: STB_button_properties):
552 | props = [
553 | *button.IntVectorProps,
554 | *button.FloatVectorProps,
555 | *button.BoolVectorProps
556 | ]
557 | for vec in props:
558 | name = vec.address.split(".")[-1]
559 | if hasattr(bpy.types.Scene, name):
560 | delattr(bpy.types.Scene, name)
561 | del bpy.context.scene[name]
562 |
563 |
564 | def delete_list_prop(button: STB_button_properties):
565 | for ele in button.ListProps:
566 | for prop in ele.prop:
567 | if prop.ptype not in ("intvector", "floatvector", "boolvector"):
568 | continue
569 | name = getattr(prop, "%s_prop" % prop.ptype).split(".")[-1]
570 | if hasattr(bpy.types.Scene, name):
571 | delattr(bpy.types.Scene, name)
572 | del bpy.context.scene[name]
573 |
574 |
575 | def update_text(linepos: int, varname: str, message: str, button: STB_button_properties):
576 | if NotOneStart[0] and bpy.data.texts.find(button.name) != -1:
577 | text = bpy.data.texts[button.name]
578 | text.lines[linepos].body = "%s= %s" % (varname, str(message))
579 | txt = text.as_string()
580 | text.clear()
581 | text.write(txt)
582 |
583 |
584 | TYPE_GETTER = {
585 | 'str': lambda v: v.str_prop,
586 | 'int': lambda v: v.int_prop,
587 | 'float': lambda v: v.float_prop,
588 | 'bool': lambda v: v.bool_prop,
589 | 'enum': lambda v: [v.enum_prop.prop, [item.item for item in v.enum_prop.items]],
590 | 'intvector': lambda v: [i for i in eval("%s['prop']" % v.intvector_prop)],
591 | 'floatvector': lambda v: [i for i in eval("%s['prop']" % v.floatvector_prop)],
592 | 'boolvector': lambda v: [bool(i) for i in eval("%s['prop']" % v.boolvector_prop)]
593 | }
594 |
595 |
596 | def type_getter(value, vtype):
597 | func = TYPE_GETTER.get(vtype, None)
598 | if func is None:
599 | return
600 | return func(value)
601 |
602 |
603 | def get_export_text(selection):
604 | text = bpy.data.texts.get(selection.name)
605 | if text:
606 | text = text.as_string()
607 | else:
608 | destination = os.path.join(get_storage_dir(), f"{selection.name}.py")
609 | with open(destination, 'r', encoding="utf-8") as file:
610 | text = file.read()
611 | return text
612 |
613 |
614 | def export(mode, selections: list, export_path: str) -> None:
615 | if mode == "py":
616 | for selection in selections:
617 | path = os.path.join(export_path, "%s.py" % selection.name)
618 | with open(path, 'w', encoding='utf8') as file:
619 | file.write(get_export_text(selection))
620 | else:
621 | folder_path = os.path.join(bpy.app.tempdir, "STB_Zip")
622 | if not os.path.exists(folder_path):
623 | os.mkdir(folder_path)
624 | with zipfile.ZipFile(export_path, 'w') as zip_it:
625 | for selection in selections:
626 | zip_path = os.path.join(folder_path, "%s.py" % selection.name)
627 | with open(zip_path, 'w', encoding='utf8') as file:
628 | file.write(get_export_text(selection))
629 | zip_it.write(zip_path, "%s.py" % selection.name)
630 | os.remove(zip_path)
631 | os.rmdir(folder_path)
632 |
633 |
634 | def import_zip(filepath: str, context: Context) -> tuple[list, list]:
635 | btnFails = ([], [])
636 | with zipfile.ZipFile(filepath, 'r') as zip_out:
637 | filepaths = []
638 | for i in zip_out.namelist():
639 | if i.endswith(".py"):
640 | filepaths.append(i)
641 | for filepath in filepaths:
642 | txt = zip_out.read(filepath).decode("utf-8").replace("\r", "")
643 | Fail = import_button(filepath, context, txt)
644 | btnFails[0].extend(Fail[0])
645 | btnFails[1].append(Fail[1])
646 | return btnFails
647 |
648 |
649 | def import_py(filepath: str, context: Context) -> tuple[list[str], tuple[list, list]]:
650 | with open(filepath, 'r', encoding='utf8') as file:
651 | txt = file.read()
652 | return import_button(filepath, context, txt)
653 |
654 |
655 | def import_button(
656 | filepath: str,
657 | context: Context,
658 | txt: str) -> tuple[list[str], tuple[list, list]]:
659 | STB_pref = get_preferences(context)
660 | stb = context.scene.stb
661 | name = check_for_duplicates(
662 | get_all_button_names(context),
663 | os.path.splitext(os.path.basename(filepath))[0]
664 | )
665 | bpy.data.texts.new(name)
666 | bpy.data.texts[name].write(txt)
667 | button: STB_button_properties = stb.add()
668 | button.name = name
669 | button.selected = True
670 |
671 | if STB_pref.autosave:
672 | save_text(bpy.data.texts[name], name)
673 | if not STB_pref.autoload:
674 | bpy.data.texts.remove(bpy.data.texts[name])
675 | Fails = add_areas_and_props(button, txt)
676 | return ([name], Fails)
677 |
678 |
679 | def check_for_duplicates(check_list: set, name: str, num: int = 1) -> str:
680 | """
681 | Check for the same name in check_list and append .001, .002 etc. if found
682 |
683 | Args:
684 | check_list (set): list to check against
685 | name (str): name to check
686 | num (int, optional): starting number to append. Defaults to 1.
687 |
688 | Returns:
689 | str: name with expansion if necessary
690 | """
691 | split = name.split(".")
692 | base_name = name
693 | if split[-1].isnumeric():
694 | base_name = ".".join(split[:-1])
695 | while name in check_list:
696 | name = "{0}.{1:03d}".format(base_name, num)
697 | num += 1
698 | return name
699 |
700 |
701 | def rename(context: Context, name: str):
702 | STB_pref = get_preferences(context)
703 | button: STB_button_properties = context.scene.stb[STB_pref.selected_button]
704 | if bpy.data.texts.find(STB_pref.selected_button) == -1:
705 | get_text(STB_pref.selected_button)
706 | text = bpy.data.texts[STB_pref.selected_button]
707 | old_path = os.path.join(get_storage_dir(), "%s.py" %
708 | STB_pref.selected_button)
709 | if name != button.name:
710 | name = check_for_duplicates(get_all_button_names(context).difference([button.name]), name)
711 | button.name = name
712 | button.selected = True
713 | text.name = name
714 | os.rename(old_path, os.path.join(get_storage_dir(), "%s.py" % name))
715 | if not STB_pref.autoload:
716 | bpy.data.texts.remove(text)
717 |
718 |
719 | def update_all_props(button: STB_button_properties, context: Context):
720 | simple_props = [
721 | button.StringProps,
722 | button.IntProps,
723 | button.FloatProps,
724 | button.BoolProps,
725 | button.EnumProps,
726 | button.ObjectProps,
727 | button.ListProps
728 | ]
729 | for prop in simple_props:
730 | for item in prop:
731 | item.update_prop(context)
732 | vector_props = [
733 | *button.IntVectorProps,
734 | *button.FloatVectorProps,
735 | *button.BoolVectorProps
736 | ]
737 | for prop in vector_props:
738 | prop = eval(prop.address)
739 | update_vector_property(prop, context)
740 |
741 |
742 | def sort_props(button: STB_button_properties, space: str) -> tuple[list, list]:
743 | sort_mapping = []
744 | simple_props = [
745 | *button.StringProps,
746 | *button.IntProps,
747 | *button.FloatProps,
748 | *button.BoolProps,
749 | *button.EnumProps
750 | ]
751 | for prop in simple_props:
752 | if prop.space != space:
753 | continue
754 | sort_mapping.append(
755 | [*parse_sort(prop.sort), functools.partial(draw_prop, prop=prop)])
756 | vector_props = [
757 | *button.IntVectorProps,
758 | *button.FloatVectorProps,
759 | *button.BoolVectorProps
760 | ]
761 | for prop in vector_props:
762 | if prop.space != space:
763 | continue
764 | sort_mapping.append([
765 | *parse_sort(prop.sort),
766 | functools.partial(draw_vector_prop, prop=prop)
767 | ])
768 | for prop in button.ListProps:
769 | if prop.space != space:
770 | continue
771 | sort_mapping.append([
772 | *parse_sort(prop.sort),
773 | functools.partial(draw_list_prop, props=prop)
774 | ])
775 | for prop in button.ObjectProps:
776 | if prop.space != space:
777 | continue
778 | sort_mapping.append([
779 | *parse_sort(prop.sort),
780 | functools.partial(
781 | draw_prop_search,
782 | prop=prop,
783 | context=bpy.data,
784 | context_prop="objects"
785 | )
786 | ])
787 | sort_mapping.sort(key=lambda x: [x[0], x[1]])
788 | back = []
789 | sort = []
790 | for ele in sort_mapping:
791 | if ele[0] == -1:
792 | back.append(ele)
793 | else:
794 | sort.append(ele)
795 | return (sort, back)
796 |
797 |
798 | def draw_sort(sort: list, back: list, baseLayout: UILayout):
799 | lastIndex = 0
800 | lastRow = [-1, None, 0, 0]
801 | for ele in sort:
802 | layout = baseLayout
803 | skip_space(ele[0] - lastIndex, layout)
804 | lastIndex = ele[0] + 1
805 | if ele[0] == lastRow[0]:
806 | layout = lastRow[1]
807 | newRow = False
808 | else:
809 | if lastRow[2] > 0:
810 | skip_space(1, lastRow[1], lastRow[2])
811 | lastRow[3] = 0
812 | newRow = True
813 | row, skipBack, lastSpace = draw_row(
814 | ele[1],
815 | ele[-1],
816 | layout,
817 | lastRow[3],
818 | newRow
819 | )
820 | lastRow = [ele[0], row, skipBack, lastSpace]
821 | else:
822 | if lastRow[2] > 0:
823 | skip_space(1, lastRow[1], lastRow[2])
824 | for ele in back:
825 | ele[-1](layout=baseLayout)
826 |
827 |
828 | def draw_row(eleParse: list, eleDraw, row: UILayout, lastSpace: int, newRow: bool) -> tuple:
829 | if newRow:
830 | row = row.row()
831 | space = eleParse[0]
832 | else:
833 | space = eleParse[0] - lastSpace
834 | lastSpace = 1
835 | if space > 0:
836 | skip_space(1, row, space)
837 | eleDraw(layout=row)
838 | if len(eleParse) > 1:
839 | back = eleParse[1]
840 | else:
841 | back = 0
842 | return (row, back, lastSpace)
843 |
844 |
845 | def skip_space(skips: int, layout: UILayout, scale: float = 1):
846 | for i in range(skips):
847 | if scale <= 0:
848 | continue
849 | col = layout.column()
850 | col.scale_x = scale
851 | col.label(text="")
852 |
853 |
854 | def parse_sort(sort: str):
855 | if sort.startswith("[") and sort.endswith("]") and (n.digit() or n == "/" or n == ',' for n in sort[1:-1]):
856 | sort = sort[1:-1].split(",")
857 | if len(sort) > 2 or len(sort) < 1:
858 | return [-1, [-1]]
859 | col = int(sort[0])
860 | if col < 0:
861 | col = -1
862 | if len(sort) > 1:
863 | split = sort[1].split("/")
864 | if len(split) > 2:
865 | split = split[:2]
866 | elif len(split) < 1:
867 | split.append('0')
868 | if "" in split:
869 | rows = [-1]
870 | else:
871 | rows = [float(i) for i in split]
872 | for row in rows:
873 | if row < 0:
874 | row = 0
875 | else:
876 | rows = [-1]
877 | return [col, rows]
878 | else:
879 | return [-1, [-1]]
880 |
881 |
882 | def draw_prop(layout: UILayout, prop):
883 | layout.prop(prop, 'prop', text=prop.name)
884 |
885 |
886 | def draw_vector_prop(layout: UILayout, prop):
887 | layout.prop(eval(prop.address), 'prop', text=prop.name)
888 |
889 |
890 | def draw_list_prop(layout: UILayout, props):
891 | box = layout.box()
892 | box.label(text=props.name)
893 | for prop in props.prop:
894 | if prop.ptype.endswith("vector"):
895 | address = getattr(prop, "%s_prop" % prop.ptype)
896 | box.prop(eval(address), 'prop', text="")
897 | elif prop.ptype == 'enum':
898 | box.prop(getattr(prop, "%s_prop" % prop.ptype), 'prop', text="")
899 | else:
900 | box.prop(prop, "%s_prop" % prop.ptype, text="")
901 |
902 |
903 | def draw_prop_search(layout: UILayout, prop, context: Context, context_prop):
904 | layout.prop_search(prop, 'prop', context, context_prop, text=prop.name)
905 |
906 |
907 | PY_TYPE_TO_BLENDER_TYPE = {
908 | str: 'String',
909 | int: 'Int',
910 | float: 'Float',
911 | bool: 'Bool',
912 | bpy.types.Object: 'Object',
913 | }
914 |
915 |
916 | def get_all_variables(text: str) -> list:
917 | variables = []
918 | last_line = ""
919 | for i, line in enumerate(text.splitlines()):
920 | line = line.strip()
921 | split = line.split("=")
922 | if len(split) != 2 or line.startswith("#") or last_line.startswith("#STB-Input"):
923 | last_line = line
924 | continue
925 |
926 | name, value = split
927 | name = name.strip()
928 | if "," in name or " " in name or name == "":
929 | last_line = line
930 | continue
931 |
932 | value = value.strip()
933 | try:
934 | evaluated = eval(value)
935 | except Exception:
936 | last_line = line
937 | continue
938 | if isinstance(evaluated, (bool, str, int, float, bpy.types.Object)):
939 | variables.append((
940 | i,
941 | line,
942 | value,
943 | PY_TYPE_TO_BLENDER_TYPE[type(evaluated)]
944 | ))
945 | elif isinstance(evaluated, (tuple, list)) and len(evaluated) > 0 and value[0] in "[(":
946 | # ENUM
947 | if (len(evaluated) == 2
948 | and isinstance(evaluated[1], (list, tuple))
949 | and isinstance(evaluated[0], str)
950 | and all(map(lambda x: isinstance(x, str), evaluated[1]))):
951 | variables.append((i, line, value, "Enum"))
952 | last_line = line
953 | continue
954 | # VECTOR
955 | is_vector = False
956 | for py_type, bl_type in {(bool, "BoolVector"), (int, "IntVector"), (float, "FloatVector")}:
957 | if (all(map(lambda x: isinstance(x, py_type), evaluated))
958 | and len(evaluated) <= 32):
959 | is_vector = True
960 | variables.append((i, line, value, bl_type))
961 | break
962 | if is_vector:
963 | last_line = line
964 | continue
965 | # LIST
966 | variables.append((i, line, value, "List"))
967 | last_line = line
968 | return variables
969 |
970 |
971 | def get_all_properties(button: STB_button_properties) -> tuple:
972 | return sorted(
973 | (
974 | *button.StringProps,
975 | *button.IntProps,
976 | *button.FloatProps,
977 | *button.BoolProps,
978 | *button.EnumProps,
979 | *button.ObjectProps,
980 | *button.ListProps,
981 | *button.IntVectorProps,
982 | *button.FloatVectorProps,
983 | *button.BoolVectorProps
984 | ),
985 | key=lambda x: x.line
986 | )
987 |
--------------------------------------------------------------------------------
/src/menus.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from bpy.types import Menu, Context
3 | from . import dynamic_panels as panels
4 |
5 |
6 | class STB_MT_ButtonMenu(bpy.types.Menu):
7 | bl_idname = "STB_MT_ButtonMenu"
8 | bl_label = "Script To Buttons"
9 |
10 | def draw(self, context: Context):
11 | layout = self.layout
12 | for index, name in enumerate(panels.panel_names):
13 | menu_name = "STB_MT_Buttons_%s" % index
14 | if getattr(bpy.types, menu_name).poll(context):
15 | layout.menu(menu_name, text=name)
16 |
17 |
18 | def register():
19 | bpy.utils.register_class(STB_MT_ButtonMenu)
20 |
21 |
22 | def unregister():
23 | bpy.utils.unregister_class(STB_MT_ButtonMenu)
24 |
--------------------------------------------------------------------------------
/src/operators.py:
--------------------------------------------------------------------------------
1 | import typing
2 | import bpy
3 | from bpy.types import Operator, Context, Event, PropertyGroup, UILayout
4 | from bpy.props import StringProperty, EnumProperty, BoolProperty, CollectionProperty
5 | from bpy_extras.io_utils import ImportHelper, ExportHelper
6 | from . functions import get_preferences
7 | from . import functions
8 | from . import properties
9 | import traceback
10 | import sys
11 | from os.path import splitext, join
12 | from types import ModuleType
13 | import os
14 |
15 |
16 | class STB_OT_AddButton(Operator):
17 | bl_idname = "stb.add_button"
18 | bl_label = "Add Button"
19 | bl_description = 'Add a script as Button to the "Buttons" Panel'
20 | bl_options = {"REGISTER", "UNDO"}
21 |
22 | show_skip: BoolProperty(default=False, name="Show Skip")
23 | mode: EnumProperty(
24 | name="Change Mode",
25 | default="add",
26 | items=[
27 | ("add", "Add", ""),
28 | ("skip", "Skip", "")
29 | ]
30 | )
31 | name: StringProperty(name="Name")
32 | text: StringProperty(name="Text")
33 |
34 | def items_text_list(self, context: Context):
35 | return [(self.text, self.text, "")]
36 | text_list: EnumProperty(name="Text", items=items_text_list)
37 |
38 | all_names = []
39 |
40 | def draw(self, context: Context):
41 | STB_pref = get_preferences(context)
42 | layout = self.layout
43 | if self.show_skip:
44 | layout.prop(self, 'mode', expand=True)
45 | if self.mode == 'skip':
46 | return
47 | if self.name in self.all_names:
48 | box = layout.box()
49 | box.alert = True
50 | box.label(
51 | text="\"%s\" will be overwritten" % self.name,
52 | icon='ERROR'
53 | )
54 | col = layout.column()
55 | col.prop(self, 'name')
56 | col = layout.column()
57 | if self.show_skip:
58 | col.enabled = False
59 | col.prop(self, 'text_list')
60 | else:
61 | if len(bpy.data.texts):
62 | col.prop(STB_pref, 'texts_list')
63 | else:
64 | col.label(text="No Text available", icon="ERROR")
65 |
66 | def invoke(self, context: Context, event: Event):
67 | self.all_names = functions.get_all_button_names(context)
68 | return context.window_manager.invoke_props_dialog(self)
69 |
70 | def execute(self, context: Context) -> set[str]:
71 | STB_pref = get_preferences(context)
72 | STB_pref.button_name = self.name
73 | self.all_names = functions.get_all_button_names(context)
74 | txt = STB_pref.texts_list
75 | if self.show_skip:
76 | txt = self.text
77 | if self.mode == 'skip':
78 | return {"FINISHED"}
79 | if self.name == '':
80 | self.report({'ERROR'}, "You need a name for the Button")
81 | return {"FINISHED"}
82 | elif self.name in self.all_names:
83 | self.report({'INFO'}, "%s has been overwritten" % txt)
84 | elif STB_pref.texts_list == '':
85 | self.report({'ERROR'}, "You need to select a Text")
86 | return {"FINISHED"}
87 | fails = functions.add_button(context, self.name, txt)
88 | if len(fails[0]) or len(fails[1]):
89 | self.report(
90 | {'ERROR'},
91 | "Not all Areas or Properties could be added because the Syntax is invalid: %s" % (
92 | functions.create_fail_message(fails))
93 | )
94 | context.area.tag_redraw()
95 | return {"FINISHED"}
96 |
97 |
98 | class STB_OT_ScriptButton(Operator):
99 | bl_idname = "stb.script_button"
100 | bl_label = "ScriptButton"
101 | bl_options = {"UNDO", "INTERNAL"}
102 |
103 | name: StringProperty()
104 |
105 | def draw(self, context: Context):
106 | layout = self.layout
107 | stb = context.scene.stb
108 | STB_pref = get_preferences(context)
109 | if len(stb):
110 | button = stb[STB_pref.selected_button]
111 | sort, back = functions.sort_props(button, 'Dialog')
112 | if len(sort) > 0 or len(back) > 0:
113 | functions.draw_sort(sort, back, layout)
114 | else:
115 | layout.label(text="No Properties")
116 |
117 | def invoke(self, context: Context, event: Event):
118 | stb = context.scene.stb
119 | if len(stb):
120 | button = stb[self.name]
121 | sort, back = functions.sort_props(button, 'Dialog')
122 | if len(sort) > 0 or len(back) > 0:
123 | return bpy.context.window_manager.invoke_props_dialog(self)
124 | else:
125 | return self.execute(context)
126 |
127 | def execute(self, context: Context):
128 | STb_pref = get_preferences(context)
129 | stb = context.scene.stb
130 | if bpy.data.texts.find(self.name) == -1:
131 | functions.get_text(self.name)
132 | functions.update_all_props(stb[self.name], context)
133 | text = bpy.data.texts[self.name]
134 | try:
135 | # similar to text.as_module() -> internal Blender function see ..\scripts\modules\bpy_types.py
136 | name = text.name
137 | mod = ModuleType(splitext(name)[0])
138 | mod.__dict__.update({
139 | "__file__": join(bpy.data.filepath, name),
140 | "__name__": "__main__"
141 | })
142 | exec(text.as_string(), mod.__dict__)
143 |
144 | if STb_pref.delete_script_after_run:
145 | bpy.data.texts.remove(text)
146 | except Exception:
147 | error = traceback.format_exception(*sys.exc_info())
148 | # corrects the filename of the exception to the text name, otherwise ""
149 | error_split = error[3].replace('""', '').split(',')
150 | error[3] = '%s "%s",%s' % (
151 | error_split[0], text.name, error_split[1])
152 | # removes exec(self.as_string(), mod.__dict__)
153 | error.pop(1)
154 | error = "".join(error)
155 | if error:
156 | self.report(
157 | {'ERROR'}, "The linked Script is not working\n\n%s" % error)
158 | if STb_pref.delete_script_after_run:
159 | bpy.data.texts.remove(text)
160 | return {'CANCELLED'}
161 | return {"FINISHED"}
162 |
163 |
164 | class STB_OT_RemoveButton(Operator):
165 | bl_idname = "stb.remove_button"
166 | bl_label = "Remove"
167 | bl_description = "Delete the selected Button"
168 | bl_options = {"REGISTER", "UNDO"}
169 |
170 | delete_file: BoolProperty(
171 | name="Delete File",
172 | description="Deletes the saved .py in the Storage",
173 | default=True
174 | )
175 | delete_text: BoolProperty(
176 | name="Delete Text",
177 | description="Deletes the linked Text in the Texteditor",
178 | default=True
179 | )
180 |
181 | @classmethod
182 | def poll(cls, context: Context):
183 | STB_pref = get_preferences(context)
184 | return STB_pref.selected_button != ""
185 |
186 | def draw(self, context: Context):
187 | STB_pref = get_preferences(context)
188 | layout = self.layout
189 | layout.prop(self, 'delete_file', text="Delete File")
190 | row = layout.row()
191 | text_enabled = bpy.data.texts.find(STB_pref.selected_button) != -1
192 | row.enabled = text_enabled
193 | self.deleteText = text_enabled
194 | row.prop(self, 'delete_text', text="Delete Text", toggle=False)
195 |
196 | def invoke(self, context: Context, event: Event):
197 | return context.window_manager.invoke_props_dialog(self)
198 |
199 | def execute(self, context):
200 | functions.remove_button(context, self.delete_file, self.delete_text)
201 | context.area.tag_redraw()
202 | return {"FINISHED"}
203 |
204 |
205 | class STB_OT_Load(Operator):
206 | bl_idname = "stb.load"
207 | bl_label = "Load"
208 | bl_description = "Load all Buttons from File or Texteditor"
209 | bl_options = {"REGISTER", "UNDO"}
210 |
211 | mode: EnumProperty(
212 | name="Load from",
213 | description="Change the Mode which to load",
214 | items=[
215 | ("file", "Load from Disk", ""),
216 | ("texteditor", "Load from Texteditor", "")
217 | ]
218 | )
219 | all: BoolProperty(
220 | name="Load all",
221 | description="Load all Buttons from the Texteditor",
222 | default=False
223 | )
224 | texts: CollectionProperty(
225 | type=properties.STB_text_property,
226 | name="Texts in Texteditor"
227 | )
228 |
229 | @classmethod
230 | def poll(cls, context: Context):
231 | STB_pref = get_preferences(context)
232 | return STB_pref.selected_button != ""
233 |
234 | def draw(self, context: Context):
235 | layout = self.layout
236 | layout.prop(self, 'mode', expand=True)
237 | if self.mode == "file":
238 | # File -------------------------------------------
239 | box = layout.box()
240 | col = box.column()
241 | col.scale_y = 0.8
242 | col.label(text="It will delete all your current Buttons", icon="INFO")
243 | col.label(
244 | text="and replace it with the Buttons from the Disk",
245 | icon="BLANK1"
246 | )
247 | else:
248 | # Texteditor -------------------------------------
249 | box = layout.box()
250 | box.prop(self, 'all', text="Load All", toggle=True)
251 | if self.all:
252 | for text in self.texts:
253 | box.label(text=text.name, icon='CHECKBOX_HLT')
254 | else:
255 | for text in self.Texts:
256 | box.prop(text, 'select', text=text.name)
257 |
258 | def invoke(self, context: Context, event: Event):
259 | self.texts.clear()
260 | for text in bpy.data.texts:
261 | new = self.texts.add()
262 | new.name = text.name
263 | return context.window_manager.invoke_props_dialog(self)
264 |
265 | def execute(self, context: Context):
266 | if self.mode == "file":
267 | fails = functions.load(context)
268 | elif self.mode == "texteditor":
269 | fails = functions.load_from_texteditor(self, context)
270 | message = "\n"
271 | for name, fail in zip(fails[0], fails[1]):
272 | if len(fail[0]) or len(fail[1]):
273 | message += "\n %s:" % name
274 | message += functions.create_fail_message(fail)
275 | if message != "\n":
276 | self.report(
277 | {'ERROR'},
278 | "Not all Areas or Properties could be added because the Syntax is invalid: %s" % message
279 | )
280 | context.area.tag_redraw()
281 | return {"FINISHED"}
282 |
283 |
284 | class STB_OT_Reload(Operator):
285 | bl_idname = "stb.reload"
286 | bl_label = "Reload"
287 | bl_description = "Reload the linked Text in the Texteditor of the selected Button"
288 | bl_options = {"REGISTER"}
289 |
290 | @classmethod
291 | def poll(cls, context: Context):
292 | STB_pref = get_preferences(context)
293 | return STB_pref.selected_button != ""
294 |
295 | def execute(self, context: Context):
296 | STB_pref = get_preferences(context)
297 | stb = context.scene.stb
298 | text_index = bpy.data.texts.find(STB_pref.selected_button)
299 | if text_index != -1:
300 | if STB_pref.autosave:
301 | functions.save_text(
302 | bpy.data.texts[text_index],
303 | STB_pref.selected_button
304 | )
305 | fails = functions.reload_button_text(
306 | stb[STB_pref.selected_button],
307 | bpy.data.texts[text_index].as_string(),
308 | context.scene
309 | )
310 | if len(fails[0]) or len(fails[1]):
311 | self.report(
312 | {'ERROR'},
313 | "Not all Areas or Properties could be added because the Syntax is invalid: %s" % (
314 | functions.create_fail_message(fails))
315 | )
316 | else:
317 | self.report(
318 | {'ERROR'},
319 | ("%s could not be reloaded, linked Text in Texteditor don't exist.\n"
320 | "\n"
321 | "INFO: The linked Text must have the same name as the Button"
322 | ) % STB_pref.selected_button
323 | )
324 | context.area.tag_redraw()
325 | return {"FINISHED"}
326 |
327 |
328 | class STB_OT_Save(Operator):
329 | bl_idname = "stb.save"
330 | bl_label = "Save"
331 | bl_description = "Save all buttons to the Storage"
332 |
333 | @classmethod
334 | def poll(cls, context: Context):
335 | STB_pref = get_preferences(context)
336 | return STB_pref.selected_button != ""
337 |
338 | def execute(self, context):
339 | Fails = []
340 | for button in context.scene.stb:
341 | if bpy.data.texts.find(button.name) != -1:
342 | functions.save_text(bpy.data.texts[button.name], button.name)
343 | else:
344 | Fails.append(button.name)
345 | if len(Fails) > 0:
346 | error_text = "Not all Scripts could be saved:"
347 | for fail in Fails:
348 | error_text += "\n%s could not be saved, linked Text is missing" % fail
349 | self.report({'ERROR'}, error_text)
350 | return {"FINISHED"}
351 |
352 |
353 | class STB_OT_Export(Operator, ExportHelper):
354 | bl_idname = "stb.export"
355 | bl_label = "Export"
356 | bl_description = "Export the selected Buttons"
357 |
358 | export_buttons: CollectionProperty(type=properties.STB_export_button)
359 |
360 | def get_all(self):
361 | return self.get("all", False)
362 |
363 | def set_all(self, value):
364 | if value == self.get("all", False):
365 | return
366 | self["all"] = value
367 | for button in self.export_buttons:
368 | button["export_all"] = value
369 |
370 | all: BoolProperty(
371 | name="All",
372 | description="Export all Buttons",
373 | get=get_all,
374 | set=set_all
375 | )
376 |
377 | def get_mode(self):
378 | return self.get("mode", 0)
379 |
380 | def set_mode(self, value):
381 | self["mode"] = value
382 | if value == 0:
383 | self.filepath = self.directory
384 |
385 | mode: EnumProperty(
386 | name="Mode",
387 | items=[
388 | ("py", "Export as .py Files", ""),
389 | ("zip", "Export as .zip File", "")
390 | ],
391 | get=get_mode,
392 | set=set_mode
393 | )
394 |
395 | def get_filter_glob(self):
396 | return "*.zip" * (self.mode == "zip")
397 |
398 | filter_glob: StringProperty(
399 | default='*.zip',
400 | options={'HIDDEN'},
401 | maxlen=255,
402 | get=get_filter_glob
403 | )
404 |
405 | def get_filename_ext(self):
406 | return ".zip" * (self.mode == "zip")
407 |
408 | filename = ""
409 | filename_ext: StringProperty(default=".", get=get_filename_ext)
410 |
411 | def get_use_filter_folder(self):
412 | return self.mode == "py"
413 |
414 | use_filter_folder: BoolProperty(default=True, get=get_use_filter_folder)
415 | filepath: StringProperty(name="File Path", maxlen=1024, default="")
416 | directory: StringProperty(name="Folder Path", maxlen=1024, default="")
417 |
418 | @classmethod
419 | def poll(cls, context: Context):
420 | STB_pref = get_preferences(context)
421 | return STB_pref.selected_button != ""
422 |
423 | def draw(self, context: Context):
424 | layout = self.layout
425 | layout.prop(self, 'mode', expand=True)
426 | box = layout.box()
427 | box.prop(self, 'all')
428 | for button in self.export_buttons:
429 | box.prop(button, 'use', text=button.name)
430 |
431 | def invoke(self, context: Context, event: Event):
432 | super().invoke(context, event)
433 | self.export_buttons.clear()
434 | for button in context.scene.stb:
435 | new = self.export_buttons.add()
436 | new.name = button.name
437 | return {'RUNNING_MODAL'}
438 |
439 | def execute(self, context: Context):
440 | if self.mode == "py":
441 | if not os.path.isdir(self.directory):
442 | self.report({'ERROR'}, "The given directory does not exists")
443 | return {'CANCELLED'}
444 | self.filepath = self.directory
445 | else:
446 | if not self.filepath.endswith(".zip"):
447 | self.report({'ERROR'}, "The given filepath is not a .zip file")
448 | return {'CANCELLED'}
449 | functions.export(
450 | self.mode,
451 | filter(lambda x: x.use, self.export_buttons),
452 | self.filepath
453 | )
454 | return {"FINISHED"}
455 |
456 |
457 | class STB_OT_Import(Operator, ImportHelper):
458 | bl_idname = "stb.import"
459 | bl_label = "Import"
460 | bl_description = "Import the selected Files"
461 |
462 | filter_glob: StringProperty(
463 | default='*.zip;*.py',
464 | options={'HIDDEN'},
465 | maxlen=255
466 | )
467 | files: CollectionProperty(type=PropertyGroup)
468 |
469 | @classmethod
470 | def poll(cls, context: Context):
471 | STB_pref = get_preferences(context)
472 | return STB_pref.selected_button != ""
473 |
474 | def execute(self, context: Context):
475 | not_added_file = []
476 | button_fails = ([], [])
477 | directory = os.path.dirname(self.filepath)
478 | for file in self.files:
479 | if file.name.endswith(".zip"):
480 | zip_fails = functions.import_zip(
481 | os.path.join(directory, file.name),
482 | context
483 | )
484 | button_fails[0].extend(zip_fails[0])
485 | button_fails[1].extend(zip_fails[1])
486 | elif file.name.endswith(".py"):
487 | py_fail = functions.import_py(
488 | os.path.join(directory, file.name),
489 | context
490 | )
491 | button_fails[0].extend(py_fail[0])
492 | button_fails[1].append(py_fail[1])
493 | else:
494 | not_added_file.append(file)
495 |
496 | has_message = False
497 | message = "Not all Files could be added:\n"
498 | for file in not_added_file:
499 | message += "%s\n" % file
500 | has_message = True
501 |
502 | has_fail_message = False
503 | fail_message = "Not all Areas or Properties could be added because the Syntax is invalid:\n"
504 | for name, fails in zip(button_fails[0], button_fails[1]):
505 | if len(fails[0]) or len(fails[1]):
506 | fail_message += "\n %s:" % name
507 | fail_message += functions.create_fail_message(fails)
508 | has_fail_message = True
509 |
510 | if has_message and has_fail_message:
511 | self.report({'ERROR'}, "%s\n\n%s" % (message, fail_message))
512 | elif has_message:
513 | self.report({'ERROR'}, message)
514 | elif has_fail_message:
515 | self.report({'ERROR'}, fail_message)
516 | context.area.tag_redraw()
517 | return {"FINISHED"}
518 |
519 |
520 | class STB_OT_Edit(Operator):
521 | bl_idname = "stb.edit"
522 | bl_label = "Edit"
523 | bl_description = "Edit the selected Button"
524 | bl_options = {"UNDO"}
525 |
526 | area_items = [ # (identifier, name, description, icon, value)
527 | ('', 'General', '', ''),
528 | ('3D_Viewport', '3D Viewport', '', 'VIEW3D'),
529 | ('Image_Editor', 'Image Editor', '', 'IMAGE'),
530 | ('UV_Editor', 'UV Editor', '', 'UV'),
531 | ('Compositor', 'Compositor', '', 'NODE_COMPOSITING'),
532 | ('Texture_Node_Editor', 'Texture Node Editor', '', 'NODE_TEXTURE'),
533 | ('Geomerty_Node_Editor', 'Geomerty Node Editor', '', 'NODETREE'),
534 | ('Shader_Editor', 'Shader Editor', '', 'NODE_MATERIAL'),
535 | ('Video_Sequencer', 'Video Sequencer', '', 'SEQUENCE'),
536 | ('Movie_Clip_Editor', 'Movie Clip Editor', '', 'TRACKER'),
537 |
538 | ('', 'Animation', '', ''),
539 | ('Dope_Sheet', 'Dope Sheet', '', 'ACTION'),
540 | ('Timeline', 'Timeline', '', 'TIME'),
541 | ('Graph_Editor', 'Graph Editor', '', 'GRAPH'),
542 | ('Drivers', 'Drivers', '', 'DRIVER'),
543 | ('Nonlinear_Animation', 'Nonlinear Animation', '', 'NLA'),
544 |
545 | ('', 'Scripting', '', ''),
546 | ('Text_Editor', 'Text Editor', '', 'TEXT')
547 | ]
548 |
549 | name: StringProperty(name="Name")
550 | stb_properties: CollectionProperty(type=properties.STB_edit_property_item)
551 |
552 | @classmethod
553 | def poll(cls, context: Context):
554 | STB_pref = get_preferences(context)
555 | return STB_pref.selected_button != ""
556 |
557 | def items_stb_select_area(self, context: Context):
558 | for item in self.stb_areas:
559 | if item.delete:
560 | self.stb_areas.remove(self.stb_areas.find(item.name))
561 | used_areas = set(area.name for area in self.stb_areas)
562 | areas = []
563 | for i, (identifier, name, description, icon) in enumerate(STB_OT_Edit.area_items):
564 | if identifier in used_areas:
565 | continue
566 | areas.append((
567 | identifier,
568 | name,
569 | description,
570 | icon,
571 | i * (identifier != '') - 1
572 | ))
573 | return areas
574 | stb_select_area: EnumProperty(items=items_stb_select_area, default=0)
575 | stb_areas: CollectionProperty(type=properties.STB_edit_area_item)
576 |
577 | def get_add_area(self):
578 | return False
579 |
580 | def set_add_area(self, value):
581 | identifier = self.stb_select_area
582 | icon = UILayout.enum_item_icon(self, 'stb_select_area', identifier)
583 | label = UILayout.enum_item_name(self, 'stb_select_area', identifier)
584 | if identifier == '':
585 | return
586 | new = self.stb_areas.add()
587 | new.name = identifier
588 | new.label = label
589 | new.icon = icon
590 | items = STB_OT_Edit.items_stb_select_area(self, bpy.context)
591 | for item in items:
592 | if item[0] == '':
593 | continue
594 | self.stb_select_area = item[0]
595 | break
596 | add_area: BoolProperty(default=False, get=get_add_area, set=set_add_area)
597 |
598 | def draw(self, context: Context):
599 | layout = self.layout
600 | layout.prop(self, 'name')
601 |
602 | layout.separator(factor=0.5)
603 | layout.label(text="Areas")
604 | row = layout.row(align=True)
605 | row.prop(self, 'stb_select_area')
606 | row.prop(self, 'add_area', icon="ADD", icon_only=True)
607 | box = layout.box()
608 | if len(self.stb_areas):
609 | for area in filter(lambda x: not x.delete, self.stb_areas):
610 | row = box.row()
611 | row.label(text=area.label, icon_value=area.icon)
612 | row.prop(area, 'delete', icon='X', icon_only=True, emboss=False)
613 | else:
614 | box.label(text="All Areas", icon='RESTRICT_COLOR_ON')
615 |
616 | properties = list(filter(lambda x: not x.use_delete, self.stb_properties))
617 | if len(properties):
618 | layout.separator(factor=0.5)
619 | layout.label(text="Properties")
620 | box = layout.box()
621 | for prop in properties:
622 | row = box.row()
623 | row.label(text=f"{prop.name} [Ln {prop.line}]")
624 | row.prop(prop, 'use_delete', icon='X', icon_only=True, emboss=False)
625 |
626 | def invoke(self, context, event):
627 | STB_pref = get_preferences(context)
628 | stb = context.scene.stb
629 | button = stb[STB_pref.selected_button]
630 | self.name = button.name
631 | self.stb_properties.clear()
632 | self.stb_areas.clear()
633 | for prop in functions.get_all_properties(button):
634 | new = self.stb_properties.add()
635 | new.name = prop.name
636 | new.line = prop.line
637 | new.linename = prop.linename
638 | return context.window_manager.invoke_props_dialog(self, width=250)
639 |
640 | def execute(self, context):
641 | functions.rename(context, self.name)
642 |
643 | STB_pref = get_preferences(context)
644 | property_changed = False
645 |
646 | text_index = bpy.data.texts.find(STB_pref.selected_button)
647 | if text_index == -1:
648 | functions.get_text(STB_pref.selected_button)
649 | text = bpy.data.texts[STB_pref.selected_button]
650 | else:
651 | text = bpy.data.texts[text_index]
652 | lines = [line.body for line in text.lines]
653 |
654 | if len(self.stb_areas):
655 | property_changed = True
656 | if lines[0].strip().startswith("#STB"):
657 | line = lines[0]
658 | line += " /// "
659 | else:
660 | line = ""
661 | lines.insert(0, line)
662 | line += " /// ".join(map(lambda x: "#STB-Area-%s" % x.name, self.stb_areas))
663 | lines[0] = line
664 |
665 | edited_lines = []
666 | for prop in filter(lambda x: x.use_delete, self.stb_properties):
667 | property_changed = True
668 | line: str = lines[prop.line - 1]
669 | line_start = line.find("#STB")
670 | if line_start == -1:
671 | continue
672 |
673 | if (init_start_position := line.find("#STB-InitValue-")) != -1:
674 | init_start_position += len("#STB-InitValue-")
675 | init_end_position = line.find("-END", init_start_position)
676 | init_value = line[init_start_position: init_end_position]
677 | lines[prop.line] = "%s= %s" % (prop.linename, init_value)
678 |
679 | and_position = line.find("///", line_start)
680 | end_position = line.find("#STB", and_position)
681 | while ((and_next := line.find("///", end_position)) != -1
682 | and (end_next := line.find("#STB", and_next)) != -1):
683 | and_position = and_next
684 | end_position = end_next
685 |
686 | if and_position != -1 and end_position != -1:
687 | line_end = line.find(" ", end_position)
688 | else:
689 | line_end = line.find(" ", line_start)
690 |
691 | if line_end == -1:
692 | line = ""
693 | else:
694 | line = line[:line_start] + line[line_end:]
695 |
696 | lines[prop.line - 1] = line
697 | edited_lines.append(prop.line - 1)
698 |
699 | for i in sorted(edited_lines, reverse=True):
700 | line = lines[i]
701 | if line.strip() == "":
702 | lines.pop(i)
703 |
704 | if property_changed:
705 | text.clear()
706 | text.write("\n".join(lines))
707 | bpy.ops.stb.reload()
708 | context.area.tag_redraw()
709 | return {"FINISHED"}
710 |
711 |
712 | class STB_OT_LoadSingleButton(Operator):
713 | bl_idname = "stb.load_single_button"
714 | bl_label = "Load Button"
715 | bl_description = "Load the script of the selected Button into the Texteditor"
716 |
717 | @classmethod
718 | def poll(cls, context: Context):
719 | STB_pref = get_preferences(context)
720 | return STB_pref.selected_button != ""
721 |
722 | def execute(self, context: Context):
723 | STB_pref = get_preferences(context)
724 | stb = context.scene.stb
725 | functions.get_text(STB_pref.selected_button)
726 | functions.update_all_props(stb[STB_pref.selected_button], context)
727 | return {"FINISHED"}
728 |
729 |
730 | class STB_OT_AddProperty(Operator):
731 | bl_idname = "stb.add_property"
732 | bl_label = "Add Property"
733 | bl_description = "Add a variable from the script as a property"
734 |
735 | text_variables: CollectionProperty(
736 | type=properties.STB_add_property_item,
737 | options={'HIDDEN'}
738 | )
739 |
740 | def text_properties_items(self, context):
741 | return [
742 | (str(i), f"{item.line} [Ln {item.position}]", "")
743 | for i, item in enumerate(self.text_variables)
744 | ]
745 | text_properties: EnumProperty(
746 | items=text_properties_items,
747 | options={'HIDDEN'}
748 | )
749 | space: EnumProperty(
750 | items=[
751 | ("Panel", "Panel", "Show this property in the Panel"),
752 | ("Dialog", "Dialog",
753 | "Show this property in a Dialog when the script is executed"),
754 | ("PanelDialog", "Panel & Dialog",
755 | "Show this property in the Panel and Dialog")
756 | ],
757 | options={'HIDDEN'}
758 | )
759 |
760 | @classmethod
761 | def poll(cls, context: Context):
762 | STB_pref = get_preferences(context)
763 | return STB_pref.selected_button != ""
764 |
765 | def invoke(self, context: Context, event: Event):
766 | STB_pref = get_preferences(context)
767 | text_index = bpy.data.texts.find(STB_pref.selected_button)
768 | if text_index == -1:
769 | functions.get_text(STB_pref.selected_button)
770 | text = bpy.data.texts[STB_pref.selected_button]
771 | else:
772 | text = bpy.data.texts[text_index]
773 | items = self.text_properties_items(context)
774 | if len(items) != 0:
775 | self.text_properties = items[0][0]
776 | self.text_variables.clear()
777 | for position, line, value, bl_type in functions.get_all_variables(text.as_string()):
778 | var = self.text_variables.add()
779 | var.position = position
780 | var.line = line
781 | var.value = value
782 | var.type = bl_type
783 | return context.window_manager.invoke_props_dialog(self)
784 |
785 | def draw(self, context: Context):
786 | layout = self.layout
787 | if len(self.text_properties_items(context)) == 0:
788 | layout.label(text="No Property to add")
789 | return
790 | layout.prop(self, 'text_properties', text="Property")
791 | layout.prop(self, 'space', text="Space")
792 |
793 | def execute(self, context: Context):
794 | if self.text_properties == "":
795 | return {'CANCELLED'}
796 | STB_pref = get_preferences(context)
797 | text = bpy.data.texts[STB_pref.selected_button]
798 | index = int(self.text_properties)
799 | item = self.text_variables[index]
800 | lines = [line.body for line in text.lines]
801 | if self.space == "PanelDialog":
802 | insert_comment = (
803 | f"#STB-Input-Panel-{item.type} /// #STB-Input-Dialog-{item.type} /// #STB-InitValue-"
804 | + f"{item.value}-END")
805 | else:
806 | insert_comment = f"#STB-Input-{self.space}-{item.type} /// #STB-InitValue-{item.value}-END"
807 | lines.insert(item.position, insert_comment)
808 | text.clear()
809 | text.write("\n".join(lines))
810 | bpy.ops.stb.reload()
811 | return {'FINISHED'}
812 |
813 |
814 | classes = [
815 | STB_OT_AddButton,
816 | STB_OT_ScriptButton,
817 | STB_OT_RemoveButton,
818 | STB_OT_Load,
819 | STB_OT_Reload,
820 | STB_OT_Save,
821 | STB_OT_Export,
822 | STB_OT_Import,
823 | STB_OT_Edit,
824 | STB_OT_LoadSingleButton,
825 | STB_OT_AddProperty
826 | ]
827 |
828 |
829 | def register():
830 | for cls in classes:
831 | bpy.utils.register_class(cls)
832 |
833 |
834 | def unregister():
835 | for cls in classes:
836 | bpy.utils.unregister_class(cls)
837 |
--------------------------------------------------------------------------------
/src/panels.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from bpy.types import Panel, Context
3 | from .functions import get_preferences
4 | from . import functions
5 |
6 | classes = []
7 |
8 | ui_space_types = [
9 | 'CLIP_EDITOR', 'NODE_EDITOR', 'TEXT_EDITOR', 'SEQUENCE_EDITOR', 'NLA_EDITOR',
10 | 'DOPESHEET_EDITOR', 'VIEW_3D', 'GRAPH_EDITOR', 'IMAGE_EDITOR'
11 | ] # blender spaces with UI region
12 |
13 |
14 | def panel_factory(space_type):
15 | class STB_PT_ScriptToButton(Panel):
16 | bl_idname = "STB_PT_ScriptToButton_%s" % space_type
17 | bl_label = "Script To Button"
18 | bl_space_type = space_type
19 | bl_region_type = "UI"
20 | bl_category = "Script To Button"
21 |
22 | def draw(self, context: Context):
23 | layout = self.layout
24 | STB_pref = get_preferences(context)
25 | col = layout.column()
26 | row = col.row(align=True)
27 | row.operator("stb.add_button", text="Add", icon='ADD')
28 | row.operator("stb.remove_button", text="Remove", icon='REMOVE')
29 | if STB_pref.autosave:
30 | row = col.row()
31 | row.operator("stb.load", text="Load")
32 | row2 = row.row(align=True)
33 | row2.scale_x = 1.2
34 | row2.operator("stb.load_single_button", text="", icon='TEXT')
35 | row2.operator("stb.reload", text="", icon='FILE_REFRESH')
36 | row2.operator("stb.edit", text="", icon='GREASEPENCIL')
37 | else:
38 | row = col.row(align=True)
39 | row.operator("stb.load", text="Load")
40 | row.operator("stb.save", text="Save")
41 | row = col.row(align=True)
42 | row.operator(
43 | "stb.load_single_button",
44 | text="Load Button",
45 | icon='TEXT'
46 | )
47 | row = col.row(align=True)
48 | row.operator("stb.reload", text="Reload", icon='FILE_REFRESH')
49 | row.operator("stb.edit", text="Rename", icon='GREASEPENCIL')
50 | row = col.row(align=True)
51 | row.operator("stb.export", text="Export", icon='EXPORT')
52 | row.operator("stb.import", text="Import", icon='IMPORT')
53 | STB_PT_ScriptToButton.__name__ = "STB_PT_ScriptToButton_%s" % space_type
54 |
55 | class STB_PT_Properties(Panel):
56 | bl_idname = "STB_PT_Properties_%s" % space_type
57 | bl_label = "Properties"
58 | bl_space_type = space_type
59 | bl_region_type = "UI"
60 | bl_category = "Script To Button"
61 | bl_parent_id = "STB_PT_ScriptToButton_%s" % space_type
62 | bl_order = 2147483647 # max size
63 |
64 | def draw_header(self, context: Context):
65 | layout = self.layout
66 | layout.alignment = 'RIGHT'
67 | layout.operator('stb.add_property', text="", icon='ADD')
68 |
69 | def draw(self, context):
70 | layout = self.layout
71 | stb = context.scene.stb
72 | STB_pref = get_preferences(context)
73 | if len(stb):
74 | if STB_pref.selected_button == "":
75 | layout.label(text="No Properties")
76 | return
77 | button = stb[STB_pref.selected_button]
78 | sort, back = functions.sort_props(button, 'Panel')
79 | if not (len(sort) > 0 or len(back) > 0):
80 | layout.label(text="No Properties")
81 | return
82 | functions.draw_sort(sort, back, layout)
83 | STB_PT_Properties.__name__ = "STB_PT_Properties_%s" % space_type
84 |
85 | global classes
86 | classes += [
87 | STB_PT_ScriptToButton,
88 | STB_PT_Properties
89 | ]
90 |
91 |
92 | for space in ui_space_types:
93 | panel_factory(space)
94 |
95 |
96 | def register():
97 | for cls in classes:
98 | bpy.utils.register_class(cls)
99 |
100 |
101 | def unregister():
102 | for cls in classes:
103 | bpy.utils.unregister_class(cls)
104 |
--------------------------------------------------------------------------------
/src/preferences.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from bpy.types import AddonPreferences, Context
3 | from bpy.props import StringProperty, BoolProperty, EnumProperty, CollectionProperty, IntProperty
4 | from .functions import get_preferences
5 | import rna_keymap_ui
6 | from . import __package__ as base_package
7 |
8 | keymaps = {}
9 | keymap_items = []
10 |
11 |
12 | class STB_preferences(AddonPreferences):
13 | bl_idname = base_package
14 |
15 | button_name: StringProperty(
16 | name="Name",
17 | description="Set the name of the Button",
18 | default=""
19 | )
20 |
21 | def text_list_item(self, context):
22 | return [(i.name, i.name, "") for i in bpy.data.texts]
23 | texts_list: EnumProperty(
24 | name="Text",
25 | description="Chose a Text to convert into a Button",
26 | items=text_list_item
27 | )
28 | autosave: BoolProperty(
29 | name="Autosave",
30 | description="Save your changes automatically to the files",
31 | default=True
32 | )
33 | autoload: BoolProperty(
34 | name="Load to Texteditor",
35 | description="Load the script into the Texteditor on start, on add or on manuell load",
36 | default=False
37 | )
38 | delete_script_after_run: BoolProperty(
39 | name="Delete Script after Run",
40 | description="Delete the script in the editor after the linked script button was pressed",
41 | default=True
42 | )
43 |
44 | def get_selected_button(self):
45 | return bpy.context.scene.get("stb_button.selected_name", "")
46 | selected_button: StringProperty(get=get_selected_button, name="INTERNAL")
47 |
48 | def draw(self, context: Context) -> None:
49 | layout = self.layout
50 | row = layout.row()
51 | row.prop(self, 'autosave')
52 | row.prop(self, 'autoload')
53 | row.prop(self, 'delete_script_after_run')
54 | layout.separator(factor=0.8)
55 | col = layout.column()
56 | kc = bpy.context.window_manager.keyconfigs.user
57 | for addon_keymap in keymaps.values():
58 | km = kc.keymaps[addon_keymap.name].active()
59 | col.context_pointer_set("keymap", km)
60 | for kmi in km.keymap_items:
61 | if not any(kmi.name == item.name and kmi.idname == item.idname for item in keymap_items):
62 | continue
63 | rna_keymap_ui.draw_kmi(kc.keymaps, kc, km, kmi, col, 0)
64 |
65 |
66 | def register():
67 | bpy.utils.register_class(STB_preferences)
68 |
69 | addon = bpy.context.window_manager.keyconfigs.addon
70 | if addon:
71 | km = addon.keymaps.new(name='Screen')
72 | keymaps['default'] = km
73 | items = km.keymap_items
74 | kmi = items.new("wm.call_menu", 'Y', 'PRESS', shift=True, alt=True)
75 | kmi.properties.name = "STB_MT_ButtonMenu"
76 | keymap_items.append(kmi)
77 |
78 |
79 | def unregister():
80 | bpy.utils.unregister_class(STB_preferences)
81 | addon = bpy.context.window_manager.keyconfigs.addon
82 | if not addon:
83 | return
84 | for km in keymaps.values():
85 | if addon.keymaps.get(km.name):
86 | addon.keymaps.remove(km)
87 |
--------------------------------------------------------------------------------
/src/properties.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from bpy.types import PropertyGroup
3 | from bpy.props import (
4 | StringProperty, IntProperty, FloatProperty, BoolProperty, EnumProperty, CollectionProperty,
5 | PointerProperty
6 | )
7 | from .functions import update_text
8 | from . import functions
9 | from contextlib import suppress
10 |
11 | classes = []
12 |
13 |
14 | class STB_property:
15 | space: StringProperty()
16 | linename: StringProperty()
17 | line: IntProperty()
18 | sort: StringProperty()
19 |
20 |
21 | class STB_property_string(STB_property, PropertyGroup):
22 |
23 | def update_prop(self, context):
24 | txt = self.prop.replace('"', '\\"').replace("'", "\\'")
25 | update_text(
26 | self.line,
27 | self.linename,
28 | '"%s"' % txt,
29 | eval("context.scene.%s" % self.path_from_id().split(".")[0])
30 | )
31 |
32 | def set_prop(self, value):
33 | self["prop"] = value
34 | self.update_prop(bpy.context)
35 |
36 | def get_prop(self):
37 | return self.get("prop", True)
38 |
39 | prop: StringProperty(set=set_prop, get=get_prop)
40 |
41 |
42 | class STB_property_int(STB_property, PropertyGroup):
43 | def update_prop(self, context):
44 | update_text(
45 | self.line,
46 | self.linename,
47 | self.prop,
48 | eval("context.scene.%s" % self.path_from_id().split(".")[0])
49 | )
50 |
51 | def set_prop(self, value):
52 | self["prop"] = value
53 | self.update_prop(bpy.context)
54 |
55 | def get_prop(self):
56 | return self.get("prop", True)
57 |
58 | prop: IntProperty(set=set_prop, get=get_prop)
59 |
60 |
61 | class STB_property_float(STB_property, PropertyGroup):
62 | def update_prop(self, context):
63 | update_text(
64 | self.line,
65 | self.linename,
66 | self.prop,
67 | eval("context.scene.%s" % self.path_from_id().split(".")[0])
68 | )
69 |
70 | def set_prop(self, value):
71 | self["prop"] = value
72 | self.update_prop(bpy.context)
73 |
74 | def get_prop(self):
75 | return self.get("prop", True)
76 |
77 | prop: FloatProperty(set=set_prop, get=get_prop)
78 |
79 |
80 | class STB_property_bool(STB_property, PropertyGroup):
81 | def update_prop(self, context):
82 | update_text(
83 | self.line,
84 | self.linename,
85 | self.prop,
86 | eval("context.scene.%s" % self.path_from_id().split(".")[0])
87 | )
88 |
89 | def set_prop(self, value):
90 | self["prop"] = value
91 | self.update_prop(bpy.context)
92 |
93 | def get_prop(self):
94 | return self.get("prop", True)
95 |
96 | prop: BoolProperty(set=set_prop, get=get_prop)
97 |
98 |
99 | class STB_enum_item(PropertyGroup):
100 | item: StringProperty()
101 |
102 |
103 | class STB_property_enum(STB_property, PropertyGroup):
104 |
105 | def prop_items(self, context):
106 | return functions.list_to_enum_items([item.item for item in self.items])
107 |
108 | def update_prop(self, context):
109 | update_text(
110 | self.line,
111 | self.linename,
112 | [self.prop, [item.item for item in self.items]],
113 | eval("context.scene.%s" % self.path_from_id().split(".")[0])
114 | )
115 |
116 | prop: EnumProperty(items=prop_items, update=update_prop)
117 | items: CollectionProperty(type=STB_enum_item)
118 |
119 |
120 | class STB_vector_property(STB_property, PropertyGroup):
121 | address: StringProperty()
122 |
123 |
124 | class STB_enum_property(PropertyGroup):
125 | def prop_items(self, context):
126 | return functions.list_to_enum_items([item.item for item in self.items])
127 |
128 | def prop_update(self, context):
129 | split = self.path_from_id().split(".")
130 | if len(split) > 1:
131 | prop = eval("context.scene.%s" % ".".join(split[:2]))
132 | else:
133 | prop = eval(self.address)
134 | update_text(
135 | prop.line,
136 | prop.linename,
137 | [functions.type_getter(ele, ele.ptype) for ele in prop.prop],
138 | eval("context.scene.%s" % self.path_from_id().split(".")[0])
139 | )
140 |
141 | prop: EnumProperty(items=prop_items, update=prop_update)
142 | items: CollectionProperty(type=STB_enum_item)
143 |
144 |
145 | class STB_property_list_item(PropertyGroup):
146 |
147 | def update_prop(self, context):
148 | split = self.path_from_id().split(".")
149 | if len(split) > 1:
150 | prop = eval("bpy.context.scene." + ".".join(split[:2]))
151 | else:
152 | prop = eval(self.address)
153 | update_text(
154 | prop.line,
155 | prop.linename,
156 | [functions.type_getter(ele, ele.ptype) for ele in prop.prop],
157 | eval("context.scene.%s" % self.path_from_id().split(".")[0])
158 | )
159 |
160 | str_prop: StringProperty(update=update_prop)
161 | int_prop: IntProperty(update=update_prop)
162 | float_prop: FloatProperty(update=update_prop)
163 | bool_prop: BoolProperty(update=update_prop)
164 | enum_prop: PointerProperty(type=STB_enum_property)
165 | intvector_prop: StringProperty()
166 | floatvector_prop: StringProperty()
167 | boolvector_prop: StringProperty()
168 | ptype: StringProperty()
169 |
170 |
171 | class STB_property_list(STB_property, PropertyGroup):
172 | def update_prop(self, context):
173 | if len(self.prop) >= 1:
174 | self.prop[0].update_prop(context)
175 |
176 | prop: CollectionProperty(type=STB_property_list_item)
177 |
178 |
179 | class STB_property_object(STB_property, PropertyGroup):
180 | def update_prop(self, context):
181 | update_text(
182 | self.line,
183 | self.linename,
184 | "bpy.data.objects['%s']" % self.prop if self.prop != '' else "''",
185 | eval("bpy.context.scene." + self.path_from_id().split(".")[0])
186 | )
187 | prop: StringProperty(update=update_prop)
188 |
189 |
190 | class STB_button_area(PropertyGroup):
191 | area: StringProperty()
192 |
193 |
194 | class STB_button_properties(PropertyGroup):
195 |
196 | def get_selected(self) -> bool:
197 | """
198 | default Blender property getter
199 |
200 | Returns:
201 | bool: selection state of the button
202 | """
203 | return self.get("selected", False)
204 |
205 | def set_selected(self, value: bool):
206 | """
207 | set the button as active, False will not change anything
208 |
209 | Args:
210 | value (bool): state of button
211 | """
212 | scene = bpy.context.scene
213 | selected_name = scene.get("stb_button.selected_name", "")
214 | # implementation similar to a UIList (only one selection of all can be active)
215 | if value:
216 | scene["stb_button.selected_name"] = self.name
217 | self['selected'] = value
218 | button = scene.stb.get(selected_name, None)
219 | if button:
220 | button.selected = False
221 | elif selected_name != self.name:
222 | self['selected'] = value
223 |
224 | selected: BoolProperty(
225 | name='Select',
226 | description='Select this Button',
227 | get=get_selected,
228 | set=set_selected
229 | )
230 | StringProps: CollectionProperty(type=STB_property_string)
231 | IntProps: CollectionProperty(type=STB_property_int)
232 | FloatProps: CollectionProperty(type=STB_property_float)
233 | BoolProps: CollectionProperty(type=STB_property_bool)
234 | EnumProps: CollectionProperty(type=STB_property_enum)
235 | IntVectorProps: CollectionProperty(type=STB_vector_property)
236 | FloatVectorProps: CollectionProperty(type=STB_vector_property)
237 | BoolVectorProps: CollectionProperty(type=STB_vector_property)
238 | ListProps: CollectionProperty(type=STB_property_list)
239 | ObjectProps: CollectionProperty(type=STB_property_object)
240 | areas: CollectionProperty(type=STB_button_area)
241 | panel: StringProperty(default="Button")
242 |
243 |
244 | class STB_text_property(PropertyGroup):
245 | name: StringProperty()
246 | select: BoolProperty(default=False)
247 |
248 |
249 | class STB_export_button(PropertyGroup):
250 | def get_use(self) -> bool:
251 | """
252 | get state whether the button will be used to export
253 | with extra check if export_all is active
254 |
255 | Returns:
256 | bool: button export state
257 | """
258 | return self.get("use", True) or self.get('export_all', False)
259 |
260 | def set_use(self, value: bool) -> None:
261 | """
262 | set state whether the button will be used to export
263 |
264 | Args:
265 | value (bool): button export state
266 | """
267 | if not self.get('export_all', False):
268 | self['use'] = value
269 |
270 | use: BoolProperty(
271 | default=True,
272 | name="Import Button",
273 | description="Decide whether to export the button",
274 | get=get_use,
275 | set=set_use
276 | )
277 |
278 |
279 | class STB_add_property_item(PropertyGroup):
280 | position: IntProperty()
281 | line: StringProperty()
282 | value: StringProperty()
283 | type: StringProperty()
284 |
285 |
286 | class STB_edit_property_item(PropertyGroup):
287 | name: StringProperty()
288 | line: IntProperty()
289 | linename: StringProperty()
290 | use_delete: BoolProperty(default=False)
291 |
292 |
293 | class STB_edit_area_item(PropertyGroup):
294 | name: StringProperty()
295 | label: StringProperty()
296 | icon: IntProperty()
297 | delete: BoolProperty(default=False)
298 |
299 |
300 | classes = [
301 | STB_property_string,
302 | STB_property_int,
303 | STB_property_float,
304 | STB_property_bool,
305 | STB_enum_item,
306 | STB_property_enum,
307 | STB_vector_property,
308 | STB_enum_property,
309 | STB_property_list_item,
310 | STB_property_list,
311 | STB_property_object,
312 | STB_button_area,
313 | STB_button_properties,
314 | STB_text_property,
315 | STB_export_button,
316 | STB_add_property_item,
317 | STB_edit_property_item,
318 | STB_edit_area_item
319 | ]
320 |
321 |
322 | def register():
323 | for cls in classes:
324 | bpy.utils.register_class(cls)
325 | bpy.types.Scene.stb = CollectionProperty(type=STB_button_properties)
326 |
327 |
328 | def unregister():
329 | for ele in bpy.context.scene.stb:
330 | for intvec in ele.IntVectorProps:
331 | with suppress(AttributeError):
332 | exec("del bpy.types.Scene.%s" % intvec.address.split(".")[-1])
333 | for floatvec in ele.FloatVectorProps:
334 | with suppress(AttributeError):
335 | exec("del bpy.types.Scene.%s" % floatvec.address.split(".")[-1])
336 | for boolvec in ele.BoolVectorProps:
337 | with suppress(AttributeError):
338 | exec("del bpy.types.Scene.%s" % boolvec.address.split(".")[-1])
339 | for cls in classes:
340 | bpy.utils.unregister_class(cls)
341 | del bpy.types.Scene.stb
342 |
--------------------------------------------------------------------------------
/src/wrapper.py:
--------------------------------------------------------------------------------
1 | # ===============================================================================
2 | # Wrapper functions for Blender API to support multiple LTS versions of Blender.
3 | # This script should be independent of any local import to avoid circular imports.
4 | #
5 | # Supported Blender versions (Updated: 2024-11-04):
6 | # - 4.2 LTS and above
7 | # - 3.6 LTS
8 | # ===============================================================================
9 |
10 | import bpy
11 | from bpy.app import version
12 |
13 | import os
14 |
15 |
16 | def get_user_path(package: str, path: str = '', create: bool = False):
17 | """
18 | Return a user writable directory associated with an extension.
19 |
20 | Args:
21 | package (str): The __package__ of the extension.
22 | path (str, optional): Optional subdirectory. Defaults to ''.
23 | create (bool, optional): Treat the path as a directory and create it if its not existing. Defaults to False.
24 | """
25 | fallback = os.path.dirname(__file__)
26 | try:
27 | if version >= (4, 2, 0):
28 | return bpy.utils.extension_path_user(package, path=path, create=create)
29 | else:
30 | return fallback # The Fallback path is also the extension user directory for Blender 3.6 LTS.
31 | except ValueError as err:
32 | print("ERROR STB: ValueError: %s" % str(err))
33 | if err.args[0] == "The \"package\" does not name an extension":
34 | print("--> This error might be caused as the addon is installed the first time.")
35 | print(" If this errors remains please try reinstalling the Add-on and report it to the developer.")
36 |
37 | print(" Fallback to old extension directory: %s." % fallback)
38 | return fallback
39 |
--------------------------------------------------------------------------------