File indexing completed on 2024-06-23 04:28:03
0001 # Photobash Images is a Krita plugin to get CC0 images based on a search, 0002 # straight from the Krita Interface. Useful for textures and concept art! 0003 # Copyright (C) 2020 Pedro Reis. 0004 # 0005 # This program is free software: you can redistribute it and/or modify 0006 # it under the terms of the GNU General Public License as published by 0007 # the Free Software Foundation, either version 3 of the License, or 0008 # (at your option) any later version. 0009 # 0010 # This program is distributed in the hope that it will be useful, 0011 # but WITHOUT ANY WARRANTY; without even the implied warranty of 0012 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0013 # GNU General Public License for more details. 0014 # 0015 # You should have received a copy of the GNU General Public License 0016 # along with this program. If not, see <http://www.gnu.org/licenses/>. 0017 0018 0019 from krita import * 0020 import copy 0021 import math 0022 from PyQt5 import QtWidgets, QtCore, uic 0023 from .photobash_images_modulo import ( 0024 Photobash_Display, 0025 Photobash_Button, 0026 ) 0027 import os.path 0028 0029 class PhotobashDocker(DockWidget): 0030 def __init__(self): 0031 super().__init__() 0032 0033 # Construct 0034 self.setupVariables() 0035 self.setupInterface() 0036 self.setupModules() 0037 self.setStyle() 0038 self.initialize() 0039 0040 def setupVariables(self): 0041 self.mainWidget = QWidget(self) 0042 0043 self.applicationName = "Photobash" 0044 self.referencesSetting = "referencesDirectory" 0045 self.fitCanvasSetting = "fitToCanvas" 0046 self.foundFavouritesSetting = "currentFavourites" 0047 0048 self.currImageScale = 100 0049 self.fitCanvasChecked = bool(Application.readSetting(self.applicationName, self.fitCanvasSetting, "True")) 0050 self.imagesButtons = [] 0051 self.foundImages = [] 0052 self.favouriteImages = [] 0053 # maps path to image 0054 self.cachedImages = {} 0055 # store order of push 0056 self.cachedPathImages = [] 0057 self.maxCachedImages = 90 0058 self.maxNumPages = 9999 0059 0060 self.currPage = 0 0061 self.directoryPath = Application.readSetting(self.applicationName, self.referencesSetting, "") 0062 favouriteImagesValues = Application.readSetting(self.applicationName, self.foundFavouritesSetting, "").split("'") 0063 0064 for value in favouriteImagesValues: 0065 if value != "[" and value != ", " and value != "]" and value != "" and value != "[]": 0066 self.favouriteImages.append(value) 0067 0068 self.bg_alpha = str("background-color: rgba(0, 0, 0, 50); ") 0069 self.bg_hover = str("background-color: rgba(0, 0, 0, 100); ") 0070 0071 def setupInterface(self): 0072 # Window 0073 self.setWindowTitle(i18nc("@title:window", "Photobash Images")) 0074 0075 # Path Name 0076 self.directoryPlugin = str(os.path.dirname(os.path.realpath(__file__))) 0077 0078 # Photo Bash Docker 0079 self.mainWidget = QWidget(self) 0080 self.setWidget(self.mainWidget) 0081 0082 self.layout = uic.loadUi(self.directoryPlugin + '/photobash_images_docker.ui', self.mainWidget) 0083 0084 self.layoutButtons = [ 0085 self.layout.imagesButtons0, 0086 self.layout.imagesButtons1, 0087 self.layout.imagesButtons2, 0088 self.layout.imagesButtons3, 0089 self.layout.imagesButtons4, 0090 self.layout.imagesButtons5, 0091 self.layout.imagesButtons6, 0092 self.layout.imagesButtons7, 0093 self.layout.imagesButtons8, 0094 ] 0095 0096 # Adjust Layouts 0097 self.layout.imageWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) 0098 self.layout.middleWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 0099 0100 # setup connections for top elements 0101 self.layout.filterTextEdit.textChanged.connect(self.textFilterChanged) 0102 self.layout.changePathButton.clicked.connect(self.changePath) 0103 # setup connections for bottom elements 0104 self.layout.previousButton.clicked.connect(lambda: self.updateCurrentPage(-1)) 0105 self.layout.nextButton.clicked.connect(lambda: self.updateCurrentPage(1)) 0106 self.layout.scaleSlider.valueChanged.connect(self.updateScale) 0107 self.layout.paginationSlider.setMinimum(0) 0108 self.layout.paginationSlider.valueChanged.connect(self.updatePage) 0109 self.layout.fitCanvasCheckBox.stateChanged.connect(self.changedFitCanvas) 0110 0111 def setupModules(self): 0112 # Display Single 0113 self.imageWidget = Photobash_Display(self.layout.imageWidget) 0114 self.imageWidget.SIGNAL_HOVER.connect(self.cursorHover) 0115 self.imageWidget.SIGNAL_CLOSE.connect(self.closePreview) 0116 0117 # Display Grid 0118 self.imagesButtons = [] 0119 for i in range(0, len(self.layoutButtons)): 0120 layoutButton = self.layoutButtons[i] 0121 imageButton = Photobash_Button(layoutButton) 0122 imageButton.setNumber(i) 0123 imageButton.SIGNAL_HOVER.connect(self.cursorHover) 0124 imageButton.SIGNAL_LMB.connect(self.buttonClick) 0125 imageButton.SIGNAL_WUP.connect(lambda: self.updateCurrentPage(-1)) 0126 imageButton.SIGNAL_WDN.connect(lambda: self.updateCurrentPage(1)) 0127 imageButton.SIGNAL_PREVIEW.connect(self.openPreview) 0128 imageButton.SIGNAL_FAVOURITE.connect(self.pinToFavourites) 0129 imageButton.SIGNAL_UN_FAVOURITE.connect(self.unpinFromFavourites) 0130 imageButton.SIGNAL_OPEN_NEW.connect(self.openNewDocument) 0131 imageButton.SIGNAL_REFERENCE.connect(self.placeReference) 0132 self.imagesButtons.append(imageButton) 0133 0134 def setStyle(self): 0135 # Displays 0136 self.cursorHover(None) 0137 0138 def initialize(self): 0139 # initialize based on what was setup 0140 if self.directoryPath != "": 0141 self.layout.changePathButton.setText(i18n("Change References Folder")) 0142 self.getImagesFromDirectory() 0143 self.layout.fitCanvasCheckBox.setChecked(self.fitCanvasChecked) 0144 0145 # initial organization of images with favourites 0146 self.reorganizeImages() 0147 self.layout.scaleSliderLabel.setText(i18n("Image Scale: {0}%").format(100)) 0148 0149 self.updateImages() 0150 0151 def reorganizeImages(self): 0152 # organize images, taking into account favourites 0153 # and their respective order 0154 favouriteFoundImages = [] 0155 for image in self.favouriteImages: 0156 if image in self.foundImages: 0157 self.foundImages.remove(image) 0158 favouriteFoundImages.append(image) 0159 0160 self.foundImages = favouriteFoundImages + self.foundImages 0161 0162 def textFilterChanged(self): 0163 stringsInText = self.layout.filterTextEdit.text().lower().split(" ") 0164 if self.layout.filterTextEdit.text().lower() == "": 0165 self.foundImages = copy.deepcopy(self.allImages) 0166 self.reorganizeImages() 0167 self.updateImages() 0168 return 0169 0170 newImages = [] 0171 for word in stringsInText: 0172 for path in self.allImages: 0173 # exclude path outside from search 0174 if word in path.replace(self.directoryPath, "").lower() and not path in newImages and word != "" and word != " ": 0175 newImages.append(path) 0176 0177 self.foundImages = newImages 0178 self.reorganizeImages() 0179 self.updateImages() 0180 0181 def getImagesFromDirectory(self): 0182 newImages = [] 0183 self.currPage = 0 0184 0185 if self.directoryPath == "": 0186 self.foundImages = [] 0187 self.favouriteImages = [] 0188 self.updateImages() 0189 return 0190 0191 it = QDirIterator(self.directoryPath, QDirIterator.Subdirectories) 0192 0193 0194 while(it.hasNext()): 0195 if (".webp" in it.filePath() or ".png" in it.filePath() or ".jpg" in it.filePath() or ".jpeg" in it.filePath()) and \ 0196 (not ".webp~" in it.filePath() and not ".png~" in it.filePath() and not ".jpg~" in it.filePath() and not ".jpeg~" in it.filePath()): 0197 newImages.append(it.filePath()) 0198 0199 it.next() 0200 0201 self.foundImages = copy.deepcopy(newImages) 0202 self.allImages = copy.deepcopy(newImages) 0203 self.reorganizeImages() 0204 self.updateImages() 0205 0206 def updateCurrentPage(self, increment): 0207 if (self.currPage == 0 and increment == -1) or \ 0208 ((self.currPage + 1) * len(self.imagesButtons) > len(self.foundImages) and increment == 1) or \ 0209 len(self.foundImages) == 0: 0210 return 0211 0212 self.currPage += increment 0213 maxNumPage = math.ceil(len(self.foundImages) / len(self.layoutButtons)) 0214 self.currPage = max(0, min(self.currPage, maxNumPage - 1)) 0215 self.updateImages() 0216 0217 def updateScale(self, value): 0218 self.currImageScale = value 0219 self.layout.scaleSliderLabel.setText(i18n("Image Scale: {0}%").format(self.currImageScale)) 0220 0221 # update layout buttons, needed when dragging 0222 self.imageWidget.setImageScale(self.currImageScale) 0223 0224 # normal images 0225 for i in range(0, len(self.imagesButtons)): 0226 self.imagesButtons[i].setImageScale(self.currImageScale) 0227 0228 def updatePage(self, value): 0229 maxNumPage = math.ceil(len(self.foundImages) / len(self.layoutButtons)) 0230 self.currPage = max(0, min(value, maxNumPage - 1)) 0231 self.updateImages() 0232 0233 def changedFitCanvas(self, state): 0234 if state == Qt.Checked: 0235 self.fitCanvasChecked = True 0236 Application.writeSetting(self.applicationName, self.fitCanvasSetting, "true") 0237 else: 0238 self.fitCanvasChecked = False 0239 Application.writeSetting(self.applicationName, self.fitCanvasSetting, "false") 0240 0241 # update layout buttons, needed when dragging 0242 self.imageWidget.setFitCanvas(self.fitCanvasChecked) 0243 0244 # normal images 0245 for i in range(0, len(self.imagesButtons)): 0246 self.imagesButtons[i].setFitCanvas(self.fitCanvasChecked) 0247 0248 def cursorHover(self, SIGNAL_HOVER): 0249 # Display Image 0250 self.layout.imageWidget.setStyleSheet(self.bg_alpha) 0251 if SIGNAL_HOVER == "D": 0252 self.layout.imageWidget.setStyleSheet(self.bg_hover) 0253 0254 # normal images 0255 for i in range(0, len(self.layoutButtons)): 0256 self.layoutButtons[i].setStyleSheet(self.bg_alpha) 0257 0258 if SIGNAL_HOVER == str(i): 0259 self.layoutButtons[i].setStyleSheet(self.bg_hover) 0260 0261 # checks if image is cached, and if it isn't, create it and cache it 0262 def getImage(self, path): 0263 if path in self.cachedPathImages: 0264 return self.cachedImages[path] 0265 0266 # need to remove from cache 0267 if len(self.cachedImages) > self.maxCachedImages: 0268 removedPath = self.cachedPathImages.pop() 0269 self.cachedImages.pop(removedPath) 0270 0271 self.cachedPathImages = [path] + self.cachedPathImages 0272 self.cachedImages[path] = QImage(path).scaled(200, 200, Qt.KeepAspectRatio, Qt.FastTransformation) 0273 0274 return self.cachedImages[path] 0275 0276 # makes sure the first 9 found images exist 0277 def checkValidImages(self): 0278 found = 0 0279 for path in self.foundImages: 0280 if found == 9: 0281 return 0282 0283 if self.checkPath(path): 0284 found = found + 1 0285 0286 def updateImages(self): 0287 self.checkValidImages() 0288 buttonsSize = len(self.imagesButtons) 0289 0290 # don't try to access image that isn't there 0291 maxRange = min(len(self.foundImages) - self.currPage * buttonsSize, buttonsSize) 0292 0293 for i in range(0, len(self.imagesButtons)): 0294 if i < maxRange: 0295 # image is within valid range, apply it 0296 path = self.foundImages[i + buttonsSize * self.currPage] 0297 self.imagesButtons[i].setFavourite(path in self.favouriteImages) 0298 self.imagesButtons[i].setImage(path, self.getImage(path)) 0299 else: 0300 # image is outside the range 0301 self.imagesButtons[i].setFavourite(False) 0302 self.imagesButtons[i].setImage("",None) 0303 0304 # update text for pagination 0305 maxNumPage = math.ceil(len(self.foundImages) / len(self.layoutButtons)) 0306 currPage = self.currPage + 1 0307 0308 if maxNumPage == 0: 0309 currPage = 0 0310 0311 # normalize string length 0312 if currPage < 10: 0313 currPage = " " + str(currPage) 0314 elif currPage < 100: 0315 currPage = " " + str(currPage) 0316 elif currPage < 1000: 0317 currPage = " " + str(currPage) 0318 0319 # currPage is the index, but we want to present it in a user friendly way, 0320 # so it starts at 1 0321 self.layout.paginationLabel.setText(i18n("Page: {0}/{1}").format(currPage, maxNumPage)) 0322 # correction since array begins at 0 0323 self.layout.paginationSlider.setRange(0, maxNumPage - 1) 0324 self.layout.paginationSlider.setSliderPosition(self.currPage) 0325 0326 def addImageLayer(self, photoPath): 0327 # file no longer exists, remove from all structures 0328 if not self.checkPath(photoPath): 0329 self.updateImages() 0330 return 0331 0332 # Get the document: 0333 doc = Krita.instance().activeDocument() 0334 0335 # Saving a non-existent document causes crashes, so lets check for that first. 0336 if doc is None: 0337 return 0338 0339 # Check if there is a valid Canvas to place the Image 0340 if self.canvas() is None or self.canvas().view() is None: 0341 return 0342 0343 scale = self.currImageScale / 100 0344 0345 # Scale Image 0346 if self.fitCanvasChecked: 0347 image = QImage(photoPath).scaled(doc.width() * scale, doc.height() * scale, Qt.KeepAspectRatio, Qt.SmoothTransformation) 0348 else: 0349 image = QImage(photoPath) 0350 # scale image 0351 image = image.scaled(image.width() * scale, image.height() * scale, Qt.KeepAspectRatio, Qt.SmoothTransformation) 0352 0353 # MimeData 0354 mimedata = QMimeData() 0355 url = QUrl().fromLocalFile(photoPath) 0356 mimedata.setUrls([url]) 0357 mimedata.setImageData(image) 0358 0359 # Set image in clipboard 0360 QApplication.clipboard().setImage(image) 0361 0362 # Place Image and Refresh Canvas 0363 Krita.instance().action('edit_paste').trigger() 0364 Krita.instance().activeDocument().refreshProjection() 0365 0366 def checkPath(self, path): 0367 if not os.path.isfile(path): 0368 if path in self.foundImages: 0369 self.foundImages.remove(path) 0370 if path in self.allImages: 0371 self.allImages.remove(path) 0372 if path in self.favouriteImages: 0373 self.favouriteImages.remove(path) 0374 0375 dlg = QMessageBox(self) 0376 dlg.setWindowTitle("Missing Image!") 0377 dlg.setText("This image you tried to open was not found. Removing from the list.") 0378 dlg.exec() 0379 0380 return False 0381 0382 return True 0383 0384 def openNewDocument(self, path): 0385 if not self.checkPath(path): 0386 self.updateImages() 0387 return 0388 0389 document = Krita.instance().openDocument(path) 0390 Application.activeWindow().addView(document) 0391 0392 def placeReference(self, path): 0393 if not self.checkPath(path): 0394 self.updateImages() 0395 return 0396 0397 # MimeData 0398 mimedata = QMimeData() 0399 url = QUrl().fromLocalFile(path) 0400 mimedata.setUrls([url]) 0401 image = QImage(path) 0402 mimedata.setImageData(image) 0403 0404 QApplication.clipboard().setImage(image) 0405 Krita.instance().action('paste_as_reference').trigger() 0406 0407 def openPreview(self, path): 0408 self.imageWidget.setImage(path, self.getImage(path)) 0409 self.layout.imageWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 0410 self.layout.middleWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) 0411 0412 def closePreview(self): 0413 self.layout.imageWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) 0414 self.layout.middleWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 0415 0416 def pinToFavourites(self, path): 0417 self.currPage = 0 0418 self.favouriteImages = [path] + self.favouriteImages 0419 0420 # save setting for next restart 0421 Application.writeSetting(self.applicationName, self.foundFavouritesSetting, str(self.favouriteImages)) 0422 self.reorganizeImages() 0423 self.updateImages() 0424 0425 def unpinFromFavourites(self, path): 0426 if path in self.favouriteImages: 0427 self.favouriteImages.remove(path) 0428 0429 Application.writeSetting(self.applicationName, self.foundFavouritesSetting, str(self.favouriteImages)) 0430 0431 # resets order to the default, but checks if foundImages is only a subset 0432 # in case it is searching 0433 orderedImages = [] 0434 for image in self.allImages: 0435 if image in self.foundImages: 0436 orderedImages.append(image) 0437 0438 self.foundImages = orderedImages 0439 self.reorganizeImages() 0440 self.updateImages() 0441 0442 def leaveEvent(self, event): 0443 self.layout.filterTextEdit.clearFocus() 0444 0445 def canvasChanged(self, canvas): 0446 pass 0447 0448 def buttonClick(self, position): 0449 if position < len(self.foundImages) - len(self.imagesButtons) * self.currPage: 0450 self.addImageLayer(self.foundImages[position + len(self.imagesButtons) * self.currPage]) 0451 0452 def changePath(self): 0453 fileDialog = QFileDialog(QWidget(self)); 0454 fileDialog.setFileMode(QFileDialog.DirectoryOnly); 0455 0456 if self.directoryPath == "": 0457 dialogDirectory = QStandardPaths.writableLocation(QStandardPaths.PicturesLocation) 0458 else: 0459 dialogDirectory = self.directoryPath 0460 self.directoryPath = fileDialog.getExistingDirectory(self.mainWidget, i18n("Change Directory for Images"), dialogDirectory) 0461 Application.writeSetting(self.applicationName, self.referencesSetting, self.directoryPath) 0462 0463 self.favouriteImages = [] 0464 self.foundImages = [] 0465 0466 Application.writeSetting(self.applicationName, self.foundFavouritesSetting, "") 0467 0468 if self.directoryPath == "": 0469 self.layout.changePathButton.setText(i18n("Set References Folder")) 0470 else: 0471 self.layout.changePathButton.setText(i18n("Change References Folder")) 0472 self.getImagesFromDirectory()