├── .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, ///< Always activate</span></p><p><a name="LC43"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> ACTIVE_IMAGE = 0x0001, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< ???</span></p><p><a name="LC54"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> PIXELS_IN_CLIPBOARD = 0x0800, ///< 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, ///< 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, ///< ???</span></p><p><a name="LC57"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> LAYERS_IN_CLIPBOARD = 0x4000, ///< ???</span></p><p><a name="LC58"/><span style=" font-family:'Hack';"/><span style=" font-family:'Hack';"> IMAGE_HAS_ANIMATION = 0x8000, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< 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, ///< ???</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, ///< 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, ///< 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, ///< ???</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, ///< ???</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, ///< 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, ///< 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, ///< 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])+''+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 |