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()