Krita Source Code Documentation
Loading...
Searching...
No Matches
photobash_images_docker.py
Go to the documentation of this file.
1# Photobash Images is a Krita plugin to get CC0 images based on a search,
2# straight from the Krita Interface. Useful for textures and concept art!
3# Copyright (C) 2020 Pedro Reis.
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18
19from krita import Krita, DockWidget, FileDialog
20from builtins import Application, i18n, i18nc
21import copy
22import math
23try:
24 from PyQt6 import uic
25 from PyQt6.QtCore import Qt, QDirIterator, QMimeData, QUrl, QStandardPaths
26 from PyQt6.QtWidgets import QWidget, QSizePolicy, QApplication, QMessageBox
27 from PyQt6.QtGui import QImage
28except:
29 from PyQt5 import uic
30 from PyQt5.QtCore import Qt, QDirIterator, QMimeData, QUrl, QStandardPaths
31 from PyQt5.QtWidgets import QWidget, QSizePolicy, QApplication, QMessageBox
32 from PyQt5.QtGui import QImage
33from .photobash_images_modulo import (
34 Photobash_Display,
35 Photobash_Button,
36)
37import os.path
38
39class PhotobashDocker(DockWidget):
40 def __init__(self):
41 super().__init__()
42
43 # Construct
44 self.setupVariables()
45 self.setupInterface()
46 self.setupModules()
47 self.setStyle()
48 self.initialize()
49
50 def setupVariables(self):
51 self.mainWidget = QWidget(self)
52
53 self.applicationName = "Photobash"
54 self.referencesSetting = "referencesDirectory"
55 self.fitCanvasSetting = "fitToCanvas"
56 self.foundFavouritesSetting = "currentFavourites"
57
58 self.currImageScale = 100
59 self.fitCanvasChecked = bool(Application.readSetting(self.applicationName, self.fitCanvasSetting, "True"))
60 self.imagesButtons = []
61 self.foundImages = []
63 # maps path to image
64 self.cachedImages = {}
65 # store order of push
68 self.maxNumPages = 9999
69
70 self.currPage = 0
71 self.directoryPath = Application.readSetting(self.applicationName, self.referencesSetting, "")
72 favouriteImagesValues = Application.readSetting(self.applicationName, self.foundFavouritesSetting, "").split("'")
73
74 for value in favouriteImagesValues:
75 if value != "[" and value != ", " and value != "]" and value != "" and value != "[]":
76 self.favouriteImages.append(value)
77
78 self.bg_alpha = str("background-color: rgba(0, 0, 0, 50); ")
79 self.bg_hover = str("background-color: rgba(0, 0, 0, 100); ")
80
81 def setupInterface(self):
82 # Window
83 self.setWindowTitle(i18nc("@title:window", "Photobash Images"))
84
85 # Path Name
86 self.directoryPlugin = str(os.path.dirname(os.path.realpath(__file__)))
87
88 # Photo Bash Docker
89 self.mainWidget = QWidget(self)
90 self.setWidget(self.mainWidget)
91
92 self.layout = uic.loadUi(self.directoryPlugin + '/photobash_images_docker.ui', self.mainWidget)
93
95 self.layout.imagesButtons0,
96 self.layout.imagesButtons1,
97 self.layout.imagesButtons2,
98 self.layout.imagesButtons3,
99 self.layout.imagesButtons4,
100 self.layout.imagesButtons5,
101 self.layout.imagesButtons6,
102 self.layout.imagesButtons7,
103 self.layout.imagesButtons8,
104 ]
105
106 # Adjust Layouts
107 self.layout.imageWidget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Ignored)
108 self.layout.middleWidget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
109
110 # setup connections for top elements
111 self.layout.filterTextEdit.textChanged.connect(self.textFilterChangedtextFilterChanged)
112 self.layout.changePathButton.clicked.connect(self.changePathchangePath)
113 # setup connections for bottom elements
114 self.layout.previousButton.clicked.connect(lambda: self.updateCurrentPage(-1))
115 self.layout.nextButton.clicked.connect(lambda: self.updateCurrentPage(1))
116 self.layout.scaleSlider.valueChanged.connect(self.updateScaleupdateScale)
117 self.layout.paginationSlider.setMinimum(0)
118 self.layout.paginationSlider.valueChanged.connect(self.updatePageupdatePage)
119 self.layout.fitCanvasCheckBox.stateChanged.connect(self.changedFitCanvaschangedFitCanvas)
120
121 def setupModules(self):
122 # Display Single
123 self.imageWidget = Photobash_Display(self.layout.imageWidget)
124 self.imageWidget.SIGNAL_HOVER.connect(self.cursorHovercursorHover)
125 self.imageWidget.SIGNAL_CLOSE.connect(self.closePreviewclosePreview)
126
127 # Display Grid
128 self.imagesButtons = []
129 for i in range(0, len(self.layoutButtons)):
130 layoutButton = self.layoutButtons[i]
131 imageButton = Photobash_Button(layoutButton)
132 imageButton.setNumber(i)
133 imageButton.SIGNAL_HOVER.connect(self.cursorHovercursorHover)
134 imageButton.SIGNAL_LMB.connect(self.buttonClickbuttonClick)
135 imageButton.SIGNAL_WUP.connect(lambda: self.updateCurrentPage(-1))
136 imageButton.SIGNAL_WDN.connect(lambda: self.updateCurrentPage(1))
137 imageButton.SIGNAL_PREVIEW.connect(self.openPreviewopenPreview)
138 imageButton.SIGNAL_FAVOURITE.connect(self.pinToFavouritespinToFavourites)
139 imageButton.SIGNAL_UN_FAVOURITE.connect(self.unpinFromFavouritesunpinFromFavourites)
140 imageButton.SIGNAL_OPEN_NEW.connect(self.openNewDocumentopenNewDocument)
141 imageButton.SIGNAL_REFERENCE.connect(self.placeReferenceplaceReference)
142 self.imagesButtons.append(imageButton)
143
144 def setStyle(self):
145 # Displays
146 self.cursorHovercursorHover(None)
147
148 def initialize(self):
149 # initialize based on what was setup
150 if self.directoryPath != "":
151 self.layout.changePathButton.setText(i18n("Change References Folder"))
153 self.layout.fitCanvasCheckBox.setChecked(self.fitCanvasChecked)
154
155 # initial organization of images with favourites
156 self.reorganizeImages()
157 self.layout.scaleSliderLabel.setText(i18n("Image Scale: {0}%").format(100))
158
159 self.updateImages()
160
162 # organize images, taking into account favourites
163 # and their respective order
164 favouriteFoundImages = []
165 for image in self.favouriteImages:
166 if image in self.foundImages:
167 self.foundImages.remove(image)
168 favouriteFoundImages.append(image)
169
170 self.foundImages = favouriteFoundImages + self.foundImages
171
173 stringsInText = self.layout.filterTextEdit.text().lower().split(" ")
174 if self.layout.filterTextEdit.text().lower() == "":
175 self.foundImages = copy.deepcopy(self.allImages)
176 self.reorganizeImages()
177 self.updateImages()
178 return
179
180 newImages = []
181 for word in stringsInText:
182 for path in self.allImages:
183 # exclude path outside from search
184 if word in path.replace(self.directoryPath, "").lower() and not path in newImages and word != "" and word != " ":
185 newImages.append(path)
186
187 self.foundImages = newImages
188 self.reorganizeImages()
189 self.updateImages()
190
192 newImages = []
193 self.currPage = 0
194
195 if self.directoryPath == "":
196 self.foundImages = []
197 self.favouriteImages = []
198 self.updateImages()
199 return
200
201 it = QDirIterator(self.directoryPath, QDirIterator.IteratorFlag.Subdirectories)
202
203
204 while(it.hasNext()):
205 if (".webp" in it.filePath() or ".png" in it.filePath() or ".jpg" in it.filePath() or ".jpeg" in it.filePath()) and \
206 (not ".webp~" in it.filePath() and not ".png~" in it.filePath() and not ".jpg~" in it.filePath() and not ".jpeg~" in it.filePath()):
207 newImages.append(it.filePath())
208
209 it.next()
210
211 self.foundImages = copy.deepcopy(newImages)
212 self.allImages = copy.deepcopy(newImages)
213 self.reorganizeImages()
214 self.updateImages()
215
216 def updateCurrentPage(self, increment):
217 if (self.currPage == 0 and increment == -1) or \
218 ((self.currPage + 1) * len(self.imagesButtons) > len(self.foundImages) and increment == 1) or \
219 len(self.foundImages) == 0:
220 return
221
222 self.currPage += increment
223 maxNumPage = math.ceil(len(self.foundImages) / len(self.layoutButtons))
224 self.currPage = max(0, min(self.currPage, maxNumPage - 1))
225 self.updateImages()
226
227 def updateScale(self, value):
228 self.currImageScale = value
229 self.layout.scaleSliderLabel.setText(i18n("Image Scale: {0}%").format(self.currImageScale))
230
231 # update layout buttons, needed when dragging
232 self.imageWidget.setImageScale(self.currImageScale)
233
234 # normal images
235 for i in range(0, len(self.imagesButtons)):
236 self.imagesButtons[i].setImageScale(self.currImageScale)
237
238 def updatePage(self, value):
239 maxNumPage = math.ceil(len(self.foundImages) / len(self.layoutButtons))
240 self.currPage = max(0, min(value, maxNumPage - 1))
241 self.updateImages()
242
243 def changedFitCanvas(self, state):
244 if state == Qt.CheckState.Checked:
245 self.fitCanvasChecked = True
246 Application.writeSetting(self.applicationName, self.fitCanvasSetting, "true")
247 else:
248 self.fitCanvasChecked = False
249 Application.writeSetting(self.applicationName, self.fitCanvasSetting, "false")
250
251 # update layout buttons, needed when dragging
252 self.imageWidget.setFitCanvas(self.fitCanvasChecked)
253
254 # normal images
255 for i in range(0, len(self.imagesButtons)):
256 self.imagesButtons[i].setFitCanvas(self.fitCanvasChecked)
257
258 def cursorHover(self, SIGNAL_HOVER):
259 # Display Image
260 self.layout.imageWidget.setStyleSheet(self.bg_alpha)
261 if SIGNAL_HOVER == "D":
262 self.layout.imageWidget.setStyleSheet(self.bg_hover)
263
264 # normal images
265 for i in range(0, len(self.layoutButtons)):
266 self.layoutButtons[i].setStyleSheet(self.bg_alpha)
267
268 if SIGNAL_HOVER == str(i):
269 self.layoutButtons[i].setStyleSheet(self.bg_hover)
270
271 # checks if image is cached, and if it isn't, create it and cache it
272 def getImage(self, path):
273 if path in self.cachedPathImages:
274 return self.cachedImages[path]
275
276 # need to remove from cache
277 if len(self.cachedImages) > self.maxCachedImages:
278 removedPath = self.cachedPathImages.pop()
279 self.cachedImages.pop(removedPath)
280
281 self.cachedPathImages = [path] + self.cachedPathImages
282 self.cachedImages[path] = QImage(path).scaled(200, 200, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.FastTransformation)
283
284 return self.cachedImages[path]
285
286 # makes sure the first 9 found images exist
288 found = 0
289 for path in self.foundImages:
290 if found == 9:
291 return
292
293 if self.checkPath(path):
294 found = found + 1
295
296 def updateImages(self):
297 self.checkValidImages()
298 buttonsSize = len(self.imagesButtons)
299
300 # don't try to access image that isn't there
301 maxRange = min(len(self.foundImages) - self.currPage * buttonsSize, buttonsSize)
302
303 for i in range(0, len(self.imagesButtons)):
304 if i < maxRange:
305 # image is within valid range, apply it
306 path = self.foundImages[i + buttonsSize * self.currPage]
307 self.imagesButtons[i].setFavourite(path in self.favouriteImages)
308 self.imagesButtons[i].setImage(path, self.getImage(path))
309 else:
310 # image is outside the range
311 self.imagesButtons[i].setFavourite(False)
312 self.imagesButtons[i].setImage("",None)
313
314 # update text for pagination
315 maxNumPage = math.ceil(len(self.foundImages) / len(self.layoutButtons))
316 currPage = self.currPage + 1
317
318 if maxNumPage == 0:
319 currPage = 0
320
321 # normalize string length
322 if currPage < 10:
323 currPage = " " + str(currPage)
324 elif currPage < 100:
325 currPage = " " + str(currPage)
326 elif currPage < 1000:
327 currPage = " " + str(currPage)
328
329 # currPage is the index, but we want to present it in a user friendly way,
330 # so it starts at 1
331 self.layout.paginationLabel.setText(i18n("Page: {0}/{1}").format(currPage, maxNumPage))
332 # correction since array begins at 0
333 self.layout.paginationSlider.setRange(0, maxNumPage - 1)
334 self.layout.paginationSlider.setSliderPosition(self.currPage)
335
336 def addImageLayer(self, photoPath):
337 # file no longer exists, remove from all structures
338 if not self.checkPath(photoPath):
339 self.updateImages()
340 return
341
342 # Get the document:
343 doc = Krita.instance().activeDocument()
344
345 # Saving a non-existent document causes crashes, so lets check for that first.
346 if doc is None:
347 return
348
349 # Check if there is a valid Canvas to place the Image
350 if self.canvas() is None or self.canvas().view() is None:
351 return
352
353 scale = self.currImageScale / 100
354
355 # Scale Image
356 if self.fitCanvasChecked:
357 image = QImage(photoPath).scaled(int(doc.width() * scale), int(doc.height() * scale), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
358 else:
359 image = QImage(photoPath)
360 # scale image
361 image = image.scaled(int(image.width() * scale), int(image.height() * scale), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
362
363 # MimeData
364 mimedata = QMimeData()
365 url = QUrl().fromLocalFile(photoPath)
366 mimedata.setUrls([url])
367 mimedata.setImageData(image)
368
369 # Set image in clipboard
370 QApplication.clipboard().setImage(image)
371
372 # Place Image and Refresh Canvas
373 Krita.instance().action('edit_paste').trigger()
374 Krita.instance().activeDocument().refreshProjection()
375
376 def checkPath(self, path):
377 if not os.path.isfile(path):
378 if path in self.foundImages:
379 self.foundImages.remove(path)
380 if path in self.allImages:
381 self.allImages.remove(path)
382 if path in self.favouriteImages:
383 self.favouriteImages.remove(path)
384
385 dlg = QMessageBox(self)
386 dlg.setWindowTitle("Missing Image!")
387 dlg.setText("This image you tried to open was not found. Removing from the list.")
388 dlg.exec()
389
390 return False
391
392 return True
393
394 def openNewDocument(self, path):
395 if not self.checkPath(path):
396 self.updateImages()
397 return
398
399 document = Krita.instance().openDocument(path)
400 Application.activeWindow().addView(document)
401
402 def placeReference(self, path):
403 if not self.checkPath(path):
404 self.updateImages()
405 return
406
407 # MimeData
408 mimedata = QMimeData()
409 url = QUrl().fromLocalFile(path)
410 mimedata.setUrls([url])
411 image = QImage(path)
412 mimedata.setImageData(image)
413
414 QApplication.clipboard().setImage(image)
415 Krita.instance().action('paste_as_reference').trigger()
416
417 def openPreview(self, path):
418 self.imageWidget.setImage(path, self.getImage(path))
419 self.layout.imageWidget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
420 self.layout.middleWidget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Ignored)
421
422 def closePreview(self):
423 self.layout.imageWidget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Ignored)
424 self.layout.middleWidget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
425
426 def pinToFavourites(self, path):
427 self.currPage = 0
428 self.favouriteImages = [path] + self.favouriteImages
429
430 # save setting for next restart
431 Application.writeSetting(self.applicationName, self.foundFavouritesSetting, str(self.favouriteImages))
432 self.reorganizeImages()
433 self.updateImages()
434
435 def unpinFromFavourites(self, path):
436 if path in self.favouriteImages:
437 self.favouriteImages.remove(path)
438
439 Application.writeSetting(self.applicationName, self.foundFavouritesSetting, str(self.favouriteImages))
440
441 # resets order to the default, but checks if foundImages is only a subset
442 # in case it is searching
443 orderedImages = []
444 for image in self.allImages:
445 if image in self.foundImages:
446 orderedImages.append(image)
447
448 self.foundImages = orderedImages
449 self.reorganizeImages()
450 self.updateImages()
451
452 def leaveEvent(self, event):
453 self.layout.filterTextEdit.clearFocus()
454
455 def canvasChanged(self, canvas):
456 pass
457
458 def buttonClick(self, position):
459 if position < len(self.foundImages) - len(self.imagesButtons) * self.currPage:
460 self.addImageLayer(self.foundImages[position + len(self.imagesButtons) * self.currPage])
461
462 def changePath(self):
463 if self.directoryPath == "":
464 dialogDirectory = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.PicturesLocation)
465 else:
466 dialogDirectory = self.directoryPath
467 self.directoryPath = FileDialog.getExistingDirectory(self.mainWidget, i18n("Change Directory for Images"), dialogDirectory)
468 if not self.directoryPath: return
469 Application.writeSetting(self.applicationName, self.referencesSetting, self.directoryPath)
470
471 self.favouriteImages = []
472 self.foundImages = []
473
474 Application.writeSetting(self.applicationName, self.foundFavouritesSetting, "")
475
476 if self.directoryPath == "":
477 self.layout.changePathButton.setText(i18n("Set References Folder"))
478 else:
479 self.layout.changePathButton.setText(i18n("Change References Folder"))
static QString getExistingDirectory(QWidget *parent=nullptr, const QString &caption=QString(), const QString &directory=QString(), const QString &dialogName=QString())
Create and show a file dialog and return the name of an existing directory selected by the user.
static Krita * instance()
instance retrieve the singleton instance of the Application object.
Definition Krita.cpp:390