├── .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 | 19 | 38 | 43 | 48 | 53 | 58 | 59 | 61 | 74 | 79 | 80 | 81 | 85 | 88 | 91 | 100 | Button 111 | 112 | 115 | 124 | 127 | 129 | < > 140 | Script 151 | 152 | 153 | 154 | 157 | 162 | To 173 | 174 | 175 | 176 | 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 | --------------------------------------------------------------------------------