├── LICENSE ├── README.md ├── tfeasycolorsmap.desktop └── tfeasycolorsmap ├── Core ├── ALERT.py ├── FILE.py ├── KRITA.py └── SYS.py ├── Forms ├── COLORSPOPUP.py ├── CONFIG.py ├── EDITOR.py ├── FormsManager.py ├── HELP.py └── guide.html ├── UI ├── ColorsMapMenu.py ├── ColorsMapRender.py ├── ColorsSlotMenu.py ├── QColorsMap.py ├── Tools.py └── UI.py ├── __init__.py └── myplugin.py /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 | # TF Easy Colors Map (Krita plugin) 2 | 3 | #### Current version: 2.3 (11/11/2022) 4 | ____ 5 | #### • WHAT IS 6 | TF Easy Colors Map is a plugin for Krita for creating a Map (a collection) of your favourite colors in a very simple way. The final result of your Map can be something like this: 7 | 8 | ![preview](https://i.ibb.co/QP9B3xY/colors.png) 9 | 10 | It's not meant to be a replacement of the current Krita's colors management system, but just a super easy-to-use alternative system. 11 | ____ 12 | #### • HOW IT WORKS 13 | All starts creating a new Colors Map, which is a simple text file with a .cmap extension. Now, you can start adding your colors, naming them and organizing them in collapsible / hideable Groups. 14 | Adding a colors is pretty simple: once you have the desired color set in the Krita document **foreground**, just **right** click your Map, type a name when requested and it's added to your collection. 15 | 16 | ![preview](https://i.ibb.co/YTBJrkk/schema.jpg) 17 | 18 | TIP: To add a color in a specific position, just **right** click with the **SHIFT** button pressed. 19 | 20 | ![preview](https://i.ibb.co/W5mV8XH/schema5.jpg) 21 | 22 | If you need to catch more colors (for example, grabbing them from an image) you can automize the process. Just press the "Auto Add Colors" button and start using the Krita's Color Sampler tool to select your desired color. Every time the foreground color changes, the Colors Map will ask you to type a name and that color is added into your Map. When you finished, press the "Auto Add Colors" button again to stop this feature. 23 | 24 | ![preview](https://i.ibb.co/RhJLxfc/anim.gif) 25 | 26 | ____ 27 | #### • SELECT A COLOR FROM YOUR MAP 28 | 29 | In your Map, just **left** click a color and your Krita document **foreground** color is set to that color. 30 | 31 | TIP: if you click with the **SHIFT** button pressed, the Krita document **background** color is set instead. 32 | 33 | ![preview](https://i.ibb.co/p3FRr8c/schema2.jpg) 34 | 35 | ____ 36 | #### • MANAGE YOUR MAP 37 | 38 | You can perform various operations to manage your colors, names and grouped colors. For example, you can rename colors and group titles, move a color in a different position, delete a color or a group, and so on. 39 | Just **right** click with the **CTRL** button pressed and a popup menu appears with all the available functionalities. 40 | 41 | ![preview](https://i.ibb.co/r02X5ZQ/schema3.jpg) 42 | 43 | ____ 44 | #### • OPEN/CLOSE, SHOW/HIDE, CUSTOMIZE THE COLORS GROUPS 45 | 46 | - Groups can be opened and closed (collapsed) so that you can focus only on the colours you need to use in that moment. Just click the [+] / [-] icon on the right of each Groups title. 47 | - Moreover, you can show and hide the Groups, making them visible or unvisible. Click the "Settings" button and check/uncheck the groups. 48 | - Finally, you can change the background color and the text colors of the Groups so as to make the view more confortable for you eyes. Click the "Settings" button and specify two colors, as HTML code, in the dedicated field. 49 | 50 | ![preview](https://i.ibb.co/rw35vj0/color-openclose.png) 51 | ____ 52 | #### • COLORS SLOTS 53 | 54 | Imagine you have a Group containing all the colors of a character. Your character has some "fixed" colors like, for example, hair, eyes and skin colors. He also has a set of different colors, for example, if he wears a uniform or some dresses. 55 | 56 | ![preview](https://i.ibb.co/fGcVFpz/immagine.png) 57 | 58 | In a complex scenario where the Group has many colors, it's not so convenient to show them all together. For example, if you are coloring pages where the character is dressed in uniform, having the colors of the various dresses is useless and may be annoying. The use of Colors Slots resolves this scenario. 59 | 60 | Just CTRL + right click a Color and select one of the available Slots from the menu. The "Main Slot" is intented for the colors you want always visible (hair, eyes and skin colors, for example). The other 5 Slots are intented for creating special "sub collections". 61 | 62 | ![preview](https://i.ibb.co/BfgB0cv/immagine.png) 63 | 64 | For example, you can group all the Uniform colors into the Slot 1, the dresses colors in Slot 2 and Slot 3. Now, in the Main Slot you have only the "shared" colors. To show the colors into the Slot, just left click on the Group's Slot icon on the right and, from the menu, select the Slot you want to activate. 65 | 66 | ![preview](https://i.ibb.co/qRH48Rm/immagine.png) 67 | 68 | For example, if you click Slot 1, you will see the "shared" colors (hair, eyes, skin) and the Uniform colors (the colors names are shown in italic): 69 | 70 | ![preview](https://i.ibb.co/Qm9W3b3/immagine.png) 71 | 72 | In any moment, you can click "Main Slot" to shown the "shared" colors only or "Show All" for showing all the Group colors. 73 | ____ 74 | #### • TEMPORARY COLORS (SECONDARY PALETTE) 75 | 76 | If you need to use a color, but you don't need to have it saved into your Colors Map, you can use the "secondary" palette placed under the Colors Map. 77 | 78 | ![preview](https://i.ibb.co/Lrs2hK7/schema4.jpg) 79 | 80 | In that palette, colors are kept in memory and not saved. It can be useful, for example, if you have to use a specific color more times in the current Krita document, but you don't need or want to have it in the Colors Map because it's used in that occasion only and it won't be used in other documents. 81 | Adding and selecting colors here use the same logic of the Colors Map (but this palette doesn't have any functionality): 82 | - **right** click this palette for adding a color 83 | - **left** click a color to select it as foreground color (+ **SHIFT** as background color) 84 | 85 | ____ 86 | #### • POPUP PALETTE 87 | 88 | You can easily access your palette by right clicking on your Krita document with the SHIFT button pressed. 89 | 90 | ![preview](https://i.ibb.co/Fh0T1yr/schema-6.png) 91 | ____ 92 | #### • YOUR KRITA DOCUMENT AND COLORS MAP ARE CONNECTED! 93 | 94 | When you create a Colors Map, it's connected to your Krita document. This means that when you open your Krita document, your Colors Map will be automatically loaded. 95 | ____ 96 | #### • KRITA COLOR PROFILES SUPPORT (rgba, cmyka, YCbCrA, xyza, laba, graya) 97 | 98 | When you add a color to your Colors Map, this plugin not only save the various channels value, but also the color characteristics: model, depth and profile. When you click a color, the plugin sends all these params to the Krita ManagedColors system, which returns a color corresponding to the given specifications. 99 | 100 | This means that you can fill your Colors Map with colors coming from different profiles. For example, you can collect RGB, CMYK, GREY scale colors, all in the same Colors Map. However, because of the different nature of the various color profiles, this may raise some variations in your resulting color. 101 | 102 | In conclusion, it's up to you creating Colors Maps with coherent profiles! 103 | 104 | - CASE TEST 105 | 106 | In the screenshot below, the Colors Map contains 3 colors: one with a CMYK profile, one with a LAB profile and the last one with a GRAY profile. Then, these colors have been used on 3 different Krita's Documents: a RGB, a CMYK and a LAB document. 107 | As result, Krita has been able to reproduce (more or less) correctly the different colors in the different documents. Just the blue and the gray colors appear a little bit darker in the CMYK document. 108 | 109 | ![preview](https://i.ibb.co/hsJJC05/Colors-profile-TESTs.png) 110 | ____ 111 | #### • INLINE HELP MANUAL 112 | 113 | Click the [?] button to show the inline manual with all the features explained. 114 | 115 | ____ 116 | #### • WHY THIS PLUGIN 117 | 118 | I'm a comic artist, so I have to apply the same colors on different pages. Krita is fantastic software for the flattening process, but personally I find the color management system unsuitable for the flexibility and speed I would like. For this reason, I created a plugin that offered what I need, such as organizing colors quickly, easily seeing the name of a color and so on. 119 | 120 | ____ 121 | #### • WHAT'S NEW 122 | 123 | #### 2.2 - 2.3 (11/11/2022) 124 | - New feature: Colors Slots 125 | - Bug: resolved a bug in the Colors Map popup window (colors where not properly selected) 126 | 127 | #### 2.1 (21/09/2022) 128 | - New feature: you can customize the colors of the Groups bar. 129 | - Various code optimization 130 | 131 | #### 2.0 (16/09/2022) 132 | - New plugin version 133 | 134 | #### 0.1 (22/06/2022) 135 | - Initial release 136 | -------------------------------------------------------------------------------- /tfeasycolorsmap.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Service 3 | ServiceTypes=Krita/PythonPlugin 4 | X-KDE-Library=tfeasycolorsmap 5 | X-Python-2-Compatible=false 6 | Name=TF Easy Colors Map 7 | Comment=[V2] Easily create an organized collection of all your colors. -------------------------------------------------------------------------------- /tfeasycolorsmap/Core/ALERT.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5 import QtGui, QtCore 3 | from PyQt5.QtGui import QCursor 4 | from PyQt5.QtCore import QTimer 5 | from krita import * 6 | 7 | # ==== Various Alerts and Prompts managemente ===================================================== 8 | 9 | class ALERT: 10 | def __init__(self): 11 | super().__init__() 12 | 13 | def info(title, text): 14 | """Show an INFO dialog.""" 15 | msgbox = QMessageBox() 16 | msgbox.setWindowTitle(title) 17 | msgbox.setText(text) 18 | msgbox.setIcon(QMessageBox.Information) 19 | msgbox.exec() 20 | pass 21 | 22 | def warn(title, text): 23 | """Show a WARNING dialog.""" 24 | msgbox = QMessageBox() 25 | msgbox.setWindowTitle(title) 26 | msgbox.setText(text) 27 | msgbox.setIcon(QMessageBox.Warning) 28 | msgbox.exec() 29 | pass 30 | 31 | def error(title, text): 32 | """Show an ERROR dialog.""" 33 | msgbox = QMessageBox() 34 | msgbox.setWindowTitle(title) 35 | msgbox.setText(text) 36 | msgbox.setIcon(QMessageBox.Critical) 37 | msgbox.exec() 38 | pass 39 | 40 | def ask(title, text): 41 | """Show an QUESTION dialog, with YES/NO buttons.""" 42 | msgbox = QMessageBox() 43 | msgbox.setWindowTitle(title) 44 | msgbox.setText(text) 45 | msgbox.setIcon(QMessageBox.Critical) 46 | msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) 47 | reply = msgbox.exec() 48 | return (reply == QMessageBox.Yes) 49 | pass 50 | 51 | def prompt(title, text, value = ""): 52 | """Show a PROMPT dialog that asks for entering a value.""" 53 | msgbox = QInputDialog() 54 | msgbox.setWindowTitle(title) 55 | msgbox.setInputMode(QInputDialog.TextInput) 56 | msgbox.setLabelText(text) 57 | msgbox.setTextValue(value.replace("\n", "")) 58 | 59 | d = dict() 60 | if (msgbox.exec()): 61 | reply = msgbox.textValue() 62 | if (reply.find("|") >= 0): 63 | ALERT.error("ATTENTION", "You can't use the '|' (pipe) character inside your text.") 64 | d["ok"] = False 65 | d["value"] = "" 66 | else: 67 | d["ok"] = True 68 | d["value"] = reply 69 | else: 70 | reply = "" 71 | d["ok"] = False 72 | d["value"] = "" 73 | 74 | d["value"] = d["value"].replace("\n", "") 75 | return d 76 | pass 77 | 78 | def dialogFileOpen(title, filter): 79 | """Show an OPEN FILE dialog.""" 80 | msgbox = QFileDialog() 81 | msgbox.setWindowTitle(title) 82 | msgbox.setNameFilters([filter]) 83 | if (msgbox.exec()): 84 | reply = msgbox.selectedFiles()[0] 85 | else: 86 | reply = "" 87 | 88 | return reply 89 | pass 90 | 91 | def ok(): 92 | """Helper: just show an 'OK' message.""" 93 | msgbox = QMessageBox() 94 | msgbox.setWindowTitle("OK") 95 | msgbox.setText("ok") 96 | msgbox.setIcon(QMessageBox.Information) 97 | msgbox.exec() 98 | pass 99 | 100 | def log(*message): 101 | """Helper: show a debug message in the Krita Console.""" 102 | string = "" 103 | for m in message: 104 | string += str(m) + " " 105 | 106 | QtCore.qDebug("[LOG] " + string) -------------------------------------------------------------------------------- /tfeasycolorsmap/Core/FILE.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | 4 | # ==== File operations management ================================================================= 5 | 6 | class FILE: 7 | def __init__(self): 8 | super().__init__() 9 | 10 | # ---- Opening data --------------------------------------------------------------------------- 11 | 12 | def open(fileName): 13 | fo = open(fileName, "r") 14 | lines = fo.readlines() 15 | fo.close() 16 | return lines 17 | 18 | def openText(fileName): 19 | fo = open(fileName, "r") 20 | lines = fo.read() 21 | fo.close() 22 | return lines 23 | 24 | # ---- Saving data ---------------------------------------------------------------------------- 25 | 26 | def save(fileName, data): 27 | """Save a string data into a file.""" 28 | fo = open(fileName, "w") 29 | fo.write(data) 30 | fo.close() 31 | 32 | def append(fileName, data): 33 | """Append a string data at the end of a file ('new line' is included).""" 34 | fo = open(fileName, "a") 35 | fo.write(data + "\n") 36 | fo.close() 37 | 38 | def saveToIndex(fileName, data, index): 39 | """Insert a string in the specified index position ('new line' is included).""" 40 | fileContent = FILE.open(fileName) 41 | fileContent.insert(index, data + "\n") 42 | FILE.saveList(fileName, fileContent) 43 | 44 | def saveList(fileName, fileContent, addNewLine = False, excludeEmptyStrings = True): 45 | """Save a List into a file. The 'new line' inclusion is controlled by the addNewLine param. By default, empty strings are not saved (controlled by excludeEmptyStrings param).""" 46 | fo = open(fileName, "w") 47 | newLine = "\n" if addNewLine else "" 48 | for item in fileContent: 49 | if (excludeEmptyStrings): 50 | if (item != ""): fo.write(item + newLine) 51 | else: 52 | fo.write(item + newLine) 53 | fo.close() 54 | 55 | # ---- Special operations --------------------------------------------------------------------- 56 | 57 | def modifyLine(fileName, lineIndex, position, value): 58 | """Open the file, get the line at lineIndex, split it, change the value at the given position, finally save the file.""" 59 | fileContent = FILE.open(fileName) 60 | line = fileContent[lineIndex].split("|") 61 | line[position] = value 62 | fileContent[lineIndex] = '|'.join(line) 63 | FILE.saveList(fileName, fileContent) 64 | 65 | def modifyLineToggle(fileName, lineIndex, position, value1, value2): 66 | """Open the file, get the line at lineIndex, split it, change the value at the given position (using a toggle logic between value1 and value2), finally save the file.""" 67 | fileContent = FILE.open(fileName) 68 | line = fileContent[lineIndex].split("|") 69 | line[position] = value1 if line[position] == value2 else value2 70 | fileContent[lineIndex] = '|'.join(line) 71 | FILE.saveList(fileName, fileContent) 72 | 73 | def extractLine(fileName, lineIndex): 74 | """Open the file and return the line at lineIndex.""" 75 | fileContent = FILE.open(fileName) 76 | return fileContent[lineIndex] 77 | 78 | # ---- Specific Groups / Colors operations ---------------------------------------------------- 79 | 80 | def getGroupsList(fileName): 81 | """Return the list of Groups from the given Colors Map.""" 82 | fileContent = FILE.open(fileName) 83 | groups = list() 84 | for item in fileContent: 85 | if (item.find("[G]") == 0): groups.append(item) 86 | 87 | return groups 88 | 89 | def getColorsList(fileName): 90 | """Return the list of Colors from the given Colors Map.""" 91 | fileContent = FILE.open(fileName) 92 | groups = list() 93 | for item in fileContent: 94 | if (item.find("[C]") == 0): groups.append(item) 95 | 96 | return groups 97 | 98 | def changeLineValue(line, position, value): 99 | """Split a line, change the value at the given position, then ricreate the line.""" 100 | data = line.split("|") 101 | data[position] = value 102 | return "|".join(data) 103 | 104 | def getLineValue(line, position): 105 | """Split a line and return the value at the given position""" 106 | data = line.split("|") 107 | return data[position] 108 | 109 | # ---- Helpers -------------------------------------------------------------------------------- 110 | 111 | def exists(fileName): 112 | """Return True if the file exists""" 113 | return (os.path.isfile(fileName)) 114 | 115 | def notExists(fileName): 116 | """Return True if the file doesn't exist""" 117 | return not(os.path.isfile(fileName)) 118 | 119 | def getExtension(fileName): 120 | """Return the Extension of the given file""" 121 | return os.path.splitext(fileName)[1] 122 | 123 | def checkExtension(fileName, extension): 124 | """Return True if the given file Extension is equal to the given Extension (it must contain .)""" 125 | return os.path.splitext(fileName)[1] == extension 126 | 127 | def currentFilePath(target): 128 | """Return the file's current path. Given target should be: __file__""" 129 | return os.path.dirname(os.path.abspath(target)) 130 | 131 | def getCurrentPathToFile(target, fileName): 132 | """Return the file's current path combined with the given fileName. Given target should be: __file__""" 133 | path = os.path.dirname(os.path.abspath(target)) 134 | return os.path.join(path, fileName) -------------------------------------------------------------------------------- /tfeasycolorsmap/Core/KRITA.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5 import QtGui, QtCore 3 | 4 | from krita import * 5 | 6 | from .ALERT import * 7 | 8 | # **** Krita components access helpers ************************************************************ 9 | 10 | class KRITA: 11 | def __init__(self): 12 | super().__init__() 13 | 14 | def App(): 15 | """The Krita instance: Krita.instance()""" 16 | return Krita.instance() 17 | 18 | def Document(): 19 | """The Krita active Document: Krita.instance().activeDocument()""" 20 | return Krita.instance().activeDocument() 21 | 22 | def AppNotifier(): 23 | """Return a Krita active Notifier.""" 24 | appNotifier = Krita.instance().notifier() 25 | appNotifier.setActive(True) 26 | return appNotifier 27 | 28 | def View(): 29 | """The Krita active View: Krita.instance().activeWindow().activeView()""" 30 | return Krita.instance().activeWindow().activeView() 31 | 32 | def Window(): 33 | """The Krita active Window: Krita.instance().activeWindow()""" 34 | return Krita.instance().activeWindow() 35 | 36 | def getSelectedColor(target): 37 | """Get the View selected color from the given target (F: foreground, B: background) as an array of 6 string. Where the color is not available a -1 value is placed. The last value (index = 5) is the color description.""" 38 | view = KRITA.View() 39 | color = view.foregroundColor() if target == "F" else view.backgroundColor() 40 | config = KRITA.colorConfig() 41 | color.setColorSpace(config["model"], config["depth"], config["profile"]) 42 | 43 | selectedColor = ["-1", "-1", "-1", "-1", "-1", ""] 44 | 45 | colorData = color.components() 46 | for index, value in enumerate(colorData): selectedColor[index] = str(value) 47 | selectedColor[5] = color.toQString() 48 | 49 | return selectedColor 50 | 51 | def setColor(target, color): 52 | """Set the Krita (F: foreground, B: background) color with the given Krita managed Color.""" 53 | view = KRITA.View() 54 | if (target == "F"): 55 | view.setForeGroundColor(color) 56 | else: 57 | view.setBackGroundColor(color) 58 | 59 | def getEvent(event): 60 | """Extract informations from a Krita event and return an organized Dictionary.""" 61 | modifierPressed = QApplication.keyboardModifiers() 62 | isShift = (modifierPressed & Qt.ShiftModifier) 63 | isCtrl = (modifierPressed & Qt.ControlModifier) 64 | isAlt = (modifierPressed & Qt.AltModifier) 65 | leftClick = (event.buttons() == Qt.LeftButton) 66 | rightClick = (event.buttons() == Qt.RightButton) 67 | x = event.pos().x() 68 | y = event.pos().y() 69 | globalPos = event.globalPos() 70 | 71 | return { 72 | "hasModifier" : modifierPressed, 73 | "noModifier" : not(modifierPressed), 74 | "isShift" : isShift, 75 | "isCtrl" : isCtrl, 76 | "isAlt" : isAlt, 77 | "left" : leftClick, 78 | "right" : rightClick, 79 | "x" : x, 80 | "y" : y, 81 | "globalPos" : globalPos 82 | } 83 | 84 | def colorConfig(): 85 | """Return a Dictionary containing the various current Document colors settings (depth, model, profile).""" 86 | doc = KRITA.Document() 87 | return { 88 | "depth": doc.colorDepth(), 89 | "model": doc.colorModel(), 90 | "profile": doc.colorProfile() 91 | } 92 | 93 | def colorParam(param): 94 | """Return the requested current Color MODEL / DEPTH / PROFILE.""" 95 | doc = KRITA.Document() 96 | if (param == "MODEL"): return doc.colorModel() 97 | if (param == "DEPTH"): return doc.colorDepth() 98 | if (param == "PROFILE"): return doc.colorProfile() 99 | return "" 100 | 101 | def cursor(icon): 102 | """Get a Krita icon into a QCursor widget.""" 103 | qIcon = KRITA.App().icon(icon) 104 | qIconPixmap = qIcon.pixmap(16, 16) 105 | return QCursor(qIconPixmap) 106 | 107 | def isNotReady(): 108 | """Return True if Krita has no opened Documents.""" 109 | return KRITA.Document() is None 110 | 111 | def isReady(): 112 | """Return True if Krita has an opened Documents.""" 113 | return not(KRITA.isNotReady()) 114 | 115 | def createColor(returnAs, model, depth, profile, color01 = -1, color02 = -1, color03 = -1, color04 = -1, color05 = -1): 116 | """Build a Krita Managed Color with the given params. returnAs can be 'QCOLOR' if a QColor is needed or 'KRITA' if a ManagedColor component is needed.""" 117 | myColor = ManagedColor(model, depth, profile) 118 | colorComponents = myColor.components() 119 | if (color01 >= 0): colorComponents[0] = color01 120 | if (color02 >= 0): colorComponents[1] = color02 121 | if (color03 >= 0): colorComponents[2] = color03 122 | if (color04 >= 0): colorComponents[3] = color04 123 | if (color05 >= 0): colorComponents[4] = color05 124 | myColor.setComponents(colorComponents) 125 | 126 | if (returnAs == "QCOLOR"): 127 | canvas = Krita.instance().activeWindow().activeView().canvas() 128 | return(myColor.colorForCanvas(canvas)) 129 | else: 130 | return myColor 131 | 132 | def annotationSet(fileName): 133 | """Set the fileName as an internal annotation for the currently active Krita Document.""" 134 | doc = KRITA.Document() 135 | doc.setAnnotation('TFECMAP2', "Path to TF Easy Colors Map V2", QByteArray(fileName.encode())) 136 | 137 | def annotationGet(): 138 | """Return the fileName saved in the internal annotation of the currently active Krita Document.""" 139 | try: 140 | doc = KRITA.Document() 141 | annotation = doc.annotation('TFECMAP2') 142 | if (annotation is None): 143 | return "" 144 | else: 145 | return bytes(annotation).decode() 146 | except: 147 | return "" 148 | 149 | def hasActiveViews(): 150 | """Return TRUE if there are opened Documents in Krita.""" 151 | win = KRITA.Window() 152 | return len(win.views()) > 1 153 | 154 | def actionTrigger(actionName): 155 | """Trigger a Krita action.""" 156 | Krita.instance().action(actionName).trigger() 157 | 158 | def getCurrentCanvas(myCanvas = None): 159 | """Return a reference to the current or given Canvas with extra infos (x/y mouse coordinates, isInside and globalPos properties).""" 160 | if (myCanvas is None): 161 | canvas = KRITA.find_current_canvas() 162 | else: 163 | canvas = myCanvas 164 | 165 | if (not canvas is None): 166 | global_pos = QCursor.pos() 167 | pos = QPoint(canvas.mapFromGlobal(global_pos)) 168 | if (pos.x() < 0 or pos.y() < 0 or pos.x() > canvas.width() or pos.y() > canvas.height()): 169 | return { "canvas": canvas, "x": 0, "y": 0, "isInside": False, "globalPos": global_pos } 170 | else: 171 | return { "canvas": canvas, "x": pos.x(), "y": pos.y(), "isInside": True, "globalPos": global_pos } 172 | return { "canvas": None, "x": 0, "y": 0, "isInside": False, "globalPos": None } 173 | 174 | def find_current_canvas(): 175 | try: 176 | app = Krita.instance() 177 | q_window = app.activeWindow().qwindow() 178 | q_stacked_widget = q_window.centralWidget() 179 | q_mdi_area = q_stacked_widget.currentWidget() 180 | q_mdi_sub_window = q_mdi_area.currentSubWindow() 181 | view = q_mdi_sub_window.widget() 182 | for c in view.children(): 183 | if c.metaObject().className() == 'KisCanvasController': 184 | viewport = c.viewport() 185 | canvas = viewport.findChild(QWidget) 186 | return canvas 187 | return None 188 | except: 189 | return None 190 | 191 | def drawingToolSelected(): 192 | """Return True if the currently selected Krita Tool is a Drawing tool (Brush, Line, Pencil, Filler, etc.).""" 193 | tools = [ 194 | "KritaShape/KisToolMultiBrush", 195 | "KritaShape/KisToolLine", 196 | "KritaShape/KisToolBrush", 197 | "KisToolPolyline", 198 | "KritaShape/KisToolRectangle", 199 | "KritaShape/KisToolDyna", 200 | "KisToolPath", 201 | "KritaShape/KisToolEllipse", 202 | "KisToolPolygon", 203 | "KisToolPencil", 204 | "KritaFill/KisToolFill", 205 | "KritaFill/KisToolGradient", 206 | "KritaShape/KisToolLazyBrush" 207 | ] 208 | qwindow = Krita.instance().activeWindow().qwindow() 209 | 210 | for item in tools: 211 | target_qobj = qwindow.findChild(QToolButton, item) 212 | if (target_qobj.isChecked()): return True 213 | 214 | return False -------------------------------------------------------------------------------- /tfeasycolorsmap/Core/SYS.py: -------------------------------------------------------------------------------- 1 | 2 | # **** System management ***********************************************************************++* 3 | 4 | class SYS: 5 | def __init__(self): 6 | super().__init__() 7 | 8 | @property 9 | def version(self): 10 | return self.__version 11 | 12 | @version.setter 13 | def version(self, value): 14 | self.__version = value 15 | 16 | def getVersionString(): 17 | return str(SYS.version / 10).replace(",", ".") 18 | 19 | def versionToNumber(versionString): 20 | return int(versionString.replace(".", "")) -------------------------------------------------------------------------------- /tfeasycolorsmap/Forms/COLORSPOPUP.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5 import QtGui, QtCore 3 | 4 | from krita import * 5 | 6 | from ..Core.KRITA import * 7 | from ..UI.ColorsMapRender import * 8 | from ..UI.QColorsMap import * 9 | 10 | class COLORSPOPUP(QWidget): 11 | def __init__(self): 12 | super().__init__() 13 | self.fileName = "" 14 | self.map = list() 15 | 16 | self.setWindowFlags(QtCore.Qt.WindowCloseButtonHint | Qt.WindowStaysOnTopHint) 17 | self.setWindowTitle("TF Easy Colors Map") 18 | self.resize(240, 320) 19 | 20 | self.colorsMap = QLabel() 21 | self.colorsMap.mousePressEvent = self.onColorsMapClick 22 | self.colorsMap.setCursor(KRITA.cursor('krita_tool_color_sampler')) 23 | 24 | self.scrollArea = QScrollArea() 25 | self.scrollArea.setWidgetResizable(True) 26 | self.scrollArea.setWidget(self.colorsMap) 27 | 28 | layout = QVBoxLayout() 29 | layout.layout().addWidget(self.scrollArea) 30 | 31 | self.setLayout(layout) 32 | 33 | def read(self, fileName): 34 | if fileName != "": 35 | self.fileName = fileName 36 | self.colorsMap.setAlignment(Qt.AlignTop | Qt.AlignLeft) 37 | self.renderFile(True) 38 | 39 | def renderFile(self, resize = False): 40 | rendered = ColorsMapRender.run(self.fileName, self.scrollArea, True) 41 | self.colorsMap.setPixmap(rendered["pixmap"]) 42 | self.map = rendered["map"] 43 | 44 | def resizeEvent(self, event): 45 | self.renderFile() 46 | 47 | def onColorsMapClick(self, event): 48 | click = KRITA.getEvent(event) 49 | 50 | # LEFT CLICK 51 | if (click["left"]): 52 | foundItem = QColorsMap.findItemIndex(click["x"], click["y"], self.map) 53 | if (foundItem["index"] < 0): return 54 | 55 | if (foundItem["isColor"]): 56 | # COLOR 57 | if (click["noModifier"]): 58 | KRITA.setColor("F", QColorsMap.createColorFromMap(foundItem["index"])) 59 | elif (click["isShift"]): 60 | KRITA.setColor("B", QColorsMap.createColorFromMap(foundItem["index"])) 61 | 62 | self.close() 63 | pass 64 | -------------------------------------------------------------------------------- /tfeasycolorsmap/Forms/CONFIG.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5 import QtGui, QtCore 3 | 4 | from krita import * 5 | 6 | from ..UI.Tools import * 7 | from ..Core.FILE import * 8 | from ..Core.ALERT import * 9 | 10 | class CONFIG(QWidget): 11 | def __init__(self): 12 | super().__init__() 13 | 14 | self.mainLayout = QVBoxLayout() 15 | self.mainLayout.setSpacing(0) 16 | self.t1 = QLineEdit() 17 | self.t2 = QLineEdit() 18 | self.t3 = QLineEdit() 19 | 20 | self.fileName = "" 21 | 22 | self.setFixedWidth(560) 23 | self.setWindowTitle("TF Easy Colors Map » SETTINGS") 24 | 25 | l1 = QLabel() 26 | l2 = QLabel() 27 | l3 = QLabel() 28 | l4 = QLabel() 29 | l5 = QLabel() 30 | l6 = QLabel() 31 | l7 = QLabel() 32 | l8 = QLabel() 33 | 34 | l1 = self.title(l1, "Color size") 35 | l2 = self.desc(l2, "Set the size of the Color boxes. Size must be a value from 24 to 96 (42 by default).") 36 | 37 | l3 = self.title(l3, "Color Name size") 38 | l4 = self.desc(l4, "Set the size of the Color name inside a box. Size must be a value from 8 to 18 (10 by default).") 39 | 40 | l7 = self.title(l7, "Groups color") 41 | l8 = self.desc(l8, "Set the colors for the Background and the Text of the Groups. The two colors must be typed\nas HTML colors, separated by a space (e.g. #000000 #FFFFFF -> black background, white text).") 42 | 43 | l5 = self.title(l5, "\nHide / Show Groups") 44 | l6 = self.desc(l6, "Check or uncheck the Groups you want to show or hide in the Colors Map view.") 45 | 46 | self.groupsLayout = QListWidget() 47 | self.groupsLayout.setFixedHeight(160) 48 | 49 | formLayout = Tools.QWidgetLayout("V", Qt.AlignLeft | Qt.AlignTop) 50 | formLayout.layout().addWidget(l1) 51 | formLayout.layout().addWidget(l2) 52 | formLayout.layout().addWidget(self.t1) 53 | formLayout.layout().addWidget(l3) 54 | formLayout.layout().addWidget(l4) 55 | formLayout.layout().addWidget(self.t2) 56 | formLayout.layout().addWidget(l7) 57 | formLayout.layout().addWidget(l8) 58 | formLayout.layout().addWidget(self.t3) 59 | formLayout.layout().addWidget(l5) 60 | formLayout.layout().addWidget(l6) 61 | formLayout.layout().addWidget(self.groupsLayout) 62 | 63 | btsLayout = Tools.QWidgetLayout("H", Qt.AlignRight) 64 | btsLayout.layout().addWidget(Tools.toolBt("", self.save, "Save and apply all these settings.", Qt.ToolButtonTextOnly, "APPLY")) 65 | btsLayout.layout().addWidget(Tools.toolBt("", self.saveDefaults, "Save and apply the default settings for Color size and name size.", Qt.ToolButtonTextOnly, "SET DEFAULTS")) 66 | 67 | self.mainLayout = QVBoxLayout() 68 | self.mainLayout.setSpacing(0) 69 | self.setLayout(self.mainLayout) 70 | self.layout().addWidget(formLayout) 71 | self.layout().addWidget(btsLayout) 72 | 73 | def save(self): 74 | colorSize = int(self.t1.text()) 75 | colorFontSize = int(self.t2.text()) 76 | groupColors = self.t3.text() 77 | 78 | if (colorSize < 24 or colorSize > 96): 79 | ALERT.error("INVALID SIZE", "The entered 'Color size' is not valid. You have to type values from 24 to 96.") 80 | return 81 | 82 | if (colorFontSize < 8 or colorFontSize > 18): 83 | ALERT.error("INVALID SIZE", "The entered 'Color Name size' is not valid. You have to type values from 8 to 18.") 84 | return 85 | 86 | if (len(groupColors) < 15): 87 | ALERT.info("INVALID COLORS", "The 'Groups color' field is empty (or not valid) and it will be filled with default values.\n\nChange that colors if you prefer something different.") 88 | self.t3.setText("#000000 #FFFFFF") 89 | return 90 | 91 | self.saveToFile(colorSize, colorFontSize, groupColors) 92 | 93 | def saveDefaults(self): 94 | self.saveToFile(42, 10) 95 | 96 | # Save the given two values and close the form. 97 | def saveToFile(self, colorSize, fontSize, groupColors): 98 | fileContent = FILE.open(self.fileName) 99 | 100 | fileContent[0] = FILE.changeLineValue(fileContent[0], 6, str(colorSize)) 101 | fileContent[0] = FILE.changeLineValue(fileContent[0], 8, str(fontSize)) 102 | fileContent[0] = FILE.changeLineValue(fileContent[0], 11, groupColors) 103 | 104 | visibles = "" 105 | for i in range(self.groupsLayout.count()): 106 | item = self.groupsLayout.item(i) 107 | if (item.checkState() == Qt.Checked): visibles += "|" + item.text() + "|" 108 | 109 | for index, line in enumerate(fileContent): 110 | if (line.find("[G]") == 0): 111 | groupName = "|" + FILE.getLineValue(line, 3) + "|" 112 | if (visibles.find(groupName) >= 0): 113 | fileContent[index] = FILE.changeLineValue(line, 2, "[V]") 114 | else: 115 | fileContent[index] = FILE.changeLineValue(line, 2, "[-]") 116 | 117 | FILE.saveList(self.fileName, fileContent) 118 | self.close() 119 | 120 | 121 | 122 | def init(self, fileName): 123 | self.fileName = fileName 124 | data = FILE.extractLine(fileName, 0).split("|") 125 | 126 | self.t1.setText(data[6]) 127 | self.t2.setText(data[8]) 128 | self.t3.setText(data[11]) 129 | 130 | self.groupsLayout.clear() 131 | 132 | groups = FILE.getGroupsList(self.fileName) 133 | for line in groups: 134 | data = line.split("|") 135 | 136 | item = QListWidgetItem(data[3], self.groupsLayout) 137 | item.setFlags(item.flags() | Qt.ItemIsUserCheckable) 138 | item.setCheckState(Qt.Checked if data[2] == "[V]" else Qt.Unchecked) 139 | self.groupsLayout.addItem(item) 140 | 141 | def title(self, l1, text): 142 | l1.setText(text) 143 | font = l1.font() 144 | font.setPixelSize(14) 145 | l1.setFont(font) 146 | l1.setStyleSheet("font-weight:bold;") 147 | return l1 148 | 149 | def desc(self, l1, text): 150 | l1.setText(text) 151 | font = l1.font() 152 | l1.setFont(font) 153 | l1.setStyleSheet("font-style:italic;") 154 | return l1 155 | -------------------------------------------------------------------------------- /tfeasycolorsmap/Forms/EDITOR.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5 import QtGui, QtCore 3 | 4 | from krita import * 5 | 6 | from ..Core.FILE import * 7 | from ..UI.Tools import * 8 | 9 | class EDITOR(QWidget): 10 | def __init__(self): 11 | super().__init__() 12 | layout = QVBoxLayout() 13 | self.fileName = "" 14 | 15 | self.setFixedWidth(640) 16 | self.setFixedHeight(480) 17 | self.setWindowTitle("TF Easy Colors Map » EDITOR") 18 | 19 | self.textArea = QPlainTextEdit(self) 20 | self.textArea.setWordWrapMode(QTextOption.NoWrap) 21 | 22 | layout.addWidget(self.textArea) 23 | layout.addWidget(Tools.bt("SAVE", self.save)) 24 | self.setLayout(layout) 25 | 26 | def read(self, fileName): 27 | self.fileName = fileName 28 | self.textArea.setPlainText(FILE.openText(fileName)) 29 | 30 | def save(self): 31 | FILE.save(self.fileName, self.textArea.toPlainText()) 32 | self.close() 33 | 34 | -------------------------------------------------------------------------------- /tfeasycolorsmap/Forms/FormsManager.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5 import QtGui, QtCore 3 | 4 | from krita import * 5 | 6 | from .CONFIG import * 7 | from .EDITOR import * 8 | from .HELP import * 9 | from .COLORSPOPUP import * 10 | 11 | # **** FORMS management *************************************************************************** 12 | 13 | class FORMS: 14 | def __init__(self): 15 | super().__init__() 16 | 17 | @property 18 | def ConfigForm(self): 19 | return self.__ConfigForm 20 | @ConfigForm.setter 21 | def ConfigForm(self, value): 22 | self.__ConfigForm = value 23 | 24 | @property 25 | def EditorForm(self): 26 | return self.__EditorForm 27 | @EditorForm.setter 28 | def EditorForm(self, value): 29 | self.__EditorForm = value 30 | 31 | @property 32 | def HelpForm(self): 33 | return self.__HelpForm 34 | @HelpForm.setter 35 | def HelpForm(self, value): 36 | self.__HelpForm = value 37 | 38 | @property 39 | def ColorsPopupForm(self): 40 | return self.__ColorsPopupForm 41 | @ColorsPopupForm.setter 42 | def ColorsPopupForm(self, value): 43 | self.__ColorsPopupForm = value 44 | 45 | def initialize(configClose, editorClose): 46 | 47 | FORMS.ConfigForm = CONFIG() 48 | FORMS.ConfigForm.closeEvent = configClose 49 | 50 | FORMS.EditorForm = EDITOR() 51 | FORMS.EditorForm.closeEvent = editorClose 52 | 53 | FORMS.HelpForm = HELP() 54 | 55 | FORMS.ColorsPopupForm = COLORSPOPUP() 56 | 57 | def show(formName, data = "", x = 0, y = 0): 58 | 59 | if (formName == "CONFIG"): 60 | FORMS.ConfigForm.init(data) 61 | FORMS.ConfigForm.show() 62 | 63 | if (formName == "EDITOR"): 64 | FORMS.EditorForm.read(data) 65 | FORMS.EditorForm.show() 66 | 67 | if (formName == "HELP"): 68 | FORMS.HelpForm.init() 69 | FORMS.HelpForm.show() 70 | 71 | if (formName == "COLORSPOPUP"): 72 | FORMS.ColorsPopupForm.read(data) 73 | FORMS.ColorsPopupForm.show() 74 | FORMS.ColorsPopupForm.move(x, y) 75 | -------------------------------------------------------------------------------- /tfeasycolorsmap/Forms/HELP.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5 import QtGui, QtCore 3 | 4 | from krita import * 5 | 6 | from ..Core.FILE import * 7 | from ..Core.ALERT import * 8 | 9 | class HELP(QWidget): 10 | def __init__(self): 11 | super().__init__() 12 | layout = QVBoxLayout() 13 | self.fileName = "" 14 | 15 | self.setFixedWidth(640) 16 | self.setFixedHeight(480) 17 | self.setWindowTitle("TF Easy Colors Map » GUIDE") 18 | 19 | self.textArea = QTextEdit(self) 20 | self.textArea.setReadOnly(True) 21 | self.textArea.setHtml("") 22 | 23 | layout.addWidget(self.textArea) 24 | self.setLayout(layout) 25 | 26 | def init(self): 27 | fileName = FILE.getCurrentPathToFile(__file__, "guide.html") 28 | html = FILE.openText(fileName) 29 | self.textArea.setHtml(html) 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tfeasycolorsmap/Forms/guide.html: -------------------------------------------------------------------------------- 1 |

TF Easy Colors Map 2.x - QUICK GUIDE

2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
QUICK REFERENCE
LEFT click  Set the foreground color
LEFT click+ SHIFT  Set the background color
LEFT click+ CTRL  Change the name of the clicked Color or Group Title
RIGHT click  Add a new color, at the end of the Map
RIGHT click+ SHIFT  Add a new color after the clicked Color or Group Title
RIGHT click+ CTRL  Show a popup menu with some functionalities



40 | Create or load a Colors Map
41 | Press the 'Open Map' button. Type a new file name to create a new Colors Map or select and open an existing Colors 42 | Map.
Note that a Colors Map file is just an ordinary text file with .cmap extension. 43 |

44 | Add a new color
45 | With a Krita document opened and the desired color set as foreground color, just right click on the Map 46 | panel. Type a short name for the color and press [OK] (if you press [CANCEL] no color is added). 47 |

48 | Add a new color in a specific position
49 | Follow the instructions above, but right click + SHIFT button on an existing color. The new color will be 50 | added after that color.
Note that you can click a Group Title to add the color as first color. 51 |

52 | Add new colors automatically
53 | Press the 'Auto Add Colors' button. Every time you change the foreground color, the new color is automatically 54 | added to the Map. Press again the 'Auto Add Colors' button to stop this functionality.
Note that the new colors are 55 | added at the end of the Map. 56 |

57 | Select a Color of the Map
58 | Left click an existing color: it will set the Krita document foreground color.
59 | Left click + SHITF button an existing color: it will set the Krita document background color. 60 |

61 | Colors Map Management
62 | Right click + CTRL button an existing Color or Group Title: a popup menu will apear with the available 63 | functionalities. 64 |

65 | - Add Group Title: add a new Group Title after the clicked Color or Group.
66 | - Rename: change the name of the clicked Color or Group (you can also Left click + CTRL the item to rename).
67 | - Cut: select a Color or a Group for a successive 'paste' operation.
68 | - Paste: move a Color or a Group Title after the clicked color (you must click a Color).
69 | - Paste Group: move a Group Title and all the Colors under it after the clicked color (you must 70 | click a Color).
71 | - Delete: delete the clicked Color or Group (the Group Title only will be deleted, not the Colors under it). 72 | - Delete Group: delete the clicked Group and all the Colors under it. 73 |

74 | Temporary Colors (Secondary palette)
75 | If you want to collect some color, but you don't want to save them into your Colors Map, you can add colors to the 76 | palette under the Colors Map. Just right click to add colors and left click for setting the foreground color (+SHIFT for 77 | setting the background instead). 78 |

79 | Settings
80 | Press the 'Settings' button. A window will appear showing supported settings.
81 | - Set Color size: allow to change the size of the Color boxes.
82 | - Set Color Name size: allow to change the size of the name inside the Color boxes. 83 | - Show / Hide Groups: allow to show or hide Groups of Colors in the Colors Map view. 84 |

85 |

86 | Colors Map Editor
87 | Press the 'Map Editor' button. A window will appear showing the content of the Colors Map file. You can manually perform 88 | modifications and save the Map.
Use this functionality with caution. 89 |

-------------------------------------------------------------------------------- /tfeasycolorsmap/UI/ColorsMapMenu.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5 import QtGui, QtCore 3 | 4 | from krita import * 5 | 6 | from .Tools import * 7 | from ..Core.FILE import * 8 | from ..Core.ALERT import * 9 | 10 | # **** Colors Maps management ********************************************************************* 11 | 12 | class ColorsMapMenu: 13 | 14 | @property 15 | def contextMenu(self): 16 | return self.__contextMenu 17 | 18 | @contextMenu.setter 19 | def contextMenu(self, value): 20 | self.__contextMenu = value 21 | 22 | @property 23 | def contextMenuItems(self): 24 | return self.__contextMenuItems 25 | 26 | @contextMenuItems.setter 27 | def contextMenuItems(self, value): 28 | self.__contextMenuItems = value 29 | 30 | @property 31 | def cutItemIndex(self): 32 | return self.__cutItemIndex 33 | 34 | @cutItemIndex.setter 35 | def cutItemIndex(self, value): 36 | self.__cutItemIndex = value 37 | 38 | @property 39 | def cutItemType(self): 40 | return self.__cutItemType 41 | 42 | @cutItemType.setter 43 | def cutItemType(self, value): 44 | self.__cutItemType = value 45 | 46 | def __init__(self): 47 | super().__init__() 48 | 49 | def show(globalPos, itemIndex, fileName): 50 | ColorsMapMenu.contextMenu = Tools.contextMenu() 51 | ColorsMapMenu.contextMenuItems = Tools.contextMenuItems( 52 | ColorsMapMenu.contextMenu, 53 | [ 54 | ["AddGroup", "Add Group Title", "Add a Group Title after this clicked Color or Group."], 55 | ["Rename", "Rename [left + CTRL]", "Change the name of the clicked Color or Group."], 56 | ["-"], 57 | ["Cut", "Cut", "Select the clicked Color or Group for the next 'Paste' operation."], 58 | ["Paste", "Paste after", "Paste the 'cut' element after this clicked Color or Group."], 59 | ["PasteGroup", "Paste Group after", "Paste the 'cut' Group and all its Colors after the clicked Color."], 60 | ["-"], 61 | ["Delete", "Delete", "Delete the clicked Color or Group (not its Colors)"], 62 | ["DeleteGroup", "Delete Group", "Delete the clicked Group and all its Colors."], 63 | ["-"], 64 | ["[Slot_0]", "Main Slot", "Move this Color to the Main Slot."], 65 | ["[Slot_1]", "Slot 1", "Move this Color to Slot 1."], 66 | ["[Slot_2]", "Slot 2", "Move this Color to Slot 2."], 67 | ["[Slot_3]", "Slot 3", "Move this Color to Slot 3."], 68 | ["[Slot_4]", "Slot 4", "Move this Color to Slot 4."], 69 | ["[Slot_5]", "Slot 5", "Move this Color to Slot 5."] 70 | ]) 71 | 72 | action = ColorsMapMenu.contextMenu.exec_(globalPos) 73 | if (not action is None): ColorsMapMenu.execute(action.iconText(), itemIndex, fileName) 74 | 75 | def execute(action, index, fileName): 76 | fileContent = FILE.open(fileName) 77 | item = fileContent[index] 78 | lineData = item.split("|") 79 | isColor = lineData[0] == "[C]" 80 | isGroup = lineData[0] == "[G]" 81 | 82 | if (action == "AddGroup"): 83 | 84 | title = ALERT.prompt("GROUP TITLE", "Type a title for a new group of Colors:") 85 | if (title["ok"]): 86 | title["value"] = Tools.sanitizeName(title["value"]) 87 | fileContent.insert(index + 1, "[G]|[O]|[V]|" + title["value"] + "|" + "\n") 88 | FILE.saveList(fileName, fileContent) 89 | 90 | elif (action == "Rename"): 91 | 92 | newName = ALERT.prompt("NEW COLOR NAME" if isColor else "NEW GROUP NAME", "Type a new short name:", lineData[3]) 93 | if (newName["ok"]): 94 | lineData[3] = newName["value"] 95 | fileContent[index] = '|'.join(lineData) 96 | FILE.saveList(fileName, fileContent) 97 | 98 | elif (action == "Cut"): 99 | 100 | ColorsMapMenu.cutItemIndex = index 101 | ColorsMapMenu.cutItemType = lineData[0] 102 | 103 | elif (action == "Paste"): 104 | 105 | if (not ColorsMapMenu.isCutIndexValid()): 106 | ALERT.warn("ATTENTION", "To 'paste' something, you must 'cut' something first.") 107 | return 108 | if (index == ColorsMapMenu.cutItemIndex): 109 | ALERT.warn("ATTENTION", "You can't 'paste' on the same item you have just 'cut'.") 110 | return 111 | if (ColorsMapMenu.cutItemIndex == 1): 112 | ALERT.warn("ATTENTION", "You can't 'cut/paste' the first Group without moving also its Colors. Use 'Paste Group' in this case.") 113 | return 114 | if (ColorsMapMenu.cutItemType == "[G]" and isGroup): 115 | ALERT.warn("ATTENTION", "You can't 'paste' a Group after another Group. Click and 'paste' after a Color instead.") 116 | return 117 | 118 | cutItem = fileContent[ColorsMapMenu.cutItemIndex] 119 | fileContent[ColorsMapMenu.cutItemIndex] = "" 120 | fileContent.insert(index + 1, cutItem) 121 | FILE.saveList(fileName, fileContent) 122 | ColorsMapMenu.cutItemIndex = -1 123 | 124 | elif (action == "PasteGroup"): 125 | 126 | if (not ColorsMapMenu.isCutIndexValid()): 127 | ALERT.warn("ATTENTION", "To 'paste' something, you must 'cut' something first.") 128 | return 129 | if (index == ColorsMapMenu.cutItemIndex): 130 | ALERT.warn("ATTENTION", "You can't 'paste' on the same item you have just 'cut'.") 131 | return 132 | if (ColorsMapMenu.cutItemType == "[C]"): 133 | ALERT.error("ATTENTION", "The element you cut was a Color. This 'paste' function can work with cut Groups only.") 134 | return 135 | if (isGroup): 136 | ALERT.error("ATTENTION", "You can't 'paste' a Group after another Group. Click and 'paste' after a Color instead.") 137 | return 138 | 139 | group = list() 140 | for i, line in enumerate(fileContent): 141 | if (i > ColorsMapMenu.cutItemIndex and ColorsMapMenu.lineIsGroup(line)): break 142 | if (i >= ColorsMapMenu.cutItemIndex): 143 | group.append(line) 144 | fileContent[i] = "" 145 | 146 | for i in range(len(group)): 147 | fileContent.insert(i + (index + 1), group[i]) 148 | 149 | FILE.saveList(fileName, fileContent) 150 | ColorsMapMenu.cutItemIndex = -1 151 | 152 | elif (action == "Delete"): 153 | 154 | del fileContent[index] 155 | FILE.saveList(fileName, fileContent) 156 | 157 | elif (action == "DeleteGroup"): 158 | 159 | if (isColor): 160 | ALERT.error("ATTENTION", "You can't 'delete' a Color with this function.") 161 | return 162 | 163 | if (ALERT.ask("ATTENTION", "This operation will delete this Group and all its Colors.\n\nAre you sure?")): 164 | deleteColors = False 165 | for i, line in enumerate(fileContent): 166 | 167 | if (deleteColors and ColorsMapMenu.lineIsColor(line)): 168 | fileContent[i] = "" 169 | 170 | if (ColorsMapMenu.lineIsGroup(line)): 171 | deleteColors = False 172 | 173 | if (i == index): 174 | fileContent[i] = "" 175 | deleteColors = True 176 | 177 | FILE.saveList(fileName, fileContent) 178 | 179 | elif ("[Slot_" in action and isColor): 180 | if ("[Slot" in item): 181 | lineData[13] = action 182 | else: 183 | lineData[13] = action + "|\n" 184 | 185 | fileContent[index] = '|'.join(lineData) 186 | FILE.saveList(fileName, fileContent) 187 | 188 | def isCutIndexValid(): 189 | if (Tools.isProperty(ColorsMapMenu.cutItemIndex)): 190 | return False 191 | elif (ColorsMapMenu.cutItemIndex < 0): 192 | return False 193 | return True 194 | 195 | def lineIsColor(line): 196 | return (line.find("[C]") == 0) 197 | 198 | def lineIsGroup(line): 199 | return (line.find("[G]") == 0) -------------------------------------------------------------------------------- /tfeasycolorsmap/UI/ColorsMapRender.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5 import QtGui, QtCore 3 | 4 | from krita import * 5 | import math 6 | 7 | from .Tools import * 8 | #from ..Core.ALERT import * 9 | from ..Core.FILE import * 10 | from ..Core.KRITA import * 11 | 12 | # **** Colors Maps rendering engine *************************************************************** 13 | 14 | class ColorsMapRender: 15 | def __init__(self): 16 | super().__init__() 17 | 18 | # Render the given Colors Map file and return a Dictionary with the rendered Pixmap and other info. 19 | # isPopUp: if the Map is rendered into the Docker (false) or into the popup window (true) 20 | def run(fileName, scrollArea, isPopUp = False): 21 | map = list() 22 | data = ColorsMapRender.readColorsMapFile(fileName) 23 | 24 | W = scrollArea.width() - data["scrollSize"] 25 | H = scrollArea.height() - data["scrollSize"] 26 | 27 | counters = ColorsMapRender.counters(W, data) 28 | cols = counters["cols"] 29 | rows = counters["rows"] 30 | colors = counters["colors"] 31 | titles = counters["titles"] 32 | 33 | counter = 0 34 | x = 0 35 | y = 0 36 | areaW = cols * data["colorSize"] 37 | areaH = (titles * data["titleSize"]) + (rows * data["colorSize"]) 38 | 39 | pixmap = QPixmap(W, areaH) 40 | pixmap.fill(Qt.transparent) 41 | painter = QPainter(pixmap) 42 | wasCollapsed = False 43 | isVisible = True 44 | groupSlotNumber = "*" 45 | colorSlotNumber = "*" 46 | canRenderColor = True 47 | 48 | for index, item in enumerate(data["items"]): 49 | if (item["isColor"] and not wasCollapsed and isVisible): 50 | 51 | canRenderColor = True 52 | if (groupSlotNumber != "*"): 53 | colorSlotNumber = ColorsMapRender.getSlotNumber(item["slot"]) 54 | canRenderColor = (colorSlotNumber == groupSlotNumber) or colorSlotNumber == "0" 55 | 56 | if (canRenderColor): 57 | painter = ColorsMapRender.drawColor(painter, item, x, y, data["colorSize"], data["colorFontSize"], colorSlotNumber) 58 | map.append(str(index) + "|C|" + str(x) + "|" + str(y) + "|" + str(data["colorSize"]) + "|" + str(data["colorSize"])) 59 | counter += 1 60 | if (counter < cols): 61 | x += data["colorSize"] 62 | else: 63 | counter = 0 64 | x = 0 65 | y += data["colorSize"] 66 | 67 | elif (item["isGroup"] and item["isVisible"]): 68 | 69 | if (index > 0): 70 | x = 0 71 | y += data["colorSize"] 72 | if (counter == 0): y -= data["colorSize"] 73 | 74 | groupSlotNumber = ColorsMapRender.getSlotNumber(item["slot"]) 75 | ColorsMapRender.drawTitle(painter, item, data["titleSize"], data["titleFontSize"], x, y, W, isPopUp, data["groupColors"], groupSlotNumber) 76 | map.append(str(index) + "|G|" + str(x) + "|" + str(y) + "|" + str(W) + "|" + str(data["titleSize"])) 77 | counter = 0 78 | x = 0 79 | y += data["titleSize"] + 1 80 | wasCollapsed = not(item["isOpen"]) 81 | isVisible = True 82 | 83 | elif (item["isGroup"] and not item["isVisible"]): 84 | isVisible = False 85 | 86 | painter.end() 87 | 88 | d = dict() 89 | d["pixmap"] = pixmap 90 | d["map"] = map 91 | d["cols"] = cols 92 | d["rows"] = rows 93 | d["titles"] = titles 94 | return d 95 | 96 | # Draw a colored box with an inner title. 97 | def drawColor(painter, item, x, y, colorSize, fontSize, slotNumber = ""): 98 | 99 | # Requested data to show 100 | colorName = item["name"] 101 | qColor = KRITA.createColor("QCOLOR", item["colorModel"], item["colorDepth"], item["colorProfile"], item["color01"], item["color02"], item["color03"], item["color04"], item["color05"]) 102 | useBlack = ColorsMapRender.getTextLuminance(qColor) > 0.5 103 | 104 | # Render 105 | style = "" 106 | if (slotNumber in "1 2 3 4 5"): style = "i" 107 | Tools.drawRect(painter, Qt.black, qColor, x, y, colorSize, colorSize) 108 | Tools.drawBoxedText(painter, colorName, Qt.black if useBlack else Qt.white, fontSize, x, y, colorSize - 4, colorSize - 4, style) 109 | 110 | return painter 111 | 112 | # Get the luminance for the name of the Color. It uses the RGB model params of the given QColor. 113 | def getTextLuminance(qColor): 114 | return qColor.redF() * 0.299 + qColor.greenF() * 0.587 + qColor.blueF() * 0.114 115 | 116 | # Draw the Group collapsible title. 117 | def drawTitle(painter, item, titleSize, titleFontSize, x, y, w, isPopUp, colors, slotNumber = ""): 118 | 119 | # The collapsible rectangle container. 120 | bgColor = QColor(colors["bg"]) 121 | txtColor = QColor(colors["txt"]) 122 | Tools.drawRect(painter, bgColor, bgColor, x, y, w, titleSize) 123 | Tools.drawText(painter, item["name"], txtColor, titleFontSize, x + 8, y + 18) 124 | 125 | if (not isPopUp): 126 | expX = w - titleSize + 2 127 | expY = y + 4 128 | expS = titleSize - 4 - 4 129 | 130 | # Variations Slots 131 | slotX = expX - expS - 4 132 | Tools.drawRect(painter, Qt.gray, QColor(83, 114, 142), slotX, expY, expS, expS) 133 | if (slotNumber == "*"): 134 | Tools.drawText(painter, slotNumber, Qt.white, titleFontSize - 4, slotX + 6, expY + 15) 135 | else: 136 | Tools.drawText(painter, slotNumber, Qt.white, titleFontSize - 4, slotX + 5, expY + 13) 137 | 138 | # The collapse icon [+] or [-] 139 | Tools.drawRect(painter, Qt.gray, QColor(83, 114, 142), expX, expY, expS, expS) 140 | 141 | expIcon = "" 142 | offset = 3 143 | if (item["isOpen"]): 144 | expIcon = "-" 145 | offset = 5 146 | else: 147 | expIcon = "+" 148 | 149 | Tools.drawText(painter, expIcon, Qt.white, titleFontSize, expX + offset, expY + 13) 150 | 151 | # Convert the Slot Code into a Slot Symbol. 152 | def getSlotNumber(slotNumber): 153 | if (len(slotNumber) < 6): slotNumber = "0" 154 | if (slotNumber == "[Slot_0]"): slotNumber = "0" 155 | if (slotNumber == "[Slot_1]"): slotNumber = "1" 156 | if (slotNumber == "[Slot_2]"): slotNumber = "2" 157 | if (slotNumber == "[Slot_3]"): slotNumber = "3" 158 | if (slotNumber == "[Slot_4]"): slotNumber = "4" 159 | if (slotNumber == "[Slot_5]"): slotNumber = "5" 160 | if (slotNumber == "[Slot_All]"): slotNumber = "*" 161 | return slotNumber 162 | 163 | # Read the .cmap file and return an organizes structure of its content, suitable for the rendering process. 164 | def readColorsMapFile(fileName): 165 | fileContent = FILE.open(fileName) 166 | 167 | data = { 168 | "colorModel": "", 169 | "colorDepth": "", 170 | "colorProfile": "", 171 | "colorSize": 0, 172 | "titleSize": 0, 173 | "colorFontSize": 0, 174 | "titleFontSize": 0, 175 | "scrollSize": 0, 176 | "groupColors": 0, 177 | "items": list(), 178 | "slot": "" 179 | } 180 | 181 | for index, line in enumerate(fileContent): 182 | 183 | if (index == 0): 184 | # Colors Map configuration 185 | tmp = line.split("|") 186 | data["colorModel"] = tmp[3] 187 | data["colorDepth"] = tmp[4] 188 | data["colorProfile"] = tmp[5] 189 | data["colorSize"] = int(tmp[6]) 190 | data["titleSize"] = int(tmp[7]) 191 | data["colorFontSize"] = int(tmp[8]) 192 | data["titleFontSize"] = int(tmp[9]) 193 | data["scrollSize"] = int(tmp[10]) 194 | data["groupColors"] = ColorsMapRender.extractGroupColors(tmp[11]) 195 | else: 196 | # Groups and Colors 197 | tmp = line.split("|") 198 | if (tmp[0] == "[G]"): 199 | data["items"].append({ 200 | "isGroup": True, 201 | "isColor": False, 202 | "isOpen": (tmp[1] == "[O]"), 203 | "isVisible": (tmp[2] == "[V]"), 204 | "name": tmp[3], 205 | "colorModel": "", 206 | "colorDepth": "", 207 | "colorProfile": "", 208 | "color01": 0, 209 | "color02": 0, 210 | "color03": 0, 211 | "color04": 0, 212 | "color05": 0, 213 | "color": "", 214 | "slot": tmp[4] 215 | }) 216 | 217 | elif (tmp[0] == "[C]"): 218 | data["items"].append({ 219 | "isGroup": False, 220 | "isColor": True, 221 | "isOpen": (tmp[1] == "[O]"), 222 | "isVisible": (tmp[2] == "[V]"), 223 | "name": tmp[3], 224 | "colorModel": tmp[4], 225 | "colorDepth": tmp[5], 226 | "colorProfile": tmp[6], 227 | "color01": float(tmp[7]), 228 | "color02": float(tmp[8]), 229 | "color03": float(tmp[9]), 230 | "color04": float(tmp[10]), 231 | "color05": float(tmp[11]), 232 | "color": tmp[12], 233 | "slot": tmp[13] 234 | }) 235 | 236 | return data 237 | 238 | # Count and estimate the number of Rows, Columns, Colors and Groups 239 | def counters(W, data): 240 | cols = math.floor(W / data["colorSize"]) 241 | rows = 0 242 | colors = 0 243 | titles = 0 244 | 245 | for index, item in enumerate(data["items"]): 246 | if (item["isColor"]): 247 | 248 | colors += 1 249 | 250 | elif (item["isGroup"]): 251 | 252 | titles += 1 253 | if (colors > 0): 254 | rows += math.ceil(colors / cols) 255 | colors = 0 256 | 257 | if (rows == 0): 258 | rows = math.ceil(colors / cols) 259 | elif (colors > 0): 260 | rows += math.ceil(colors / cols) 261 | 262 | return { "rows" : rows, "cols": cols, "colors": colors, "titles": titles } 263 | 264 | def renderTempColors(colorsMap, scrollArea): 265 | size = 16 266 | 267 | W = scrollArea.width() 268 | H = scrollArea.height() 269 | 270 | pixmap = QPixmap(W, size) 271 | pixmap.fill(Qt.transparent) 272 | painter = QPainter(pixmap) 273 | 274 | x = 0 275 | y = 0 276 | 277 | colorsList = ColorsMapRender.readTempMap(colorsMap) 278 | 279 | for index, item in enumerate(colorsList["items"]): 280 | painter = ColorsMapRender.drawColor(painter, item, x, y, size, 4) 281 | x += size 282 | 283 | painter.end() 284 | return pixmap 285 | 286 | def readTempMap(colorsMap): 287 | 288 | data = { 289 | "colorModel": "", 290 | "colorDepth": "", 291 | "colorProfile": "", 292 | "colorSize": 0, 293 | "titleSize": 0, 294 | "colorFontSize": 0, 295 | "titleFontSize": 0, 296 | "scrollSize": 0, 297 | "items": list() 298 | } 299 | 300 | for index, line in enumerate(colorsMap): 301 | 302 | tmp = line.split("|") 303 | data["items"].append({ 304 | "isGroup": False, 305 | "isColor": True, 306 | "isOpen": True, 307 | "isVisible": True, 308 | "name": "", 309 | "colorModel": tmp[4], 310 | "colorDepth": tmp[5], 311 | "colorProfile": tmp[6], 312 | "color01": float(tmp[7]), 313 | "color02": float(tmp[8]), 314 | "color03": float(tmp[9]), 315 | "color04": float(tmp[10]), 316 | "color05": float(tmp[11]), 317 | "color": tmp[12] 318 | }) 319 | 320 | return data 321 | 322 | def extractGroupColors(data): 323 | 324 | if (data == "" or len(data) < 15): return { "bg": "#000000", "txt": "#FFFFFF"} 325 | if (data.find("#") < 0 or data.find(" ") < 0): return { "bg": "#000000", "txt": "#FFFFFF"} 326 | tmp = data.split(" ") 327 | if (len(tmp[0]) < 7 or len(tmp[1]) < 7): return { "bg": "#000000", "txt": "#FFFFFF"} 328 | 329 | return { "bg": tmp[0], "txt": tmp[1]} -------------------------------------------------------------------------------- /tfeasycolorsmap/UI/ColorsSlotMenu.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5 import QtGui, QtCore 3 | 4 | from krita import * 5 | 6 | from .Tools import * 7 | from ..Core.FILE import * 8 | from ..Core.ALERT import * 9 | 10 | # **** Colors Slots management ********************************************************************* 11 | 12 | class ColorsSlotMenu: 13 | 14 | @property 15 | def contextMenu(self): 16 | return self.__contextMenu 17 | 18 | @contextMenu.setter 19 | def contextMenu(self, value): 20 | self.__contextMenu = value 21 | 22 | @property 23 | def contextMenuItems(self): 24 | return self.__contextMenuItems 25 | 26 | @contextMenuItems.setter 27 | def contextMenuItems(self, value): 28 | self.__contextMenuItems = value 29 | 30 | @property 31 | def slot(self): 32 | return self.__slot 33 | 34 | @slot.setter 35 | def slot(self, value): 36 | self.__slot = value 37 | 38 | def __init__(self): 39 | super().__init__() 40 | 41 | def show(globalPos, itemIndex, fileName): 42 | ColorsSlotMenu.contextMenu = Tools.contextMenu() 43 | ColorsSlotMenu.contextMenuItems = Tools.contextMenuItems( 44 | ColorsSlotMenu.contextMenu, 45 | [ 46 | ["[Slot_0]", "Main Slot", "Show the Colors of the Main Slot (shared Colors)."], 47 | ["-"], 48 | ["[Slot_1]", "Slot 1", "Show the Colors of the Slot 1."], 49 | ["[Slot_2]", "Slot 2", "Show the Colors of the Slot 2."], 50 | ["[Slot_3]", "Slot 3", "Show the Colors of the Slot 3."], 51 | ["[Slot_4]", "Slot 4", "Show the Colors of the Slot 4."], 52 | ["[Slot_5]", "Slot 5", "Show the Colors of the Slot 5."], 53 | ["-"], 54 | ["[Slot_All]", "Show All", "Show all the Colors of this Group."], 55 | ]) 56 | 57 | action = ColorsSlotMenu.contextMenu.exec_(globalPos) 58 | ColorsSlotMenu.slot = "" 59 | if (not action is None): 60 | ColorsSlotMenu.slot = action.iconText() 61 | 62 | fileContent = FILE.open(fileName) 63 | item = fileContent[itemIndex] 64 | lineData = item.split("|") 65 | 66 | if ("[Slot_" in item): 67 | lineData[4] = ColorsSlotMenu.slot 68 | else: 69 | lineData[4] = ColorsSlotMenu.slot + "|\n" 70 | 71 | fileContent[itemIndex] = '|'.join(lineData) 72 | FILE.saveList(fileName, fileContent) -------------------------------------------------------------------------------- /tfeasycolorsmap/UI/QColorsMap.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5 import QtGui, QtCore 3 | 4 | from krita import * 5 | import math 6 | 7 | from .Tools import * 8 | from .ColorsMapRender import * 9 | from .ColorsMapMenu import * 10 | from .ColorsSlotMenu import * 11 | from ..Core.KRITA import * 12 | from ..Core.ALERT import * 13 | from ..Core.FILE import * 14 | from ..Core.SYS import * 15 | 16 | # **** Colors Maps management ********************************************************************* 17 | 18 | class QColorsMap: 19 | def __init__(self): 20 | super().__init__() 21 | 22 | @property 23 | def fileName(self): 24 | return self.__fileName 25 | @fileName.setter 26 | def fileName(self, value): 27 | self.__fileName = value 28 | 29 | @property 30 | def map(self): 31 | return self.__map 32 | @map.setter 33 | def map(self, value): 34 | self.__map = value 35 | 36 | @property 37 | def pluginSelf(self): 38 | return self.__pluginSelf 39 | @pluginSelf.setter 40 | def pluginSelf(self, value): 41 | self.__pluginSelf = value 42 | 43 | @property 44 | def autoColor(self): 45 | return self.__autoColor 46 | @autoColor.setter 47 | def autoColor(self, value): 48 | self.__autoColor = value 49 | 50 | @property 51 | def tempColorsMap(self): 52 | return self.__tempColorsMap 53 | @tempColorsMap.setter 54 | def tempColorsMap(self, value): 55 | self.__tempColorsMap = value 56 | 57 | 58 | # ---- Interface ------------------------------------------------------------------------------ 59 | 60 | # UI render. 61 | def render(mainWidget, pluginSelf): 62 | QColorsMap.fileName = "" 63 | QColorsMap.autoColor = None 64 | QColorsMap.map = list() 65 | QColorsMap.tempColorsMap = list() 66 | QColorsMap.pluginSelf = pluginSelf # Reference to plugin's 'self'. 67 | 68 | QColorsMap.colorsMap = QLabel(mainWidget) 69 | QColorsMap.colorsMap.setCursor(KRITA.cursor('krita_tool_color_sampler')) 70 | QColorsMap.colorsMap.mousePressEvent = QColorsMap.onColorsMapClick 71 | 72 | QColorsMap.colorLabel = QLabel(mainWidget) 73 | QColorsMap.colorLabel.setAlignment(Qt.AlignCenter) 74 | QColorsMap.colorLabel.setText("...") 75 | 76 | QColorsMap.scrollMapArea = QScrollArea() 77 | QColorsMap.scrollMapArea.setWidgetResizable(True) 78 | QColorsMap.scrollMapArea.setWidget(QColorsMap.colorsMap) 79 | QColorsMap.lastScrollMapAreaSize = QColorsMap.scrollMapArea.size() 80 | 81 | QColorsMap.tempMap = QLabel(mainWidget) 82 | QColorsMap.tempMap.setAlignment(Qt.AlignTop | Qt.AlignLeft) 83 | QColorsMap.tempMap.setCursor(KRITA.cursor('krita_tool_color_sampler')) 84 | QColorsMap.tempMap.mousePressEvent = QColorsMap.onTempMapClick 85 | 86 | QColorsMap.scrollTempArea = QScrollArea() 87 | QColorsMap.scrollTempArea.setWidgetResizable(True) 88 | QColorsMap.scrollTempArea.setWidget(QColorsMap.tempMap) 89 | QColorsMap.scrollTempArea.setStyleSheet("height:16px;max-height:16px") 90 | QColorsMap.scrollTempArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 91 | 92 | maps = Tools.QWidgetLayout("V", "", True) 93 | maps.layout().addWidget(QColorsMap.scrollMapArea) 94 | maps.layout().addWidget(QColorsMap.colorLabel) 95 | maps.layout().addWidget(QColorsMap.scrollTempArea) 96 | 97 | QColorsMap.lifeCycleFastTimer = Tools.interval(QColorsMap.lifeCycleFast, 500, True) 98 | QColorsMap.lifeCycleSlowTimer = Tools.interval(QColorsMap.lifeCycleSlow, 1000, True) 99 | 100 | return maps 101 | 102 | 103 | # ---- Colors Map click ----------------------------------------------------------------------- 104 | 105 | # Click on the main Colors Map. 106 | # If there's no Map, it asks to open/create a Map file. 107 | def onColorsMapClick(event): 108 | if (KRITA.isNotReady()): 109 | KRITA.actionTrigger("file_open") 110 | return 111 | 112 | if (QColorsMap.mapExists()): 113 | QColorsMap.executeColorsMapOperations(event) 114 | else: 115 | QColorsMap.openFileDialog() 116 | 117 | # Ask the user to select or create a Colors Map file. 118 | def openFileDialog(): 119 | fileName = ALERT.dialogFileOpen("Colors Map", "Colors Map file (*.cmap)") 120 | QColorsMap.fileName = QColorsMap.checkFileName(fileName) 121 | if (QColorsMap.fileName == ""): return 122 | 123 | if (FILE.exists(QColorsMap.fileName)): 124 | QColorsMap.updateCheck() 125 | QColorsMap.load() 126 | else: 127 | QColorsMap.createNewColorsMapFile() 128 | KRITA.annotationSet(QColorsMap.fileName) 129 | 130 | # Execute the various Colors Map operations when the user clicks on the Colors Map. 131 | def executeColorsMapOperations(event): 132 | click = KRITA.getEvent(event) 133 | 134 | # RIGHT CLICK 135 | if (click["right"]): 136 | if (click["noModifier"]): 137 | QColorsMap.newColorAdd() 138 | elif (click["isShift"]): 139 | foundItem = QColorsMap.findItemIndex(click["x"], click["y"]) 140 | if (foundItem["index"] >= 0): QColorsMap.newColorAdd(foundItem["index"] + 1) 141 | elif (click["isCtrl"]): 142 | foundItem = QColorsMap.findItemIndex(click["x"], click["y"]) 143 | if (foundItem["index"] >= 0): ColorsMapMenu.show(click["globalPos"], foundItem["index"], QColorsMap.fileName) 144 | QColorsMap.load() 145 | 146 | # LEFT CLICK 147 | if (click["left"]): 148 | foundItem = QColorsMap.findItemIndex(click["x"], click["y"]) 149 | if (foundItem["index"] < 0): return 150 | 151 | if (click["isCtrl"]): 152 | ColorsMapMenu.execute("Rename", foundItem["index"], QColorsMap.fileName) 153 | QColorsMap.load() 154 | else: 155 | if (foundItem["isColor"]): 156 | # COLOR 157 | if (click["noModifier"]): 158 | KRITA.setColor("F", QColorsMap.createColorFromMap(foundItem["index"])) 159 | elif (click["isShift"]): 160 | KRITA.setColor("B", QColorsMap.createColorFromMap(foundItem["index"])) 161 | else: 162 | # GROUP 163 | if (foundItem["isCollapse"]): 164 | FILE.modifyLineToggle(QColorsMap.fileName, foundItem["index"], 1, "[O]", "[X]") 165 | QColorsMap.load() 166 | if (foundItem["isSlot"]): 167 | ColorsSlotMenu.show(click["globalPos"], foundItem["index"], QColorsMap.fileName) 168 | QColorsMap.load() 169 | 170 | 171 | 172 | # Add a new color to the Colors Map. If index = 0 (default) it's added to the end, otherwise it's added after the given index. 173 | def newColorAdd(index = 0): 174 | newColor = KRITA.getSelectedColor("F") 175 | colorName = ALERT.prompt("NEW COLOR NAME", "Type a short name for this new " + KRITA.colorParam("MODEL") + " Color:") 176 | if (colorName["ok"]): 177 | colorName["value"] = Tools.sanitizeName(colorName["value"]) 178 | data = "[C]|[O]|[V]|" + colorName["value"] + "|" + KRITA.colorParam("MODEL") + "|" + KRITA.colorParam("DEPTH") + "|" + KRITA.colorParam("PROFILE") + "|" 179 | for value in newColor: data += value + "|" 180 | if (index == 0): 181 | FILE.append(QColorsMap.fileName, data) 182 | else: 183 | FILE.saveToIndex(QColorsMap.fileName, data, index) 184 | 185 | # ---- Colors Map render ---------------------------------------------------------------------- 186 | 187 | # Graphically render the Colors Map on the QColorsMap widget. 188 | def colorsMapRender(): 189 | canvas = KRITA.getCurrentCanvas() 190 | if (not canvas["canvas"] is None): canvas["canvas"].installEventFilter(QColorsMap.pluginSelf) 191 | 192 | data = ColorsMapRender.run(QColorsMap.fileName, QColorsMap.scrollMapArea) 193 | QColorsMap.colorsMap.setAlignment(Qt.AlignTop | Qt.AlignLeft) 194 | QColorsMap.colorsMap.setPixmap(data["pixmap"]) 195 | 196 | # This special 'map' is used for finding the index of an item inside the Colors Map 197 | QColorsMap.map = data["map"] 198 | 199 | # Return the index (1..n) and type (isColor, isGroup) of an item into the Colors Map file. It requires the QColorsMap.map property correctly filled and the click x/y coordinates. 200 | # map : for using a different map (i.e. popup Map) 201 | def findItemIndex(x, y, map = None): 202 | if (map is None): map = QColorsMap.map 203 | for index, item in enumerate(map): 204 | tmp = item.split("|") 205 | itemIndex = int(tmp[0]) 206 | itemType = tmp[1] 207 | X1 = int(tmp[2]) 208 | Y1 = int(tmp[3]) 209 | itemW = int(tmp[4]) 210 | itemH = int(tmp[5]) 211 | 212 | X2 = X1 + itemW 213 | Y2 = Y1 + itemH 214 | 215 | isCollapse = False 216 | isSlot = False 217 | if (itemType == "G"): 218 | cX1 = itemW - 16 219 | cY1 = Y1 220 | cX2 = cX1 + 16 221 | cY2 = cY1 + 16 222 | if (x >= cX1 and x <= cX2 and y >= cY1 and y <= cY2): isCollapse = True 223 | cX1 = itemW - 32 224 | cX2 = cX1 + 16 225 | if (x >= cX1 and x <= cX2 and y >= cY1 and y <= cY2): isSlot = True 226 | 227 | if (x >= X1 and x <= X2 and y >= Y1 and y <= Y2): return { "index": itemIndex + 1, "isColor": itemType == "C", "isGroup": itemType == "G", "isCollapse": isCollapse, "isSlot": isSlot } 228 | 229 | return { "index": -1, "type": "" } 230 | 231 | 232 | 233 | # ---- Colors Map file ------------------------------------------------------------------------ 234 | 235 | # Start the creation of a new Colors Map file. 236 | def createNewColorsMapFile(): 237 | color = KRITA.colorConfig() 238 | 239 | data = "" 240 | data += "TF Easy Colors Map|2|" + str(SYS.version) + "|" + color["model"] + "|" + color["depth"] + "|" + color["profile"] + "|" 241 | data += "42|24|10|16|16|#000000 #CCCCCC" + "||||||||||\n" # colorSize, titleSize, colorFontSize, titleFontSize, scrollSize, Groups BG and text colors (and some empty positions for future data) 242 | data += "[G]|[O]|[V]|NEW COLOURS GROUP|" + "\n" 243 | 244 | FILE.save(QColorsMap.fileName, data) 245 | QColorsMap.load() 246 | 247 | # Open an existing Colors Map file and render the Map. 248 | def load(): 249 | QColorsMap.colorsMapRender() 250 | 251 | 252 | # ---- Life Cycles ---------------------------------------------------------------------------- 253 | 254 | # Perform various operation, in the meantime that Krita is opened, which must be executed pretty fast. 255 | def lifeCycleFast(): 256 | try: 257 | if (KRITA.isNotReady()): QColorsMap.showStartInstructions(True) 258 | if (KRITA.isReady() and not QColorsMap.mapExists()): QColorsMap.showMapInstructions(True) 259 | 260 | if (KRITA.isReady() and QColorsMap.mapExists()): 261 | # Krita app has an opened document and a Colors Map has been loaded / created 262 | if (QColorsMap.lastScrollMapAreaSize != QColorsMap.scrollMapArea.size()): 263 | QColorsMap.lastScrollMapAreaSize = QColorsMap.scrollMapArea.size() 264 | QColorsMap.load() 265 | except: 266 | pass 267 | 268 | # Perform various operation, in the meantime that Krita is opened, which must be executed pretty slow. 269 | def lifeCycleSlow(): 270 | try: 271 | if (KRITA.isReady()): 272 | 273 | # Krita Document annotation management 274 | annotation = KRITA.annotationGet() 275 | if (annotation != ""): 276 | if (annotation != QColorsMap.fileName): 277 | QColorsMap.fileName = annotation 278 | QColorsMap.load() 279 | else: 280 | if (QColorsMap.fileName != ""): QColorsMap.reset() 281 | 282 | # Auto-Color feature 283 | if (not QColorsMap.autoColor is None): 284 | currentColor = KRITA.getSelectedColor("F") 285 | if (QColorsMap.autoColor[5] != currentColor[5]): 286 | QColorsMap.autoColor = currentColor 287 | QColorsMap.newColorAdd() 288 | QColorsMap.load() 289 | 290 | except: 291 | pass 292 | 293 | 294 | # ---- Various -------------------------------------------------------------------------------- 295 | 296 | # Show/hide the start instructions. 297 | def showStartInstructions(show): 298 | if (show): 299 | QColorsMap.colorsMap.setText("Open a Krita document!\n(You can click here)") 300 | QColorsMap.colorsMap.setAlignment(Qt.AlignCenter) 301 | else: 302 | QColorsMap.colorsMap.setText("") 303 | 304 | # Show/hide map istructions. 305 | def showMapInstructions(show): 306 | if (show): 307 | QColorsMap.colorsMap.setText("Click here (or the [OPEN MAP] button below)\nto open an existing Colors Map\nor to create a new one.") 308 | QColorsMap.colorsMap.setAlignment(Qt.AlignCenter) 309 | else: 310 | QColorsMap.colorsMap.setText("") 311 | 312 | # Return True if a Colors Map has been loaded. 313 | def mapExists(): 314 | return QColorsMap.fileName != "" 315 | 316 | # Return True if there isn't a Colors Map yet. 317 | def hasNoMap(): 318 | return QColorsMap.fileName == "" 319 | 320 | # Check the validity of the fileName and return a valid fileName. Return "" if it's not valid. 321 | def checkFileName(fileName): 322 | if (fileName == ""): return "" 323 | if (FILE.exists(fileName)): 324 | if (not(FILE.checkExtension(fileName, ".cmap"))): fileName = "" 325 | else: 326 | if (fileName.find(".cmap") < 0): fileName += ".cmap" 327 | 328 | if (fileName == ""): ALERT.error("FILE NOT VALID", "The selected file is not valid.\n\nSelect a valid Colors Map file (.cmap file).") 329 | 330 | return fileName 331 | 332 | # Get the Color of the given index from the Colors Map file and return a Krita managed Color. Also, write the Color Label with the Color description. 333 | def createColorFromMap(index): 334 | fileContent = FILE.open(QColorsMap.fileName) 335 | tmp = fileContent[index].split("|") 336 | item = { 337 | "colorModel": tmp[4], 338 | "colorDepth": tmp[5], 339 | "colorProfile": tmp[6], 340 | "color01": float(tmp[7]), 341 | "color02": float(tmp[8]), 342 | "color03": float(tmp[9]), 343 | "color04": float(tmp[10]), 344 | "color05": float(tmp[11]) 345 | } 346 | 347 | color = tmp[12] 348 | dist = " " 349 | if (color.find("Red") >= 0): 350 | color = color.replace("Red", "R").replace("Green", dist + "G").replace("Blue", dist + "B").replace("Alpha", dist + "A") 351 | elif (color.find("Yellow") >= 0): 352 | color = color.replace("Cyan", "C").replace("Magenta", dist + "M").replace("Yellow", dist + "Y").replace("Black", dist + "K").replace("Alpha", dist + "A") 353 | elif (color.find("Lightness") >= 0): 354 | color = color.replace("Lightness", "L").replace("a*", dist + "a*").replace("b*", dist + "b*").replace("Alpha", dist + "A") 355 | elif (color.find("Z") >= 0): 356 | color = color.replace("Y", dist + "Y").replace("Z", dist + "Z").replace("Alpha", dist + "A") 357 | elif (color.find("Cb") >= 0): 358 | color = color.replace("Cb", dist + "Cb").replace("Cr", dist + "Cr").replace("b*", dist + "b*").replace("Alpha", dist + "A") 359 | elif (color.find("Gray") >= 0): 360 | color = color.replace("Alpha", dist + "A") 361 | 362 | QColorsMap.colorLabel.setText(color) 363 | 364 | qColor = KRITA.createColor("KRITA", item["colorModel"], item["colorDepth"], item["colorProfile"], item["color01"], item["color02"], item["color03"], item["color04"], item["color05"]) 365 | return qColor 366 | 367 | # Reset a Colors Map (usually, when a Krita document has been closed). 368 | def reset(): 369 | QColorsMap.fileName = "" 370 | QColorsMap.map = list() 371 | try: 372 | pixmap = QPixmap(1, 1) 373 | pixmap.fill(Qt.transparent) 374 | QColorsMap.colorsMap.setPixmap(pixmap) 375 | QColorsMap.tempMap.setPixmap(pixmap) 376 | 377 | QColorsMap.colorLabel.setText("...") 378 | except: 379 | pass 380 | 381 | # Execute a check confronting the current Document Color Profile with the loaded Colors Map file profile. 382 | def colorsProfileCheck(): 383 | currentProfile = KRITA.colorConfig() 384 | fileContent = FILE.open(QColorsMap.fileName) 385 | 386 | mismatch = False 387 | mismatchLine = "" 388 | for index, line in enumerate(fileContent): 389 | if (line.find(currentProfile["model"]) < 0 or line.find(currentProfile["depth"]) < 0): 390 | mismatch = True 391 | mismatchLine = line.split("|") 392 | 393 | if (mismatch): 394 | ALERT.info( 395 | "COLORS PROFILE MISMATCH", 396 | "Your Krita's Document has " + currentProfile["model"] + " (" + currentProfile["depth"] + ") color profile.\n\n" + 397 | "Your Colors Map contains one or more colors with different profiles (found a color with " + mismatchLine[4] + " (" + mismatchLine[5] + ") profile).\n\n" + 398 | "Keep in mind that, even if this plugin can manage Colors with different profiles, in this case you may encounter some variation in your Colors." 399 | ) 400 | 401 | 402 | 403 | 404 | # ---- Temporary Map click -------------------------------------------------------------------- 405 | 406 | def onTempMapClick(event): 407 | if (KRITA.isNotReady()): return 408 | 409 | click = KRITA.getEvent(event) 410 | 411 | # RIGHT CLICK: just add a color at the end. 412 | if (click["right"]): 413 | if (click["noModifier"]): 414 | QColorsMap.newTempAdd() 415 | 416 | # LEFT CLICK: set Foreground, +SHIFT set Background. 417 | if (click["left"]): 418 | foundItem = QColorsMap.findTempIndex(click["x"], click["y"]) 419 | if (foundItem["index"] < 0): return 420 | 421 | if (click["noModifier"]): 422 | KRITA.setColor("F", QColorsMap.createTempColor(foundItem["index"])) 423 | elif (click["isShift"]): 424 | KRITA.setColor("B", QColorsMap.createTempColor(foundItem["index"])) 425 | 426 | # Temporary Colors are added to the tempColorsMap List. 427 | def newTempAdd(): 428 | newColor = KRITA.getSelectedColor("F") 429 | data = "[C]|[O]|[V]|" + "TEMPORARY_COLOR" + "|" + KRITA.colorParam("MODEL") + "|" + KRITA.colorParam("DEPTH") + "|" + KRITA.colorParam("PROFILE") + "|" 430 | for value in newColor: data += value + "|" 431 | 432 | QColorsMap.tempColorsMap.insert(0, data) 433 | pixmap = ColorsMapRender.renderTempColors(QColorsMap.tempColorsMap, QColorsMap.scrollTempArea) 434 | QColorsMap.tempMap.setPixmap(pixmap) 435 | 436 | # Return the index of the clicked Temporary color. 437 | def findTempIndex(x, y): 438 | index = math.floor(x / 18) 439 | l = len(QColorsMap.tempColorsMap) 440 | return { "index" : -1 } if (index < 0 or index >= l) else { "index": index } 441 | 442 | # Create a Color from the clicked Temporary color (its index). 443 | def createTempColor(index): 444 | tmp = QColorsMap.tempColorsMap[index].split("|") 445 | item = { 446 | "colorModel": tmp[4], 447 | "colorDepth": tmp[5], 448 | "colorProfile": tmp[6], 449 | "color01": float(tmp[7]), 450 | "color02": float(tmp[8]), 451 | "color03": float(tmp[9]), 452 | "color04": float(tmp[10]), 453 | "color05": float(tmp[11]) 454 | } 455 | qColor = KRITA.createColor("KRITA", item["colorModel"], item["colorDepth"], item["colorProfile"], item["color01"], item["color02"], item["color03"], item["color04"], item["color05"]) 456 | return qColor 457 | 458 | # Check the version of the Colors Map and update it if required. 459 | def updateCheck(): 460 | fileContent = FILE.open(QColorsMap.fileName) 461 | fileVersion = SYS.versionToNumber(FILE.getLineValue(fileContent[0], 2)) 462 | currentVersion = SYS.version 463 | 464 | if (fileVersion < currentVersion): 465 | fileContent[0] = FILE.changeLineValue(fileContent[0], 2, str(currentVersion)) 466 | 467 | if (fileVersion == 20): 468 | fileContent[0] = fileContent[0].replace("\n", "") + "#000000 #CCCCCC||||||||||\n" 469 | 470 | FILE.saveList(QColorsMap.fileName, fileContent) -------------------------------------------------------------------------------- /tfeasycolorsmap/UI/Tools.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5 import QtGui, QtCore 3 | from PyQt5.QtCore import Qt 4 | from PyQt5.QtCore import QTimer 5 | 6 | from krita import * 7 | from ..Core.ALERT import * 8 | 9 | # **** UI Helpers ********************************************************************************* 10 | 11 | class Tools: 12 | def __init__(self): 13 | super().__init__() 14 | 15 | # Create a TOOLBAR Button. 16 | def toolBt(icon, f, toolTip = "", style = Qt.ToolButtonIconOnly, text = ""): 17 | bt = QToolButton() 18 | bt.setToolButtonStyle(style) 19 | bt.setIcon(Krita.instance().icon(icon)) 20 | if (toolTip != ""): bt.setToolTip(toolTip) 21 | if (text != ""): bt.setText(text) 22 | bt.clicked.connect(f) 23 | return bt 24 | 25 | # Create a simple PUSH Button. 26 | def bt(text, f, toolTip=""): 27 | bt = QPushButton() 28 | if (toolTip != ""): 29 | bt.setToolTip(toolTip) 30 | bt.setText(text) 31 | bt.clicked.connect(f) 32 | return bt 33 | 34 | # Create a QWidget with a Layout direction and alignment. 35 | def QWidgetLayout(direction, alignment = "", enableMargins = False): 36 | formLayout = QWidget() 37 | fL = QVBoxLayout() 38 | if (direction == "H"): fL = QHBoxLayout() 39 | if (alignment != ""): fL.setAlignment(alignment) 40 | if (enableMargins): fL.setContentsMargins(0, 10, 6, 0) 41 | formLayout.setLayout(fL) 42 | return formLayout 43 | 44 | # Return a configured QTimer interval Widget. 45 | def interval(f, time, start = False): 46 | timer = QTimer() 47 | timer.timeout.connect(f) 48 | timer.setInterval(time) 49 | if (start): timer.start() 50 | return timer 51 | 52 | # Return a configured QTimer timeout Widget. 53 | def timeout(f, time, start = True): 54 | timer = QTimer() 55 | timer.timeout.connect(f) 56 | timer.setInterval(time) 57 | timer.setSingleShot(True) 58 | if (start): timer.start() 59 | return timer 60 | 61 | # Draw a rectangle into the given 'painter' 62 | def drawRect(painter, penColor, brushColor, x, y, w, h, penSize = 1, brushPattern = Qt.SolidPattern): 63 | painter.setPen(QPen(penColor, penSize, Qt.SolidLine)) 64 | painter.setBrush(QBrush(brushColor, brushPattern)) 65 | painter.drawRect(x, y, w, h) 66 | return painter 67 | 68 | # Draw a text into the given 'painter' 69 | def drawText(painter, text, textColor, fontSize, x, y, style = ""): 70 | font = painter.font() 71 | font.setPixelSize(fontSize) 72 | font.setItalic(True if style == "i" else False) 73 | 74 | painter.setFont(font) 75 | painter.setPen(QPen(textColor, 1, Qt.SolidLine)) 76 | painter.drawText(x, y, text) 77 | return painter 78 | 79 | # Draw a boxed text into the given 'painter' 80 | def drawBoxedText(painter, text, textColor, fontSize, x, y, width, height, style = ""): 81 | font = painter.font() 82 | font.setPixelSize(fontSize) 83 | font.setItalic(True if style == "i" else False) 84 | 85 | painter.setFont(font) 86 | painter.setPen(QPen(textColor, 1, Qt.SolidLine)) 87 | rectangle = QRect(x + 2, y, width, height) 88 | painter.drawText(rectangle, Qt.TextWordWrap, text) 89 | return painter 90 | 91 | def contextMenu(): 92 | contextMenu = QMenu() 93 | contextMenu.setToolTipsVisible(True) 94 | return contextMenu 95 | 96 | def contextMenuItems(cM, data): 97 | menuItems = dict() 98 | for item in data: 99 | if (item[0] == "-"): 100 | cM.addSeparator() 101 | else: 102 | menuItems[item[0]] = cM.addAction(item[1]) 103 | menuItems[item[0]].setToolTip(item[2]) 104 | menuItems[item[0]].setIconText(item[0]) 105 | return menuItems 106 | 107 | def isProperty(value): 108 | string = str(value) 109 | return string.find("property") >= 0 110 | 111 | # Remove strings that may compromise the Colors Map. 112 | def sanitizeName(name): 113 | name = name.replace("|", "") 114 | name = name.replace("[C]", "") 115 | name = name.replace("[G]", "") 116 | name = name.replace("[O]", "") 117 | name = name.replace("[V]", "") 118 | name = name.replace("[X]", "") 119 | name = name.replace("[-]", "") 120 | name = name.replace("\n", "") 121 | return name -------------------------------------------------------------------------------- /tfeasycolorsmap/UI/UI.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5 import QtGui, QtCore 3 | 4 | from krita import * 5 | 6 | from ..Core.SYS import * 7 | from ..Core.KRITA import * 8 | from ..Core.ALERT import * 9 | from .QColorsMap import * 10 | from .Tools import * 11 | from ..Forms.FormsManager import * 12 | 13 | # **** Plugin UI management *********************************************************************** 14 | 15 | class UI: 16 | def __init__(self): 17 | super().__init__() 18 | 19 | @property 20 | def body(self): 21 | return self.__body 22 | 23 | @body.setter 24 | def body(self, value): 25 | self.__body = value 26 | 27 | def render(): 28 | UI.body.setWindowTitle("TF Easy Colors Map v." + SYS.getVersionString()) 29 | mainWidget = QWidget(UI.body) 30 | UI.body.setWidget(mainWidget) 31 | 32 | mainLayout = QVBoxLayout() 33 | mainLayout.setSpacing(0) 34 | mainWidget.setLayout(mainLayout) 35 | 36 | # ---- MAPS 37 | 38 | # The Colors Maps are managed by the QColorsMap class. 39 | mainWidget.layout().addWidget(QColorsMap.render(mainWidget, UI.body)) 40 | 41 | # ---- TOOLBAR 42 | 43 | bt_Open = Tools.toolBt('document-open', UI.toolbar_FileOpen, "OPEN MAP\nLoad an existing Colors Map or create a new one.") 44 | bt_Edit = Tools.toolBt('document-edit', UI.toolbar_Edit, "MAP EDITOR\nEdit the current Colors Map.") 45 | bt_Config = Tools.toolBt('config-performance', UI.toolbar_Config, "SETTINGS\nChange the settings of this Colors Map.") 46 | UI.bt_AutoColor = Tools.toolBt('fillLayer', UI.toolbar_AutoColor, "AUTO ADD COLORS\nStart/stop the Colors auto-acquisition system.") 47 | UI.bt_AutoColor.setCheckable(True) 48 | bt_Help = Tools.toolBt('document-open', UI.toolbar_Help, "GUIDE\nShow the inline guide.", Qt.ToolButtonTextOnly, "?") 49 | 50 | toolbar = Tools.QWidgetLayout("H") 51 | toolbar.layout().addWidget(bt_Open) 52 | toolbar.layout().addWidget(UI.bt_AutoColor) 53 | toolbar.layout().addWidget(bt_Edit) 54 | toolbar.layout().addWidget(bt_Config) 55 | toolbar.layout().addWidget(bt_Help) 56 | 57 | mainWidget.layout().addWidget(toolbar) 58 | 59 | # ---- Plugin behaviours on Krita application statuses 60 | 61 | an = KRITA.AppNotifier() 62 | an.viewCreated.connect(UI.notifier_View_Created) 63 | an.viewClosed.connect(UI.notifier_View_Closed) 64 | an.windowCreated.connect(UI.notifier_Window_Created) 65 | an.applicationClosing.connect(UI.notifier_App_Closing) 66 | 67 | # ---- FORMS initialization 68 | 69 | FORMS.initialize(UI.form_ConfigClose, UI.form_EditorClose) 70 | 71 | # ==== TOOLBAR ================================================================================ 72 | 73 | def toolbar_FileOpen(): 74 | QColorsMap.openFileDialog() 75 | 76 | def toolbar_Edit(): 77 | if (KRITA.isNotReady() or QColorsMap.hasNoMap()): 78 | ALERT.warn("ATTENTION", "There is not a Colors Map yet.") 79 | return 80 | 81 | ALERT.info("COLORS MAP FILE EDITOR", "This Editor will open your Colors Map file and allows you to manually edit it.\n\nUse it with caution!\n\nA wrong modification may corrupt this file.") 82 | FORMS.show("EDITOR", QColorsMap.fileName) 83 | 84 | def toolbar_Config(): 85 | if (KRITA.isNotReady() or QColorsMap.hasNoMap()): 86 | ALERT.warn("ATTENTION", "There is not a Colors Map yet.") 87 | return 88 | 89 | FORMS.show("CONFIG", QColorsMap.fileName) 90 | 91 | def toolbar_AutoColor(): 92 | if (not QColorsMap.autoColor is None): 93 | QColorsMap.autoColor = None 94 | return 95 | 96 | if (KRITA.isNotReady() or QColorsMap.hasNoMap()): 97 | ALERT.warn("ATTENTION", "There is not a Colors Map yet.") 98 | return 99 | 100 | ALERT.info("AUTO-COLOR TOOL READY", "The Auto-Color acquisition tool is ready!\n\nNow, use the Krita Color Sampler tool for catching the Colors you want. For each Color, this plugin will ask you for a name.\n\nRemember to click this button again to stop Auto-Color!") 101 | QColorsMap.autoColor = KRITA.getSelectedColor("F") 102 | KRITA.actionTrigger("KritaSelected/KisToolColorSampler") 103 | 104 | def toolbar_Help(): 105 | FORMS.show("HELP") 106 | 107 | # ==== NOTIFIERS ============================================================================== 108 | 109 | # When a new or an existing Document is opened (click on 'New File' or 'Open File'). 110 | def notifier_View_Created(): 111 | pass 112 | 113 | # When a document is closed (just a Document, not Krita). 114 | def notifier_View_Closed(): 115 | QColorsMap.reset() 116 | pass 117 | 118 | # When Krita app has been opened, it's ready and accessible. 119 | def notifier_Window_Created(): 120 | pass 121 | 122 | 123 | # When Krita is closing. 124 | def notifier_App_Closing(): 125 | pass 126 | 127 | # ==== FORMS ================================================================================== 128 | 129 | def form_ConfigClose(self): 130 | QColorsMap.load() 131 | 132 | def form_EditorClose(self): 133 | QColorsMap.load() 134 | 135 | def showColorsPopUp(x, y): 136 | FORMS.show("COLORSPOPUP", QColorsMap.fileName, x, y) -------------------------------------------------------------------------------- /tfeasycolorsmap/__init__.py: -------------------------------------------------------------------------------- 1 | from .myplugin import * 2 | -------------------------------------------------------------------------------- /tfeasycolorsmap/myplugin.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5 import QtGui, QtCore 3 | from PyQt5.QtGui import QCursor 4 | from PyQt5.QtCore import QTimer 5 | 6 | from krita import * 7 | 8 | from .Core.SYS import * 9 | from .Core.KRITA import * 10 | from .Core.ALERT import * 11 | from .UI.UI import * 12 | 13 | # **** TF Easy Colors Map - Krita Plugin ********************************************************** 14 | 15 | class TFEasyColorsMapDocker(DockWidget): 16 | 17 | def __init__(self): 18 | super().__init__() 19 | 20 | # Plugin version. 21 | SYS.version = 23 22 | 23 | # The plugin UI is managed by the UI class. 24 | UI.body = self 25 | UI.render() 26 | 27 | 28 | def canvasChanged(self, canvas): 29 | # Mandatory class: must be declared. 30 | pass 31 | 32 | # Define an Event Filter for this plugin when Right Click + SHIFT is permormed on the Krita Document Canvas. 33 | def eventFilter(self, object, event): 34 | if event.type() == QEvent.MouseButtonPress: 35 | if event.button() == Qt.RightButton and event.modifiers() == Qt.ShiftModifier: 36 | 37 | if (KRITA.drawingToolSelected()): 38 | c = KRITA.getCurrentCanvas() 39 | pos = c["globalPos"] 40 | UI.showColorsPopUp(pos.x(), pos.y()) 41 | 42 | return super().eventFilter(object, event) 43 | 44 | Krita.instance().addDockWidgetFactory(DockWidgetFactory( 45 | "TF_Easy_Colors_Map", DockWidgetFactoryBase.DockRight, TFEasyColorsMapDocker)) 46 | 47 | 48 | --------------------------------------------------------------------------------