Krita Source Code Documentation
Loading...
Searching...
No Matches
krita_script_starter.py
Go to the documentation of this file.
2# SPDX-License-Identifier: GPL-3.0-or-later
3#
4
5"""
6BBD's Krita script starter
7
8This script does the boring stuff involved in creating a script for Krita.
9it creates
10* a directory for the script in the correct Krita resources subdirectory,
11* populates that directory with:
12-- a __init__.py file,
13-- a skeleton file for the script proper
14-- a Manual.html file
15* creates a .desktop file for the script
16It also:
17* correctly imports the script proper nto __init__.py, creates
18* creates basic skeleton code depending on whether the script is intended
19to be an extension or a docker
20* creates skeleton code in the Manual.html file
21* (optionally) automatically enables the script in the Krita menu
22
23Script can be run from the command line. This can be used to
24bootstrap the script into a Krita menu entry - create a new script
25called Krita Script Starter, then copy the script (and the .ui file)
26into the directory you have just created, overwriting the existing
27files.
28
29BBD
3016 March 2018
31"""
32
33import os
34import sys
35try:
36 from PyQt6.QtWidgets import QApplication, QWidget, QMessageBox
37 import PyQt6.uic as uic
38except:
39 from PyQt5.QtWidgets import QApplication, QWidget, QMessageBox
40 import PyQt5.uic as uic
41
42try:
43 import krita
44 CONTEXT_KRITA = True
45 EXTENSION = krita.Extension
46
47except ImportError:
48 # script being run in testing environment without Krita
49 CONTEXT_KRITA = False
50 EXTENSION = QWidget
51
52# TESTING = True
53TESTING = False
54
55MAIN_KRITA_ID = "Krita Script Starter"
56MAIN_KRITA_MENU_ENTRY = "Krita Script Starter"
57
58SCRIPT_NAME = "script_name"
59SCRIPT_COMMENT = "script_comment"
60KRITA_ID = "krita_id"
61LIBRARY_NAME = "library_name"
62MENU_ENTRY = "menu_entry"
63SCRIPT_TYPE = "script_type" # extension v docker
64PYTHON_FILE_NAME = "python_file"
65CLASS_NAME = "class_name"
66
67# from LIBRARY_NAME get:
68# the name of the directory
69# the name of the main python file
70# the name of the class
71
72SCRIPT_EXTENSION = "Extension"
73SCRIPT_DOCKER = "Docker`"
74
75SCRIPT_SETTINGS = 'python'
76
77UI_FILE = "bbdkss.ui"
78
79
80def load_ui(ui_file):
81 """If this script has been distributed with a ui file in the same
82 directory, then find that directory (since it will likely be
83 different from krita's current working directory) and use that to
84 load the ui file.
85
86 return the loaded ui
87 """
88 abs_path = os.path.dirname(os.path.realpath(__file__))
89 ui_file = os.path.join(abs_path, UI_FILE)
90 return uic.loadUi(ui_file)
91
92
93DESKTOP_TEMPLATE = """[Desktop Entry]
94Type=Service
95ServiceTypes=Krita/PythonPlugin
96X-KDE-Library={library_name}
97X-Krita-Manual=Manual.html
98Name={script_name}
99Comment={script_comment}
100"""
101
102INIT_TEMPLATE_EXTENSION = """from .{library_name} import {class_name}
103
104# And add the extension to Krita's list of extensions:
105app = Krita.instance()
106# Instantiate your class:
107extension = {class_name}(parent = app)
108app.addExtension(extension)
109"""
110
111INIT_TEMPLATE_DOCKER = """from .{library_name} import {class_name}
112"""
113
114
115EXTENSION_TEMPLATE = """# BBD's Krita Script Starter Feb 2018
116
117from krita import Extension
118
119EXTENSION_ID = '{krita_id}'
120MENU_ENTRY = '{menu_entry}'
121
122
123class {class_name}(Extension):
124
125 def __init__(self, parent):
126 # Always initialise the superclass.
127 # This is necessary to create the underlying C++ object
128 super().__init__(parent)
129
130 def setup(self):
131 pass
132
133 def createActions(self, window):
134 action = window.createAction(EXTENSION_ID, MENU_ENTRY, "tools/scripts")
135 # parameter 1 = the name that Krita uses to identify the action
136 # parameter 2 = the text to be added to the menu entry for this script
137 # parameter 3 = location of menu entry
138 action.triggered.connect(self.action_triggered)
139
140 def action_triggered(self):
141 pass # your active code goes here.
142"""
143
144DOCKER_TEMPLATE = """#BBD's Krita Script Starter Feb 2018
145from krita import DockWidget, DockWidgetFactory, DockWidgetFactoryBase
146
147DOCKER_NAME = '{script_name}'
148DOCKER_ID = '{krita_id}'
149
150
151class {class_name}(DockWidget):
152
153 def __init__(self):
154 super().__init__()
155 self.setWindowTitle(DOCKER_NAME)
156
157 def canvasChanged(self, canvas):
158 pass
159
160
161instance = Krita.instance()
162dock_widget_factory = DockWidgetFactory(DOCKER_ID,
163 DockWidgetFactoryBase.DockPosition.DockRight,
164 {class_name})
165
166instance.addDockWidgetFactory(dock_widget_factory)
167"""
168
169MANUAL_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
170<!DOCTYPE html>
171
172<html xmlns="http://www.w3.org/1999/xhtml">
173<!--BBD's Krita Script Starter, Feb 2018 -->
174<head><title>{script_name}</title>
175</head>
176<body>
177<h3>{script_name}</h3>
178Tell people about what your script does here.
179This is an html document so you can format it with html tags.
180<h3>Usage</h3>
181Tell people how to use your script here.
182
183</body>
184</html>"""
185
186
187class KritaScriptStarter(EXTENSION):
188
189 def __init__(self, parent):
190 super().__init__(parent)
191
192 def setup(self):
193 self.script_abs_path = os.path.dirname(os.path.realpath(__file__))
194 self.ui_file = os.path.join(self.script_abs_path, UI_FILE)
195 self.ui = load_ui(self.ui_file)
196
197 self.ui.e_name_of_script.textChanged.connect(self.name_changename_change)
198 self.ui.cancel_button.clicked.connect(self.cancelcancel)
199 self.ui.create_button.clicked.connect(self.createcreate)
200
201 target_directory = Krita.instance().getAppDataLocation()
202 if not CONTEXT_KRITA:
203 target_directory = os.path.join(target_directory, "krita")
204 target_directory = os.path.join(target_directory, "pykrita")
205 self.target_directory = target_directory
206
207 def createActions(self, window):
208 """ Called by Krita to create actions."""
209 action = window.createAction(
210 MAIN_KRITA_ID, MAIN_KRITA_MENU_ENTRY, "tools/scripts")
211 # parameter 1 = the name that Krita uses to identify the action
212 # parameter 2 = the text to be added to the menu entry for this script
213 # parameter 3 = location of menu entry
214 action.triggered.connect(self.action_triggeredaction_triggered)
215
217 self.ui.show()
218 self.ui.activateWindow()
219
220 def cancel(self):
221 self.ui.close()
222
223 def create(self):
224 """Go ahead and create the relevant files. """
225
226 if self.ui.e_name_of_script.text().strip() == "":
227 # Don't create script with empty name
228 return
229
230 def full_dir(path):
231 # convenience function
232 return os.path.join(self.target_directory, path)
233
234 # should already be done, but just in case:
235 self.calculate_file_names(self.ui.e_name_of_script.text())
236
237 menu_entry = self.ui.e_menu_entry.text()
238 if menu_entry.strip() == "":
239 menu_entry = self.ui.e_name_of_script.text()
240
241 comment = self.ui.e_comment.text()
242 if comment.strip() == "":
243 comment = "Replace this text with your description"
244
245 values = {
246 SCRIPT_NAME: self.ui.e_name_of_script.text(),
247 SCRIPT_COMMENT: comment,
248 KRITA_ID: "pykrita_" + self.package_name,
249 SCRIPT_TYPE: SCRIPT_DOCKER if self.ui.rb_docker.isChecked() else SCRIPT_EXTENSION, # noqa: E501
250 MENU_ENTRY: menu_entry,
251 LIBRARY_NAME: self.package_name,
252 CLASS_NAME: self.class_name
253 }
254
255 try:
256 # create package directory
257 package_directory = full_dir(self.package_name)
258 # needs to be lowercase and no spaces
259 os.mkdir(package_directory)
260 except FileExistsError:
261 # if package directory exists write into it, overwriting
262 # existing files.
263 pass
264
265 # create desktop file
266 fn = full_dir(self.desktop_fn)
267 with open(fn, 'w+t') as f:
268 f.write(DESKTOP_TEMPLATE.format(**values))
269
270 fn = full_dir(self.init_name)
271 with open(fn, 'w+t') as f:
272 if self.ui.rb_docker.isChecked():
273 f.write(INIT_TEMPLATE_DOCKER.format(**values))
274 else:
275 f.write(INIT_TEMPLATE_EXTENSION.format(**values))
276
277
278 # create main package file
279 fn = full_dir(self.package_file)
280
281 if self.ui.rb_docker.isChecked():
282 with open(fn, 'w+t') as f:
283 f.write(DOCKER_TEMPLATE.format(**values))
284 else:
285 # Default to extension type
286 with open(fn, 'w+t') as f:
287 f.write(EXTENSION_TEMPLATE.format(**values))
288
289 # create manual file.
290 fn = full_dir(self.manual_file)
291 with open(fn, 'w+t') as f:
292 f.write(MANUAL_TEMPLATE.format(**values))
293 # enable script in krita settings (!)
294
295 if self.ui.cb_enable_script.isChecked():
296 Application.writeSetting(SCRIPT_SETTINGS, 'enable_'+self.package_name, 'true')
297
298 # notify success
299 # Assemble message
300 title = "Krita Script files created"
301 message = []
302 message.append("<h3>Directory</h3>")
303 message.append("Project files were created in the directory<p>%s"
304 % self.target_directory)
305 message.append(
306 "<h3>Files Created</h3>The following files were created:<p>")
307 for f in self.files:
308 message.append("%s<p>" % f)
309 message.append("%s<p>" % self.manual_file)
310 message.append("<h3>Location of script</h3>")
311 message.append("Open this file to edit your script:<p>")
312 script_path = os.path.join(self.target_directory, self.package_file)
313 message.append("%s<p>" % script_path)
314 message.append("Open this file to edit your Manual:<p>")
315 script_path = os.path.join(self.target_directory, self.manual_file)
316 message.append("%s<p>" % script_path)
317 message.append("<h3>Record these locations</h3>")
318 message.append(
319 "Make a note of these locations before you click ok.<p>")
320 message = "\n".join(message)
321
322 # Display message box
323 if CONTEXT_KRITA:
324 msgbox = QMessageBox()
325 else:
326 msgbox = QMessageBox(self)
327 msgbox.setWindowTitle(title)
328 msgbox.setText(message)
329 msgbox.setStandardButtons(QMessageBox.StandardButton.Ok)
330 msgbox.setDefaultButton(QMessageBox.StandardButton.Ok)
331 msgbox.setIcon(QMessageBox.Icon.Information)
332 msgbox.exec()
333
334 self.ui.close()
335
336 def name_change(self, text):
337 """
338 name of script has changed,
339 * calculate consequential names
340 * update the e_files_display edit
341 """
342
343 text = text.strip()
344 if len(text) == 0:
345 return
346
347 self.calculate_file_names(text)
348 edit_text = self.calculate_edit_text()
349 self.ui.e_files_display.setText(edit_text)
350
351 def calculate_file_names(self, text):
352 # name of class
353
354 spam = text.split(" ")
355 for i, s in enumerate(spam):
356 s = s.strip()
357 s = s.lower()
358 try:
359 spam[i] = s[0].upper()+s[1:]
360 except IndexError:
361 continue
362 self.class_name = "".join(spam)
363
364 # Normalise library name
365 if TESTING:
366 self.package_name = "bbdsss_"+text.lower().replace(" ", "_")
367 else:
368 self.package_name = text.lower().replace(" ", "_")
369
370 # create desktop file
371 self.desktop_fn = self.package_name+'.desktop'
372
373 self.init_name = os.path.join(self.package_name, "__init__.py")
374 self.package_file = os.path.join(
375 self.package_name, self.package_name + ".py")
376 self.manual_file = os.path.join(self.package_name, "Manual.html")
377 self.files = [self.desktop_fn, self.init_name,
378 self.package_name, self.package_file,
379 self.manual_file]
380
382 """
383 Determine if any of the intended files will collide with existing files.
384
385 """
386 conflict_template = "%s - FILE ALREADY EXISTS<p>"
387 non_conflict_template = "%s<p>"
388 file_conflict = False
389
390 html = ["<h3>Will create the following files:</h3>"]
391 for path in self.files:
392 target_file = os.path.join(self.target_directory, path)
393 if os.path.exists(path):
394 html.append(conflict_template % target_file)
395 file_conflict = True
396 else:
397 html.append(non_conflict_template % target_file)
398
399 if file_conflict:
400 html.append("""<h2><span style="color:red;font-weight:bold">
401 Warning:</span></h2><p>
402 <span style="color:red;font-weight:bold">
403 One or more of the files to be created already exists.
404 If you click "Create Script" those files will be deleted
405 and new files created in their place.</span><p>""")
406
407 return "\n".join(html)
408
409
410if __name__ == "__main__":
411 # this includes when the script is run from the command line or
412 # from the Scripter plugin.
413 if CONTEXT_KRITA:
414 # scripter plugin
415 # give up - has the wrong context to create widgets etc.
416 # maybe in the future change this.
417 pass
418 else:
419 app = QApplication([])
420
421 extension = KritaScriptStarter(None)
422 extension.setup()
423 extension.action_triggered()
424 sys.exit(app.exec())
static Krita * instance()
instance retrieve the singleton instance of the Application object.
Definition Krita.cpp:390