├── .gitignore ├── README.md └── plugindevtools ├── PluginDevTools.desktop ├── PluginDevTools ├── ActionGeneratorWidget.ui ├── DockerWidget.ui ├── EventViewerWidget.ui ├── GetKritaAPI.py ├── Manual.html ├── PluginDevToolsDocker.py ├── PluginDevToolsExtension.py ├── PluginDevToolsWidget.py ├── PluginGenerator.py ├── PluginGeneratorTemplates │ ├── BlankDocker │ │ ├── [SHORTNAME].py │ │ └── __init__.py │ ├── BlankExtension │ │ ├── [SHORTNAME].py │ │ └── __init__.py │ ├── SimpleDockerGUI │ │ ├── [SHORTNAME].py │ │ └── __init__.py │ ├── SimpleDockerGUIQML │ │ ├── [SHORTNAME].py │ │ ├── [SHORTNAME].ui │ │ └── __init__.py │ ├── SimpleExtensionDialogGUI │ │ ├── [SHORTNAME].py │ │ └── __init__.py │ ├── SimpleExtensionDialogGUIQML │ │ ├── [SHORTNAME].py │ │ ├── [SHORTNAME].ui │ │ └── __init__.py │ └── index.json ├── PluginGeneratorWidget.ui ├── ThemeIcons.txt ├── __init__.py └── kritadoc.html └── actions └── PluginDevTools.action /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Krita-PythonPluginDeveloperTools 2 | Python plugin for Krita that assists with making python plugins for Krita 3 | 4 | **Introducing Python Plugin developer Tools!** 5 | 6 | The goal of these tools is to make it easier for people to develop python plugins. 7 | 8 | You can learn about the basics of making python plugins here: 9 | 10 | https://scripting.krita.org/lessons/introduction 11 | 12 | ## Install instructions: 13 | 14 | Download by clicking Code on top and Download ZIP, then follow the instructions shown here: 15 | 16 | https://docs.krita.org/en/user_manual/python_scripting/install_custom_python_plugin.html 17 | 18 | ## Current features include: 19 | 20 | **Selector/Sampler** - for selecting with the mouse PyQt5 objects. Hold the shift key(or optionally ctrl/alt/meta key) and move the mouse as you wish, when you find the item you want, let go of the shift and it will show up in inspector. 21 | 22 | **Inspector** - Lets you browse/search the PyQt5 tree and also view all the properties (including inherited ones). 23 | With quick access to QT5 docs, parent traversing, code generation (code generation is primitive at this point but will improve with time), and show location of widget(when holding the button). 24 | 25 | ***Event/Signal Viewer*** - Ability to monitor signals and events and inject code into them. Just select the object in question in Inspector and press the "Event and Signal Viewer/Debugger" button. (Beta) 26 | 27 | 28 | **Console** - A more basic version of scripter made for mostly quick tests or actions. Enter will execute the code (Use shift+enter for new lines if needed) 29 | 30 | You can also bind console to your favorite text editor and send the code from the text editor directly to console. 31 | 32 | **Icons List** - Shows you a full list of Krita icons available for usage. Clicking an icon gives you the code for it as well. There is also Theme icons, but I would be careful with those. If you are on windows, the icons that do show will probably work on other platforms, but if you are on linux, linux has a lot more theme icons that windows does not. 33 | 34 | **Actions List** - Full list of actions and their descriptions. Double clicking will give you the trigger code for the action. 35 | 36 | **Krita API** - Collection of Krita API commands and optionally download API documentation. Can also generate python autocomplete file. 37 | 38 | **Simple Plugin Generator** - A simple generator that creates a starter for your own plugin 39 | 40 | 41 | I have a bit more nice features planned, but not sure how long it will take as work and other commitments are getting in the way. But it'll probably be before the end of the year... 42 | 43 | Anyways, hope developers find this useful and make more plugins! 44 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Service 3 | ServiceTypes=Krita/PythonPlugin 4 | X-KDE-Library=PluginDevTools 5 | X-Python-2-Compatible=false 6 | X-Krita-Manual=Manual.html 7 | Name=Krita Plugin Developer Tools 8 | Comment=This plugin helps develop plugins for Krita 9 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/ActionGeneratorWidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ActionGeneratorWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 842 10 | 657 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | Action Shortcuts: 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | New 35 | 36 | 37 | 38 | 39 | 40 | 41 | Delete 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Category 53 | 54 | 55 | 56 | 57 | 58 | 59 | category 60 | 61 | 62 | Scripts 63 | 64 | 65 | 66 | 67 | 68 | 69 | This will create a sub-category under scripts named after the category to add your shortcuts to. 70 | 71 | 72 | Category Text 73 | 74 | 75 | 76 | 77 | 78 | 79 | This will create a sub-category under scripts named after the category to add your shortcuts to. 80 | 81 | 82 | categoryText 83 | 84 | 85 | My Scripts 86 | 87 | 88 | 89 | 90 | 91 | 92 | This should be the unique ID you made for your action when creating it in the setup of the extension. 93 | 94 | 95 | Unique ID 96 | 97 | 98 | 99 | 100 | 101 | 102 | This should be the unique ID you made for your action when creating it in the setup of the extension. 103 | 104 | 105 | action.name 106 | 107 | 108 | myAction 109 | 110 | 111 | 112 | 113 | 114 | 115 | The text that it will show in the shortcut editor. 116 | 117 | 118 | Shortcut Editor Text 119 | 120 | 121 | 122 | 123 | 124 | 125 | The text that it will show in the shortcut editor. 126 | 127 | 128 | action.text 129 | 130 | 131 | My Script 132 | 133 | 134 | 135 | 136 | 137 | 138 | The tool tip, this will show up on hover-over. 139 | 140 | 141 | Tool Tip 142 | 143 | 144 | 145 | 146 | 147 | 148 | The tool tip, this will show up on hover-over. 149 | 150 | 151 | action.toolTip 152 | 153 | 154 | 155 | 156 | 157 | 158 | The status tip that is displayed on a status bar. 159 | 160 | 161 | Status Tip 162 | 163 | 164 | 165 | 166 | 167 | 168 | The status tip that is displayed on a status bar. 169 | 170 | 171 | action.statusTip 172 | 173 | 174 | 175 | 176 | 177 | 178 | The text it will show when a Qt application specifically calls for ‘what is this’, which is a help action. 179 | 180 | 181 | What's This 182 | 183 | 184 | 185 | 186 | 187 | 188 | The text it will show when a Qt application specifically calls for ‘what is this’, which is a help action. 189 | 190 | 191 | action.whatsThis 192 | 193 | 194 | 195 | 196 | 197 | 198 | <html><head/><body><p>This determines when an action is disabled or not.</p><p><br/></p><p><a name="LC41"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> enum ActivationFlag {</span></p><p><a name="LC42"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> NONE = 0x0000, ///&lt; Always activate</span></p><p><a name="LC43"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_IMAGE = 0x0001, ///&lt; Activate if there is at least one image</span></p><p><a name="LC44"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> MULTIPLE_IMAGES = 0x0002, ///&lt; Activate if there is more than one image open</span></p><p><a name="LC45"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> CURRENT_IMAGE_MODIFIED = 0x0004, ///&lt; Activate if the current image is modified</span></p><p><a name="LC46"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_NODE = 0x0008, ///&lt; Activate if there's an active node (layer or mask)</span></p><p><a name="LC47"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_DEVICE = 0x0010, ///&lt; Activate if the active node has a paint device, i.e. there are pixels to be modified</span></p><p><a name="LC48"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_LAYER = 0x0020, ///&lt; Activate if the current node is a layer (vector or pixel)</span></p><p><a name="LC49"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_TRANSPARENCY_MASK = 0x0040, ///&lt; Activate if the current node is a transparency mask</span></p><p><a name="LC50"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_SHAPE_LAYER = 0x0080, ///&lt; Activate if the current node is a vector layer</span></p><p><a name="LC51"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> PIXELS_SELECTED = 0x0100, ///&lt; Activate if any pixels are selected (with any kind of selection)</span></p><p><a name="LC52"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> SHAPES_SELECTED = 0x0200, ///&lt; Activate if any vector shape is selected</span></p><p><a name="LC53"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ANY_SELECTION_WITH_PIXELS = 0x0400, ///&lt; ???</span></p><p><a name="LC54"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> PIXELS_IN_CLIPBOARD = 0x0800, ///&lt; Activate if the clipboard contains pixels</span></p><p><a name="LC55"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> SHAPES_IN_CLIPBOARD = 0x1000, ///&lt; Activate if the clipboard contains vector data</span></p><p><a name="LC56"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> NEVER_ACTIVATE = 0x2000, ///&lt; ???</span></p><p><a name="LC57"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> LAYERS_IN_CLIPBOARD = 0x4000, ///&lt; ???</span></p><p><a name="LC58"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> IMAGE_HAS_ANIMATION = 0x8000, ///&lt; Activate if the image has an animation</span></p><p><a name="LC59"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> SHAPE_SELECTION_WITH_SHAPES = 0x10000, ///&lt; Activate there is a vector selection active</span></p><p><a name="LC60"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> PIXEL_SELECTION_WITH_PIXELS = 0x20000, ///&lt; Activate there is a raster selection active</span></p><p><a name="LC61"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> };</span></p></body></html> 199 | 200 | 201 | Activation Flags 202 | 203 | 204 | 205 | 206 | 207 | 208 | <html><head/><body><p>This determines when an action is disabled or not.</p><p><br/></p><p><a name="LC41"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';">enum ActivationFlag {</span></p><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC42"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> NONE </span><span style=" font-family:'Hack';">= 0x0000, ///&lt; Always activate</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC43"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_IMAGE = </span><span style=" font-family:'Hack';">0x0001, ///&lt; Activate if there is at least one image</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC44"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> MULTIPLE_IMAGES = </span><span style=" font-family:'Hack';">0x0002, ///&lt; Activate if there is more than one image open</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC45"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> CURRENT_IMAGE_MODIFIED = </span><span style=" font-family:'Hack';">0x0004, ///&lt; Activate if the current image is modified</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC46"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_NODE = </span><span style=" font-family:'Hack';">0x0008, ///&lt; Activate if there's an active node (layer or mask)</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC47"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_DEVICE = </span><span style=" font-family:'Hack';">0x0010, ///&lt; Activate if the active node has a paint device, i.e. there are pixels to be modified</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC48"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_LAYER = </span><span style=" font-family:'Hack';">0x0020, ///&lt; Activate if the current node is a layer (vector or pixel)</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC49"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_TRANSPARENCY_MASK = </span><span style=" font-family:'Hack';">0x0040, ///&lt; Activate if the current node is a transparency mask</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC50"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_SHAPE_LAYER = </span><span style=" font-family:'Hack';">0x0080, ///&lt; Activate if the current node is a vector layer</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC51"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> PIXELS_SELECTED = </span><span style=" font-family:'Hack';">0x0100, ///&lt; Activate if any pixels are selected (with any kind of selection)</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC52"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> SHAPES_SELECTED = </span><span style=" font-family:'Hack';">0x0200, ///&lt; Activate if any vector shape is selected</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC53"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ANY_SELECTION_WITH_PIXELS = </span><span style=" font-family:'Hack';">0x0400, ///&lt; ???</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC54"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> PIXELS_IN_CLIPBOARD = </span><span style=" font-family:'Hack';">0x0800, ///&lt; Activate if the clipboard contains pixels</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC55"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> SHAPES_IN_CLIPBOARD = </span><span style=" font-family:'Hack';">0x1000, ///&lt; Activate if the clipboard contains vector data</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC56"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> NEVER_ACTIVATE = </span><span style=" font-family:'Hack';">0x2000, ///&lt; ???</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC57"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> LAYERS_IN_CLIPBOARD = </span><span style=" font-family:'Hack';">0x4000, ///&lt; ???</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC58"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> IMAGE_HAS_ANIMATION = </span><span style=" font-family:'Hack';">0x8000, ///&lt; Activate if the image has an animation</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC59"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> SHAPE_SELECTION_WITH_SHAPES = </span><span style=" font-family:'Hack';">0x10000, ///&lt; Activate there is a vector selection active</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC60"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> PIXEL_SELECTION_WITH_PIXELS = </span><span style=" font-family:'Hack';">0x20000, ///&lt; Activate there is a raster selection active</span></pre><pre style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC61"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> };</span></pre></body></html> 209 | 210 | 211 | action.activationFlags 212 | 213 | 214 | 10000 215 | 216 | 217 | 218 | 219 | 220 | 221 | <html><head/><body><p>This determines activation conditions (e.g. activate only when selection is editable)</p><p><br/></p><p><a name="LC64"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> enum ActivationCondition {</span></p><p><a name="LC65"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> NO_CONDITION = 0,</span></p><p><a name="LC66"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_NODE_EDITABLE = 0x1,</span></p><p><a name="LC67"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_NODE_EDITABLE_PAINT_DEVICE = 0x2,</span></p><p><a name="LC68"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> SELECTION_EDITABLE = 0x4,</span></p><p><a name="LC69"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> OPENGL_ENABLED = 0x8,</span></p><p><a name="LC70"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> };</span></p></body></html> 222 | 223 | 224 | Activation Conditions 225 | 226 | 227 | 228 | 229 | 230 | 231 | <html><head/><body><p>This determines activation conditions (e.g. activate only when selection is editable)</p><p><br/></p><p><a name="LC64"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';">enum ActivationCondition {</span></p><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC65"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> NO_CONDITION </span><span style=" font-family:'Hack';">= 0,</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC66"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_NODE_EDITABLE = </span><span style=" font-family:'Hack';">0x1,</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC67"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_NODE_EDITABLE_PAINT_DEVICE = </span><span style=" font-family:'Hack';">0x2,</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC68"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> SELECTION_EDITABLE = </span><span style=" font-family:'Hack';">0x4,</span></pre><pre style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC69"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> OPENGL_ENABLED = </span><span style=" font-family:'Hack';">0x8,</span></pre><pre style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="LC70"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> };</span></pre></body></html> 232 | 233 | 234 | action.activationConditions 235 | 236 | 237 | 0 238 | 239 | 240 | 241 | 242 | 243 | 244 | Whether it is a checkbox or not. 245 | 246 | 247 | Checkable 248 | 249 | 250 | 251 | 252 | 253 | 254 | Whether it is a checkbox or not. 255 | 256 | 257 | action.isCheckable 258 | 259 | 260 | 261 | 262 | 263 | 264 | Shortcut 265 | 266 | 267 | 268 | 269 | 270 | 271 | action.shortcut 272 | 273 | 274 | 275 | 276 | 277 | 278 | The text it will show when displayed in a toolbar. So for example, “Resize Image to New Size” could be shortened to “Resize Image” to save space, so we’d put that in here. 279 | 280 | 281 | Icon Text 282 | 283 | 284 | 285 | 286 | 287 | 288 | The text it will show when displayed in a toolbar. So for example, “Resize Image to New Size” could be shortened to “Resize Image” to save space, so we’d put that in here. 289 | 290 | 291 | action.iconText 292 | 293 | 294 | 295 | 296 | 297 | 298 | The name of a possible icon. These will only show up on KDE plasma, because Gnome and Windows users complained they look ugly. 299 | 300 | 301 | Icon 302 | 303 | 304 | 305 | 306 | 307 | 308 | Save 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | Done 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/DockerWidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 814 10 | 279 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | 6 21 | 22 | 23 | 24 | 25 | ../../../../../../../../../../../.designer/backup../../../../../../../../../../../.designer/backup 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 16 38 | 39 | 40 | 41 | Hold the 42 | 43 | 44 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 45 | 46 | 47 | true 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 16 56 | 57 | 58 | 59 | 60 | Shift 61 | 62 | 63 | 64 | 65 | Ctrl 66 | 67 | 68 | 69 | 70 | Alt 71 | 72 | 73 | 74 | 75 | Meta 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 16 85 | 86 | 87 | 88 | key to sample widgets and let go of the key to inspect 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Current Selection: 98 | 99 | 100 | Qt::AlignCenter 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 12 109 | 110 | 111 | 112 | None 113 | 114 | 115 | Qt::AlignCenter 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | Inspector 124 | 125 | 126 | 127 | 128 | 129 | Qt::Horizontal 130 | 131 | 132 | 133 | 134 | 135 | 136 | Filter (min. 3 chars)... 137 | 138 | 139 | true 140 | 141 | 142 | 143 | 144 | 145 | 146 | QAbstractItemView::NoEditTriggers 147 | 148 | 149 | 180 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | Refresh Inspector 163 | 164 | 165 | ... 166 | 167 | 168 | 169 | ../../../../../../../../.designer/backup../../../../../../../../.designer/backup 170 | 171 | 172 | 173 | 174 | 175 | 176 | Parent Widget 177 | 178 | 179 | ... 180 | 181 | 182 | 183 | ../../../../../../../../.designer/backup../../../../../../../../.designer/backup 184 | 185 | 186 | 187 | 188 | 189 | 190 | Get Code Path 191 | 192 | 193 | ... 194 | 195 | 196 | 197 | ../../../../../../../../.designer/backup../../../../../../../../.designer/backup 198 | 199 | 200 | 201 | 202 | 203 | 204 | Widget Documentation 205 | 206 | 207 | ... 208 | 209 | 210 | 211 | ../../../../../../../../.designer/backup../../../../../../../../.designer/backup 212 | 213 | 214 | 215 | 216 | 217 | 218 | Widget Property/Method Documentation 219 | 220 | 221 | ... 222 | 223 | 224 | 225 | 226 | 227 | 228 | Highlight Widget Position 229 | 230 | 231 | ... 232 | 233 | 234 | 235 | ../../../../../../../../.designer/backup../../../../../../../../.designer/backup 236 | 237 | 238 | true 239 | 240 | 241 | 242 | 243 | 244 | 245 | Event and Signal Viewer/Debugger 246 | 247 | 248 | ... 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | Filter... 259 | 260 | 261 | true 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 0 272 | 0 273 | 274 | 275 | 276 | 250 277 | 278 | 279 | true 280 | 281 | 282 | false 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 0 291 | 292 | 293 | 0 294 | 295 | 296 | 0 297 | 298 | 299 | 0 300 | 301 | 302 | 0 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | Update 311 | 312 | 313 | 314 | 315 | 316 | 317 | Hide 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | Console 333 | 334 | 335 | 336 | 337 | 338 | Qt::Vertical 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | Clear Console 348 | 349 | 350 | ... 351 | 352 | 353 | 354 | 355 | 356 | 357 | Execute code key 358 | 359 | 360 | 361 | 362 | 363 | 364 | Filter... 365 | 366 | 367 | true 368 | 369 | 370 | 371 | 372 | 373 | 374 | Binds STDOUT onto console as long as the console tab is open 375 | 376 | 377 | Bind stdout 378 | 379 | 380 | 381 | 382 | 383 | 384 | Create a temporary file and open it in a default text editor. If execute on save is selected, saving will automatically execute the script inside krita making your favorite text editor work like scripter 385 | 386 | 387 | ... 388 | 389 | 390 | true 391 | 392 | 393 | 394 | 395 | 396 | 397 | Pick a file and open it in a default text editor. If execute on save is selected, saving will automatically execute the script inside krita making your favorite text editor work like scripter 398 | 399 | 400 | ... 401 | 402 | 403 | true 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | Execute on save 412 | 413 | 414 | 415 | 416 | Execute manually 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | false 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | Type script commands here... 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | Icons 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | Filter... 458 | 459 | 460 | true 461 | 462 | 463 | 464 | 465 | 466 | 467 | Krita Icons 468 | 469 | 470 | true 471 | 472 | 473 | 474 | 475 | 476 | 477 | Krita Extra Icons 478 | 479 | 480 | 481 | 482 | 483 | 484 | Theme Icons 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | QListView::Adjust 494 | 495 | 496 | QListView::IconMode 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | Actions 505 | 506 | 507 | 508 | 509 | 510 | Filter... 511 | 512 | 513 | true 514 | 515 | 516 | 517 | 518 | 519 | 520 | Double-click for code... 521 | 522 | 523 | QAbstractItemView::NoEditTriggers 524 | 525 | 526 | QAbstractItemView::SingleSelection 527 | 528 | 529 | QAbstractItemView::SelectRows 530 | 531 | 532 | 250 533 | 534 | 535 | true 536 | 537 | 538 | false 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | Krita API 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | Filter... 555 | 556 | 557 | true 558 | 559 | 560 | 561 | 562 | 563 | 564 | Export API AutoComplete 565 | 566 | 567 | ... 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | Current 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | Download API from KDE git 584 | 585 | 586 | ... 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | Qt::Horizontal 596 | 597 | 598 | 599 | 350 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | Welcome 610 | 611 | 612 | 613 | 614 | 615 | Welcome to Plugin Developer Tools! 616 | 617 | 618 | Qt::AlignCenter 619 | 620 | 621 | true 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/EventViewerWidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | EventViewerWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 474 10 | 711 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 1 19 | 20 | 21 | 1 22 | 23 | 24 | 1 25 | 26 | 27 | 1 28 | 29 | 30 | 1 31 | 32 | 33 | 34 | 35 | Events For: None 36 | 37 | 38 | 39 | 40 | 41 | 42 | 1 43 | 44 | 45 | 46 | 47 | Start/Stop monitoring events 48 | 49 | 50 | ... 51 | 52 | 53 | 54 | .. 55 | 56 | 57 | 58 | 59 | 60 | 61 | Clear events 62 | 63 | 64 | ... 65 | 66 | 67 | 68 | .. 69 | 70 | 71 | 72 | 73 | 74 | 75 | Where to output? 76 | ev = where events are outputed 77 | std = where stdout/stderr is outputted 78 | show = show events/stdout/stderr into this gui 79 | terminal = show events/stdout/stderr into terminal 80 | 81 | 82 | 83 | 84 | ev: show|std: terminal 85 | 86 | 87 | 88 | 89 | ev: show|std: show 90 | 91 | 92 | 93 | 94 | ev: terminal|std: terminal 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | How to handle duplicate signal/event output in a row? 103 | First = show only first, ignore the rest 104 | All folded = Show them all but fold them 105 | All unfolded = Show them all unfolded 106 | 107 | 108 | 109 | 110 | First only 111 | 112 | 113 | 114 | 115 | All folded 116 | 117 | 118 | 119 | 120 | All unfold 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | Qt::Horizontal 131 | 132 | 133 | 134 | 10 135 | 136 | 137 | true 138 | 139 | 140 | 100 141 | 142 | 143 | true 144 | 145 | 146 | 147 | 148 | 0 149 | 150 | 151 | 152 | Signals 153 | 154 | 155 | 156 | 1 157 | 158 | 159 | 1 160 | 161 | 162 | 1 163 | 164 | 165 | 1 166 | 167 | 168 | 1 169 | 170 | 171 | 172 | 173 | 10 174 | 175 | 176 | false 177 | 178 | 179 | 30 180 | 181 | 182 | 30 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | Events 191 | 192 | 193 | 194 | 1 195 | 196 | 197 | 1 198 | 199 | 200 | 1 201 | 202 | 203 | 1 204 | 205 | 206 | 1 207 | 208 | 209 | 210 | 211 | Events are monitored via installEventFilter. 212 | widget.installEventFilter = installs the event filter on the widget itself 213 | QApplication.instance().installEventFilter = installs event filter on the QApplication. 214 | (This is slower than widget as QApplication process a lot of events, but some events don't bubble down and this is the only way) 215 | 216 | 217 | 218 | 219 | widget.installEventFilter(self) 220 | 221 | 222 | 223 | 224 | QApplication.instance().installEventFilter(self) 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 10 233 | 234 | 235 | false 236 | 237 | 238 | 30 239 | 240 | 241 | 30 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | Data 250 | 251 | 252 | 253 | 1 254 | 255 | 256 | 1 257 | 258 | 259 | 1 260 | 261 | 262 | 1 263 | 264 | 265 | 1 266 | 267 | 268 | 269 | 270 | true 271 | 272 | 273 | false 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | Code For: 288 | 289 | 290 | 291 | 292 | 293 | 294 | UPDATE 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | Parameters are stored in params list, ex: first element is params[0] 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/GetKritaAPI.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | import urllib.request 3 | import zipfile 4 | import json 5 | import http.client 6 | import socket 7 | import ssl 8 | import time 9 | import re 10 | import io 11 | import os 12 | import krita 13 | try: 14 | if int(krita.qVersion().split('.')[0]) == 5: 15 | raise 16 | from PyQt6.QtCore import * 17 | from PyQt6.QtGui import * 18 | except: 19 | from PyQt5.QtCore import * 20 | from PyQt5.QtGui import * 21 | 22 | class GetKritaAPI(QObject): 23 | 24 | MAP_TYPES = { 25 | 'QStringList': ['List[str]'], 26 | 'QString': ['str'], 27 | 'qreal': ['float'], 28 | 'double': ['float'], 29 | 'QByteArray': ['Union[QByteArray, bytes, bytearray]','QByteArray'], 30 | 'QList':['List'], 31 | 'QVector':['List'], 32 | 'QMap':['Dict'], 33 | 'true':['True'], 34 | 'false':['False'], 35 | 'void': ['None'] 36 | } 37 | 38 | DECLARE_CLASS = { 39 | 'Document': 'Krita.instance().activeDocument()', 40 | 'Node': 'Krita.instance().activeDocument().activeNode()' 41 | } 42 | 43 | 44 | def updateData(self, kritaVersion, defaultTimeout = 5): 45 | headers = { 46 | 'Accept': '*/*', 47 | "User-Agent": "Krita-DevTools Plugin" 48 | } 49 | 50 | 51 | # Get List of tags and confirm if IPv6 is working properly 52 | tagList = "https://invent.kde.org/api/v4/projects/graphics%2Fkrita/repository/tags" 53 | 54 | s1 = time.time() 55 | req = urllib.request.Request(tagList, headers=headers) 56 | with urllib.request.urlopen(req, timeout=defaultTimeout) as source: 57 | if time.time() - s1 > defaultTimeout: 58 | ForceIPv4Connection() 59 | defaultTimeout += 25 60 | 61 | try: 62 | jres = json.loads(source.read()) 63 | 64 | tag = 'master' 65 | 66 | for tagDict in jres: 67 | if (tagDict['name'].split('-'))[0] == 'v'+ kritaVersion: 68 | tag = tagDict['name'] 69 | 70 | # Figure out when API was last updated 71 | lastCommit = "https://invent.kde.org/api/graphql" 72 | 73 | urldata={"query":"query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {\n project(fullPath: $projectPath) {\n id\n __typename\n repository {\n __typename\n tree(path: $path, ref: $ref) {\n __typename\n lastCommit {\n __typename\n sha\n title\n titleHtml\n descriptionHtml\n message\n webPath\n authoredDate\n authorName\n authorGravatar\n author {\n __typename\n name\n avatarUrl\n webPath\n }\n signatureHtml\n pipelines(ref: $ref, first: 1) {\n __typename\n edges {\n __typename\n node {\n __typename\n detailedStatus {\n __typename\n detailsPath\n icon\n tooltip\n text\n group\n }\n }\n }\n }\n }\n }\n }\n }\n}\n","variables":{"projectPath":"graphics/krita","ref":tag,"path":"libs/libkis"}} 74 | 75 | 76 | req = urllib.request.Request(lastCommit, headers=headers) 77 | req.add_header('Content-Type', 'application/json; charset=utf-8') 78 | jsondata = json.dumps(urldata) 79 | jsondataasbytes = jsondata.encode('utf-8') # needs to be bytes 80 | req.add_header('Content-Length', len(jsondataasbytes)) 81 | 82 | with urllib.request.urlopen(req, jsondataasbytes, timeout=defaultTimeout) as source: 83 | jres = json.loads(source.read()) 84 | date = jres['data']['project']['repository']['tree']['lastCommit']['authoredDate'] 85 | 86 | # Download the API details 87 | zipUrl = 'https://invent.kde.org/graphics/krita/-/archive/'+tag+'/krita-'+tag+'.zip?path=libs/libkis' 88 | req = urllib.request.Request(zipUrl, headers=headers) 89 | with urllib.request.urlopen(req, timeout=defaultTimeout) as source: 90 | data = source.read() 91 | with zipfile.ZipFile(io.BytesIO(data)) as myzip: 92 | if 'krita-'+tag+'-libs-libkis/libs/libkis/Window.h' in myzip.namelist(): 93 | respath = Krita.instance().readSetting('','ResourceDirectory','') 94 | if respath == '': 95 | respath = os.path.dirname(os.path.realpath(__file__)) 96 | else: 97 | respath=os.path.join(respath,'pykrita','PluginDevTools') 98 | with open( respath + '.KritaAPI.'+kritaVersion+'.zip', 'wb') as f: 99 | f.write(data) 100 | f.close() 101 | return { 'status': 1, 'data': { 'updated': date } } 102 | 103 | else: 104 | raise Exception("Archive is invalid.") 105 | 106 | 107 | 108 | except Exception as e: 109 | 110 | return { 'status':0, 'error': str(e) } 111 | 112 | 113 | def parseData(self, kritaVersion): 114 | mapDict = {} 115 | respath = Krita.instance().readSetting('','ResourceDirectory','') 116 | if respath == '': 117 | respath = os.path.dirname(os.path.realpath(__file__)) 118 | else: 119 | respath=os.path.join(respath,'pykrita','PluginDevTools') 120 | with zipfile.ZipFile( respath + '.KritaAPI.'+kritaVersion+'.zip' ) as myzip: 121 | for fn in myzip.namelist(): 122 | if fn.endswith('.h') and '/Test/' not in fn: 123 | content = str(myzip.read(fn),'utf-8') 124 | className = re.search(r'^.*[/\\](.+?)\.h$', fn).group(1) 125 | 126 | #print ("CLASSNAME", className) 127 | 128 | #print ( content ) 129 | regexFile = re.compile(r"^(?:.+?\/\*\*\s*(.+?)\s*\*\/.*?|.+?)\n\s*?class KRITALIBKIS_EXPORT (\w+)\s*?[\:\n]\s*(?:public\s+|)(\w+|)", re.DOTALL) 130 | 131 | match = regexFile.search(content) 132 | content = regexFile.sub('', content) 133 | #print ( content ) 134 | 135 | regexClass = re.compile(r"\n\s*?(public|private)\s*(Q_SLOTS|)\s*:\s*?\n(.+?)(\n\s*?(?:public|private)\s*(?:Q_SLOTS|)\s*:\s*?\n|\#endif \/\/ LIBKIS_)", re.DOTALL) 136 | 137 | regexMethod = re.compile(r"^(?:.+?(?P\/\*\*\s*.+?\s*\*\/|\/\/\/[^\r\n]+?)\s*?|\s+?)\n\s*?(?:(?P(?:virtual|static|)\s*?[\w\*]+(?:\<[\w \*,]+\>|))\s+(?P[\w\*]+)|(?P"+className+r"))\((?P[^\r\n]*)\)(?P[^\r\n])*?;", re.DOTALL) 138 | 139 | regexParam = re.compile(r'(?:^|\s)(\S+?(?:\<.+?\>|))\s*[\&\*]*(\w+?)(?:\s*=\s*(.+?)|)\s*$', re.DOTALL) 140 | 141 | if match: 142 | #print( "match=",match.group(2), match.group(1) ) 143 | modName = match.group(2) 144 | doc = match.group(1) if match.group(1) else '' 145 | doc = re.sub(r'^\s*(?:/\*\*|///)\s*\**', r'', doc) 146 | doc = re.sub(r'/\s*$', r'', doc) 147 | doc = re.sub(r'\n\s*?\* \@', r'\n@', doc ) 148 | start = doc.find('@code') 149 | 150 | if start > -1: 151 | end = doc.find('@endcode',start) 152 | doc = doc.replace( doc[start:end],re.sub(r'\n\s*?\* ',r'\n', doc[start:end])) 153 | 154 | doc = re.sub(r'\n\s*?\*', r'', doc ) 155 | mapDict[modName] = { 'doc':doc, 'methods':{}, 'declare':[], 'parent':match.group(3) } 156 | if modName in self.DECLARE_CLASS: 157 | mapDict[modName]['declare'].append({ 158 | 'doc': self.DECLARE_CLASS[modName], 159 | 'return': className, 160 | 'params': [], 161 | 'access': '' 162 | }) 163 | 164 | while True: 165 | match = regexClass.search(content) 166 | if match is not None: 167 | 168 | #print ( "matchMethod", match.group(1), match.group(2), match.group(3) ) 169 | content = regexClass.sub(match.group(4), content, 1) 170 | 171 | methodAccess = match.group(1) + " " + match.group(2) 172 | subcontent = match.group(3) 173 | 174 | while True: 175 | matchMethod = regexMethod.search(subcontent) 176 | subcontent = regexMethod.sub('', subcontent, 1) 177 | if matchMethod: 178 | #if matchMethod.group('cname'): print ("CNAME!", matchMethod.group('cname') ) 179 | docMethod = matchMethod.group('doc') if matchMethod.group('doc') else '' 180 | docMethod = re.sub(r'^\s*(?:/\*\*|///)\s*\**', r'', docMethod) 181 | docMethod = re.sub(r'/\s*$', r'', docMethod) 182 | docMethod = re.sub(r'\n\s*?\* \@', r'\n@', docMethod) 183 | start2 = docMethod.find('@code') 184 | if start2 > -1: 185 | #print ("SELECTION FOUND!") 186 | end2 = docMethod.find('@endcode',start2) 187 | docMethod = docMethod.replace( docMethod[start2:end2],re.sub(r'\n\s*?\* ',r'\n', docMethod[start2:end2])) 188 | 189 | docMethod = re.sub(r'\n\s*?\*', r'', docMethod ) 190 | methName = (matchMethod.group('name') if matchMethod.group('name') else matchMethod.group('cname')).replace('*','') 191 | 192 | params = [] 193 | #print ('mp', matchMethod.group('params')) 194 | paramText = matchMethod.group('params') 195 | if paramText: 196 | paramText = re.sub(r'\<(.*?),(.*?)\>',r'<\1&&\2>', paramText) 197 | for param in paramText.split(','): 198 | pmatch = regexParam.search(param) 199 | #print ( "p["+param+']', pmatch ) 200 | params.append({ 201 | 'type': re.sub(r'\<(.*?)&&(.*?)\>',r'<\1,\2>', pmatch.group(1)), 202 | 'name': pmatch.group(2), 203 | 'optional': re.sub(r'\<(.*?)&&(.*?)\>',r'<\1,\2>', pmatch.group(3)) if pmatch.group(3) else None 204 | }) 205 | #print ("met", className, methName, params) 206 | #if 'internal use only' in docMethod: continue 207 | if methName == modName: 208 | 209 | mapDict[modName]['declare'].append({ 210 | 'doc': docMethod, 211 | 'return': className if matchMethod.group('cname') else matchMethod.group('type'), 212 | 'params': params, 213 | 'access': methodAccess 214 | }) 215 | else: 216 | mapDict[modName]['methods'][methName]={ 217 | 'name': methName, 218 | 'doc':docMethod, 219 | 'return': matchMethod.group('type').strip(), 220 | 'params': params, 221 | 'access': methodAccess 222 | } 223 | else: 224 | break 225 | #print ("cont=",content) 226 | else: 227 | #print ("NO MATCH!") 228 | break 229 | 230 | 231 | #print ("cont=",content) 232 | 233 | #methods = decl.group(3) 234 | 235 | 236 | return mapDict 237 | 238 | def convertType(self, vtype, classKeys, param = True): 239 | for k in reversed(sorted(self.MAP_TYPES.keys())): 240 | v = self.MAP_TYPES[k] 241 | rv = v[0] if param or len(v) == 1 else v[1] 242 | if k == vtype: 243 | return rv 244 | elif k in vtype: 245 | vtype = vtype.replace(k, rv) 246 | 247 | vtype = vtype.replace('<','[').replace('>',']').replace(' *','').replace('*','').replace('&','').replace('virtual ','') 248 | 249 | #if param: 250 | for k in reversed(sorted(classKeys)): 251 | vtype = re.sub( r"(^|[\[\( ,])"+k, r"\1'"+k+"'", vtype) 252 | 253 | vtype = re.sub( r"((?:Kis|Ko)\w+)", r"'\1'", vtype) 254 | 255 | return vtype 256 | 257 | def genAutoComplete(self, kritaVersion): 258 | output = "from PyQt5.QtCore import *\nfrom PyQt5.QtGui import *\nfrom PyQt5.QtWidgets import *\nfrom typing import List, Dict, Union\n" 259 | parseData = self.parseData(kritaVersion) 260 | 261 | sortedLists = { 'QObject':[], 'Other':[] } 262 | for k in reversed(sorted(parseData.keys())): 263 | v = parseData[k] 264 | 265 | if v['parent'] == 'QObject': 266 | sortedLists['QObject'].append( k ) 267 | else: 268 | sortedLists['Other'].append( k ) 269 | 270 | if v['parent'].startswith('Ko') or v['parent'].startswith('Kis'): 271 | output += v['parent']+" = QObject\n" 272 | 273 | 274 | 275 | for k in sortedLists['QObject'] + sortedLists['Other']: 276 | v = parseData[k] 277 | output += "class " + k + "(" + v['parent'] +"):\n" 278 | output += '\t"""' + v['doc'] + ' """\n\n' 279 | 280 | if v['declare']: 281 | paramList = ['self'] 282 | 283 | for param in m['params']: 284 | paramList.append( param['name'] + ": " + self.convertType(param['type'], parseData.keys() ) + (" = " + self.convertType(param['optional'], parseData.keys() ) if param['optional'] else "") ) 285 | 286 | output += "\tdef __init__(" + ', '.join(paramList) + ") -> " + self.convertType(m['return'], parseData.keys() , False) +":\n" 287 | output += '\t\t"""' + m['doc'] + ' """\n\n' 288 | 289 | for km, m in v['methods'].items(): 290 | paramList = [] 291 | paramNames = [] 292 | 293 | #print ( "METHOD=", m ) 294 | if "static" not in m['return']: 295 | paramList.append('self') 296 | else: 297 | m['return'] = m['return'].replace('static','') 298 | output += "\t@staticmethod\n" 299 | 300 | for param in m['params']: 301 | paramNames.append( param['name'] ) 302 | paramList.append( param['name'] + ": " + self.convertType(param['type'], parseData.keys() ) + (" = " + self.convertType(param['optional'], parseData.keys() ) if param['optional'] else "") ) 303 | 304 | output += "\tdef " + m['name'] + "(" + ', '.join(paramList) + ") -> " + self.convertType(m['return'], parseData.keys() , False) +":\n" 305 | output += "\t\t# type: (" + ', '.join(paramNames) + ") -> " + self.convertType(m['return'], {}.keys() , False) +":\n" 306 | output += '\t\t"""@access ' + m['access'] + "\n" + m['doc'] + ' """\n\n' 307 | 308 | return output 309 | 310 | 311 | class ForceIPv4Connection(): 312 | def __init__(self, caller = None): 313 | super().__init__() 314 | urllib.request.HTTPSHandler = self.OverloadHTTPSHandler 315 | print ("overloaded!") 316 | 317 | class OverloadHTTPSConnection(http.client.HTTPSConnection): 318 | def connect(self): 319 | self.sock = socket.socket(socket.AF_INET) 320 | self.sock.connect((self.host, self.port)) 321 | if self._tunnel_host: 322 | self._tunnel() 323 | self.sock = ssl.wrap_socket(self.sock, self.key_file, self.cert_file) 324 | 325 | 326 | 327 | class OverloadHTTPSHandler(urllib.request.AbstractHTTPHandler): 328 | 329 | def __init__(self, debuglevel=0, context=None, check_hostname=None): 330 | urllib.request.AbstractHTTPHandler.__init__(self, debuglevel) 331 | self._context = context 332 | self._check_hostname = check_hostname 333 | 334 | def https_open(self, req): 335 | return self.do_open(ForceIPv4Connection.OverloadHTTPSConnection, req, 336 | context=self._context, check_hostname=self._check_hostname) 337 | 338 | https_request = urllib.request.AbstractHTTPHandler.do_request_ 339 | 340 | 341 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/Manual.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Krita Plugin Developer Tools 6 | 7 | 8 |

Krita Plugin Developer Tools

9 |

v.0.01

10 | The goal of these tools is to make it easier for people to develop python plugins. 11 | 12 | 13 |

Current features include:

14 | 15 |

Selector/Sampler

- for selecting with the mouse PyQt5 objects. Hold the shift key(or optionally ctrl/alt/meta key) and move the mouse as you wish, when you find the item you want, let go of the shift and it will show up in inspector. 16 | 17 |

Inspector

- Lets you browse/search the PyQt5 tree and also view all the properties (including inherited ones). 18 | With quick access to QT5 docs, parent traversing, code generation (code generation is primitive at this point but will improve with time), and show location of widget(when holding the button). 19 | 20 |
Event/Signal Viewer
- Ability to monitor signals and events and inject code into them. Just select the object in question in Inspector and press the "Event and Signal Viewer/Debugger" button. (Beta) 21 | 22 |

Console

- A more basic version of scripter made for mostly quick tests or actions. Enter will execute the code (Use shift+enter for new lines if needed) 23 | 24 | You can also bind console to your favorite text editor and send the code from the text editor directly to console. 25 | 26 |

Icons List

- Shows you a full list of Krita icons available for usage. Clicking an icon gives you the code for it as well. There is also Theme icons, but I would be careful with those. If you are on windows, the icons that do show will probably work on other platforms, but if you are on linux, linux has a lot more theme icons that windows does not. 27 | 28 |

Actions List

- Full list of actions and their descriptions. Double clicking will give you the trigger code for the action. 29 | 30 |

Krita API

- Collection of Krita API commands and optionally download API documentation. Can also generate python autocomplete file. 31 | 32 |

Simple Plugin Generator

- A simple generator that creates a starter for your own plugin 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginDevToolsDocker.py: -------------------------------------------------------------------------------- 1 | from krita import * 2 | from .PluginDevToolsWidget import * 3 | 4 | 5 | DOCKER_TITLE = 'Plugin Developer Tools' 6 | 7 | 8 | class PluginDevToolsDialog(QMainWindow): 9 | signal_closed = pyqtSignal() 10 | def __init__(self, parent=None): 11 | super().__init__(parent) 12 | self.setWindowTitle(DOCKER_TITLE) 13 | 14 | def updateWindowTitle(self): 15 | if isinstance(self.parentWidget(), QWidget): 16 | self.setObjectName("PluginDevToolsDialog_" + self.parentWidget().objectName()) 17 | if self.parentWidget().windowTitle().__len__() == 0: 18 | self.setWindowTitle(DOCKER_TITLE) 19 | else: 20 | self.setWindowTitle(self.parentWidget().windowTitle() + ' - ' + DOCKER_TITLE) 21 | 22 | def closeEvent(self, event: QtGui.QCloseEvent): 23 | self.signal_closed.emit() 24 | return super().closeEvent(event) 25 | 26 | 27 | 28 | class PluginDevToolsDocker(DockWidget): 29 | signal_leaveFloating = pyqtSignal() 30 | signal_manualOpenDocker = pyqtSignal() 31 | def __init__(self): 32 | super().__init__() 33 | self.setWindowTitle(DOCKER_TITLE) 34 | self.titleBarEventListening = False 35 | # Prepare a standalone window 36 | self.floatModeDialog = PluginDevToolsDialog(self.parent()) 37 | # Prepare content widget. This widget will be added into docker itself or self.floatModeDialog. Only keep one instance. 38 | self.centralWidget = PluginDevToolsWidget() 39 | self.mutex = QMutex() 40 | # Status route: modeAllHide <--> modeDocker <--> modeDialog 41 | self.floatModeDialog.signal_closed.connect(lambda : self.applyDockerMode('dialog.signal_closed')) 42 | self.signal_leaveFloating.connect(lambda : self.applyDialogMode('docker.signal_leaveFloating')) 43 | self.signal_manualOpenDocker.connect(lambda : self.applyDockerMode('docker.signal_manualOpenDocker')) 44 | 45 | def canvasChanged(self, canvas): 46 | pass 47 | 48 | def showEvent(self, event: QtGui.QShowEvent) -> None: 49 | #print('PluginDevToolsDocker showEvent') 50 | #print(' sender= ', self.sender()) 51 | if self.titleBarEventListening == False: 52 | if isinstance(self.titleBarWidget(), QWidget): 53 | self.titleBarWidget().installEventFilter(self) 54 | 55 | if isinstance(self.sender(), QAction): 56 | self.signal_manualOpenDocker.emit() 57 | return super().showEvent(event) 58 | # Failed to catch mousePressEvent/mouseReleaseEvent when moving docker 59 | # Failed to catch clicked/toggled/pressed/released signal when click the docker float button 60 | # Use showEvent instead 61 | if (QGuiApplication.mouseButtons() & Qt.MouseButton.LeftButton): 62 | # Do not switch immediately when dragging the docker to move 63 | return super().showEvent(event) 64 | 65 | # LeftButton is released and the docker is not docked 66 | if self.isFloating(): 67 | self.signal_leaveFloating.emit() 68 | return super().showEvent(event) 69 | 70 | def changeEvent(self, event: QtCore.QEvent) -> None: 71 | if event.type() == QEvent.Type.ParentChange: 72 | if isinstance(self.parentWidget(), QWidget): 73 | self.floatModeDialog.setParent(self.parentWidget()) 74 | # Always reset window type after manually setParent 75 | self.floatModeDialog.setWindowFlag(Qt.WindowType.Window) 76 | self.parentWidget().windowTitleChanged.connect(self.floatModeDialog.updateWindowTitle) 77 | 78 | return super().changeEvent(event) 79 | 80 | 81 | def eventFilter(self, obj: QObject, event: QEvent) -> bool: 82 | if obj is self.titleBarWidget(): 83 | if event.type() == QEvent.Type.MouseButtonDblClick: 84 | if not self.isFloating(): 85 | self.signal_leaveFloating.emit() 86 | return True 87 | 88 | return super().eventFilter(obj, event) 89 | 90 | 91 | def setFirstAfterStart(self): 92 | # Initialize the first status after start Krita 93 | if self.isFloating(): 94 | self.applyDialogMode('initiallize') 95 | elif self.isVisible(): 96 | self.applyDockerMode('initiallize') 97 | else: 98 | self.applyHideMode('initiallize') 99 | 100 | 101 | def applyHideMode(self, senderName=str()): 102 | if self.mutex.tryLock() == False: 103 | #print('skipped applyHideMode') 104 | #print(' mutex.tryLock()= ', False) 105 | #print(' sender= ', senderName) 106 | #print('') 107 | return 108 | self.setFloating(False) 109 | self.setWidget(self.centralWidget) 110 | self.close() 111 | self.floatModeDialog.close() 112 | self.mutex.unlock() 113 | 114 | 115 | def applyDockerMode(self, senderName=str()): 116 | if self.mutex.tryLock() == False: 117 | #print('skipped applyDockerMode') 118 | #print(' mutex.tryLock()= ', False) 119 | #print(' sender= ', senderName) 120 | #print('') 121 | return 122 | self.setFloating(False) 123 | self.setWidget(self.centralWidget) 124 | self.show() 125 | self.floatModeDialog.close() 126 | self.mutex.unlock() 127 | 128 | 129 | def applyDialogMode(self, senderName=str()): 130 | if self.mutex.tryLock() == False: 131 | #print('skipped applyDialogMode') 132 | #print(' mutex.tryLock()= ', False) 133 | #print(' sender= ', senderName) 134 | #print('') 135 | return 136 | self.setFloating(False) 137 | self.close() 138 | self.floatModeDialog.setCentralWidget(self.centralWidget) 139 | self.floatModeDialog.show() 140 | self.floatModeDialog.activateWindow() 141 | newWidth = self.floatModeDialog.size().width() 142 | newPoint = QCursor.pos() 143 | newPoint.setX(newPoint.x()-int(newWidth/2)) 144 | self.floatModeDialog.move(self.floatModeDialog.mapFrom(self.floatModeDialog, newPoint)) 145 | self.mutex.unlock() 146 | 147 | 148 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginDevToolsExtension.py: -------------------------------------------------------------------------------- 1 | from krita import * 2 | 3 | ## TODO: Planed to add some functions in future. 4 | 5 | #class PluginDevToolsExtension(Extension): 6 | # def __init__(self, parent): 7 | # super().__init__(parent) 8 | # 9 | # # Krita.instance() exists, so do any setup work 10 | # def setup(self): 11 | # pass 12 | # 13 | # # called after setup(self) 14 | # def createActions(self, window): 15 | # pass 16 | 17 | 18 | # This is a test extension 19 | import inspect 20 | class PluginDevToolsTestExtension(Extension): 21 | def __init__(self, parent): 22 | super().__init__(parent) 23 | 24 | def setup(self): 25 | pass 26 | 27 | def createActions(self, window: Window): 28 | action = window.createAction('PluginDevTools', 'PluginDevTools', 'tools/scripts/PluginDevTools') 29 | self.menu = QMenu('PluginDevTools', window.qwindow()) 30 | action.setMenu(self.menu) 31 | # Add some fixed Action here: 32 | #action = window.createAction('actionID', 'actionDisplayName', 'tools/scripts/PluginDevTools') 33 | #self.menu.addAction(action) 34 | #action.triggered.connect(someConnectObject) 35 | 36 | 37 | def dynamicAddEntry(self, *args): 38 | # Only for debug 39 | # How to use in console: 40 | #debugentry = next((w for w in Krita.instance().extensions() if str(type(w)).__contains__('PluginDevToolsTestExtension')), None) 41 | #print(vars(debugentry)) 42 | # then use debugentry.yourVariable to access yourVariable 43 | frame = inspect.currentframe() 44 | frame = inspect.getouterframes(frame)[1] 45 | string = inspect.getframeinfo(frame[0]).code_context 46 | if string is not None: 47 | string = string[0].strip() 48 | else: 49 | return 50 | 51 | args_name = string[string.find('(') + 1:-1].replace(' ', '').split(',') 52 | 53 | for name, value in zip(args_name, args): 54 | setattr(self, name, value) 55 | 56 | 57 | def dynamicCreateAction(self, connectObject, window: QObject, id_text: str, displayName: str = str(), addToMenu=False) ->QAction: 58 | if not isinstance(window, Window): 59 | print("PluginDevToolsExtension.dynamicCreateAction: {window} is not an instance of Krita.Window type. Empty QAction was returned.") 60 | return QAction() 61 | else: 62 | action = window.createAction(id_text, displayName) 63 | action.triggered.connect(connectObject) 64 | if addToMenu: 65 | self.menu.addAction(action) 66 | 67 | return action 68 | 69 | 70 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGenerator.py: -------------------------------------------------------------------------------- 1 | from krita import * 2 | try: 3 | if int(qVersion().split('.')[0]) == 5: 4 | raise 5 | from PyQt6 import uic 6 | except: 7 | from PyQt5 import uic 8 | import re 9 | import os 10 | import subprocess 11 | import json 12 | 13 | from .GetKritaAPI import * 14 | 15 | class ActionGeneratorDialog(QDialog): 16 | 17 | def __init__(self): 18 | super().__init__() 19 | 20 | 21 | self.data = {} 22 | self.centralWidget = uic.loadUi( os.path.join(os.path.dirname(os.path.realpath(__file__)),'ActionGeneratorWidget.ui') ) 23 | layout = QVBoxLayout() 24 | layout.addWidget(self.centralWidget) 25 | 26 | self.model = QStandardItemModel() 27 | self.centralWidget.listView.setModel(self.model) 28 | 29 | self.setLayout(layout) 30 | 31 | self.centralWidget.newBtn.clicked.connect(self.newItem) 32 | self.centralWidget.deleteBtn.clicked.connect(self.deleteItem) 33 | self.centralWidget.saveBtn.clicked.connect(self.saveItem) 34 | 35 | self.centralWidget.doneBtn.clicked.connect(self.doneClicked) 36 | 37 | self.centralWidget.listView.selectionModel().selectionChanged.connect(self.openItem) 38 | 39 | 40 | 41 | def doneClicked(self): 42 | data={} 43 | for row in range(0,self.model.rowCount()): 44 | index = self.model.index(row,0) 45 | rec=index.data(Qt.ItemDataRole.UserRole+1) 46 | if rec['category'] not in data: 47 | data[rec['category']]={ 'text':rec['categoryText'], 'actions':[] } 48 | newRec={} 49 | for k in rec.keys(): 50 | newRec[ k.replace('action.','') ]=rec[k] 51 | data[rec['category']]['actions'].append(newRec) 52 | self.data = data 53 | 54 | self.close() 55 | 56 | 57 | 58 | def newItem(self, name=None, data=None): 59 | if not name: 60 | name='new' 61 | if not data: 62 | data=self.defaultForm 63 | item = QStandardItem(name) 64 | item.setData(data, Qt.ItemDataRole.UserRole+1) 65 | self.model.appendRow(item) 66 | index = self.model.indexFromItem(item) 67 | self.centralWidget.listView.selectionModel().select(index, QtCore.QItemSelectionModel.SelectionFlag.ClearAndSelect) 68 | def deleteItem(self): 69 | items = self.centralWidget.listView.selectionModel().selectedIndexes() 70 | for item in items: 71 | self.model.removeRow(item.row(),item.parent()) 72 | def openItem(self, old, new): 73 | items = self.centralWidget.listView.selectionModel().selectedIndexes() 74 | for item in items: 75 | data = item.data(Qt.ItemDataRole.UserRole+1) 76 | print ("open form", data ) 77 | if data: 78 | self.loadForm(self.centralWidget, data) 79 | def saveItem(self): 80 | items = self.centralWidget.listView.selectionModel().selectedIndexes() 81 | for item in items: 82 | data={} 83 | self.saveForm(self.centralWidget, data) 84 | print ("SAVE", data, item) 85 | 86 | self.model.setData(item,data, Qt.ItemDataRole.UserRole+1) 87 | self.model.setData(item,data['action.name'], Qt.ItemDataRole.DisplayRole) 88 | self.defaultForm['category']=data['category'] 89 | self.defaultForm['categoryText']=data['categoryText'] 90 | 91 | def loadForm(self, form, items): 92 | for w in form.findChildren(QWidget): 93 | if '_' not in w.objectName() and hasattr(w,'statusTip') and w.statusTip() != '': 94 | name = w.statusTip() 95 | if name in items: 96 | v = items[name] 97 | if isinstance(w,QLineEdit): 98 | w.setText(v) 99 | elif isinstance(w,QSpinBox): 100 | w.setValue(v) 101 | elif isinstance(w,QCheckBox): 102 | w.setChecked(v == 'true') 103 | elif isinstance(w,QKeySequenceEdit): 104 | w.setKeySequence(QKeySequence.fromString(v)) 105 | elif isinstance(w,QComboBox): 106 | if w.currentData(): 107 | w.setCurrentIndex(w.findData(v)) 108 | else: 109 | w.setCurrentIndex(w.findText(v)) 110 | elif isinstance(w,QListView) and w.model(): 111 | index = w.model().match(w.model().index(0, 0), 112 | Qt.ItemDataRole.DisplayRole, 113 | v, 114 | 1); 115 | items[name]=w.setCurrentIndex(index) 116 | 117 | def saveForm(self, form, items): 118 | for w in form.findChildren(QWidget): 119 | if '_' not in w.objectName() and hasattr(w,'statusTip') and w.statusTip() != '': 120 | name = w.statusTip() 121 | if isinstance(w,QLineEdit): 122 | items[name]=w.text() 123 | elif isinstance(w,QSpinBox): 124 | items[name]=w.value() 125 | elif isinstance(w,QCheckBox): 126 | items[name]='true' if w.isChecked() else 'false' 127 | elif isinstance(w,QKeySequenceEdit): 128 | items[name]=w.keySequence().toString() 129 | elif isinstance(w,QComboBox): 130 | if w.currentData(): 131 | items[name]=w.currentData() 132 | else: 133 | items[name]=w.currentText() 134 | elif isinstance(w,QListView) and w.model(): 135 | items[name]=w.currentIndex().data(Qt.ItemDataRole.DisplayRole) 136 | 137 | 138 | def getData(self, data): 139 | self.defaultForm = {} 140 | self.saveForm(self.centralWidget,self.defaultForm) 141 | if data: 142 | for cat in data.keys(): 143 | for action in range(0,len(data[cat]['actions'])): 144 | rec={ 145 | 'category':cat, 146 | 'categoryText':data[cat]['text'] 147 | } 148 | for k in data[cat]['actions'][action].keys(): 149 | rec['action.'+k]=data[cat]['actions'][action][k] 150 | self.newItem(rec['action.name'],rec) 151 | else: 152 | self.newItem() 153 | 154 | 155 | 156 | self.exec() 157 | return self.data 158 | 159 | class PluginGeneratorDialog(QDialog): 160 | 161 | def __init__(self): 162 | super().__init__() 163 | 164 | self.plan=[] 165 | self.respath = Krita.instance().readSetting('','ResourceDirectory','') 166 | 167 | self.centralWidget = uic.loadUi( os.path.join(os.path.dirname(os.path.realpath(__file__)),'PluginGeneratorWidget.ui') ) 168 | layout = QVBoxLayout() 169 | layout.addWidget(self.centralWidget) 170 | 171 | self.setLayout(layout) 172 | 173 | f = open( os.path.join(os.path.dirname(os.path.realpath(__file__)),'PluginGeneratorTemplates', 'index.json') ) 174 | self.templates = json.load( f ) 175 | 176 | for tpl in self.templates: 177 | item = QListWidgetItem(tpl['name'] + ' - ' + tpl['text']) 178 | item.setToolTip( os.path.join(os.path.dirname(os.path.realpath(__file__)),'PluginGeneratorTemplates', tpl['name'].replace(' ','')) ) 179 | self.centralWidget.templateListWidget.addItem(item) 180 | 181 | 182 | 183 | 184 | 185 | 186 | self.centralWidget.hotkeysBtn.setToolTip( json.dumps({'Scripts': {'text': 'My Scripts', 'actions': [{'activationConditions': '0', 'activationFlags': '10000', 'iconText': '', 'isCheckable': 'false', 'name': 'myAction', 'shortcut': '', 'statusTip': '', 'text': 'My Script', 'toolTip': '', 'whatsThis': '', 'category': 'Scripts', 'categoryText': 'My Scripts'}]}},indent=2) ) 187 | 188 | self.centralWidget.projectPathBtn.clicked.connect(self.projectPath) 189 | self.centralWidget.setupGitBtn.clicked.connect(self.gitRemotePath) 190 | self.centralWidget.hotkeysBtn.clicked.connect(self.genActions) 191 | self.centralWidget.doneBtn.clicked.connect(self.doneClicked) 192 | self.centralWidget.projectPathSymlinkChk.toggled.connect(self.toggleSymlink) 193 | 194 | self.centralWidget.autocompleteChk.stateChanged.connect(self.autocompleteCheck) 195 | 196 | result = subprocess.run(['git --version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 197 | if b'git version' in result.stdout and not result.stderr: 198 | self.centralWidget.setupGitChk.setEnabled(True) 199 | self.centralWidget.setupGitBtn.setEnabled(True) 200 | else: 201 | self.centralWidget.setupGitChk.setEnabled(False) 202 | self.centralWidget.setupGitBtn.setEnabled(False) 203 | 204 | 205 | 206 | 207 | def toggleSymlink(self): 208 | if self.centralWidget.projectPathBtn.isEnabled(): 209 | self.centralWidget.projectPathBtn.setEnabled(False) 210 | self.centralWidget.projectPathHeaderLabel.setText('Project Path:') 211 | self.centralWidget.projectPathLabel.setText(os.path.join(self.respath,'pykrita')) 212 | else: 213 | self.centralWidget.projectPathBtn.setEnabled(True) 214 | self.centralWidget.projectPathHeaderLabel.setText('Project Path: (project path will symlink to your pykrita folder)') 215 | self.centralWidget.projectPathLabel.setText('') 216 | 217 | def doneClicked(self): 218 | res = self.buildPlan() 219 | 220 | if 'error' in res: 221 | QMessageBox.warning(self, "Error", res['error']) 222 | elif 'success' in res: 223 | QMessageBox.about(self, "Done!", "Plugin Created! Please restart Krita") 224 | self.close() 225 | 226 | def buildActionCollection(self,data): 227 | xml = '\n' 228 | 229 | for cat in data.keys(): 230 | xml+='\n ' 231 | xml+='\n '+data[cat]['text']+'' 232 | 233 | for action in range(0,len(data[cat]['actions'])): 234 | xml+='\n ' 235 | for k in data[cat]['actions'][action].keys(): 236 | if k != 'name': 237 | xml+='\n <'+k+'>'+str(data[cat]['actions'][action][k])+'' 238 | xml+='\n ' 239 | 240 | xml+='\n ' 241 | 242 | 243 | xml+='\n' 244 | 245 | #print ("XML", xml) 246 | return xml 247 | 248 | def buildPlan(self): 249 | self.plan=[] 250 | projectPath = self.centralWidget.projectPathLabel.text() 251 | if projectPath == 'Select Path...' or not os.path.isdir(projectPath): 252 | return { 'error':"Invalid 'Project Path'" } 253 | shortName = self.centralWidget.shortNameEdit.text() 254 | if shortName == '' or re.search(r'[^a-zA-Z0-9_]', shortName): 255 | return { 'error':"Invalid 'Short Name', stick to 'a-zA-Z0-9_' characters" } 256 | pluginTitle = self.centralWidget.titleEdit.text() 257 | if pluginTitle == '': 258 | return { 'error':"Invalid 'Plugin Title'" } 259 | shortDesc = self.centralWidget.shortDescEdit.text() 260 | if shortDesc == '': 261 | return { 'error':"Invalid 'Short Description'" } 262 | template = self.centralWidget.templateListWidget.currentItem() 263 | if not template: 264 | return { 'error':"No template was selected" } 265 | 266 | projectRoot = os.path.join(projectPath,shortName) 267 | if self.centralWidget.projectPathSymlinkChk.isChecked(): 268 | self.plan.append([ lambda: os.mkdir(projectRoot), "Create Directory "+projectRoot ]) 269 | else: 270 | projectRoot= os.path.join(self.respath,'pykrita') 271 | 272 | projectDesktopFile = os.path.join(projectRoot,shortName+'.desktop') 273 | projectDesktopFileContent = '''[Desktop Entry] 274 | Type=Service 275 | ServiceTypes=Krita/PythonPlugin 276 | X-KDE-Library=%s 277 | X-Python-2-Compatible=false 278 | X-Krita-Manual=Manual.html 279 | Name=%s 280 | Comment=%s''' % (shortName, pluginTitle, shortDesc) 281 | self.plan.append([ lambda: self.writeToFile(projectDesktopFile,projectDesktopFileContent), "Create File "+projectDesktopFile+" with the following content:\n"+projectDesktopFileContent ]) 282 | 283 | projectFolderPath = os.path.join(projectRoot,shortName) 284 | self.plan.append([ lambda: os.mkdir(projectFolderPath), "Create Directory "+projectFolderPath ]) 285 | 286 | projectManualFile = os.path.join(projectFolderPath,'Manual.html') 287 | projectManualContent = self.centralWidget.manualTextEdit.toPlainText() 288 | self.plan.append([ lambda: self.writeToFile(projectManualFile,projectManualContent), "Create File "+projectManualFile+" with the following content:\n"+projectManualContent ]) 289 | 290 | autocomplete='from krita import *' 291 | if self.centralWidget.autocompleteChk.isChecked(): 292 | getAPI = GetKritaAPI() 293 | ver = (Krita.instance().version().split('-'))[0] 294 | 295 | projectAutoCompleteFile = os.path.join(projectFolderPath,'PyKrita.py') 296 | projectAutoCompleteContent = getAPI.genAutoComplete(ver) 297 | self.plan.append([ lambda: self.writeToFile(projectAutoCompleteFile,projectAutoCompleteContent), "Generating Auto Complete File: "+projectAutoCompleteFile ]) 298 | autocomplete = ''' 299 | # load autocomplete 300 | from typing import TYPE_CHECKING 301 | if TYPE_CHECKING: 302 | from .PyKrita import * 303 | else: 304 | from krita import * 305 | ''' 306 | 307 | self.plan.append([ lambda: self.genTemplate( template.toolTip(), { 308 | 'SHORTNAME':shortName, 309 | 'PLUGINTITLE':pluginTitle, 310 | 'PROJECTROOT':projectRoot, 311 | 'AUTOCOMPLETE':autocomplete 312 | },projectFolderPath), "Copying template "+template.toolTip()+" to "+projectFolderPath]) 313 | 314 | if self.centralWidget.hotkeysChk.isChecked(): 315 | projectActionFile = os.path.join(projectRoot,shortName+'.action') 316 | projectActionContent = self.buildActionCollection( json.loads(self.centralWidget.hotkeysBtn.toolTip()) ) 317 | self.plan.append([ lambda: self.writeToFile(projectActionFile,projectActionContent), "Create File "+projectActionFile+" with the following content:\n"+projectActionContent ]) 318 | 319 | if self.centralWidget.setupGitChk.isChecked(): 320 | readmeFile = os.path.join(projectRoot,'README.md') 321 | self.plan.append([ lambda: self.writeToFile(readmeFile,projectManualContent), "Create File "+readmeFile+" with the following content:\n"+projectManualContent ]) 322 | self.plan.append([ lambda: subprocess.run(['git init '+projectRoot], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True), "Creating local git with: git init "+projectRoot ]) 323 | gitRemotePath = self.centralWidget.setupGitChk.toolTip() 324 | if gitRemotePath: 325 | self.plan.append([ lambda: subprocess.run(['cd "'+projectRoot+'" && git remote add origin '+gitRemotePath], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True), 'Creating local git with: cd "'+projectRoot+'" && git remote add origin '+gitRemotePath ]) 326 | 327 | if self.centralWidget.projectPathSymlinkChk.isChecked(): 328 | symlinkFolder = os.path.join(self.respath,'pykrita',shortName) 329 | self.plan.append([ lambda: os.symlink(projectFolderPath,symlinkFolder), "Symlinking "+projectFolderPath+" to "+symlinkFolder ]) 330 | self.plan.append([ lambda: os.symlink(projectFolderPath+'.desktop',symlinkFolder+'.desktop'), "Symlinking "+projectFolderPath+".desktop to "+symlinkFolder+'.desktop' ]) 331 | if self.centralWidget.hotkeysChk.isChecked(): 332 | symlinkActionFolder = os.path.join(self.respath,'actions',shortName) 333 | self.plan.append([ lambda: os.symlink(projectFolderPath+'.action',symlinkActionFolder+'.action'), "Symlinking "+projectFolderPath+".action to "+symlinkActionFolder+'.action' ]) 334 | 335 | 336 | 337 | 338 | self.plan.append([lambda: Application.writeSetting( 339 | 'python', 340 | 'enable_%s' % shortName, 341 | 'true'), "Activate Plugin in Plugin Manager"]) 342 | 343 | confirmActions='' 344 | for item in self.plan: 345 | confirmActions+=item[1]+"\n\n" 346 | 347 | 348 | _ignore, ok = QInputDialog.getMultiLineText(self,"Confirm","The following work will be executed:",confirmActions) 349 | if ok: 350 | for step in self.plan: 351 | print ("Generating Step:", step[1]) 352 | step[0]() 353 | return { 'success':"Done!" } 354 | else: 355 | return {} 356 | 357 | def genTemplate(self,tpl,v,outputPath=None): 358 | def vsub(p): 359 | return v[p.group(1)] 360 | folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),'PluginGeneratorTemplates',tpl) 361 | 362 | 363 | for dp, dn, filenames in os.walk( folder ): 364 | for fn in filenames: 365 | f = open( os.path.join(dp,fn), "r") 366 | fileContent = f.read() 367 | fn = fn.replace('[SHORTNAME]',v['SHORTNAME']) 368 | 369 | fileContent=re.sub(r"'''\[\%([A-Z0-9]+?)\%\]'''", vsub, fileContent) 370 | 371 | if outputPath: 372 | self.writeToFile(os.path.join(outputPath,fn), fileContent) 373 | else: 374 | print ("Template Output:", os.path.join(outputPath,fn), fileContent) 375 | f.close() 376 | 377 | 378 | def genActions(self): 379 | d = json.loads(self.centralWidget.hotkeysBtn.toolTip()) if self.centralWidget.hotkeysBtn.toolTip() else None 380 | 381 | d = ActionGeneratorDialog().getData( d ) 382 | #print ("Data", d) 383 | self.centralWidget.hotkeysBtn.setToolTip(json.dumps(d, indent=2)) 384 | 385 | def gitRemotePath(self): 386 | url, ok = QInputDialog.getText(self, 'Git Remote Path', 'Remote path to git repository:') 387 | if ok: 388 | self.centralWidget.setupGitChk.setToolTip(url) 389 | 390 | 391 | def projectPath(self): 392 | destDir = QFileDialog.getExistingDirectory(None, 'Select Project Directory',QDir.homePath(), QFileDialog.Option.ShowDirsOnly) 393 | self.centralWidget.projectPathLabel.setText(destDir) 394 | 395 | def writeToFile(self,path,s): 396 | f = open(path, 'w') 397 | n = f.write(s) 398 | f.close() 399 | def autocompleteCheck(self, state): 400 | if state == Qt.Checked: 401 | if not self.downloadKritaAPI(): 402 | self.centralWidget.autocompleteChk.setChecked(False) 403 | 404 | def downloadKritaAPI(self): 405 | ver = (Krita.instance().version().split('-'))[0] 406 | respath = Krita.instance().readSetting('','ResourceDirectory','') 407 | if respath == '': 408 | respath = os.path.dirname(os.path.realpath(__file__)) 409 | else: 410 | respath=os.path.join(respath,'pykrita','PluginDevTools') 411 | 412 | if os.path.isfile(respath + ".KritaAPI."+ver+".zip"): 413 | return True 414 | msgbox = QMessageBox(QMessageBox.Icon.Question,'Would you like to download the API details automatically?', 415 | '', QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) 416 | msgbox.setTextFormat(Qt.TextFormat.RichText) 417 | 418 | msgbox.setText("""Developer Tools would like to connect to the internet to download Krita API details. 419 | This process will only access Krita's offical git repository at invent.kde.org. 420 |
421 | The API is still accessable even without downloading this file, but may not be fully complete, no documentation will be available and you will not be able to generate autocomplete files. 422 |
423 | You can also do this manually by downloading the following (Unless you are on Krita Next nightly, 'master' should be replaced with the tag you plan to target, ex. 'v"""+ver+"""' or 'v"""+ver+"""-beta1'):
424 | https://invent.kde.org/graphics/krita/-/archive/master/krita-master.zip?path=libs/libkis 425 |
426 | And place it in:
427 | """ + respath + """.KritaAPI."""+ver+""".zip 428 |
429 | This only needs to be done once per new version of Krita. Do note that Krita may freeze up for about a minute. 430 |
431 | Would you like to download the API details(less than 200kb of data) automatically? 432 | """) 433 | 434 | if msgbox.exec() == QMessageBox.StandardButton.Yes: 435 | getAPI = GetKritaAPI() 436 | res = {} 437 | try: 438 | res=getAPI.updateData(ver) 439 | except: 440 | QMessageBox(QMessageBox.Icon.Warning,'Failed!', "Failed to download API details! Make sure you have internet connection and your python urlib ssl is working properly").exec() 441 | return False 442 | print ("RES", res) 443 | 444 | if res['status'] == 0: 445 | msgbox = QMessageBox(QMessageBox.Icon.Warning,'Error',str(res['error'])) 446 | 447 | msgbox.exec() 448 | return False 449 | else: 450 | QMessageBox(QMessageBox.Icon.Information,'Success!', "API details have been downloaded successfully!").exec() 451 | else: 452 | return False 453 | 454 | 455 | return True 456 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorTemplates/BlankDocker/[SHORTNAME].py: -------------------------------------------------------------------------------- 1 | import krita 2 | try: 3 | if int(krita.qVersion().split('.')[0]) == 5: 4 | raise 5 | from PyQt6.QtWidgets import * 6 | except: 7 | from PyQt5.QtWidgets import * 8 | '''[%AUTOCOMPLETE%]''' 9 | 10 | class '''[%SHORTNAME%]'''(DockWidget): 11 | 12 | def __init__(self): 13 | super().__init__() 14 | self.setWindowTitle("'''[%PLUGINTITLE%]'''") 15 | 16 | def canvasChanged(self, canvas): 17 | pass 18 | 19 | Krita.instance().addDockWidgetFactory(DockWidgetFactory("'''[%SHORTNAME%]'''", DockWidgetFactoryBase.DockPosition.DockRight, '''[%SHORTNAME%]''')) 20 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorTemplates/BlankDocker/__init__.py: -------------------------------------------------------------------------------- 1 | from .'''[%SHORTNAME%]''' import * 2 | 3 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorTemplates/BlankExtension/[SHORTNAME].py: -------------------------------------------------------------------------------- 1 | '''[%AUTOCOMPLETE%]''' 2 | 3 | class '''[%SHORTNAME%]'''(Extension): 4 | 5 | def __init__(self, parent): 6 | # This is initialising the parent, always important when subclassing. 7 | super().__init__(parent) 8 | 9 | def setup(self): 10 | #This runs only once when app is installed 11 | pass 12 | 13 | def createActions(self, window): 14 | ''' 15 | Example: 16 | action = window.createAction("uniqueIdOfAction", "Text shown in menu of the action", "tools/scripts") 17 | action.triggered.connect(self.methodToRunOnClick) 18 | ''' 19 | pass 20 | 21 | # And add the extension to Krita's list of extensions: 22 | Krita.instance().addExtension('''[%SHORTNAME%]'''(Krita.instance())) 23 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorTemplates/BlankExtension/__init__.py: -------------------------------------------------------------------------------- 1 | from .'''[%SHORTNAME%]''' import * 2 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorTemplates/SimpleDockerGUI/[SHORTNAME].py: -------------------------------------------------------------------------------- 1 | import krita 2 | try: 3 | if int(krita.qVersion().split('.')[0]) == 5: 4 | raise 5 | from PyQt6.QtWidgets import * 6 | except: 7 | from PyQt5.QtWidgets import * 8 | '''[%AUTOCOMPLETE%]''' 9 | 10 | class '''[%SHORTNAME%]'''(DockWidget): 11 | 12 | def __init__(self): 13 | super().__init__() 14 | self.setWindowTitle("'''[%PLUGINTITLE%]'''") 15 | label = QLabel(self) 16 | label.setObjectName("label") 17 | label.setAlignment(Qt.AlignmentFlag.AlignCenter) 18 | label.setText("Hello World") 19 | 20 | self.centralWidget = QWidget() 21 | layout = QVBoxLayout() 22 | layout.addWidget(label) 23 | self.centralWidget.setLayout(layout) 24 | 25 | self.setWidget(self.centralWidget) 26 | 27 | 28 | 29 | def canvasChanged(self, canvas): 30 | pass 31 | 32 | Krita.instance().addDockWidgetFactory(DockWidgetFactory("'''[%SHORTNAME%]'''", DockWidgetFactoryBase.DockPosition.DockRight, '''[%SHORTNAME%]''')) 33 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorTemplates/SimpleDockerGUI/__init__.py: -------------------------------------------------------------------------------- 1 | from .'''[%SHORTNAME%]''' import * 2 | 3 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorTemplates/SimpleDockerGUIQML/[SHORTNAME].py: -------------------------------------------------------------------------------- 1 | import krita 2 | import os 3 | try: 4 | if int(krita.qVersion().split('.')[0]) == 5: 5 | raise 6 | from PyQt6 import uic 7 | from PyQt6.QtWidgets import * 8 | except: 9 | from PyQt5 import uic 10 | from PyQt5.QtWidgets import * 11 | 12 | '''[%AUTOCOMPLETE%]''' 13 | 14 | class '''[%SHORTNAME%]'''(DockWidget): 15 | 16 | def __init__(self): 17 | super().__init__() 18 | self.setWindowTitle("'''[%PLUGINTITLE%]'''") 19 | self.centralWidget = uic.loadUi( os.path.join(os.path.dirname(os.path.realpath(__file__)),"'''[%SHORTNAME%]'''.ui")) 20 | 21 | layout = QVBoxLayout() 22 | layout.addWidget(self.centralWidget) 23 | 24 | self.setLayout(layout) 25 | 26 | def canvasChanged(self, canvas): 27 | pass 28 | 29 | Krita.instance().addDockWidgetFactory(DockWidgetFactory("'''[%SHORTNAME%]'''", DockWidgetFactoryBase.DockPosition.DockRight, '''[%SHORTNAME%]''')) 30 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorTemplates/SimpleDockerGUIQML/[SHORTNAME].ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | Hello World 21 | 22 | 23 | Qt::AlignCenter 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorTemplates/SimpleDockerGUIQML/__init__.py: -------------------------------------------------------------------------------- 1 | from .'''[%SHORTNAME%]''' import * 2 | 3 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorTemplates/SimpleExtensionDialogGUI/[SHORTNAME].py: -------------------------------------------------------------------------------- 1 | import krita 2 | try: 3 | if int(krita.qVersion().split('.')[0]) == 5: 4 | raise 5 | from PyQt6 import uic 6 | from PyQt6.QtWidgets import * 7 | except: 8 | from PyQt5 import uic 9 | from PyQt5.QtWidgets import * 10 | 11 | '''[%AUTOCOMPLETE%]''' 12 | 13 | class '''[%SHORTNAME%]'''Dialog(QDialog): 14 | 15 | def __init__(self): 16 | super().__init__() 17 | self.setWindowTitle("'''[%PLUGINTITLE%]'''") 18 | label = QLabel(self) 19 | label.setObjectName("label") 20 | label.setAlignment(Qt.AlignmentFlag.AlignCenter) 21 | label.setText("Hello World") 22 | 23 | layout = QVBoxLayout() 24 | layout.addWidget(label) 25 | 26 | self.setLayout(layout) 27 | 28 | class '''[%SHORTNAME%]'''(Extension): 29 | 30 | def __init__(self, parent): 31 | # This is initialising the parent, always important when subclassing. 32 | super().__init__(parent) 33 | self.dialog = '''[%SHORTNAME%]'''Dialog() 34 | 35 | def setup(self): 36 | #This runs only once when app is installed 37 | pass 38 | 39 | def createActions(self, window): 40 | action = window.createAction("'''[%SHORTNAME%]'''OpenDialog", "Open '''[%SHORTNAME%]''' Dialog", "tools/scripts") 41 | action.triggered.connect(self.dialog.show) 42 | 43 | 44 | # And add the extension to Krita's list of extensions: 45 | Krita.instance().addExtension('''[%SHORTNAME%]'''(Krita.instance())) 46 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorTemplates/SimpleExtensionDialogGUI/__init__.py: -------------------------------------------------------------------------------- 1 | from .'''[%SHORTNAME%]''' import * 2 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorTemplates/SimpleExtensionDialogGUIQML/[SHORTNAME].py: -------------------------------------------------------------------------------- 1 | import krita 2 | import os 3 | try: 4 | if int(krita.qVersion().split('.')[0]) == 5: 5 | raise 6 | from PyQt6 import uic 7 | from PyQt6.QtWidgets import * 8 | except: 9 | from PyQt5 import uic 10 | from PyQt5.QtWidgets import * 11 | 12 | '''[%AUTOCOMPLETE%]''' 13 | 14 | class '''[%SHORTNAME%]'''Dialog(QDialog): 15 | 16 | def __init__(self): 17 | super().__init__() 18 | self.setWindowTitle("'''[%PLUGINTITLE%]'''") 19 | self.centralWidget = uic.loadUi( os.path.join(os.path.dirname(os.path.realpath(__file__)),"'''[%SHORTNAME%]'''.ui")) 20 | 21 | layout = QVBoxLayout() 22 | layout.addWidget(self.centralWidget) 23 | 24 | self.setLayout(layout) 25 | 26 | class '''[%SHORTNAME%]'''(Extension): 27 | 28 | def __init__(self, parent): 29 | # This is initialising the parent, always important when subclassing. 30 | super().__init__(parent) 31 | self.dialog = '''[%SHORTNAME%]'''Dialog() 32 | 33 | def setup(self): 34 | #This runs only once when app is installed 35 | pass 36 | 37 | def createActions(self, window): 38 | action = window.createAction("'''[%SHORTNAME%]'''OpenDialog", "Open '''[%SHORTNAME%]''' Dialog", "tools/scripts") 39 | action.triggered.connect(self.dialog.show) 40 | 41 | 42 | # And add the extension to Krita's list of extensions: 43 | Krita.instance().addExtension('''[%SHORTNAME%]'''(Krita.instance())) 44 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorTemplates/SimpleExtensionDialogGUIQML/[SHORTNAME].ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | Hello World 21 | 22 | 23 | Qt::AlignCenter 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorTemplates/SimpleExtensionDialogGUIQML/__init__.py: -------------------------------------------------------------------------------- 1 | from .'''[%SHORTNAME%]''' import * 2 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorTemplates/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"name":"Blank Docker","text":"A blank docker template (use this if your extension is centered around a docker)"}, 3 | {"name":"Blank Extension","text":"A blank extension template (use this if your extension is not centered around a docker, you can still add dockers to extensions)"}, 4 | {"name":"Simple Docker GUI QML","text":"A simple Hello World Docker with GUI externally loaded from a UI file that can be easily made visually with Qt Designer"}, 5 | {"name":"Simple Docker GUI","text":"A simple Hello World Docker with GUI written declaratively, you can still use Qt Designer to make these via Form->View Python but you will not be able to load up the python code for editing"}, 6 | {"name":"Simple Extension Dialog GUI QML","text":"A simple Hello World Dialog with GUI externally loaded from a UI file that can be easily made visually with Qt Designer"}, 7 | {"name":"Simple Extension Dialog GUI","text":"A simple Hello World Dialog with GUI written declaratively, you can still use Qt Designer to make these via Form->View Python but you will not be able to load up the python code for editing"} 8 | ] 9 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/PluginGeneratorWidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | PluginGenerator 4 | 5 | 6 | 7 | 0 8 | 0 9 | 622 10 | 728 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | Desktop Entry file - [Resource Folder]/pykrita/[short name].desktop 21 | 22 | 23 | 24 | 25 | 26 | Plugin Title: (Name in plugin manager) 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Short Name: (code name without spaces or special characters used for folder name) 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Short Description: (Shows up in plugin manager) 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Manual - [Resource Folder]/pykrita/[short name]/Manual.html 60 | 61 | 62 | 63 | 64 | 65 | Long Description/Manual: (basic HTML format, you can edit it in a text editor later) 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | Setup - [Resource Folder]/pykrita/[short name]/* 79 | 80 | 81 | 82 | 83 | 84 | Project Path: (project path will symlink to your pykrita folder) 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | Select Path... 94 | 95 | 96 | 97 | 98 | 99 | 100 | Use Symlink 101 | 102 | 103 | true 104 | 105 | 106 | 107 | 108 | 109 | 110 | ... 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | Template: 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | Generate custom hotkey shortcuts ([Resource Folder]/actions/[short name].action) 132 | 133 | 134 | 135 | 136 | 137 | 138 | ... 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | Setup Git (requires git to be installed) 150 | 151 | 152 | 153 | 154 | 155 | 156 | ... 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | Generate AutoComplete 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | Done 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/ThemeIcons.txt: -------------------------------------------------------------------------------- 1 | address-book-new 2 | application-exit 3 | appointment-new 4 | call-start 5 | call-stop 6 | contact-new 7 | document-new 8 | document-open 9 | document-open-recent 10 | document-page-setup 11 | document-print 12 | document-print-preview 13 | document-properties 14 | document-revert 15 | document-save 16 | document-save-as 17 | document-send 18 | edit-clear 19 | edit-copy 20 | edit-cut 21 | edit-delete 22 | edit-find 23 | edit-find-replace 24 | edit-paste 25 | edit-redo 26 | edit-select-all 27 | edit-undo 28 | folder-new 29 | format-indent-less 30 | format-indent-more 31 | format-justify-center 32 | format-justify-fill 33 | format-justify-left 34 | format-justify-right 35 | format-text-direction-ltr 36 | format-text-direction-rtl 37 | format-text-bold 38 | format-text-italic 39 | format-text-underline 40 | format-text-strikethrough 41 | go-bottom 42 | go-down 43 | go-first 44 | go-home 45 | go-jump 46 | go-last 47 | go-next 48 | go-previous 49 | go-top 50 | go-up 51 | help-about 52 | help-contents 53 | help-faq 54 | insert-image 55 | insert-link 56 | insert-object 57 | insert-text 58 | list-add 59 | list-remove 60 | mail-forward 61 | mail-mark-important 62 | mail-mark-junk 63 | mail-mark-notjunk 64 | mail-mark-read 65 | mail-mark-unread 66 | mail-message-new 67 | mail-reply-all 68 | mail-reply-sender 69 | mail-send 70 | mail-send-receive 71 | media-eject 72 | media-playback-pause 73 | media-playback-start 74 | media-playback-stop 75 | media-record 76 | media-seek-backward 77 | media-seek-forward 78 | media-skip-backward 79 | media-skip-forward 80 | object-flip-horizontal 81 | object-flip-vertical 82 | object-rotate-left 83 | object-rotate-right 84 | process-stop 85 | system-lock-screen 86 | system-log-out 87 | system-run 88 | system-search 89 | system-reboot 90 | system-shutdown 91 | tools-check-spelling 92 | view-fullscreen 93 | view-refresh 94 | view-restore 95 | view-sort-ascending 96 | view-sort-descending 97 | window-close 98 | window-new 99 | zoom-fit-best 100 | zoom-in 101 | zoom-original 102 | zoom-out 103 | process-working 104 | accessories-calculator 105 | accessories-character-map 106 | accessories-dictionary 107 | accessories-text-editor 108 | help-browser 109 | multimedia-volume-control 110 | preferences-desktop-accessibility 111 | preferences-desktop-font 112 | preferences-desktop-keyboard 113 | preferences-desktop-locale 114 | preferences-desktop-multimedia 115 | preferences-desktop-screensaver 116 | preferences-desktop-theme 117 | preferences-desktop-wallpaper 118 | system-file-manager 119 | system-software-install 120 | system-software-update 121 | utilities-system-monitor 122 | utilities-terminal 123 | applications-accessories 124 | applications-development 125 | applications-engineering 126 | applications-games 127 | applications-graphics 128 | applications-internet 129 | applications-multimedia 130 | applications-office 131 | applications-other 132 | applications-science 133 | applications-system 134 | applications-utilities 135 | preferences-desktop 136 | preferences-desktop-peripherals 137 | preferences-desktop-personal 138 | preferences-other 139 | preferences-system 140 | preferences-system-network 141 | system-help 142 | audio-card 143 | audio-input-microphone 144 | battery 145 | camera-photo 146 | camera-video 147 | camera-web 148 | computer 149 | drive-harddisk 150 | drive-optical 151 | drive-removable-media 152 | input-gaming 153 | input-keyboard 154 | input-mouse 155 | input-tablet 156 | media-flash 157 | media-floppy 158 | media-optical 159 | media-tape 160 | modem 161 | multimedia-player 162 | network-wired 163 | network-wireless 164 | pda 165 | phone 166 | printer 167 | scanner 168 | video-display 169 | emblem-default 170 | emblem-documents 171 | emblem-downloads 172 | emblem-favorite 173 | emblem-important 174 | emblem-mail 175 | emblem-photos 176 | emblem-readonly 177 | emblem-shared 178 | emblem-symbolic-link 179 | emblem-synchronized 180 | emblem-system 181 | emblem-unreadable 182 | face-angel 183 | face-angry 184 | face-cool 185 | face-crying 186 | face-devilish 187 | face-embarrassed 188 | face-kiss 189 | face-laugh 190 | face-monkey 191 | face-plain 192 | face-raspberry 193 | face-sad 194 | face-sick 195 | face-smile 196 | face-smile-big 197 | face-smirk 198 | face-surprise 199 | face-tired 200 | face-uncertain 201 | face-wink 202 | face-worried 203 | flag-aa 204 | application-x-executable 205 | audio-x-generic 206 | font-x-generic 207 | image-x-generic 208 | package-x-generic 209 | text-html 210 | text-x-generic 211 | text-x-generic-template 212 | text-x-script 213 | video-x-generic 214 | x-office-address-book 215 | x-office-calendar 216 | x-office-document 217 | x-office-presentation 218 | x-office-spreadsheet 219 | folder 220 | folder-remote 221 | network-server 222 | network-workgroup 223 | start-here 224 | user-bookmarks 225 | user-desktop 226 | user-home 227 | user-trash 228 | appointment-missed 229 | appointment-soon 230 | audio-volume-high 231 | audio-volume-low 232 | audio-volume-medium 233 | audio-volume-muted 234 | battery-caution 235 | battery-low 236 | dialog-error 237 | dialog-information 238 | dialog-password 239 | dialog-question 240 | dialog-warning 241 | folder-drag-accept 242 | folder-open 243 | folder-visiting 244 | image-loading 245 | image-missing 246 | mail-attachment 247 | mail-unread 248 | mail-read 249 | mail-replied 250 | mail-signed 251 | mail-signed-verified 252 | media-playlist-repeat 253 | media-playlist-shuffle 254 | network-error 255 | network-idle 256 | network-offline 257 | network-receive 258 | network-transmit 259 | network-transmit-receive 260 | printer-error 261 | printer-printing 262 | security-high 263 | security-medium 264 | security-low 265 | software-update-available 266 | software-update-urgent 267 | sync-error 268 | sync-synchronizing 269 | task-due 270 | task-past-due 271 | user-available 272 | user-away 273 | user-idle 274 | user-offline 275 | user-trash-full 276 | weather-clear 277 | weather-clear-night 278 | weather-few-clouds 279 | weather-few-clouds-night 280 | weather-fog 281 | weather-overcast 282 | weather-severe-alert 283 | weather-showers 284 | weather-showers-scattered 285 | weather-snow 286 | weather-storm 287 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/__init__.py: -------------------------------------------------------------------------------- 1 | from krita import * 2 | from .PluginDevToolsWidget import * 3 | from .PluginDevToolsDocker import * 4 | from .PluginDevToolsExtension import * 5 | from .PluginGenerator import * 6 | 7 | app = Krita.instance() 8 | 9 | ## TODO: Planed to add some functions in future. 10 | ## Add to extension list 11 | #extension = PluginDevToolsExtension(parent=app) #instantiate your class 12 | #app.addExtension(extension) 13 | 14 | testExtension = PluginDevToolsTestExtension(parent=app) 15 | app.addExtension(testExtension) 16 | 17 | # Add to docker list 18 | DOCKER_ID = 'pluginDevToolsDocker' 19 | dock_widget_factory = DockWidgetFactory(DOCKER_ID, DockWidgetFactoryBase.DockPosition.DockBottom, PluginDevToolsDocker) 20 | app.addDockWidgetFactory(dock_widget_factory) 21 | 22 | def retrieveDocker(docker_id: str)->PluginDevToolsDocker: 23 | # Only retrieve docker instance after the main window was completely created 24 | try: 25 | Krita.instance().activeWindow().qwindow() 26 | except: 27 | print('main window not created at this moment') 28 | return PluginDevToolsDocker() 29 | 30 | for d in app.dockers(): 31 | if d.objectName() == docker_id: 32 | if isinstance(d, PluginDevToolsDocker): 33 | return d 34 | print("cannot find docker: {docker_id}") 35 | exit(1) 36 | 37 | def registerPluginGenerator(): 38 | docker = retrieveDocker(DOCKER_ID) 39 | pluginGen = PluginGeneratorDialog() 40 | pluginGen.exec() 41 | 42 | 43 | def setup(): 44 | 45 | qwin = Krita.instance().activeWindow().qwindow() 46 | 47 | docker = retrieveDocker(DOCKER_ID) 48 | 49 | docker.setFirstAfterStart() 50 | 51 | # Only for debug 52 | testExtension.dynamicAddEntry(DOCKER_ID, dock_widget_factory, docker) 53 | 54 | # Dynamically load some object into actions menu 55 | testExtension.dynamicCreateAction(docker.applyDockerMode, Krita.instance().activeWindow(), 'PluginDevToolsActionOpenAsDocker', 'Open as Docker', True) 56 | testExtension.dynamicCreateAction(docker.applyDialogMode, Krita.instance().activeWindow(), 'PluginDevToolsActionOpenAsDialog', 'Open as Dialog', True) 57 | 58 | testExtension.dynamicCreateAction(registerPluginGenerator, Krita.instance().activeWindow(), 'PluginDevToolsActionPluginGen', 'Create your own new plugin generator...', True) 59 | 60 | def toggleOnAndOff(): 61 | if docker.isVisible(): 62 | docker.applyHideMode() 63 | else: 64 | docker.applyDockerMode() 65 | testExtension.dynamicCreateAction(toggleOnAndOff, Krita.instance().activeWindow(), 'PluginDevToolsToggleOnOff', 'Toggle On or Off') 66 | 67 | 68 | 69 | # Some settings require Krita.instance().activeWindow().qwindow() as parameter 70 | # Wait Krita.instance().activeWindow().qwindow() completely created then set the widgets 71 | appNotifier = Krita.instance().notifier() 72 | appNotifier.setActive(True) 73 | appNotifier.windowCreated.connect(setup) 74 | 75 | 76 | -------------------------------------------------------------------------------- /plugindevtools/PluginDevTools/kritadoc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 72 | 73 | 74 |
75 | 76 |

Classes (Compare to Prev Branch: )

77 |
78 | 79 | 80 |
81 |
82 |
83 |

84 | 85 |
86 |
87 | 88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | 96 |
97 |
98 |
99 | () -> 100 |
101 |
102 |
103 |
104 |
105 | 106 |
107 | 108 | : 109 | 110 |
111 |
112 | 113 | 649 | 650 |
651 |
652 | 653 | 654 | -------------------------------------------------------------------------------- /plugindevtools/actions/PluginDevTools.action: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PluginDevTools 5 | 6 | 7 | 8 | Toggle On or Off 9 | 10 | 11 | 12 | 10000 13 | 0 14 | f12 15 | false 16 | 17 | 18 | 19 | 20 | 21 | 28 | --------------------------------------------------------------------------------