From 884b2b84c015549e23b2edf798805b72c2dddb99 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Mon, 14 Nov 2022 12:16:21 -0800 Subject: [PATCH 001/137] Add missing TOML requirement --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e1de2d8..30411a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ tesserocr~=2.5.1 manga-ocr~=0.1.5 pyqtkeybind~=0.0.9 rarfile~=4.0 -pdf2image~=1.16.0 \ No newline at end of file +pdf2image~=1.16.0 +toml~=0.10.2 From 94ab50e941d6f269a83a4073c99801f245b22012 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Thu, 22 Dec 2022 14:02:33 +0800 Subject: [PATCH 002/137] Update text preview behavior to remain on screen --- code/Popups.py | 16 ++++++++++++++-- code/Views.py | 6 +++++- code/utils/config.toml | 1 + 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/code/Popups.py b/code/Popups.py index 29024ce..37cbf44 100644 --- a/code/Popups.py +++ b/code/Popups.py @@ -123,10 +123,18 @@ def __init__(self, parent, tracker): self.pickBot.setCurrentIndex(config["SELECTED_INDEX"]["fontSize"]) self.nameBot.setText("Font Size: ") - self.fontStyleText = " font-family: 'Poppins';\n" - self.fontSizeText = " font-size: 16pt;\n" + self.persistText = QComboBox() + self.persistText.addItems(["Disabled", "Enabled"]) + self.layout.addWidget(QLabel("Persist Text: "), 2, 0) + self.layout.addWidget(self.persistText, 2, 1) + self.persistText.setCurrentIndex(config["PERSIST_TEXT_MODE"]) + self.persistText.currentIndexChanged.connect(self.changePersistText) + + self.fontStyleText = f" font-family: '{self.pickTop.currentText().strip()}';\n" + self.fontSizeText = f" font-size: {self.pickBot.currentText().strip()}pt;\n" self.fontStyleIndex = self.pickTop.currentIndex() self.fontSizeIndex = self.pickBot.currentIndex() + self.persistTextIndex = self.persistText.currentIndex() def changeFontStyle(self, i): self.fontStyleIndex = i @@ -139,11 +147,15 @@ def changeFontSize(self, i): selectedFontSize = int(self.pickBot.currentText().strip()) replacementText = f" font-size: {selectedFontSize}pt;\n" self.fontSizeText = replacementText + + def changePersistText(self, i): + self.persistTextIndex = i def applyChanges(self): self.applySelections(['fontStyle', 'fontSize']) editStylesheet(41, self.fontStyleText) editStylesheet(42, self.fontSizeText) + self.parent.config["PERSIST_TEXT_MODE"] = self.persistTextIndex return True diff --git a/code/Views.py b/code/Views.py index bef47b1..3fcb1f1 100644 --- a/code/Views.py +++ b/code/Views.py @@ -64,7 +64,11 @@ def mouseReleaseEvent(self, event): logToFile = self.tracker.writeMode text = self.canvasText.text() logText(text, mode=logToFile, path=logPath) - self.canvasText.hide() + try: + if not self.parent.config["PERSIST_TEXT_MODE"]: + self.canvasText.hide() + except AttributeError: + pass super().mouseReleaseEvent(event) @pyqtSlot() diff --git a/code/utils/config.toml b/code/utils/config.toml index 7c21217..abdadde 100644 --- a/code/utils/config.toml +++ b/code/utils/config.toml @@ -46,6 +46,7 @@ NAV_VIEW_RATIO = [ 3, 11,] # Mode VIEW_IMAGE_MODE = 0 SPLIT_VIEW_MODE = false +PERSIST_TEXT_MODE = 1 [SELECTED_INDEX] language = 0 From 4f81a4b677b6aad1941013c459de11b02093de83 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Thu, 22 Dec 2022 14:12:37 +0800 Subject: [PATCH 003/137] Set split view direction to RTL --- code/Trackers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/Trackers.py b/code/Trackers.py index aebbee7..4c449d3 100644 --- a/code/Trackers.py +++ b/code/Trackers.py @@ -47,7 +47,7 @@ def __init__(self, filename=config["HOME_IMAGE"], filenext=config["ABOUT_IMAGE"] self._ocrModel = None def twoFileToImage(self, fileLeft, fileRight): - imageLeft, imageRight = PImage(fileLeft), PImage(fileRight) + imageLeft, imageRight = PImage(fileRight), PImage(fileLeft) if not (imageLeft.isValid()): return From 7970360816515cddc1412c9642addbda6d36d888 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Thu, 22 Dec 2022 14:43:31 +0800 Subject: [PATCH 004/137] Add button to hide file explorer --- code/MainWindow.py | 3 +++ code/assets/images/icons/hideExplorer.png | Bin 0 -> 670 bytes code/utils/config.toml | 9 +++++++++ 3 files changed, 12 insertions(+) create mode 100644 code/assets/images/icons/hideExplorer.png diff --git a/code/MainWindow.py b/code/MainWindow.py index 75d4514..0f1c472 100644 --- a/code/MainWindow.py +++ b/code/MainWindow.py @@ -203,6 +203,9 @@ def scaleImage(self): confirmation = PickerPopup(ScaleImagePicker(self, self.tracker)) confirmation.exec() + def hideExplorer(self): + self.explorer.setVisible(not self.explorer.isVisible()) + # ----------------------------- Control Functions ---------------------------- # def toggleMouseMode(self): diff --git a/code/assets/images/icons/hideExplorer.png b/code/assets/images/icons/hideExplorer.png new file mode 100644 index 0000000000000000000000000000000000000000..9a7d5a6cdc43c9a24d56efada6b5a193835519af GIT binary patch literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|Qfl zr;B4q#hkZueDkh22)NDWE@09-;2)OrfG@|AlY2=gOUB6mCZIuYn5(Kai@Jyd%x9hig`F+nUf5SVq;m0rZ zKK1<7d12dIThIJEf6Gfgne6L{pJZ0rqpEHC^J3?vFB>%@r_ars=Ayo0?k=GqJ@?YN zJ$`-9y7^V5@1}{)ol&+-YlqfN35&Ni_w%;TKhEVD&pzq1>!qh1g?E(`!Z%%9#2sM{Q5`KVLg0 zlf@0TC*2#;gf2|}^Odt>2j}dY#-8?7b>a5AYF>4|KFT@cx5TY4=l}fgn$+9FG^=w` zKf_(!183xTbgVgMEM~g%*OaO0JHvbrr;~2SY!kdVpSG~J+-cbxe`wb`#){n6*;(7?oUXHo+LE9VOpoAXt^YFnm7V|H9bfggx@2b`-xaC5p!8#? nZ1+stu5;Rm_-0^mu(bTgaCp9zPj$M(B#?xstDnm{r-UW|&F2pl literal 0 HcmV?d00001 diff --git a/code/utils/config.toml b/code/utils/config.toml index 7c21217..1e4fa94 100644 --- a/code/utils/config.toml +++ b/code/utils/config.toml @@ -163,6 +163,15 @@ navClicked = "viewImageFromExplorer" iconH = 1.0 iconW = 1.0 + [TBAR_FUNCS.VIEW.hideExplorer] + helpTitle = "Hide explorer" + helpMsg = "Hide the file explorer from view" + path = "hideExplorer.png" + toggle = true + align = "AlignLeft" + iconH = 1.0 + iconW = 1.0 + [TBAR_FUNCS.VIEW.modifyFontSettings] helpTitle = "Modify preview text" helpMsg = "Change font style and font size of preview text." From 7bfbe876bae121d3fc9922df69952e488b1474aa Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Thu, 22 Dec 2022 15:18:26 +0800 Subject: [PATCH 005/137] Implement splitter to adjust explorer width --- code/MainWindow.py | 22 ++++++++++++++-------- code/utils/config.toml | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/code/MainWindow.py b/code/MainWindow.py index 0f1c472..da62735 100644 --- a/code/MainWindow.py +++ b/code/MainWindow.py @@ -23,8 +23,8 @@ import toml from manga_ocr import MangaOcr from PyQt5.QtCore import (Qt, QAbstractNativeEventFilter, QThreadPool) -from PyQt5.QtWidgets import (QHBoxLayout, QVBoxLayout, QWidget, - QPushButton, QFileDialog, QInputDialog, QMainWindow, QApplication) +from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QMainWindow, QApplication, + QPushButton, QFileDialog, QInputDialog, QSplitter) from utils.image_io import mangaFileToImageDir from utils.config import config, saveOnClose @@ -59,13 +59,14 @@ def __init__(self, parent=None, tracker=None): self.canvas = OCRCanvas(self, self.tracker) self.explorer = ImageExplorer(self, self.tracker) - _viewWidget = QWidget() - hLayout = QHBoxLayout(_viewWidget) - hLayout.addWidget(self.explorer, config["NAV_VIEW_RATIO"][0]) - hLayout.addWidget(self.canvas, config["NAV_VIEW_RATIO"][1]) - hLayout.setContentsMargins(0, 0, 0, 0) + self.splitter = QSplitter() + self.splitter.addWidget(self.explorer) + self.splitter.addWidget(self.canvas) + self.splitter.setChildrenCollapsible(False) + for i, s in enumerate(config["NAV_VIEW_RATIO"]): + self.splitter.setStretchFactor(i, s) - self.vLayout.addWidget(_viewWidget) + self.vLayout.addWidget(self.splitter) _mainWidget = QWidget() _mainWidget.setLayout(self.vLayout) self.setCentralWidget(_mainWidget) @@ -85,6 +86,11 @@ def viewImageFromExplorer(self, filename, filenext): self.canvas.viewImage() return True + def resizeEvent(self, event): + self.explorer.setMinimumWidth(0.1*self.width()) + self.explorer.setMaximumWidth(0.3*self.width()) + return super().resizeEvent(event) + def closeEvent(self, event): try: rmtree("./poricom_cache") diff --git a/code/utils/config.toml b/code/utils/config.toml index 1e4fa94..002812b 100644 --- a/code/utils/config.toml +++ b/code/utils/config.toml @@ -41,7 +41,7 @@ TBAR_ICON_DEFAULT = "./assets/images/icons/default_icon.png" RBN_HEIGHT = 2.4 TBAR_ISIZE_REL = 0.1 TBAR_ISIZE_MARGIN = 1.3 -NAV_VIEW_RATIO = [ 3, 11,] +NAV_VIEW_RATIO = [ 1, 9,] # Mode VIEW_IMAGE_MODE = 0 From 237f83c253129365bf498134f8c8669cb2174aeb Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Thu, 22 Dec 2022 22:17:39 +0800 Subject: [PATCH 006/137] Set file dialog to remember last path --- code/MainWindow.py | 16 +++++++++++----- code/Trackers.py | 24 +++++++++++++++++------- code/utils/config.toml | 5 +++-- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/code/MainWindow.py b/code/MainWindow.py index 75d4514..cb2ace2 100644 --- a/code/MainWindow.py +++ b/code/MainWindow.py @@ -90,6 +90,7 @@ def closeEvent(self, event): rmtree("./poricom_cache") except FileNotFoundError: pass + self.config["NAV_ROOT"] = self.tracker.filepath saveOnClose(self.config) return QMainWindow.closeEvent(self, event) @@ -105,19 +106,24 @@ def openDir(self): filepath = QFileDialog.getExistingDirectory( self, "Open Directory", - "." # , QFileDialog.DontUseNativeDialog + self.tracker.filepath # , QFileDialog.DontUseNativeDialog ) if filepath: - # self.tracker.pixImage = filename - self.tracker.filepath = filepath - self.explorer.setDirectory(filepath) + try: + self.tracker.filepath = filepath + self.explorer.setDirectory(filepath) + except FileNotFoundError: + MessagePopup( + f"No images found in the directory", + f"Please select a directory with images." + ).exec() def openManga(self): filename, _ = QFileDialog.getOpenFileName( self, "Open Manga File", - ".", + self.tracker.filepath, "Manga (*.cbz *.cbr *.zip *.rar *.pdf)" ) diff --git a/code/Trackers.py b/code/Trackers.py index aebbee7..3ebac9a 100644 --- a/code/Trackers.py +++ b/code/Trackers.py @@ -26,8 +26,16 @@ class Tracker: - - def __init__(self, filename=config["HOME_IMAGE"], filenext=config["ABOUT_IMAGE"]): + def __init__(self): + try: + self.filepath = abspath(config["NAV_ROOT"]) + except FileNotFoundError: + self.filepath = abspath(config["DEFAULT_NAV_ROOT"]) + try: + filename, filenext, *_ = self._imageList + except ValueError: + filename, *_ = self._imageList + filenext = None if not config["SPLIT_VIEW_MODE"]: self._pixImage = PImage(filename) if config["SPLIT_VIEW_MODE"]: @@ -35,7 +43,6 @@ def __init__(self, filename=config["HOME_IMAGE"], filenext=config["ABOUT_IMAGE"] self._pixImage = PImage(splitImage, filename) self._pixMask = PImage(filename) - self._filepath = abspath(dirname(filename)) self._writeMode = False self._imageList = [] @@ -104,11 +111,14 @@ def filepath(self): @filepath.setter def filepath(self, filepath): + fileList = filter(lambda f: isfile(join(filepath, f)), listdir(filepath)) + imageList = list(map(lambda p: normpath(join(filepath, p)), filter( + (lambda f: ('*'+splitext(f)[1]) in config["IMAGE_EXTENSIONS"]), fileList))) + if len(imageList) <= 0: + raise FileNotFoundError("Empty directory") + self._filepath = filepath - filelist = filter(lambda f: isfile(join(self.filepath, - f)), listdir(self.filepath)) - self._imageList = list(map(lambda p: normpath(join(self.filepath, p)), filter( - (lambda f: ('*'+splitext(f)[1]) in config["IMAGE_EXTENSIONS"]), filelist))) + self._imageList = imageList @property def language(self): diff --git a/code/utils/config.toml b/code/utils/config.toml index 7c21217..8b5abf2 100644 --- a/code/utils/config.toml +++ b/code/utils/config.toml @@ -21,11 +21,12 @@ LANGUAGE = [ " Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English ORIENTATION = [ " Vertical", " Horizontal",] FONT_STYLE = [ " Helvetica", " Poppins", " Arial", " Verdana", " Times New Roman",] FONT_SIZE = [ " 12", " 14", " 16", " 20", " 24", " 32", " 40", " 56", " 72",] -IMAGE_SCALING= [ " Fit to Width", " Fit to Height", " Fit to Screen",] -MODIFIER = [ " Ctrl", " Shift", " Alt", " Ctrl+Alt", " Shift+Alt", " Shift+Ctrl", " Shift+Alt+Ctrl", " No Modifier"] +IMAGE_SCALING = [ " Fit to Width", " Fit to Height", " Fit to Screen",] +MODIFIER = [ " Ctrl", " Shift", " Alt", " Ctrl+Alt", " Shift+Alt", " Shift+Ctrl", " Shift+Alt+Ctrl", " No Modifier",] # Filepath STYLES_PATH = "./assets/" +DEFAULT_NAV_ROOT = "./assets/images/" NAV_ROOT = "./assets/images/" TBAR_ICONS = "./assets/images/icons/" TBAR_ICONS_LIGHT = "./assets/images/icons/" From eb7e513ab234e07986dd1580619981271d3e87ef Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Thu, 22 Dec 2022 22:18:46 +0800 Subject: [PATCH 007/137] Set first file as default selected index --- code/Explorers.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/code/Explorers.py b/code/Explorers.py index 1e8283d..ad5d290 100644 --- a/code/Explorers.py +++ b/code/Explorers.py @@ -46,15 +46,30 @@ def __init__(self, parent=None, tracker=None): def currentChanged(self, current, previous): if not current.isValid(): - current = self.model.index(0, 0, self.rootIndex()) + current = self.model.index(self.getTopIndex(), 0, self.rootIndex()) filename = self.model.fileInfo(current).absoluteFilePath() nextIndex = self.indexBelow(current) filenext = self.model.fileInfo(nextIndex).absoluteFilePath() self.parent.viewImageFromExplorer(filename, filenext) QTreeView.currentChanged(self, current, previous) + def getTopIndex(self): + r = self.model.rowCount(self.rootIndex()) // 2 + while True: + item = self.model.index(r, 0, self.rootIndex()) + if not item.isValid(): + break + if self.model.fileInfo(item).isFile(): + r //= 2 + elif not self.model.fileInfo(item).isFile(): + r += 1 + item = self.model.index(r, 0, self.rootIndex()) + if self.model.fileInfo(item).isFile(): + break + return r + def setTopIndex(self): - topIndex = self.model.index(0, 0, self.rootIndex()) + topIndex = self.model.index(self.getTopIndex(), 0, self.rootIndex()) if topIndex.isValid(): self.setCurrentIndex(topIndex) if self.layoutCheck: From 32d5ce71a962b3660b700208229684249e74a139 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Thu, 22 Dec 2022 22:26:09 +0800 Subject: [PATCH 008/137] Load language settings on app start --- code/Trackers.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/code/Trackers.py b/code/Trackers.py index aebbee7..1f99eee 100644 --- a/code/Trackers.py +++ b/code/Trackers.py @@ -40,12 +40,31 @@ def __init__(self, filename=config["HOME_IMAGE"], filenext=config["ABOUT_IMAGE"] self._imageList = [] - self._language = "jpn" - self._orientation = "_vert" + selectedLanguage = config["LANGUAGE"][config["SELECTED_INDEX"]["language"]] + self._language = self.selectionToLangCode(selectedLanguage.strip()) + selectedOrientation = config["ORIENTATION"][config["SELECTED_INDEX"]["orientation"]] + self._orientation = self.selectionToOrientCode(selectedOrientation.strip()) self._betterOCR = False self._ocrModel = None + def selectionToLangCode(self, selectedLanguage): + if selectedLanguage == "Japanese": + langCode = "jpn" + if selectedLanguage == "Korean": + langCode = "kor" + if selectedLanguage == "Chinese SIM": + langCode = "chi_sim" + if selectedLanguage == "Chinese TRA": + langCode = "chi_tra" + if selectedLanguage == "English": + langCode = "eng" + return langCode + + def selectionToOrientCode(self, selectedOrientation): + isVert = selectedOrientation == "Vertical" + return "_vert" if isVert else "" + def twoFileToImage(self, fileLeft, fileRight): imageLeft, imageRight = PImage(fileLeft), PImage(fileRight) if not (imageLeft.isValid()): From 3967e7eea3aeb251862c790503f177a522ec2035 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Thu, 22 Dec 2022 23:01:55 +0800 Subject: [PATCH 009/137] Add flow to disable load model popup --- code/MainWindow.py | 7 ++++--- code/Popups.py | 10 +++++++++- code/utils/config.toml | 4 ++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/code/MainWindow.py b/code/MainWindow.py index 75d4514..01fac67 100644 --- a/code/MainWindow.py +++ b/code/MainWindow.py @@ -33,7 +33,7 @@ from Explorers import (ImageExplorer) from Views import (OCRCanvas, FullScreen) from Popups import (FontPicker, LanguagePicker, ScaleImagePicker, - ShortcutPicker, PickerPopup, MessagePopup) + ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) class WinEventFilter(QAbstractNativeEventFilter): @@ -223,8 +223,8 @@ def loadModel(self): loadModelButton = self.ribbon.findChild(QPushButton, "loadModel") loadModelButton.setChecked(not self.tracker.ocrModel) - if loadModelButton.isChecked(): - confirmation = MessagePopup( + if loadModelButton.isChecked() and self.config["LOAD_MODEL_POPUP"]: + confirmation = CheckboxPopup( "Load the MangaOCR model?", "If you are running this for the first time, this will " + "download the MangaOcr model which is about 400 MB in size. " + @@ -234,6 +234,7 @@ def loadModel(self): MessagePopup.Ok | MessagePopup.Cancel ) ret = confirmation.exec() + self.config["LOAD_MODEL_POPUP"] = not confirmation.checkBox().isChecked() if (ret == MessagePopup.Ok): pass else: diff --git a/code/Popups.py b/code/Popups.py index 29024ce..5bbfbb2 100644 --- a/code/Popups.py +++ b/code/Popups.py @@ -18,7 +18,7 @@ """ from PyQt5.QtCore import (Qt) -from PyQt5.QtWidgets import (QGridLayout, QVBoxLayout, QWidget, QLabel, +from PyQt5.QtWidgets import (QGridLayout, QVBoxLayout, QWidget, QLabel, QCheckBox, QLineEdit, QComboBox, QDialog, QDialogButtonBox, QMessageBox) from utils.config import (editSelectionConfig, editStylesheet) @@ -30,6 +30,14 @@ def __init__(self, title, message, flags=QMessageBox.Ok): QMessageBox.NoIcon, title, message, flags) +class CheckboxPopup(MessagePopup): + def __init__(self, title, message, flags=QMessageBox.Ok, + checkboxMessage="Don't show this dialog again"): + super(MessagePopup, self).__init__( + MessagePopup.NoIcon, title, message, flags) + self.checkbox = QCheckBox(checkboxMessage) + self.setCheckBox(self.checkbox) + class BasePicker(QWidget): def __init__(self, parent, tracker, optionLists=[]): super(QWidget, self).__init__() diff --git a/code/utils/config.toml b/code/utils/config.toml index 7c21217..6b1f7bc 100644 --- a/code/utils/config.toml +++ b/code/utils/config.toml @@ -47,6 +47,10 @@ NAV_VIEW_RATIO = [ 3, 11,] VIEW_IMAGE_MODE = 0 SPLIT_VIEW_MODE = false +# Popups +LOAD_MODEL_POPUP = true +CHECK_INTERNET_POPUP = true + [SELECTED_INDEX] language = 0 orientation = 0 From 31bf3ea54411ee900e8df4ae23a210d28721ce19 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Thu, 22 Dec 2022 23:17:33 +0800 Subject: [PATCH 010/137] Add flow to disable connection error --- code/MainWindow.py | 17 +++++++++++++---- code/utils/config.toml | 1 + 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/code/MainWindow.py b/code/MainWindow.py index 01fac67..d120e42 100644 --- a/code/MainWindow.py +++ b/code/MainWindow.py @@ -246,12 +246,15 @@ def loadModelHelper(tracker): if betterOCR: import http.client as httplib - def isConnected(url="8.8.8.8"): + def isConnected(url=self.config["CHECK_INTERNET_URL"]): + if not self.config["CHECK_INTERNET_POPUP"]: + return True connection = httplib.HTTPSConnection(url, timeout=2) try: connection.request("HEAD", "/") return True except Exception: + tracker.switchOCRMode() return False finally: connection.close() @@ -274,10 +277,16 @@ def modelLoadedConfirmation(typeConnectionTuple): ).exec() elif not connected: - MessagePopup( + connectionErrorMessage = CheckboxPopup( "Connection Error", - "Please try again or make sure your Internet connection is on." - ).exec() + "Please try again or make sure your Internet connection is on.", + checkboxMessage=( + "Check this box if you keep getting this error even with connection on." + ) + ) + connectionErrorMessage.exec() + self.config["CHECK_INTERNET_POPUP"] = \ + not connectionErrorMessage.checkBox().isChecked() loadModelButton.setChecked(False) worker = BaseWorker(loadModelHelper, self.tracker) diff --git a/code/utils/config.toml b/code/utils/config.toml index 6b1f7bc..ae8ba2f 100644 --- a/code/utils/config.toml +++ b/code/utils/config.toml @@ -50,6 +50,7 @@ SPLIT_VIEW_MODE = false # Popups LOAD_MODEL_POPUP = true CHECK_INTERNET_POPUP = true +CHECK_INTERNET_URL = "8.8.8.8" [SELECTED_INDEX] language = 0 From f2f73e905cbe8f9cd35ecb28b22117313f90919f Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Thu, 22 Dec 2022 23:24:50 +0800 Subject: [PATCH 011/137] Fix ValueError when loading MangaOcr model --- code/MainWindow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/code/MainWindow.py b/code/MainWindow.py index d120e42..71a4894 100644 --- a/code/MainWindow.py +++ b/code/MainWindow.py @@ -261,7 +261,10 @@ def isConnected(url=self.config["CHECK_INTERNET_URL"]): connected = isConnected() if connected: - tracker.ocrModel = MangaOcr() + try: + tracker.ocrModel = MangaOcr() + except ValueError: + return (betterOCR, False) return (betterOCR, connected) else: tracker.ocrModel = None From ca3944301540963fb2b2ac4c4bdc5e01cabdc286 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Fri, 23 Dec 2022 14:03:15 +0800 Subject: [PATCH 012/137] Implement keyboard controls --- code/MainWindow.py | 1 + code/Views.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/code/MainWindow.py b/code/MainWindow.py index 75d4514..1bf33a9 100644 --- a/code/MainWindow.py +++ b/code/MainWindow.py @@ -83,6 +83,7 @@ def viewImageFromExplorer(self, filename, filenext): self.canvas.currentScale = 1 self.canvas.verticalScrollBar().setSliderPosition(0) self.canvas.viewImage() + self.canvas.setFocus() return True def closeEvent(self, event): diff --git a/code/Views.py b/code/Views.py index bef47b1..7cd36af 100644 --- a/code/Views.py +++ b/code/Views.py @@ -265,3 +265,18 @@ def mouseDoubleClickEvent(self, event): self.currentScale = 1 self.viewImage(self.currentScale) QGraphicsView.mouseDoubleClickEvent(self, event) + + def keyPressEvent(self, event): + if event.key() == Qt.Key_Left: + self.parent.loadPrevImage() + return + if event.key() == Qt.Key_Right: + self.parent.loadNextImage() + return + if event.key() == Qt.Key_Minus: + self.zoomView(isZoomIn=False, usingButton=True) + return + if event.key() == Qt.Key_Plus: + self.zoomView(isZoomIn=True, usingButton=True) + return + super().keyPressEvent(event) From 5678f7f85974c98fbf495f84115141888630e0d3 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Fri, 23 Dec 2022 14:17:01 +0800 Subject: [PATCH 013/137] Add multi display support --- code/MainWindow.py | 9 +++++++-- code/Views.py | 12 ++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/code/MainWindow.py b/code/MainWindow.py index 75d4514..6b62920 100644 --- a/code/MainWindow.py +++ b/code/MainWindow.py @@ -23,7 +23,7 @@ import toml from manga_ocr import MangaOcr from PyQt5.QtCore import (Qt, QAbstractNativeEventFilter, QThreadPool) -from PyQt5.QtWidgets import (QHBoxLayout, QVBoxLayout, QWidget, +from PyQt5.QtWidgets import (QHBoxLayout, QVBoxLayout, QWidget, QDesktopWidget, QPushButton, QFileDialog, QInputDialog, QMainWindow, QApplication) from utils.image_io import mangaFileToImageDir @@ -150,7 +150,12 @@ def captureExternal(self): externalWindow.setCentralWidget( FullScreen(externalWindow, self.tracker)) - externalWindow.centralWidget().takeScreenshot() + fullScreen = externalWindow.centralWidget() + + screenIndex = fullScreen.getActiveScreenIndex() + screen = QDesktopWidget().screenGeometry(screenIndex) + fullScreen.takeScreenshot(screenIndex) + externalWindow.move(screen.left(), screen.top()) externalWindow.showFullScreen() # ------------------------------ View Functions ------------------------------ # diff --git a/code/Views.py b/code/Views.py index bef47b1..e8e3f9b 100644 --- a/code/Views.py +++ b/code/Views.py @@ -19,11 +19,11 @@ from time import sleep -from PyQt5.QtCore import (Qt, QRectF, QTimer, QThreadPool, pyqtSlot) -from PyQt5.QtCore import (Qt, QRect, QSize, QRectF, +from PyQt5.QtCore import (Qt, QRect, QRectF, QSize, QTimer, QThreadPool, pyqtSlot) from PyQt5.QtWidgets import ( QApplication, QGraphicsView, QGraphicsScene, QLabel) +from PyQt5.QtGui import QCursor from Workers import BaseWorker from utils.image_io import logText, pixboxToText @@ -94,13 +94,17 @@ def __init__(self, parent=None, tracker=None): self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - def takeScreenshot(self): - screen = QApplication.primaryScreen() + def takeScreenshot(self, screenIndex): + screen = QApplication.screens()[screenIndex] s = screen.size() self.pixmap.setPixmap(screen.grabWindow( 0).scaled(s.width(), s.height())) self.scene.setSceneRect(QRectF(self.pixmap.pixmap().rect())) + def getActiveScreenIndex(self): + cursor = QCursor.pos() + return QApplication.desktop().screenNumber(cursor) + def mouseReleaseEvent(self, event): BaseCanvas.mouseReleaseEvent(self, event) self.parent.close() From e5a6909e5caaa7e32b8bebf8dc293728cec860ed Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Fri, 23 Dec 2022 14:19:55 +0800 Subject: [PATCH 014/137] Fix memory leak --- code/MainWindow.py | 1 + code/Views.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/code/MainWindow.py b/code/MainWindow.py index 6b62920..91535d0 100644 --- a/code/MainWindow.py +++ b/code/MainWindow.py @@ -147,6 +147,7 @@ def captureExternal(self): externalWindow = QMainWindow() externalWindow.layout().setContentsMargins(0, 0, 0, 0) externalWindow.setStyleSheet("border:0px; margin:0px") + externalWindow.setAttribute(Qt.WA_DeleteOnClose) externalWindow.setCentralWidget( FullScreen(externalWindow, self.tracker)) diff --git a/code/Views.py b/code/Views.py index e8e3f9b..420fc2d 100644 --- a/code/Views.py +++ b/code/Views.py @@ -67,6 +67,18 @@ def mouseReleaseEvent(self, event): self.canvasText.hide() super().mouseReleaseEvent(event) + def handleTextResult(self, result): + try: + self.canvasText.setText(result) + except RuntimeError: + pass + + def handleTextFinished(self): + try: + self.canvasText.adjustSize() + except RuntimeError: + pass + @pyqtSlot() def rubberBandStopped(self): @@ -79,8 +91,8 @@ def rubberBandStopped(self): pixbox = self.grab(self.rubberBandRect()) worker = BaseWorker(pixboxToText, pixbox, lang, self.tracker.ocrModel) - worker.signals.result.connect(self.canvasText.setText) - worker.signals.finished.connect(self.canvasText.adjustSize) + worker.signals.result.connect(self.handleTextResult) + worker.signals.finished.connect(self.handleTextFinished) self.timer_.timeout.disconnect(self.rubberBandStopped) worker.signals.finished.connect( lambda: self.timer_.timeout.connect(self.rubberBandStopped)) From cd906979ac6dfc0dc026c8c86ba45a50634e6979 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Fri, 23 Dec 2022 14:17:01 +0800 Subject: [PATCH 015/137] Add multi display support --- code/MainWindow.py | 9 +++++++-- code/Views.py | 12 ++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/code/MainWindow.py b/code/MainWindow.py index fc9677d..21b84a0 100644 --- a/code/MainWindow.py +++ b/code/MainWindow.py @@ -23,7 +23,7 @@ import toml from manga_ocr import MangaOcr from PyQt5.QtCore import (Qt, QAbstractNativeEventFilter, QThreadPool) -from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QMainWindow, QApplication, +from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QDesktopWidget, QMainWindow, QApplication, QPushButton, QFileDialog, QInputDialog, QSplitter) from utils.image_io import mangaFileToImageDir @@ -163,7 +163,12 @@ def captureExternal(self): externalWindow.setCentralWidget( FullScreen(externalWindow, self.tracker)) - externalWindow.centralWidget().takeScreenshot() + fullScreen = externalWindow.centralWidget() + + screenIndex = fullScreen.getActiveScreenIndex() + screen = QDesktopWidget().screenGeometry(screenIndex) + fullScreen.takeScreenshot(screenIndex) + externalWindow.move(screen.left(), screen.top()) externalWindow.showFullScreen() # ------------------------------ View Functions ------------------------------ # diff --git a/code/Views.py b/code/Views.py index 5e5ea48..24b1f6d 100644 --- a/code/Views.py +++ b/code/Views.py @@ -19,11 +19,11 @@ from time import sleep -from PyQt5.QtCore import (Qt, QRectF, QTimer, QThreadPool, pyqtSlot) -from PyQt5.QtCore import (Qt, QRect, QSize, QRectF, +from PyQt5.QtCore import (Qt, QRect, QRectF, QSize, QTimer, QThreadPool, pyqtSlot) from PyQt5.QtWidgets import ( QApplication, QGraphicsView, QGraphicsScene, QLabel) +from PyQt5.QtGui import QCursor from Workers import BaseWorker from utils.image_io import logText, pixboxToText @@ -98,13 +98,17 @@ def __init__(self, parent=None, tracker=None): self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - def takeScreenshot(self): - screen = QApplication.primaryScreen() + def takeScreenshot(self, screenIndex): + screen = QApplication.screens()[screenIndex] s = screen.size() self.pixmap.setPixmap(screen.grabWindow( 0).scaled(s.width(), s.height())) self.scene.setSceneRect(QRectF(self.pixmap.pixmap().rect())) + def getActiveScreenIndex(self): + cursor = QCursor.pos() + return QApplication.desktop().screenNumber(cursor) + def mouseReleaseEvent(self, event): BaseCanvas.mouseReleaseEvent(self, event) self.parent.close() From cd279fbe541992d1206ae5f3d1c1580040af86d0 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Fri, 23 Dec 2022 14:19:55 +0800 Subject: [PATCH 016/137] Fix memory leak --- code/MainWindow.py | 1 + code/Views.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/code/MainWindow.py b/code/MainWindow.py index 21b84a0..ca4c977 100644 --- a/code/MainWindow.py +++ b/code/MainWindow.py @@ -160,6 +160,7 @@ def captureExternal(self): externalWindow = QMainWindow() externalWindow.layout().setContentsMargins(0, 0, 0, 0) externalWindow.setStyleSheet("border:0px; margin:0px") + externalWindow.setAttribute(Qt.WA_DeleteOnClose) externalWindow.setCentralWidget( FullScreen(externalWindow, self.tracker)) diff --git a/code/Views.py b/code/Views.py index 24b1f6d..de0704e 100644 --- a/code/Views.py +++ b/code/Views.py @@ -71,6 +71,18 @@ def mouseReleaseEvent(self, event): pass super().mouseReleaseEvent(event) + def handleTextResult(self, result): + try: + self.canvasText.setText(result) + except RuntimeError: + pass + + def handleTextFinished(self): + try: + self.canvasText.adjustSize() + except RuntimeError: + pass + @pyqtSlot() def rubberBandStopped(self): @@ -83,8 +95,8 @@ def rubberBandStopped(self): pixbox = self.grab(self.rubberBandRect()) worker = BaseWorker(pixboxToText, pixbox, lang, self.tracker.ocrModel) - worker.signals.result.connect(self.canvasText.setText) - worker.signals.finished.connect(self.canvasText.adjustSize) + worker.signals.result.connect(self.handleTextResult) + worker.signals.finished.connect(self.handleTextFinished) self.timer_.timeout.disconnect(self.rubberBandStopped) worker.signals.finished.connect( lambda: self.timer_.timeout.connect(self.rubberBandStopped)) From d3aa456989e9bea9e43709855535cee82e057299 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 00:08:54 +0800 Subject: [PATCH 017/137] Fix infinite loop bug when opening directory --- code/Explorers.py | 4 ++++ code/MainWindow.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/code/Explorers.py b/code/Explorers.py index ad5d290..68777fa 100644 --- a/code/Explorers.py +++ b/code/Explorers.py @@ -54,6 +54,10 @@ def currentChanged(self, current, previous): QTreeView.currentChanged(self, current, previous) def getTopIndex(self): + item = self.model.index(0, 0, self.rootIndex()) + if self.model.fileInfo(item).isFile(): + return 0 + r = self.model.rowCount(self.rootIndex()) // 2 while True: item = self.model.index(r, 0, self.rootIndex()) diff --git a/code/MainWindow.py b/code/MainWindow.py index ca4c977..4f0be31 100644 --- a/code/MainWindow.py +++ b/code/MainWindow.py @@ -89,7 +89,7 @@ def viewImageFromExplorer(self, filename, filenext): def resizeEvent(self, event): self.explorer.setMinimumWidth(0.1*self.width()) - self.explorer.setMaximumWidth(0.3*self.width()) + self.canvas.setMinimumWidth(0.6*self.width()) return super().resizeEvent(event) def closeEvent(self, event): From 707632615a49c8a027f6bf2e13dc87ecae5a2d98 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 00:09:39 +0800 Subject: [PATCH 018/137] Add alternatives and dev setup to README --- README.md | 18 ++++++++++++++---- environment/base.yaml | 19 +++++++++++++++++++ environment/build.yaml | 9 +++++++++ environment/dev.yaml | 9 +++++++++ requirements.txt | 8 -------- 5 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 environment/base.yaml create mode 100644 environment/build.yaml create mode 100644 environment/dev.yaml delete mode 100644 requirements.txt diff --git a/README.md b/README.md index d2d7f87..c4c2ae1 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,11 @@ ## Contents - [About](#about) +- [Alternatives](#alternatives) - [User Guide](#user_guide) - [Installation](#installation) - [Acknowledgments](#acknowledgements) -

+
## About Poricom is a desktop program for optical character recognition in manga images. Although it is a manga OCR application, it can recognize text on other type of images as well. The project is a GUI implementation of the [Manga OCR library](https://pypi.org/project/manga-ocr/0.1.5/) (supports Japanese only) and the Tesseract-API python wrapper [tesserocr](https://github.com/sirfz/tesserocr) (supports other languages). See demo below to see how it works. @@ -25,6 +26,11 @@ Perform OCR on the current screen by pressing `Alt+Q`: https://user-images.githubusercontent.com/45705751/161961152-29070fde-03f6-42a7-8569-0ff22ae9b014.mp4 +## Alternatives + - [Cloe](https://github.com/bluaxees/Cloe) - The app is based on Poricom's global snipping functionality. If you downloaded Poricom to use _only_ the global shortcut, it might be better if you use Cloe instead. + - [mokuro](https://github.com/kha-white/mokuro) - Converts manga images to web pages with selectable text. This saves you time and manual effort since textboxes are automatically detected with an almost 100% accuracy. + + ## User Guide Follow the installation instructions [here](#installation). Load a directory with manga images and select text boxes with Japanese text. If you are not getting good results using the default settings, [use the MangaOcr model](#load_model) to improve text detection. @@ -69,11 +75,10 @@ Listed below are some of the features of Poricom. Smaller features that are not + ## Installation Download the latest zip file [here](https://github.com/bluaxees/Poricom/releases/latest/). Decompress the file in the desired directory. Make sure that the `app` folder is in the same folder as the shortcut `Poricom`. -For developers, clone this repo and install requirements: `pip install -r requirements.txt`. Run the app in the command line using `python main.py`. - ### System Requirements Recommended: @@ -82,7 +87,12 @@ Recommended: Approximately 250 MB of free space and 200 MB of memory is needed to run the application using the Tesseract API. If using the Manga OCR model, an additional 450 MB of free space and 800 MB of memory is required. -For developers, the following Python versions are supported: 3.7, 3.8, and 3.9. +### Development Setup + - Clone this repo and install [conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html). + - Install dependencies by running `conda env create -f environment/base.yaml`. + - Activate the environment with `conda activate poricom-py39` and run the app using `python main.py`. + - If you want to build the app locally, install build dependencies by running `conda env update -f environment/build.yaml`. Then run `pyinstaller main.spec` in the `build` directory. + ## Acknowledgements This project will not be possible without the MangaOcr model by [Maciej Budyƛ](https://github.com/kha-white) and the Tesseract python wrapper by [sirfz](https://github.com/sirfz) and [the tesserocr contributors](https://github.com/sirfz/tesserocr/graphs/contributors). diff --git a/environment/base.yaml b/environment/base.yaml new file mode 100644 index 0000000..b97fb86 --- /dev/null +++ b/environment/base.yaml @@ -0,0 +1,19 @@ +name: poricom-py39 + +channels: + - conda-forge + - defaults + +dependencies: + - python=3.9 + - pip + - conda-forge:tesserocr + - pip: + - pyqt5 + - pillow + - manga-ocr + - pyqtkeybind + - rarfile + - pdf2image + - toml + - huggingface-hub==0.7.0 diff --git a/environment/build.yaml b/environment/build.yaml new file mode 100644 index 0000000..2ab9f1b --- /dev/null +++ b/environment/build.yaml @@ -0,0 +1,9 @@ +name: poricom-py39 + +channels: + - conda-forge + - defaults + +dependencies: + - pip: + - pyinstaller diff --git a/environment/dev.yaml b/environment/dev.yaml new file mode 100644 index 0000000..78f1ddb --- /dev/null +++ b/environment/dev.yaml @@ -0,0 +1,9 @@ +name: poricom-py39 + +channels: + - conda-forge + - defaults + +dependencies: + - pip: + - black diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 30411a8..0000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -pyqt5~=5.15.6 -pillow~=9.0.1 -tesserocr~=2.5.1 -manga-ocr~=0.1.5 -pyqtkeybind~=0.0.9 -rarfile~=4.0 -pdf2image~=1.16.0 -toml~=0.10.2 From e3ecde1d1a5f9506f6b8f1fef91f07ddd07dd3ef Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 00:36:40 +0800 Subject: [PATCH 019/137] Add pyinstaller spec file --- build/main.spec | 101 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 build/main.spec diff --git a/build/main.spec b/build/main.spec new file mode 100644 index 0000000..98e4b89 --- /dev/null +++ b/build/main.spec @@ -0,0 +1,101 @@ +# -*- mode: python ; coding: utf-8 -*- +from PyInstaller.utils.hooks import collect_data_files +from PyInstaller.utils.hooks import copy_metadata + +datas = [] +datas += collect_data_files('unidic_lite') +datas += collect_data_files('manga_ocr') +datas += copy_metadata('tqdm') +datas += copy_metadata('regex') +datas += copy_metadata('sacremoses') +datas += copy_metadata('requests') +datas += copy_metadata('packaging') +datas += copy_metadata('filelock') +datas += copy_metadata('numpy') +datas += copy_metadata('tokenizers') + +added_files = [ + ('./assets', './assets'), + ('./utils', './utils'), + ('path\\to\\user\\.conda\\pkgs\\poppler-22.01.0-h24fffdf_2', './poppler') +] + + +block_cipher = None + + +a = Analysis(['main.py'], + pathex=[], + binaries=[], + datas=datas+added_files, + hiddenimports=['toml'], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) + + +PATH_TO_TORCH_LIB = "path\\to\\env\\lib\\site-packages\\torch\\lib\\" +excluded_files = [ + 'asmjit.lib', + 'c10.lib', + 'clog.lib', + 'cpuinfo.lib', + 'dnnl.lib', + 'caffe2_detectron_ops.dll', + 'caffe2_detectron_ops.lib', + 'caffe2_module_test_dynamic.dll', + 'caffe2_module_test_dynamic.lib', + 'caffe2_observers.dll', + 'caffe2_observers.lib', + 'Caffe2_perfkernels_avx.lib', + 'Caffe2_perfkernels_avx2.lib', + 'Caffe2_perfkernels_avx512.lib', + 'fbgemm.lib', + 'kineto.lib', + 'libprotobuf-lite.lib', + 'libprotobuf.lib', + 'libprotoc.lib', + 'mkldnn.lib', + 'pthreadpool.lib', + 'shm.lib', + 'torch.lib', + 'torch_cpu.lib', + 'torch_python.lib', + 'XNNPACK.lib', + '_C.lib' +] +excluded_files = [PATH_TO_TORCH_LIB + x for x in excluded_files] +a.datas = [x for x in a.datas if not + os.path.abspath(x[1]) in excluded_files] + + +exe = EXE(pyz, + a.scripts, + [], + exclude_binaries=True, + name='Poricom', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon="./assets/images/icons/logo.ico") +coll = COLLECT(exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='app') From 4d9c817b37927df38e1ad356ea16908ea1219b7e Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 15:31:42 +0800 Subject: [PATCH 020/137] Rename source code directory to app --- {code => app}/Explorers.py | 0 {code => app}/MainWindow.py | 0 {code => app}/Popups.py | 0 {code => app}/Ribbon.py | 0 {code => app}/Trackers.py | 0 {code => app}/Views.py | 0 {code => app}/Workers.py | 0 {code => app}/assets/images/home.png | Bin .../assets/images/icons/captureExternalHelper.png | Bin {code => app}/assets/images/icons/cb_contract.png | Bin {code => app}/assets/images/icons/cb_expand.png | Bin {code => app}/assets/images/icons/default_icon.png | Bin {code => app}/assets/images/icons/hideExplorer.png | Bin .../assets/images/icons/input_dialog_down.png | Bin .../assets/images/icons/input_dialog_up.png | Bin .../assets/images/icons/loadImageAtIndex.png | Bin {code => app}/assets/images/icons/loadModel.png | Bin {code => app}/assets/images/icons/loadNextImage.png | Bin {code => app}/assets/images/icons/loadPrevImage.png | Bin {code => app}/assets/images/icons/logo.ico | Bin .../assets/images/icons/modifyFontSettings.png | Bin {code => app}/assets/images/icons/modifyHotkeys.png | Bin .../assets/images/icons/modifyTesseract.png | Bin {code => app}/assets/images/icons/openDir.png | Bin {code => app}/assets/images/icons/openManga.png | Bin {code => app}/assets/images/icons/scaleImage.png | Bin {code => app}/assets/images/icons/toggleLogging.png | Bin .../assets/images/icons/toggleMouseMode.png | Bin .../assets/images/icons/toggleSplitView.png | Bin .../assets/images/icons/toggleStylesheet.png | Bin {code => app}/assets/images/icons/zoomIn.png | Bin {code => app}/assets/images/icons/zoomOut.png | Bin {code => app}/assets/images/poricom-about.png | Bin {code => app}/assets/languages/chi_sim.traineddata | Bin .../assets/languages/chi_sim_vert.traineddata | Bin {code => app}/assets/languages/chi_tra.traineddata | Bin .../assets/languages/chi_tra_vert.traineddata | Bin {code => app}/assets/languages/eng.traineddata | Bin {code => app}/assets/languages/eng_vert.traineddata | Bin {code => app}/assets/languages/jpn.traineddata | Bin {code => app}/assets/languages/jpn_vert.traineddata | Bin {code => app}/assets/languages/kor.traineddata | Bin {code => app}/assets/languages/kor_vert.traineddata | Bin {code => app}/assets/styles-dark.qss | 0 {code => app}/assets/styles.qss | 0 {code => app}/main.py | 0 {code => app}/old/config.py | 0 {code => app}/old/memory.py | 0 {code => app}/old/ribbon.py | 0 {code => app}/old/viewer.py | 0 {code => app}/utils/config.py | 0 {code => app}/utils/config.toml | 0 {code => app}/utils/image_io.py | 0 {code => app}/utils/unrar.exe | Bin 54 files changed, 0 insertions(+), 0 deletions(-) rename {code => app}/Explorers.py (100%) rename {code => app}/MainWindow.py (100%) rename {code => app}/Popups.py (100%) rename {code => app}/Ribbon.py (100%) rename {code => app}/Trackers.py (100%) rename {code => app}/Views.py (100%) rename {code => app}/Workers.py (100%) rename {code => app}/assets/images/home.png (100%) rename {code => app}/assets/images/icons/captureExternalHelper.png (100%) rename {code => app}/assets/images/icons/cb_contract.png (100%) rename {code => app}/assets/images/icons/cb_expand.png (100%) rename {code => app}/assets/images/icons/default_icon.png (100%) rename {code => app}/assets/images/icons/hideExplorer.png (100%) rename {code => app}/assets/images/icons/input_dialog_down.png (100%) rename {code => app}/assets/images/icons/input_dialog_up.png (100%) rename {code => app}/assets/images/icons/loadImageAtIndex.png (100%) rename {code => app}/assets/images/icons/loadModel.png (100%) rename {code => app}/assets/images/icons/loadNextImage.png (100%) rename {code => app}/assets/images/icons/loadPrevImage.png (100%) rename {code => app}/assets/images/icons/logo.ico (100%) rename {code => app}/assets/images/icons/modifyFontSettings.png (100%) rename {code => app}/assets/images/icons/modifyHotkeys.png (100%) rename {code => app}/assets/images/icons/modifyTesseract.png (100%) rename {code => app}/assets/images/icons/openDir.png (100%) rename {code => app}/assets/images/icons/openManga.png (100%) rename {code => app}/assets/images/icons/scaleImage.png (100%) rename {code => app}/assets/images/icons/toggleLogging.png (100%) rename {code => app}/assets/images/icons/toggleMouseMode.png (100%) rename {code => app}/assets/images/icons/toggleSplitView.png (100%) rename {code => app}/assets/images/icons/toggleStylesheet.png (100%) rename {code => app}/assets/images/icons/zoomIn.png (100%) rename {code => app}/assets/images/icons/zoomOut.png (100%) rename {code => app}/assets/images/poricom-about.png (100%) rename {code => app}/assets/languages/chi_sim.traineddata (100%) rename {code => app}/assets/languages/chi_sim_vert.traineddata (100%) rename {code => app}/assets/languages/chi_tra.traineddata (100%) rename {code => app}/assets/languages/chi_tra_vert.traineddata (100%) rename {code => app}/assets/languages/eng.traineddata (100%) rename {code => app}/assets/languages/eng_vert.traineddata (100%) rename {code => app}/assets/languages/jpn.traineddata (100%) rename {code => app}/assets/languages/jpn_vert.traineddata (100%) rename {code => app}/assets/languages/kor.traineddata (100%) rename {code => app}/assets/languages/kor_vert.traineddata (100%) rename {code => app}/assets/styles-dark.qss (100%) rename {code => app}/assets/styles.qss (100%) rename {code => app}/main.py (100%) rename {code => app}/old/config.py (100%) rename {code => app}/old/memory.py (100%) rename {code => app}/old/ribbon.py (100%) rename {code => app}/old/viewer.py (100%) rename {code => app}/utils/config.py (100%) rename {code => app}/utils/config.toml (100%) rename {code => app}/utils/image_io.py (100%) rename {code => app}/utils/unrar.exe (100%) diff --git a/code/Explorers.py b/app/Explorers.py similarity index 100% rename from code/Explorers.py rename to app/Explorers.py diff --git a/code/MainWindow.py b/app/MainWindow.py similarity index 100% rename from code/MainWindow.py rename to app/MainWindow.py diff --git a/code/Popups.py b/app/Popups.py similarity index 100% rename from code/Popups.py rename to app/Popups.py diff --git a/code/Ribbon.py b/app/Ribbon.py similarity index 100% rename from code/Ribbon.py rename to app/Ribbon.py diff --git a/code/Trackers.py b/app/Trackers.py similarity index 100% rename from code/Trackers.py rename to app/Trackers.py diff --git a/code/Views.py b/app/Views.py similarity index 100% rename from code/Views.py rename to app/Views.py diff --git a/code/Workers.py b/app/Workers.py similarity index 100% rename from code/Workers.py rename to app/Workers.py diff --git a/code/assets/images/home.png b/app/assets/images/home.png similarity index 100% rename from code/assets/images/home.png rename to app/assets/images/home.png diff --git a/code/assets/images/icons/captureExternalHelper.png b/app/assets/images/icons/captureExternalHelper.png similarity index 100% rename from code/assets/images/icons/captureExternalHelper.png rename to app/assets/images/icons/captureExternalHelper.png diff --git a/code/assets/images/icons/cb_contract.png b/app/assets/images/icons/cb_contract.png similarity index 100% rename from code/assets/images/icons/cb_contract.png rename to app/assets/images/icons/cb_contract.png diff --git a/code/assets/images/icons/cb_expand.png b/app/assets/images/icons/cb_expand.png similarity index 100% rename from code/assets/images/icons/cb_expand.png rename to app/assets/images/icons/cb_expand.png diff --git a/code/assets/images/icons/default_icon.png b/app/assets/images/icons/default_icon.png similarity index 100% rename from code/assets/images/icons/default_icon.png rename to app/assets/images/icons/default_icon.png diff --git a/code/assets/images/icons/hideExplorer.png b/app/assets/images/icons/hideExplorer.png similarity index 100% rename from code/assets/images/icons/hideExplorer.png rename to app/assets/images/icons/hideExplorer.png diff --git a/code/assets/images/icons/input_dialog_down.png b/app/assets/images/icons/input_dialog_down.png similarity index 100% rename from code/assets/images/icons/input_dialog_down.png rename to app/assets/images/icons/input_dialog_down.png diff --git a/code/assets/images/icons/input_dialog_up.png b/app/assets/images/icons/input_dialog_up.png similarity index 100% rename from code/assets/images/icons/input_dialog_up.png rename to app/assets/images/icons/input_dialog_up.png diff --git a/code/assets/images/icons/loadImageAtIndex.png b/app/assets/images/icons/loadImageAtIndex.png similarity index 100% rename from code/assets/images/icons/loadImageAtIndex.png rename to app/assets/images/icons/loadImageAtIndex.png diff --git a/code/assets/images/icons/loadModel.png b/app/assets/images/icons/loadModel.png similarity index 100% rename from code/assets/images/icons/loadModel.png rename to app/assets/images/icons/loadModel.png diff --git a/code/assets/images/icons/loadNextImage.png b/app/assets/images/icons/loadNextImage.png similarity index 100% rename from code/assets/images/icons/loadNextImage.png rename to app/assets/images/icons/loadNextImage.png diff --git a/code/assets/images/icons/loadPrevImage.png b/app/assets/images/icons/loadPrevImage.png similarity index 100% rename from code/assets/images/icons/loadPrevImage.png rename to app/assets/images/icons/loadPrevImage.png diff --git a/code/assets/images/icons/logo.ico b/app/assets/images/icons/logo.ico similarity index 100% rename from code/assets/images/icons/logo.ico rename to app/assets/images/icons/logo.ico diff --git a/code/assets/images/icons/modifyFontSettings.png b/app/assets/images/icons/modifyFontSettings.png similarity index 100% rename from code/assets/images/icons/modifyFontSettings.png rename to app/assets/images/icons/modifyFontSettings.png diff --git a/code/assets/images/icons/modifyHotkeys.png b/app/assets/images/icons/modifyHotkeys.png similarity index 100% rename from code/assets/images/icons/modifyHotkeys.png rename to app/assets/images/icons/modifyHotkeys.png diff --git a/code/assets/images/icons/modifyTesseract.png b/app/assets/images/icons/modifyTesseract.png similarity index 100% rename from code/assets/images/icons/modifyTesseract.png rename to app/assets/images/icons/modifyTesseract.png diff --git a/code/assets/images/icons/openDir.png b/app/assets/images/icons/openDir.png similarity index 100% rename from code/assets/images/icons/openDir.png rename to app/assets/images/icons/openDir.png diff --git a/code/assets/images/icons/openManga.png b/app/assets/images/icons/openManga.png similarity index 100% rename from code/assets/images/icons/openManga.png rename to app/assets/images/icons/openManga.png diff --git a/code/assets/images/icons/scaleImage.png b/app/assets/images/icons/scaleImage.png similarity index 100% rename from code/assets/images/icons/scaleImage.png rename to app/assets/images/icons/scaleImage.png diff --git a/code/assets/images/icons/toggleLogging.png b/app/assets/images/icons/toggleLogging.png similarity index 100% rename from code/assets/images/icons/toggleLogging.png rename to app/assets/images/icons/toggleLogging.png diff --git a/code/assets/images/icons/toggleMouseMode.png b/app/assets/images/icons/toggleMouseMode.png similarity index 100% rename from code/assets/images/icons/toggleMouseMode.png rename to app/assets/images/icons/toggleMouseMode.png diff --git a/code/assets/images/icons/toggleSplitView.png b/app/assets/images/icons/toggleSplitView.png similarity index 100% rename from code/assets/images/icons/toggleSplitView.png rename to app/assets/images/icons/toggleSplitView.png diff --git a/code/assets/images/icons/toggleStylesheet.png b/app/assets/images/icons/toggleStylesheet.png similarity index 100% rename from code/assets/images/icons/toggleStylesheet.png rename to app/assets/images/icons/toggleStylesheet.png diff --git a/code/assets/images/icons/zoomIn.png b/app/assets/images/icons/zoomIn.png similarity index 100% rename from code/assets/images/icons/zoomIn.png rename to app/assets/images/icons/zoomIn.png diff --git a/code/assets/images/icons/zoomOut.png b/app/assets/images/icons/zoomOut.png similarity index 100% rename from code/assets/images/icons/zoomOut.png rename to app/assets/images/icons/zoomOut.png diff --git a/code/assets/images/poricom-about.png b/app/assets/images/poricom-about.png similarity index 100% rename from code/assets/images/poricom-about.png rename to app/assets/images/poricom-about.png diff --git a/code/assets/languages/chi_sim.traineddata b/app/assets/languages/chi_sim.traineddata similarity index 100% rename from code/assets/languages/chi_sim.traineddata rename to app/assets/languages/chi_sim.traineddata diff --git a/code/assets/languages/chi_sim_vert.traineddata b/app/assets/languages/chi_sim_vert.traineddata similarity index 100% rename from code/assets/languages/chi_sim_vert.traineddata rename to app/assets/languages/chi_sim_vert.traineddata diff --git a/code/assets/languages/chi_tra.traineddata b/app/assets/languages/chi_tra.traineddata similarity index 100% rename from code/assets/languages/chi_tra.traineddata rename to app/assets/languages/chi_tra.traineddata diff --git a/code/assets/languages/chi_tra_vert.traineddata b/app/assets/languages/chi_tra_vert.traineddata similarity index 100% rename from code/assets/languages/chi_tra_vert.traineddata rename to app/assets/languages/chi_tra_vert.traineddata diff --git a/code/assets/languages/eng.traineddata b/app/assets/languages/eng.traineddata similarity index 100% rename from code/assets/languages/eng.traineddata rename to app/assets/languages/eng.traineddata diff --git a/code/assets/languages/eng_vert.traineddata b/app/assets/languages/eng_vert.traineddata similarity index 100% rename from code/assets/languages/eng_vert.traineddata rename to app/assets/languages/eng_vert.traineddata diff --git a/code/assets/languages/jpn.traineddata b/app/assets/languages/jpn.traineddata similarity index 100% rename from code/assets/languages/jpn.traineddata rename to app/assets/languages/jpn.traineddata diff --git a/code/assets/languages/jpn_vert.traineddata b/app/assets/languages/jpn_vert.traineddata similarity index 100% rename from code/assets/languages/jpn_vert.traineddata rename to app/assets/languages/jpn_vert.traineddata diff --git a/code/assets/languages/kor.traineddata b/app/assets/languages/kor.traineddata similarity index 100% rename from code/assets/languages/kor.traineddata rename to app/assets/languages/kor.traineddata diff --git a/code/assets/languages/kor_vert.traineddata b/app/assets/languages/kor_vert.traineddata similarity index 100% rename from code/assets/languages/kor_vert.traineddata rename to app/assets/languages/kor_vert.traineddata diff --git a/code/assets/styles-dark.qss b/app/assets/styles-dark.qss similarity index 100% rename from code/assets/styles-dark.qss rename to app/assets/styles-dark.qss diff --git a/code/assets/styles.qss b/app/assets/styles.qss similarity index 100% rename from code/assets/styles.qss rename to app/assets/styles.qss diff --git a/code/main.py b/app/main.py similarity index 100% rename from code/main.py rename to app/main.py diff --git a/code/old/config.py b/app/old/config.py similarity index 100% rename from code/old/config.py rename to app/old/config.py diff --git a/code/old/memory.py b/app/old/memory.py similarity index 100% rename from code/old/memory.py rename to app/old/memory.py diff --git a/code/old/ribbon.py b/app/old/ribbon.py similarity index 100% rename from code/old/ribbon.py rename to app/old/ribbon.py diff --git a/code/old/viewer.py b/app/old/viewer.py similarity index 100% rename from code/old/viewer.py rename to app/old/viewer.py diff --git a/code/utils/config.py b/app/utils/config.py similarity index 100% rename from code/utils/config.py rename to app/utils/config.py diff --git a/code/utils/config.toml b/app/utils/config.toml similarity index 100% rename from code/utils/config.toml rename to app/utils/config.toml diff --git a/code/utils/image_io.py b/app/utils/image_io.py similarity index 100% rename from code/utils/image_io.py rename to app/utils/image_io.py diff --git a/code/utils/unrar.exe b/app/utils/unrar.exe similarity index 100% rename from code/utils/unrar.exe rename to app/utils/unrar.exe From 55bfd754b0dd913a7283e8ad93b117f1735865f5 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 15:44:19 +0800 Subject: [PATCH 021/137] Remove legacy code --- app/old/config.py | 219 ---------------------------------------------- app/old/memory.py | 62 ------------- app/old/ribbon.py | 120 ------------------------- app/old/viewer.py | 116 ------------------------ 4 files changed, 517 deletions(-) delete mode 100644 app/old/config.py delete mode 100644 app/old/memory.py delete mode 100644 app/old/ribbon.py delete mode 100644 app/old/viewer.py diff --git a/app/old/config.py b/app/old/config.py deleted file mode 100644 index 604307c..0000000 --- a/app/old/config.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -Poricom -Copyright (C) `2021-2022` `` - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -stylesheet_path = './assets/styles.qss' -combobox_selected_index = { - 'language': 0, - 'orientation': 0, - 'font_style': 0, - 'font_size': 2, -} -picker_index = { - 'language': 20, - 'orientation': 21, - 'font_style': 22, - 'font_size': 23 -} -cfg = { - "IMAGE_EXTENSIONS": ["*.bmp", "*.gif", "*.jpg", "*.jpeg", "*.png", - "*.pbm", "*.pgm", "*.ppm", "*.webp", "*.xbm", "*.xpm"], - - "LANGUAGE": [" Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English"], - "ORIENTATION": [" Vertical", " Horizontal"], - "LANG_PATH": "./assets/languages/", - - "FONT_STYLE": [" Poppins", " Arial", " Verdana", " Helvetica", " Times New Roman"], - "FONT_SIZE": [" 12", " 14", " 16", " 20", " 24", " 32", " 40", " 56", " 72"], - - "SELECTED_INDEX": combobox_selected_index, - "PICKER_INDEX": picker_index, - - "STYLES_PATH": "./assets/", - "STYLES_DEFAULT": stylesheet_path, - - "NAV_VIEW_RATIO": [3,11], - "NAV_ROOT": "./assets/images/", - - "NAV_FUNCS": { - "path_changed": "view_image_from_fdialog", - "nav_clicked": "view_image_from_explorer" - }, - - "LOGO": "./assets/images/icons/logo.ico", - "HOME_IMAGE": "./assets/images/home.png", - - "RBN_HEIGHT": 2.4, - - "TBAR_ISIZE_REL": 0.1, - "TBAR_ISIZE_MARGIN": 1.3, - - "TBAR_ICONS": "./assets/images/icons/", - "TBAR_ICONS_LIGHT": "./assets/images/icons/", - "TBAR_ICON_DEFAULT": "./assets/images/icons/default_icon.png", - - "TBAR_FUNCS": { - "FILE": { - "open_dir": { - "help_title": "Open manga directory", - "help_msg": "Open a directory containing images.", - "path": "open_dir.png", - "toggle": False, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - }, - "open_manga": { - "help_title": "Open manga file", - "help_msg": "Supports the following formats: cbr, cbz, pdf.", - "path": "open_manga.png", - "toggle": False, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - } - }, - "VIEW": { - "toggle_stylesheet": { - "help_title": "Change theme", - "help_msg": "Switch between light and dark mode.", - "path": "toggle_stylesheet.png", - "toggle": False, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - }, - "modify_font_settings": { - "help_title": "Modify preview text", - "help_msg": "Change font style and font size of preview text.", - "path": "modify_font_settings.png", - "toggle": False, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - }, - "fit_horizontally": { - "help_title": "Fit image horizontally", - "help_msg": "", - "path": "fit_horizontally.png", - "toggle": True, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - }, - "fit_vertically": { - "help_title": "Fit image vertically", - "help_msg": "", - "path": "fit_vertically.png", - "toggle": True, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - } - }, - "SETTINGS": { - "load_model": { - "help_title": "Switch detection model", - "help_msg": "Switch between MangaOCR and Tesseract models.", - "path": "load_model.png", - "toggle": True, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - }, - "modify_tesseract": { - "help_title": "Tesseract settings", - "help_msg": "Set the language and orientation for the \ - Tesseract model.", - "path": "modify_tesseract.png", - "toggle": False, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - }, - "toggle_logging": { - "help_title": "Enable text logging", - "help_msg": "Save detected text to a text file located in the \ - current project directory.", - "path": "toggle_logging.png", - "toggle": True, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - }, - "toggle_mouse_mode": { - "help_title": "Change mouse behavior", - "help_msg": "This will disable text detection. Turn this on \ - only if do not want to hold CTRL key to zoom and pan \ - on an image.", - "path": "toggle_mouse_mode.png", - "toggle": True, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - } - } - }, - - "MODE_FUNCS": { - "zoom_in": { - "help_title": "Zoom in", - "help_msg": "Hint: Double click the image to reset zoom.", - "path": "zoom_in.png", - "toggle": False, - "align": "AlignRight", - "icon_h": 0.45, - "icon_w": 0.45 - }, - "zoom_out": { - "help_title": "Zoom out", - "help_msg": "Hint: Double click the image to reset zoom.", - "path": "zoom_out.png", - "toggle": False, - "align": "AlignRight", - "icon_h": 0.45, - "icon_w": 0.45 - }, - "load_image_at_idx": { - "help_title": "", - "help_msg": "Jump to page", - "path": "load_image_at_idx.png", - "toggle": False, - "align": "AlignRight", - "icon_h": 0.45, - "icon_w": 1.3 - }, - "load_prev_image": { - "help_title": "", - "help_msg": "Show previous image", - "path": "load_prev_image.png", - "toggle": False, - "align": "AlignRight", - "icon_h": 0.45, - "icon_w": 0.6 - }, - "load_next_image": { - "help_title": "", - "help_msg": "Show next image", - "path": "load_next_image.png", - "toggle": False, - "align": "AlignRight", - "icon_h": 0.45, - "icon_w": 0.6 - } - } -} \ No newline at end of file diff --git a/app/old/memory.py b/app/old/memory.py deleted file mode 100644 index fc4781f..0000000 --- a/app/old/memory.py +++ /dev/null @@ -1,62 +0,0 @@ -# TODO: Rewrite this as a Tracker object that will -# save image paths, pixmaps of original images and -# masks, button states, and window state -# Use decorators - -class Tracker: - pass - -from default import cfg -from os import listdir -from os.path import isfile, join, splitext, normpath - -img_index = 0 -img_paths = [] -mask_paths = [] -curr_dir = cfg["NAV_ROOT"] -curr_img = cfg["HOME_IMAGE"] - -def get_img_path(): - #global curr_dir - return curr_dir - -def set_img_path(path): - global curr_dir, img_paths - curr_dir = normpath(path) - filelist = filter(lambda f: isfile(join(path, f)), listdir(path)) - img_paths = list(map(lambda p: normpath(join(path, p)), - filter((lambda f: ('*'+splitext(f)[1]) in - cfg["IMAGE_EXTENSIONS"]), filelist))) - -def get_img_list(): - #global curr_dir - return img_paths - -def get_curr_img(): - #global curr_img - return curr_img - -def get_prev_img(): - pass - -def set_curr_img(filepath): - global curr_img - curr_img = filepath - -def set_prev_img(): - pass - -def get_curr_mask(): - pass - -def get_prev_mask(): - pass - -def set_curr_mask(): - pass - -def set_prev_mask(): - pass - -def get_img_index(): - pass \ No newline at end of file diff --git a/app/old/ribbon.py b/app/old/ribbon.py deleted file mode 100644 index 8bf17ca..0000000 --- a/app/old/ribbon.py +++ /dev/null @@ -1,120 +0,0 @@ -from PyQt5.QtWidgets import (QMainWindow, QApplication, QPushButton, - QWidget, QAction, QTabWidget, QVBoxLayout, QGridLayout, - QLabel, QHBoxLayout, QFileDialog) - -from PyQt5.QtGui import QIcon -from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot - -import memory as mem -from viewer import ImageViewer, ImageNavigator - -from default import cfg -from os.path import exists - -class PMainWindow(QWidget): - def __init__(self, parent=None): - super(QWidget, self).__init__(parent) - self.layout = QVBoxLayout(self) - - self.ribbon = QTabWidget() - self.createRibbon() - self.layout.addWidget(self.ribbon) - - self.img_viewer = ImageNavigator() - self.layout.addWidget(self.img_viewer) - - def createRibbon(self): - h = self.frameGeometry().height() * cfg["TBAR_ISIZE_REL"] * cfg["RBN_HEIGHT"] - self.ribbon.setFixedHeight(h) - for tab_name, tools in cfg["TBAR_FUNCS"].items(): - self.ribbon.addTab(Toolbar(parent=self, fxns=tools), tab_name) - - def update_window(self, mode=0): - self.img_viewer.set_proj_path(mem.get_img_path()) - - def open_dir(self): - path = str(QFileDialog.getExistingDirectory(self, "Select Directory")) - if path: - mem.set_img_path(path) - self.update_window() - - def save_img(self): - print("Image saved to ", mem.get_img_path()) - pass - - def delete_img(self): - print("Image deleted in ", mem.get_img_path()) - pass - - def get_mask(self): - print("Generating mask for ", mem.get_img_path()) - pass - - def delete_text(self): - print("Text deleted from mask ", mem.get_img_path()) - pass - - def edit_mask_(self): - #TODO - pass - - def edit_mask(self): - # Connect the trigger signal to a slot. - self.img_viewer.some_signal.connect(self.handle_trigger) - - # Emit the signal. - self.img_viewer.some_signal.emit("1") - - @pyqtSlot(str) - def handle_trigger(self, r): - #print("I got a signal" + r) - pass - - def compare_img(self): - #self.img_viewer.mask_viewer.setHidden( - # not self.img_viewer.mask_viewer.isHidden()) - pass - -class Toolbar(QWidget): - - acquire_index = pyqtSignal(int) - - def __init__(self, parent=None, fxns=None): - super(QWidget, self).__init__() - self.layout = QHBoxLayout(self) - self.buttons = [] - self.parent = parent - - s = self.parent.frameGeometry().height() * cfg["TBAR_ISIZE_REL"] - m = s * cfg["TBAR_ISIZE_MARGIN"] - self.layout.setAlignment(Qt.AlignLeft) - count = 0 - for fxn in fxns: - icon = QIcon() - path = cfg["TBAR_IMG_ASSETS"] + fxn + ".png" - if (exists(path)): - icon = QIcon(path) - else: icon = QIcon(cfg["TBAR_ICON_IMG"]) - - self.buttons.append(QPushButton(self)) - self.buttons[-1].setIcon(icon) - self.buttons[-1].setIconSize(QSize(s,s)) - self.buttons[-1].setFixedSize(QSize(m,m)) - #if count == 0: - # self.buttons.append(QPushButton(self)) - # self.buttons[-1].setIcon(QIcon("../assets/images/" + fxn + ".png")) - # self.buttons[-1].setIconSize(QSize(s,s)) - # self.buttons[-1].setFixedSize(QSize(m,m)) - #r = cfg[fxn]["button_code"] - #print(r) - #self.buttons[-1].clicked.connect(lambda s=r: self.on_click(s)) - #count += 1 - #else: - # self.buttons.append(QPushButton("", self)) - # self.buttons[-1].setFixedSize(QSize(m,m)) - self.buttons[-1].clicked.connect(getattr(self.parent, fxn)) - self.layout.addWidget(self.buttons[-1]) - - @pyqtSlot(str) - def on_click(self, index): - print("hahahaha", index) \ No newline at end of file diff --git a/app/old/viewer.py b/app/old/viewer.py deleted file mode 100644 index 5a8048c..0000000 --- a/app/old/viewer.py +++ /dev/null @@ -1,116 +0,0 @@ -from PyQt5.QtCore import Qt, QDir, pyqtSignal, pyqtSlot -from PyQt5.QtGui import (QImage, QPixmap) -from PyQt5.QtWidgets import (QLabel, QScrollArea, QSizePolicy) -from PyQt5.QtWidgets import (QWidget, QFileSystemModel, QTreeView, - QHBoxLayout) - -import memory as mem -from default import cfg - -class ImageNavigator(QWidget): - - some_signal = pyqtSignal(str) - - def __init__(self, parent=None): - super(QWidget, self).__init__(parent) - - _layout = QHBoxLayout(self) - _layout.setContentsMargins(0,0,2,0) - - self.model = QFileSystemModel() - self.init_fs_model() - - self.treeview = QTreeView() - self.treeview.setModel(self.model) - self.init_treeview() - _layout.addWidget(self.treeview, cfg["NAV_VIEW_RATIO"][0]) - - self.image_viewer = ImageViewer() - _layout.addWidget(self.image_viewer, cfg["NAV_VIEW_RATIO"][1]) - - self.mask_viewer = ImageViewer() - _layout.addWidget(self.mask_viewer, cfg["NAV_VIEW_RATIO"][1]) - self.mask_viewer.setHidden(True) - - self.set_proj_path(mem.get_img_path()) - - def init_fs_model(self): - self.model.setFilter(QDir.Files) - self.model.setNameFilterDisables(False) - self.model.setNameFilters(cfg["IMAGE_EXTENSIONS"]) - - self.model.directoryLoaded.connect(self.load_default_img) - #self.model.rootPathChanged.connect(self.set_proj_path) - - def init_treeview(self): - for i in range(1,4): - self.treeview.hideColumn(i) - self.treeview.setIndentation(5) - - self.treeview.clicked.connect(self.view_image_from_explorer) - - def view_image_from_explorer(self, index): - fp = self.model.fileInfo(index).absoluteFilePath() - mem.set_curr_img(fp) - self.image_viewer.view_image() - - def view_image_from_toolbar(self, mode=0): - fp = mem.get_curr_img() - mem.set_curr_img(fp) - self.image_viewer.view_image() - pass - - def set_proj_path(self, path): - if path is None: - #TODO: Error Handling - pass - mem.set_img_path(path) - self.treeview.setRootIndex(self.model.setRootPath(path)) - - def load_default_img(self): - fp = self.model.index(0, 0, self.model.index(self.model.rootPath())) - mem.set_curr_img(self.model.rootPath()+"/"+self.model.data(fp)) - self.image_viewer.view_image() - - - -class ImageViewer(QScrollArea): - def __init__(self, parent = None): - super(QScrollArea, self).__init__(parent) - - self._img_label = QLabel() - self.setWidget(self._img_label) - self.init_img_label() - - self.setWidgetResizable(True) - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - - self.verticalScrollBar().valueChanged.connect(lambda idx: self.kekw(idx)) - - def init_img_label(self): - self._img_label.setContentsMargins(10,10,10,0) - - def view_image(self, filepath=None, q_image=None, mode=0): - - w = self.frameGeometry().width() - h = self.frameGeometry().height() - - filepath = mem.get_curr_img() - image = q_image - if filepath: - image = QImage(filepath) - if image is None: - #TODO: Error Handling - return - pixmap_img = QPixmap.fromImage(image) - self._img_label.setPixmap(pixmap_img.scaledToWidth( - w-20, Qt.SmoothTransformation)) - self._img_label.adjustSize() - - def resizeEvent(self, event): - self.view_image() - QScrollArea.resizeEvent(self, event) - - def kekw(self,idx): - print(idx) \ No newline at end of file From 9d0beb538a1b4261d887bbe015d294bc9fe62c70 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 15:46:17 +0800 Subject: [PATCH 022/137] Refactor services file structure --- app/MainWindow.py | 2 +- app/Views.py | 2 +- app/__init__.py | 17 ++++++++++ app/components/__init__.py | 17 ++++++++++ app/components/services/__init__.py | 19 +++++++++++ app/components/services/workers/__init__.py | 20 ++++++++++++ .../services/workers/base.py} | 23 +++++++------ app/components/services/workers/signal.py | 32 +++++++++++++++++++ 8 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 app/__init__.py create mode 100644 app/components/__init__.py create mode 100644 app/components/services/__init__.py create mode 100644 app/components/services/workers/__init__.py rename app/{Workers.py => components/services/workers/base.py} (70%) create mode 100644 app/components/services/workers/signal.py diff --git a/app/MainWindow.py b/app/MainWindow.py index 4f0be31..fa82d3a 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -28,7 +28,7 @@ from utils.image_io import mangaFileToImageDir from utils.config import config, saveOnClose -from Workers import BaseWorker +from components.services import BaseWorker from Ribbon import (Ribbon) from Explorers import (ImageExplorer) from Views import (OCRCanvas, FullScreen) diff --git a/app/Views.py b/app/Views.py index de0704e..8c74890 100644 --- a/app/Views.py +++ b/app/Views.py @@ -25,7 +25,7 @@ QApplication, QGraphicsView, QGraphicsScene, QLabel) from PyQt5.QtGui import QCursor -from Workers import BaseWorker +from components.services import BaseWorker from utils.image_io import logText, pixboxToText diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..b1e2d50 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,17 @@ +""" +Poricom +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" diff --git a/app/components/__init__.py b/app/components/__init__.py new file mode 100644 index 0000000..d8be93f --- /dev/null +++ b/app/components/__init__.py @@ -0,0 +1,17 @@ +""" +Poricom Components +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" diff --git a/app/components/services/__init__.py b/app/components/services/__init__.py new file mode 100644 index 0000000..96e5c75 --- /dev/null +++ b/app/components/services/__init__.py @@ -0,0 +1,19 @@ +""" +Poricom Services +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .workers import BaseWorker, BaseWorkerSignal diff --git a/app/components/services/workers/__init__.py b/app/components/services/workers/__init__.py new file mode 100644 index 0000000..9b002c5 --- /dev/null +++ b/app/components/services/workers/__init__.py @@ -0,0 +1,20 @@ +""" +Poricom Services +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BaseWorker +from .signal import BaseWorkerSignal diff --git a/app/Workers.py b/app/components/services/workers/base.py similarity index 70% rename from app/Workers.py rename to app/components/services/workers/base.py index 64142c7..486a17b 100644 --- a/app/Workers.py +++ b/app/components/services/workers/base.py @@ -1,5 +1,5 @@ """ -Poricom Multithreaded Workers +Poricom Services Copyright (C) `2021-2022` `` @@ -17,24 +17,29 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (QRunnable, QObject, pyqtSignal, pyqtSlot) +from typing import Callable +from PyQt5.QtCore import (pyqtSlot, QRunnable) + +from .signal import BaseWorkerSignal class BaseWorker(QRunnable): - def __init__(self, fn, *args, **kwargs): + """Runnable object to support multithreading + + Args: + fn (Callable): Long running task or function + *Note: args/kwargs passed onto the BaseWorker are passed onto fn + """ + + def __init__(self, fn: Callable, *args, **kwargs): super(BaseWorker, self).__init__() self.fn = fn self.args = args self.kwargs = kwargs - self.signals = WorkerSignal() + self.signals = BaseWorkerSignal() @pyqtSlot() def run(self): output = self.fn(*self.args, **self.kwargs) self.signals.result.emit(output) self.signals.finished.emit() - - -class WorkerSignal(QObject): - finished = pyqtSignal() - result = pyqtSignal(object) diff --git a/app/components/services/workers/signal.py b/app/components/services/workers/signal.py new file mode 100644 index 0000000..a13446a --- /dev/null +++ b/app/components/services/workers/signal.py @@ -0,0 +1,32 @@ +""" +Poricom Services + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (pyqtSignal, QObject) + + +class BaseWorkerSignal(QObject): + """Base signal object + + Signals: + finished: Emit when thread finished the task + result: Emit the result of the task + """ + + finished = pyqtSignal() + result = pyqtSignal(object) From 845f40c67ad2ff6e8598edbddeb2ba2928adcb32 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 17:54:33 +0800 Subject: [PATCH 023/137] Refactor toolbar file structure --- app/MainWindow.py | 4 +- app/components/toolbar/__init__.py | 19 ++++ app/components/toolbar/base.py | 47 +++++++++ app/components/toolbar/tabs/__init__.py | 20 ++++ .../toolbar/tabs/base.py} | 95 ++++++++----------- app/components/toolbar/tabs/navigate.py | 50 ++++++++++ 6 files changed, 175 insertions(+), 60 deletions(-) create mode 100644 app/components/toolbar/__init__.py create mode 100644 app/components/toolbar/base.py create mode 100644 app/components/toolbar/tabs/__init__.py rename app/{Ribbon.py => components/toolbar/tabs/base.py} (50%) create mode 100644 app/components/toolbar/tabs/navigate.py diff --git a/app/MainWindow.py b/app/MainWindow.py index fa82d3a..96b5bd4 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -29,7 +29,7 @@ from utils.image_io import mangaFileToImageDir from utils.config import config, saveOnClose from components.services import BaseWorker -from Ribbon import (Ribbon) +from components.toolbar import BaseToolbar from Explorers import (ImageExplorer) from Views import (OCRCanvas, FullScreen) from Popups import (FontPicker, LanguagePicker, ScaleImagePicker, @@ -54,7 +54,7 @@ def __init__(self, parent=None, tracker=None): self.config = config self.vLayout = QVBoxLayout() - self.ribbon = Ribbon(self, self.tracker) + self.ribbon = BaseToolbar(self, self.tracker) self.vLayout.addWidget(self.ribbon) self.canvas = OCRCanvas(self, self.tracker) self.explorer = ImageExplorer(self, self.tracker) diff --git a/app/components/toolbar/__init__.py b/app/components/toolbar/__init__.py new file mode 100644 index 0000000..f5d9ef7 --- /dev/null +++ b/app/components/toolbar/__init__.py @@ -0,0 +1,19 @@ +""" +Poricom Toolbar +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BaseToolbar diff --git a/app/components/toolbar/base.py b/app/components/toolbar/base.py new file mode 100644 index 0000000..d167b1f --- /dev/null +++ b/app/components/toolbar/base.py @@ -0,0 +1,47 @@ +""" +Poricom Toolbar + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from os.path import exists + +from PyQt5.QtGui import (QIcon) +from PyQt5.QtCore import (Qt, QSize) +from PyQt5.QtWidgets import ( + QGridLayout, QHBoxLayout, QWidget, QTabWidget, QPushButton) + +from .tabs import BaseToolbarTab, NavigateToolbarContainer +from utils.config import config + + +class BaseToolbar(QTabWidget): + def __init__(self, parent=None, tracker=None): + super(QTabWidget, self).__init__(parent) + self.parent = parent + self.tracker = tracker + + h = self.parent.frameGeometry().height( + ) * config["TBAR_ISIZE_REL"] * config["RBN_HEIGHT"] + self.setFixedHeight(h) + + for tabName, tools in config["TBAR_FUNCS"].items(): + tab = BaseToolbarTab(parent=self.parent, funcs=tools, + tracker=self.tracker, tabName=tabName) + tab.layout().addStretch() + tab.layout().addWidget( + NavigateToolbarContainer(self.parent, self.tracker)) + self.addTab(tab, tabName) diff --git a/app/components/toolbar/tabs/__init__.py b/app/components/toolbar/tabs/__init__.py new file mode 100644 index 0000000..8dc745e --- /dev/null +++ b/app/components/toolbar/tabs/__init__.py @@ -0,0 +1,20 @@ +""" +Poricom Toolbar +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BaseToolbarTab +from .navigate import NavigateToolbarContainer diff --git a/app/Ribbon.py b/app/components/toolbar/tabs/base.py similarity index 50% rename from app/Ribbon.py rename to app/components/toolbar/tabs/base.py index 73c6669..bbece5f 100644 --- a/app/Ribbon.py +++ b/app/components/toolbar/tabs/base.py @@ -1,5 +1,5 @@ """ -Poricom Ribbon Components +Poricom Toolbar Copyright (C) `2021-2022` `` @@ -19,42 +19,53 @@ from os.path import exists -from PyQt5.QtGui import (QIcon) from PyQt5.QtCore import (Qt, QSize) -from PyQt5.QtWidgets import ( - QGridLayout, QHBoxLayout, QWidget, QTabWidget, QPushButton) +from PyQt5.QtGui import (QIcon) +from PyQt5.QtWidgets import (QHBoxLayout, QMainWindow, QPushButton, QWidget) from utils.config import config - -class RibbonTab(QWidget): - - def __init__(self, parent=None, funcs=None, tracker=None, tabName=""): +# TODO: Refactor this as a container +class BaseToolbarTab(QWidget): + """Widget that contains the toolbar functions + + Args: + parent (QWidget, optional): Toolbar tab parent. Set to main window. + funcs (Any, optional): Toolbar function configuration. Defaults to {}. + tracker (Any, optional): State tracker. Defaults to None. + tabName (str, optional): Toolbar tab name. Defaults to "". + """ + def __init__(self, parent: QMainWindow, funcs={}, tracker=None, tabName=""): + # TODO: Add type to funcs and tracker + # TODO: tracker and tabName might not be needed super(QWidget, self).__init__() - self.parent = parent + + # Manually set parent since `addTab` method will reparent the widget + self.mainWindow = parent self.tracker = tracker self.tabName = tabName - self.buttonList = [] - self.layout = QHBoxLayout(self) - self.layout.setAlignment(Qt.AlignLeft) + self.buttonList: list[QPushButton] = [] + self.setLayout(QHBoxLayout()) + # self.layout().setAlignment(Qt.AlignLeft) - self.initButtons(funcs) + self.initializeButtons(funcs) - def initButtons(self, funcs): + def initializeButtons(self, funcs): for funcName, funcConfig in funcs.items(): self.loadButtonConfig(funcName, funcConfig) - self.layout.addWidget(self.buttonList[-1], + # TODO: Alignment might be obsolete + self.layout().addWidget(self.buttonList[-1], alignment=getattr(Qt, funcConfig["align"])) - self.layout.addStretch() - self.layout.addWidget(PageNavigator(self.parent)) + # self.layout.addStretch() + # self.layout.addWidget(PageNavigator(self.mainWindow)) def loadButtonConfig(self, buttonName, buttonConfig): - - w = self.parent.frameGeometry().height( + # TODO: Base icon size on screen height instead of parent height + w = self.mainWindow.frameGeometry().height( )*config["TBAR_ISIZE_REL"]*buttonConfig["iconW"] - h = self.parent.frameGeometry().height( + h = self.mainWindow.frameGeometry().height( )*config["TBAR_ISIZE_REL"]*buttonConfig["iconH"] m = config["TBAR_ISIZE_MARGIN"] @@ -66,10 +77,13 @@ def loadButtonConfig(self, buttonName, buttonConfig): icon = QIcon(config["TBAR_ICON_DEFAULT"]) self.buttonList.append(QPushButton(self)) + + # Allows to programmatically interact with buttons self.buttonList[-1].setObjectName(buttonName) self.buttonList[-1].setIcon(icon) self.buttonList[-1].setIconSize(QSize(w, h)) + # TODO: Do not set fixed size self.buttonList[-1].setFixedSize(QSize(w*m, h*m)) tooltip = f"

{buttonConfig['helpTitle']}\ @@ -77,44 +91,9 @@ def loadButtonConfig(self, buttonName, buttonConfig): self.buttonList[-1].setToolTip(tooltip) self.buttonList[-1].setCheckable(buttonConfig["toggle"]) - if hasattr(self.parent, buttonName): + if hasattr(self.mainWindow, buttonName): self.buttonList[-1].clicked.connect( - getattr(self.parent, buttonName)) + getattr(self.mainWindow, buttonName)) else: self.buttonList[-1].clicked.connect( - getattr(self.parent, 'poricomNoop')) - - -class PageNavigator(RibbonTab): - - def __init__(self, parent=None, tracker=None): - super(QWidget, self).__init__() - self.parent = parent - self.tracker = tracker - self.buttonList = [] - - self.layout = QGridLayout(self) - self.layout.setContentsMargins(0, 0, 0, 0) - for funcName, funcConfig in config["MODE_FUNCS"].items(): - self.loadButtonConfig(funcName, funcConfig) - - self.layout.addWidget(self.buttonList[0], 0, 0, 1, 1) - self.layout.addWidget(self.buttonList[1], 1, 0, 1, 1) - self.layout.addWidget(self.buttonList[2], 0, 1, 1, 2) - self.layout.addWidget(self.buttonList[3], 1, 1, 1, 1) - self.layout.addWidget(self.buttonList[4], 1, 2, 1, 1) - - -class Ribbon(QTabWidget): - def __init__(self, parent=None, tracker=None): - super(QTabWidget, self).__init__(parent) - self.parent = parent - self.tracker = tracker - - h = self.parent.frameGeometry().height( - ) * config["TBAR_ISIZE_REL"] * config["RBN_HEIGHT"] - self.setFixedHeight(h) - - for tabName, tools in config["TBAR_FUNCS"].items(): - self.addTab(RibbonTab(parent=self.parent, funcs=tools, - tracker=self.tracker, tabName=tabName), tabName) + getattr(self.mainWindow, 'poricomNoop')) diff --git a/app/components/toolbar/tabs/navigate.py b/app/components/toolbar/tabs/navigate.py new file mode 100644 index 0000000..f5060c6 --- /dev/null +++ b/app/components/toolbar/tabs/navigate.py @@ -0,0 +1,50 @@ +""" +Poricom Toolbar + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtWidgets import ( + QGridLayout, QPushButton) + +from .base import BaseToolbarTab +from utils.config import config + +class NavigateToolbarContainer(BaseToolbarTab): + """Widget that contains the toolbar navigation functions + + Args: + parent (QWidget, optional): Toolbar tab parent. Set to main window. Defaults to None. + tracker (Any, optional): State tracker. Defaults to None. + """ + + def __init__(self, parent=None, tracker=None): + super().__init__(parent) + self.parent = parent + self.tracker = tracker + self.buttonList: QPushButton = [] + + self.layout = QGridLayout(self) + self.layout.setContentsMargins(0, 0, 0, 0) + + for funcName, funcConfig in config["MODE_FUNCS"].items(): + self.loadButtonConfig(funcName, funcConfig) + + self.layout.addWidget(self.buttonList[0], 0, 0, 1, 1) + self.layout.addWidget(self.buttonList[1], 1, 0, 1, 1) + self.layout.addWidget(self.buttonList[2], 0, 1, 1, 2) + self.layout.addWidget(self.buttonList[3], 1, 1, 1, 1) + self.layout.addWidget(self.buttonList[4], 1, 2, 1, 1) From 6c9d770cf26fd345b4c60bdb2457310643bcc6ac Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 18:05:15 +0800 Subject: [PATCH 024/137] Refactor tab to separate button initialization --- app/MainWindow.py | 2 +- app/components/toolbar/base.py | 23 ++--- app/components/toolbar/tabs/__init__.py | 2 +- app/components/toolbar/tabs/base.py | 79 +++-------------- .../toolbar/tabs/containers/__init__.py | 21 +++++ .../toolbar/tabs/containers/base.py | 86 +++++++++++++++++++ .../toolbar/tabs/containers/navigate.py | 48 +++++++++++ app/components/toolbar/tabs/navigate.py | 50 ----------- 8 files changed, 179 insertions(+), 132 deletions(-) create mode 100644 app/components/toolbar/tabs/containers/__init__.py create mode 100644 app/components/toolbar/tabs/containers/base.py create mode 100644 app/components/toolbar/tabs/containers/navigate.py delete mode 100644 app/components/toolbar/tabs/navigate.py diff --git a/app/MainWindow.py b/app/MainWindow.py index 96b5bd4..7cc425d 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -54,7 +54,7 @@ def __init__(self, parent=None, tracker=None): self.config = config self.vLayout = QVBoxLayout() - self.ribbon = BaseToolbar(self, self.tracker) + self.ribbon = BaseToolbar(self) self.vLayout.addWidget(self.ribbon) self.canvas = OCRCanvas(self, self.tracker) self.explorer = ImageExplorer(self, self.tracker) diff --git a/app/components/toolbar/base.py b/app/components/toolbar/base.py index d167b1f..61b43b2 100644 --- a/app/components/toolbar/base.py +++ b/app/components/toolbar/base.py @@ -17,31 +17,32 @@ along with this program. If not, see . """ -from os.path import exists - -from PyQt5.QtGui import (QIcon) -from PyQt5.QtCore import (Qt, QSize) -from PyQt5.QtWidgets import ( - QGridLayout, QHBoxLayout, QWidget, QTabWidget, QPushButton) +from PyQt5.QtWidgets import (QMainWindow, QTabWidget) from .tabs import BaseToolbarTab, NavigateToolbarContainer from utils.config import config class BaseToolbar(QTabWidget): - def __init__(self, parent=None, tracker=None): + """ + Toolbar widget + + Args: + parent (QWidget, optional): Toolbar parent. Set to main window. + Notes: + Parent must be passed to children to call main window functions. + """ + def __init__(self, parent: QMainWindow): super(QTabWidget, self).__init__(parent) self.parent = parent - self.tracker = tracker h = self.parent.frameGeometry().height( ) * config["TBAR_ISIZE_REL"] * config["RBN_HEIGHT"] self.setFixedHeight(h) for tabName, tools in config["TBAR_FUNCS"].items(): - tab = BaseToolbarTab(parent=self.parent, funcs=tools, - tracker=self.tracker, tabName=tabName) + tab = BaseToolbarTab(parent=self.parent, funcs=tools) tab.layout().addStretch() tab.layout().addWidget( - NavigateToolbarContainer(self.parent, self.tracker)) + NavigateToolbarContainer(self.parent)) self.addTab(tab, tabName) diff --git a/app/components/toolbar/tabs/__init__.py b/app/components/toolbar/tabs/__init__.py index 8dc745e..ab83bc8 100644 --- a/app/components/toolbar/tabs/__init__.py +++ b/app/components/toolbar/tabs/__init__.py @@ -17,4 +17,4 @@ """ from .base import BaseToolbarTab -from .navigate import NavigateToolbarContainer +from .containers import NavigateToolbarContainer diff --git a/app/components/toolbar/tabs/base.py b/app/components/toolbar/tabs/base.py index bbece5f..2290a03 100644 --- a/app/components/toolbar/tabs/base.py +++ b/app/components/toolbar/tabs/base.py @@ -17,83 +17,24 @@ along with this program. If not, see . """ -from os.path import exists +from PyQt5.QtWidgets import (QHBoxLayout, QMainWindow) -from PyQt5.QtCore import (Qt, QSize) -from PyQt5.QtGui import (QIcon) -from PyQt5.QtWidgets import (QHBoxLayout, QMainWindow, QPushButton, QWidget) +from .containers import BaseToolbarContainer -from utils.config import config - -# TODO: Refactor this as a container -class BaseToolbarTab(QWidget): - """Widget that contains the toolbar functions +class BaseToolbarTab(BaseToolbarContainer): + """Widget to contain all toolbar tab functions Args: parent (QWidget, optional): Toolbar tab parent. Set to main window. funcs (Any, optional): Toolbar function configuration. Defaults to {}. - tracker (Any, optional): State tracker. Defaults to None. - tabName (str, optional): Toolbar tab name. Defaults to "". """ - def __init__(self, parent: QMainWindow, funcs={}, tracker=None, tabName=""): - # TODO: Add type to funcs and tracker - # TODO: tracker and tabName might not be needed - super(QWidget, self).__init__() - - # Manually set parent since `addTab` method will reparent the widget - self.mainWindow = parent - self.tracker = tracker - self.tabName = tabName - - self.buttonList: list[QPushButton] = [] - self.setLayout(QHBoxLayout()) - # self.layout().setAlignment(Qt.AlignLeft) + def __init__(self, parent: QMainWindow, funcs={}): + super().__init__(parent) self.initializeButtons(funcs) def initializeButtons(self, funcs): - - for funcName, funcConfig in funcs.items(): - self.loadButtonConfig(funcName, funcConfig) - # TODO: Alignment might be obsolete - self.layout().addWidget(self.buttonList[-1], - alignment=getattr(Qt, funcConfig["align"])) - # self.layout.addStretch() - # self.layout.addWidget(PageNavigator(self.mainWindow)) - - def loadButtonConfig(self, buttonName, buttonConfig): - # TODO: Base icon size on screen height instead of parent height - w = self.mainWindow.frameGeometry().height( - )*config["TBAR_ISIZE_REL"]*buttonConfig["iconW"] - h = self.mainWindow.frameGeometry().height( - )*config["TBAR_ISIZE_REL"]*buttonConfig["iconH"] - m = config["TBAR_ISIZE_MARGIN"] - - icon = QIcon() - path = config["TBAR_ICONS"] + buttonConfig["path"] - if (exists(path)): - icon = QIcon(path) - else: - icon = QIcon(config["TBAR_ICON_DEFAULT"]) - - self.buttonList.append(QPushButton(self)) - - # Allows to programmatically interact with buttons - self.buttonList[-1].setObjectName(buttonName) - - self.buttonList[-1].setIcon(icon) - self.buttonList[-1].setIconSize(QSize(w, h)) - # TODO: Do not set fixed size - self.buttonList[-1].setFixedSize(QSize(w*m, h*m)) - - tooltip = f"

{buttonConfig['helpTitle']}\ -

{buttonConfig['helpMsg']}

" - self.buttonList[-1].setToolTip(tooltip) - self.buttonList[-1].setCheckable(buttonConfig["toggle"]) - - if hasattr(self.mainWindow, buttonName): - self.buttonList[-1].clicked.connect( - getattr(self.mainWindow, buttonName)) - else: - self.buttonList[-1].clicked.connect( - getattr(self.mainWindow, 'poricomNoop')) + self.setLayout(QHBoxLayout()) + for name, config in funcs.items(): + self.initializeButton(name, config) + self.layout().addWidget(self.buttonList[-1]) diff --git a/app/components/toolbar/tabs/containers/__init__.py b/app/components/toolbar/tabs/containers/__init__.py new file mode 100644 index 0000000..09f3348 --- /dev/null +++ b/app/components/toolbar/tabs/containers/__init__.py @@ -0,0 +1,21 @@ +""" +Poricom Toolbar + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BaseToolbarContainer +from .navigate import NavigateToolbarContainer diff --git a/app/components/toolbar/tabs/containers/base.py b/app/components/toolbar/tabs/containers/base.py new file mode 100644 index 0000000..09ba4de --- /dev/null +++ b/app/components/toolbar/tabs/containers/base.py @@ -0,0 +1,86 @@ +""" +Poricom Toolbar + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from os.path import exists + +from PyQt5.QtCore import (QSize) +from PyQt5.QtGui import (QIcon) +from PyQt5.QtWidgets import (QMainWindow, QPushButton, QWidget) + +# TODO: config should be a constant +from utils.config import config + +class BaseToolbarContainer(QWidget): + """Widget that contains the toolbar functions + + Args: + parent (QMainWindow, optional): Container parent. Set to main window. + """ + def __init__(self, parent: QMainWindow): + super().__init__(parent) + + # Manually set parent since `addTab` method will reparent the widget + self.mainWindow = parent + self.buttonList: list[QPushButton] = [] + + def addButton(self): + """Adds a QPushButton object to `buttonList` + + Returns: + QPushButton: Recently added QPushButton + """ + self.buttonList.append(QPushButton(self)) + return self.buttonList[-1] + + def initializeButton(self, buttonName, buttonConfig): + # TODO: Base icon size on screen height instead of parent height + w = self.mainWindow.frameGeometry().height( + )*config["TBAR_ISIZE_REL"]*buttonConfig["iconW"] + h = self.mainWindow.frameGeometry().height( + )*config["TBAR_ISIZE_REL"]*buttonConfig["iconH"] + m = config["TBAR_ISIZE_MARGIN"] + + icon = QIcon() + path = config["TBAR_ICONS"] + buttonConfig["path"] + if (exists(path)): + icon = QIcon(path) + else: + icon = QIcon(config["TBAR_ICON_DEFAULT"]) + + button = self.addButton() + + # Allows to programmatically interact with buttons + button.setObjectName(buttonName) + + button.setIcon(icon) + button.setIconSize(QSize(w, h)) + # TODO: Do not set fixed size + button.setFixedSize(QSize(w*m, h*m)) + + tooltip = f"

{buttonConfig['helpTitle']}\ +

{buttonConfig['helpMsg']}

" + button.setToolTip(tooltip) + button.setCheckable(buttonConfig["toggle"]) + + if hasattr(self.mainWindow, buttonName): + button.clicked.connect( + getattr(self.mainWindow, buttonName)) + else: + button.clicked.connect( + getattr(self.mainWindow, 'poricomNoop')) diff --git a/app/components/toolbar/tabs/containers/navigate.py b/app/components/toolbar/tabs/containers/navigate.py new file mode 100644 index 0000000..d29e4a1 --- /dev/null +++ b/app/components/toolbar/tabs/containers/navigate.py @@ -0,0 +1,48 @@ +""" +Poricom Toolbar + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtWidgets import (QGridLayout, QMainWindow) + +from .base import BaseToolbarContainer +from utils.config import config + +class NavigateToolbarContainer(BaseToolbarContainer): + """Widget that contains the toolbar navigation functions + + Args: + parent (QWidget, optional): Container parent. Set to main window. + """ + + def __init__(self, parent: QMainWindow): + super().__init__(parent) + + self.initializeButtons() + + def initializeButtons(self): + self.setLayout(QGridLayout()) + self.layout().setContentsMargins(0, 0, 0, 0) + + for funcName, funcConfig in config["MODE_FUNCS"].items(): + self.initializeButton(funcName, funcConfig) + + self.layout().addWidget(self.buttonList[0], 0, 0, 1, 1) + self.layout().addWidget(self.buttonList[1], 1, 0, 1, 1) + self.layout().addWidget(self.buttonList[2], 0, 1, 1, 2) + self.layout().addWidget(self.buttonList[3], 1, 1, 1, 1) + self.layout().addWidget(self.buttonList[4], 1, 2, 1, 1) diff --git a/app/components/toolbar/tabs/navigate.py b/app/components/toolbar/tabs/navigate.py deleted file mode 100644 index f5060c6..0000000 --- a/app/components/toolbar/tabs/navigate.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Poricom Toolbar - -Copyright (C) `2021-2022` `` - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -from PyQt5.QtWidgets import ( - QGridLayout, QPushButton) - -from .base import BaseToolbarTab -from utils.config import config - -class NavigateToolbarContainer(BaseToolbarTab): - """Widget that contains the toolbar navigation functions - - Args: - parent (QWidget, optional): Toolbar tab parent. Set to main window. Defaults to None. - tracker (Any, optional): State tracker. Defaults to None. - """ - - def __init__(self, parent=None, tracker=None): - super().__init__(parent) - self.parent = parent - self.tracker = tracker - self.buttonList: QPushButton = [] - - self.layout = QGridLayout(self) - self.layout.setContentsMargins(0, 0, 0, 0) - - for funcName, funcConfig in config["MODE_FUNCS"].items(): - self.loadButtonConfig(funcName, funcConfig) - - self.layout.addWidget(self.buttonList[0], 0, 0, 1, 1) - self.layout.addWidget(self.buttonList[1], 1, 0, 1, 1) - self.layout.addWidget(self.buttonList[2], 0, 1, 1, 2) - self.layout.addWidget(self.buttonList[3], 1, 1, 1, 1) - self.layout.addWidget(self.buttonList[4], 1, 2, 1, 1) From e673e2d35c7f9e3fbd18748f1d329cde85afbc41 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 19:01:35 +0800 Subject: [PATCH 025/137] Refactor toolbar to inherit screen aware widget Allow relative instead of fixed sizing --- app/components/misc/__init__.py | 20 ++ app/components/misc/screenAware.py | 37 ++++ app/components/toolbar/base.py | 15 +- .../toolbar/tabs/containers/base.py | 51 ++--- .../toolbar/tabs/containers/navigate.py | 6 +- app/utils/__init__.py | 17 ++ app/utils/constants.py | 205 ++++++++++++++++++ app/utils/types.py | 30 +++ 8 files changed, 342 insertions(+), 39 deletions(-) create mode 100644 app/components/misc/__init__.py create mode 100644 app/components/misc/screenAware.py create mode 100644 app/utils/__init__.py create mode 100644 app/utils/constants.py create mode 100644 app/utils/types.py diff --git a/app/components/misc/__init__.py b/app/components/misc/__init__.py new file mode 100644 index 0000000..f9553f0 --- /dev/null +++ b/app/components/misc/__init__.py @@ -0,0 +1,20 @@ +""" +Poricom Misc Components + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .screenAware import ScreenAwareWidget diff --git a/app/components/misc/screenAware.py b/app/components/misc/screenAware.py new file mode 100644 index 0000000..2b4372a --- /dev/null +++ b/app/components/misc/screenAware.py @@ -0,0 +1,37 @@ +""" +Poricom Screen Aware Component + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtWidgets import (QApplication, QWidget) + + +class ScreenAwareWidget(QWidget): + """ + Screen-aware widget. Allows retrieving desktop screen dimensions + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def primaryScreen(self): + return QApplication.primaryScreen() + + def primaryScreenWidth(self): + return self.primaryScreen().geometry().width() + + def primaryScreenHeight(self): + return self.primaryScreen().geometry().height() diff --git a/app/components/toolbar/base.py b/app/components/toolbar/base.py index 61b43b2..cacaed2 100644 --- a/app/components/toolbar/base.py +++ b/app/components/toolbar/base.py @@ -17,11 +17,10 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QMainWindow, QTabWidget) +from PyQt5.QtWidgets import (QMainWindow, QSizePolicy, QTabWidget) from .tabs import BaseToolbarTab, NavigateToolbarContainer -from utils.config import config - +from utils.constants import TOOLBAR_FUNCTIONS class BaseToolbar(QTabWidget): """ @@ -36,13 +35,11 @@ def __init__(self, parent: QMainWindow): super(QTabWidget, self).__init__(parent) self.parent = parent - h = self.parent.frameGeometry().height( - ) * config["TBAR_ISIZE_REL"] * config["RBN_HEIGHT"] - self.setFixedHeight(h) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - for tabName, tools in config["TBAR_FUNCS"].items(): - tab = BaseToolbarTab(parent=self.parent, funcs=tools) + for tabName, funcs in TOOLBAR_FUNCTIONS.items(): + tab = BaseToolbarTab(parent=self.parent, funcs=funcs) tab.layout().addStretch() tab.layout().addWidget( NavigateToolbarContainer(self.parent)) - self.addTab(tab, tabName) + self.addTab(tab, tabName.upper()) diff --git a/app/components/toolbar/tabs/containers/base.py b/app/components/toolbar/tabs/containers/base.py index 09ba4de..56b9e62 100644 --- a/app/components/toolbar/tabs/containers/base.py +++ b/app/components/toolbar/tabs/containers/base.py @@ -21,12 +21,13 @@ from PyQt5.QtCore import (QSize) from PyQt5.QtGui import (QIcon) -from PyQt5.QtWidgets import (QMainWindow, QPushButton, QWidget) +from PyQt5.QtWidgets import (QMainWindow, QPushButton) -# TODO: config should be a constant -from utils.config import config +from components.misc import ScreenAwareWidget +from utils.constants import TOOLBAR_ICON_DEFAULT, TOOLBAR_ICON_SIZE, TOOLBAR_ICONS +from utils.types import ButtonConfig -class BaseToolbarContainer(QWidget): +class BaseToolbarContainer(ScreenAwareWidget): """Widget that contains the toolbar functions Args: @@ -48,39 +49,35 @@ def addButton(self): self.buttonList.append(QPushButton(self)) return self.buttonList[-1] - def initializeButton(self, buttonName, buttonConfig): - # TODO: Base icon size on screen height instead of parent height - w = self.mainWindow.frameGeometry().height( - )*config["TBAR_ISIZE_REL"]*buttonConfig["iconW"] - h = self.mainWindow.frameGeometry().height( - )*config["TBAR_ISIZE_REL"]*buttonConfig["iconH"] - m = config["TBAR_ISIZE_MARGIN"] - - icon = QIcon() - path = config["TBAR_ICONS"] + buttonConfig["path"] - if (exists(path)): - icon = QIcon(path) - else: - icon = QIcon(config["TBAR_ICON_DEFAULT"]) - + def initializeButton(self, name: str, config: ButtonConfig): button = self.addButton() # Allows to programmatically interact with buttons - button.setObjectName(buttonName) + button.setObjectName(name) + # Set button icon and size + path = TOOLBAR_ICONS + config["path"] + if (exists(path)): + icon = QIcon(path) + else: + icon = QIcon(TOOLBAR_ICON_DEFAULT) button.setIcon(icon) + w = self.primaryScreenHeight()*TOOLBAR_ICON_SIZE*config["iconWidth"] + h = self.primaryScreenHeight()*TOOLBAR_ICON_SIZE*config["iconHeight"] button.setIconSize(QSize(w, h)) - # TODO: Do not set fixed size - button.setFixedSize(QSize(w*m, h*m)) - tooltip = f"

{buttonConfig['helpTitle']}\ -

{buttonConfig['helpMsg']}

" + tooltip = f"\ +

{config['title']}

\ +

{config['message']}

\ + " button.setToolTip(tooltip) - button.setCheckable(buttonConfig["toggle"]) - if hasattr(self.mainWindow, buttonName): + button.setCheckable(config["toggle"]) + + # Connect button to main window function + if hasattr(self.mainWindow, name): button.clicked.connect( - getattr(self.mainWindow, buttonName)) + getattr(self.mainWindow, name)) else: button.clicked.connect( getattr(self.mainWindow, 'poricomNoop')) diff --git a/app/components/toolbar/tabs/containers/navigate.py b/app/components/toolbar/tabs/containers/navigate.py index d29e4a1..3e985af 100644 --- a/app/components/toolbar/tabs/containers/navigate.py +++ b/app/components/toolbar/tabs/containers/navigate.py @@ -20,7 +20,7 @@ from PyQt5.QtWidgets import (QGridLayout, QMainWindow) from .base import BaseToolbarContainer -from utils.config import config +from utils.constants import NAVIGATION_FUNCTIONS class NavigateToolbarContainer(BaseToolbarContainer): """Widget that contains the toolbar navigation functions @@ -38,8 +38,8 @@ def initializeButtons(self): self.setLayout(QGridLayout()) self.layout().setContentsMargins(0, 0, 0, 0) - for funcName, funcConfig in config["MODE_FUNCS"].items(): - self.initializeButton(funcName, funcConfig) + for name, config in NAVIGATION_FUNCTIONS.items(): + self.initializeButton(name, config) self.layout().addWidget(self.buttonList[0], 0, 0, 1, 1) self.layout().addWidget(self.buttonList[1], 1, 0, 1, 1) diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..0e99b78 --- /dev/null +++ b/app/utils/__init__.py @@ -0,0 +1,17 @@ +""" +Poricom Utilities +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" diff --git a/app/utils/constants.py b/app/utils/constants.py new file mode 100644 index 0000000..7be1a43 --- /dev/null +++ b/app/utils/constants.py @@ -0,0 +1,205 @@ +""" +Poricom Constants +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .types import ButtonConfigDict + +# ------------------------------------- General ------------------------------------- # + +# Paths +TOOLBAR_ICONS = './assets/images/icons/' +TOOLBAR_ICON_DEFAULT = './assets/images/icons/default_icon.png' + +# --------------------------------------- UI ---------------------------------------- # + +# Toolbar +TOOLBAR_ICON_SIZE = 0.05 # Fraction of primary screen height + +NAVIGATION_FUNCTIONS: ButtonConfigDict = { + "zoomIn": { + "title": "Zoom in", + "message": "Hint: Double click the image to reset zoom.", + "path": "zoomIn.png", + "toggle": False, + "align": "AlignRight", + "iconHeight": 0.45, + "iconWidth": 0.45 + }, + "zoomOut": { + "title": "Zoom out", + "message": "Hint: Double click the image to reset zoom.", + "path": "zoomOut.png", + "toggle": False, + "align": "AlignRight", + "iconHeight": 0.45, + "iconWidth": 0.45 + }, + "loadImageAtIndex": { + "title": "", + "message": "Jump to page", + "path": "loadImageAtIndex.png", + "toggle": False, + "align": "AlignRight", + "iconHeight": 0.45, + "iconWidth": 1.3 + }, + "loadPrevImage": { + "title": "", + "message": "Show previous image", + "path": "loadPrevImage.png", + "toggle": False, + "align": "AlignRight", + "iconHeight": 0.45, + "iconWidth": 0.6 + }, + "loadNextImage": { + "title": "", + "message": "Show next image", + "path": "loadNextImage.png", + "toggle": False, + "align": "AlignRight", + "iconHeight": 0.45, + "iconWidth": 0.6 + } +} +TOOLBAR_FUNCTIONS: dict[str, ButtonConfigDict] = { + "file": { + "openDir": { + "title": "Open manga directory", + "message": "Open a directory containing images.", + "path": "openDir.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "openManga": { + "title": "Open manga file", + "message": "Supports the following formats: cbr, cbz, pdf.", + "path": "openManga.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "captureExternalHelper": { + "title": "External capture", + "message": "This will minimize the app and perform OCR on the current screen. Alternatively, you may use the shortcut Alt+Q.", + "path": "captureExternalHelper.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + } + }, + "view": { + "toggleStylesheet": { + "title": "Change theme", + "message": "Switch between light and dark mode.", + "path": "toggleStylesheet.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "hideExplorer": { + "title": "Hide explorer", + "message": "Hide the file explorer from view", + "path": "hideExplorer.png", + "toggle": True, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "modifyFontSettings": { + "title": "Modify preview text", + "message": "Change font style and font size of preview text.", + "path": "modifyFontSettings.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "toggleSplitView": { + "title": "Turn on split view", + "message": "View two images at once.", + "path": "toggleSplitView.png", + "toggle": True, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "scaleImage": { + "title": "Adjust image scaling", + "message": "Fit an image according to the available options: fit to width, fit to height, fit to screen", + "path": "scaleImage.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + } + }, + "controls": { + "toggleMouseMode": { + "title": "Change mouse behavior", + "message": "This will disable text detection. Turn this on only if do not want to hold CTRL key to zoom and pan on an image.", + "path": "toggleMouseMode.png", + "toggle": True, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "modifyHotkeys": { + "title": "Remap hotkeys", + "message": "Change shortcut for external captures.", + "path": "modifyHotkeys.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + } + }, + "misc": { + "loadModel": { + "title": "Switch detection model", + "message": "Switch between MangaOCR and Tesseract models.", + "path": "loadModel.png", + "toggle": True, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "modifyTesseract": { + "title": "Tesseract settings", + "message": "Set the language and orientation for the Tesseract model.", + "path": "modifyTesseract.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "toggleLogging": { + "title": "Enable text logging", + "message": "Save detected text to a text file located in the current project directory.", + "path": "toggleLogging.png", + "toggle": True, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + } + } +} diff --git a/app/utils/types.py b/app/utils/types.py new file mode 100644 index 0000000..8db2d63 --- /dev/null +++ b/app/utils/types.py @@ -0,0 +1,30 @@ +""" +Poricom Types +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from typing import TypedDict + +class ButtonConfig(TypedDict): + title: str + message: str + path: str + toggle: bool + align: str + iconHeight: float + iconWidth: float + +ButtonConfigDict = dict[str, ButtonConfig] From cea3c856499ef2dfedda195a8f49f9a6adc88a1d Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 19:07:41 +0800 Subject: [PATCH 026/137] Update types and docstring --- app/components/toolbar/base.py | 2 +- app/components/toolbar/tabs/base.py | 11 ++++++----- app/components/toolbar/tabs/containers/base.py | 2 +- app/components/toolbar/tabs/containers/navigate.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/components/toolbar/base.py b/app/components/toolbar/base.py index cacaed2..73362ca 100644 --- a/app/components/toolbar/base.py +++ b/app/components/toolbar/base.py @@ -27,7 +27,7 @@ class BaseToolbar(QTabWidget): Toolbar widget Args: - parent (QWidget, optional): Toolbar parent. Set to main window. + parent (QMainWindow): Toolbar parent. Set to main window. Notes: Parent must be passed to children to call main window functions. """ diff --git a/app/components/toolbar/tabs/base.py b/app/components/toolbar/tabs/base.py index 2290a03..63df709 100644 --- a/app/components/toolbar/tabs/base.py +++ b/app/components/toolbar/tabs/base.py @@ -20,20 +20,21 @@ from PyQt5.QtWidgets import (QHBoxLayout, QMainWindow) from .containers import BaseToolbarContainer +from utils.types import ButtonConfigDict class BaseToolbarTab(BaseToolbarContainer): - """Widget to contain all toolbar tab functions + """Tab widget to arrange toolbar tab containers Args: - parent (QWidget, optional): Toolbar tab parent. Set to main window. - funcs (Any, optional): Toolbar function configuration. Defaults to {}. + parent (QMainWindow): Toolbar tab parent. Set to main window. + funcs (ButtonConfigDict, optional): Toolbar function configuration. Defaults to {}. """ - def __init__(self, parent: QMainWindow, funcs={}): + def __init__(self, parent: QMainWindow, funcs: ButtonConfigDict={}): super().__init__(parent) self.initializeButtons(funcs) - def initializeButtons(self, funcs): + def initializeButtons(self, funcs: ButtonConfigDict): self.setLayout(QHBoxLayout()) for name, config in funcs.items(): self.initializeButton(name, config) diff --git a/app/components/toolbar/tabs/containers/base.py b/app/components/toolbar/tabs/containers/base.py index 56b9e62..db05f82 100644 --- a/app/components/toolbar/tabs/containers/base.py +++ b/app/components/toolbar/tabs/containers/base.py @@ -31,7 +31,7 @@ class BaseToolbarContainer(ScreenAwareWidget): """Widget that contains the toolbar functions Args: - parent (QMainWindow, optional): Container parent. Set to main window. + parent (QMainWindow): Container parent. Set to main window. """ def __init__(self, parent: QMainWindow): super().__init__(parent) diff --git a/app/components/toolbar/tabs/containers/navigate.py b/app/components/toolbar/tabs/containers/navigate.py index 3e985af..df04d29 100644 --- a/app/components/toolbar/tabs/containers/navigate.py +++ b/app/components/toolbar/tabs/containers/navigate.py @@ -26,7 +26,7 @@ class NavigateToolbarContainer(BaseToolbarContainer): """Widget that contains the toolbar navigation functions Args: - parent (QWidget, optional): Container parent. Set to main window. + parent (QWidget): Container parent. Set to main window. """ def __init__(self, parent: QMainWindow): From 7424254b10ee05646f6f613eb212b5faa39d16c2 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 22:05:15 +0800 Subject: [PATCH 027/137] Refactor explorers file structure --- app/Explorers.py | 89 --------------------- app/MainWindow.py | 4 +- app/components/explorers/__init__.py | 19 +++++ app/components/explorers/image.py | 76 ++++++++++++++++++ app/components/explorers/models/__init__.py | 19 +++++ app/components/explorers/models/image.py | 58 ++++++++++++++ app/utils/constants.py | 4 + 7 files changed, 178 insertions(+), 91 deletions(-) delete mode 100644 app/Explorers.py create mode 100644 app/components/explorers/__init__.py create mode 100644 app/components/explorers/image.py create mode 100644 app/components/explorers/models/__init__.py create mode 100644 app/components/explorers/models/image.py diff --git a/app/Explorers.py b/app/Explorers.py deleted file mode 100644 index 68777fa..0000000 --- a/app/Explorers.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Poricom Explorer Components - -Copyright (C) `2021-2022` `` - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -from PyQt5.QtCore import (Qt, QDir) -from PyQt5.QtWidgets import (QTreeView, QFileSystemModel) - -from utils.config import config - - -class ImageExplorer(QTreeView): - layoutCheck = False - - def __init__(self, parent=None, tracker=None): - super(QTreeView, self).__init__() - self.parent = parent - self.tracker = tracker - - self.model = QFileSystemModel() - # self.model.setFilter(QDir.Files) - self.model.setNameFilterDisables(False) - self.model.setNameFilters(config["IMAGE_EXTENSIONS"]) - self.setModel(self.model) - - for i in range(1, 4): - self.hideColumn(i) - self.setIndentation(0) - - self.setDirectory(tracker.filepath) - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - - def currentChanged(self, current, previous): - if not current.isValid(): - current = self.model.index(self.getTopIndex(), 0, self.rootIndex()) - filename = self.model.fileInfo(current).absoluteFilePath() - nextIndex = self.indexBelow(current) - filenext = self.model.fileInfo(nextIndex).absoluteFilePath() - self.parent.viewImageFromExplorer(filename, filenext) - QTreeView.currentChanged(self, current, previous) - - def getTopIndex(self): - item = self.model.index(0, 0, self.rootIndex()) - if self.model.fileInfo(item).isFile(): - return 0 - - r = self.model.rowCount(self.rootIndex()) // 2 - while True: - item = self.model.index(r, 0, self.rootIndex()) - if not item.isValid(): - break - if self.model.fileInfo(item).isFile(): - r //= 2 - elif not self.model.fileInfo(item).isFile(): - r += 1 - item = self.model.index(r, 0, self.rootIndex()) - if self.model.fileInfo(item).isFile(): - break - return r - - def setTopIndex(self): - topIndex = self.model.index(self.getTopIndex(), 0, self.rootIndex()) - if topIndex.isValid(): - self.setCurrentIndex(topIndex) - if self.layoutCheck: - self.model.layoutChanged.disconnect(self.setTopIndex) - self.layoutCheck = False - else: - if not self.layoutCheck: - self.model.layoutChanged.connect(self.setTopIndex) - self.layoutCheck = True - - def setDirectory(self, path): - self.setRootIndex(self.model.setRootPath(path)) - self.setTopIndex() diff --git a/app/MainWindow.py b/app/MainWindow.py index 7cc425d..28e9699 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -28,9 +28,9 @@ from utils.image_io import mangaFileToImageDir from utils.config import config, saveOnClose +from components.explorers import ImageExplorer from components.services import BaseWorker from components.toolbar import BaseToolbar -from Explorers import (ImageExplorer) from Views import (OCRCanvas, FullScreen) from Popups import (FontPicker, LanguagePicker, ScaleImagePicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) @@ -57,7 +57,7 @@ def __init__(self, parent=None, tracker=None): self.ribbon = BaseToolbar(self) self.vLayout.addWidget(self.ribbon) self.canvas = OCRCanvas(self, self.tracker) - self.explorer = ImageExplorer(self, self.tracker) + self.explorer = ImageExplorer(self, self.tracker.filepath) self.splitter = QSplitter() self.splitter.addWidget(self.explorer) diff --git a/app/components/explorers/__init__.py b/app/components/explorers/__init__.py new file mode 100644 index 0000000..7912357 --- /dev/null +++ b/app/components/explorers/__init__.py @@ -0,0 +1,19 @@ +""" +Poricom Explorers +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .image import ImageExplorer diff --git a/app/components/explorers/image.py b/app/components/explorers/image.py new file mode 100644 index 0000000..2ac7ba8 --- /dev/null +++ b/app/components/explorers/image.py @@ -0,0 +1,76 @@ +""" +Poricom Explorers +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (Qt) +from PyQt5.QtWidgets import (QMainWindow, QTreeView) + +from .models import ImageModel +from utils.constants import EXPLORER_ROOT_DEFAULT + +class ImageExplorer(QTreeView): + """View to allow exploring images + + Args: + parent (QMainWindow): Image explorer parent. Set to main window. + initialDir (str, optional): Initial directory. Defaults to EXPLORER_ROOT_DEFAULT. + """ + def __init__(self, parent: QMainWindow, initialDir: str = EXPLORER_ROOT_DEFAULT): + super().__init__(parent) + # TODO: It might be better if the parent is set to the QSplitter + # Then add property getter methods to main window to access its children + # Manually set parent since `addWidget` method will reparent the widget + self.mainWindow = parent + + self.setModel(ImageModel()) + + for i in range(1, 4): + self.hideColumn(i) + self.setIndentation(0) + + self.layoutCheck = False + self.setDirectory(initialDir) + + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + def currentChanged(self, current, previous): + if not current.isValid(): + current = self.model().index(self.getTopIndex(), 0, self.rootIndex()) + filename = self.model().fileInfo(current).absoluteFilePath() + nextIndex = self.indexBelow(current) + filenext = self.model().fileInfo(nextIndex).absoluteFilePath() + self.mainWindow.viewImageFromExplorer(filename, filenext) + super().currentChanged(current, previous) + + def getTopIndex(self): + return self.model().getTopIndex(self.rootIndex()) + + def setTopIndex(self): + topIndex = self.model().index(self.getTopIndex(), 0, self.rootIndex()) + if topIndex.isValid(): + self.setCurrentIndex(topIndex) + if self.layoutCheck: + self.model().layoutChanged.disconnect(self.setTopIndex) + self.layoutCheck = False + else: + if not self.layoutCheck: + self.model().layoutChanged.connect(self.setTopIndex) + self.layoutCheck = True + + def setDirectory(self, path: str): + self.setRootIndex(self.model().setRootPath(path)) + self.setTopIndex() diff --git a/app/components/explorers/models/__init__.py b/app/components/explorers/models/__init__.py new file mode 100644 index 0000000..2ab9145 --- /dev/null +++ b/app/components/explorers/models/__init__.py @@ -0,0 +1,19 @@ +""" +Poricom Explorers +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .image import ImageModel diff --git a/app/components/explorers/models/image.py b/app/components/explorers/models/image.py new file mode 100644 index 0000000..d676676 --- /dev/null +++ b/app/components/explorers/models/image.py @@ -0,0 +1,58 @@ +""" +Poricom Explorers +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (QModelIndex) +from PyQt5.QtWidgets import (QFileSystemModel) + +from utils.constants import IMAGE_EXTENSIONS + +class ImageModel(QFileSystemModel): + """ + Image model based on the native file system + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setNameFilterDisables(False) + self.setNameFilters(IMAGE_EXTENSIONS) + + def getTopIndex(self, parentIndex: QModelIndex): + """Get the index of the top most file in the view + + Args: + parentIndex (QModelIndex): Root index of the parent view + + Returns: + int: Index of the top most file + """ + item = self.index(0, 0, parentIndex) + if self.fileInfo(item).isFile(): + return 0 + + r = self.rowCount(parentIndex) // 2 + while True: + item = self.index(r, 0, parentIndex) + if not item.isValid(): + break + if self.fileInfo(item).isFile(): + r //= 2 + elif not self.fileInfo(item).isFile(): + r += 1 + item = self.index(r, 0, parentIndex) + if self.fileInfo(item).isFile(): + break + return r diff --git a/app/utils/constants.py b/app/utils/constants.py index 7be1a43..25a2684 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -20,10 +20,14 @@ # ------------------------------------- General ------------------------------------- # +IMAGE_EXTENSIONS = ["*.bmp", "*.gif", "*.jpeg", "*.jpg", "*.pbm", "*.pgm", "*.png", "*.ppm", "*.webp", "*.xbm", "*.xpm",] + # Paths TOOLBAR_ICONS = './assets/images/icons/' TOOLBAR_ICON_DEFAULT = './assets/images/icons/default_icon.png' +EXPLORER_ROOT_DEFAULT = './assets/images/' + # --------------------------------------- UI ---------------------------------------- # # Toolbar From 0729945174b16860579b53f74fc3a2a38e8df095 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 22:50:06 +0800 Subject: [PATCH 028/137] Refactor view file structure --- app/MainWindow.py | 6 +- app/components/views/__init__.py | 21 +++ app/components/views/image/__init__.py | 20 +++ .../views/image/base.py} | 128 +++--------------- app/components/views/ocr/__init__.py | 21 +++ app/components/views/ocr/base.py | 105 ++++++++++++++ app/components/views/ocr/fullscreen.py | 49 +++++++ 7 files changed, 236 insertions(+), 114 deletions(-) create mode 100644 app/components/views/__init__.py create mode 100644 app/components/views/image/__init__.py rename app/{Views.py => components/views/image/base.py} (64%) create mode 100644 app/components/views/ocr/__init__.py create mode 100644 app/components/views/ocr/base.py create mode 100644 app/components/views/ocr/fullscreen.py diff --git a/app/MainWindow.py b/app/MainWindow.py index 28e9699..584f7e6 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -31,7 +31,7 @@ from components.explorers import ImageExplorer from components.services import BaseWorker from components.toolbar import BaseToolbar -from Views import (OCRCanvas, FullScreen) +from components.views import BaseImageView, FullScreenOCRView from Popups import (FontPicker, LanguagePicker, ScaleImagePicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) @@ -56,7 +56,7 @@ def __init__(self, parent=None, tracker=None): self.vLayout = QVBoxLayout() self.ribbon = BaseToolbar(self) self.vLayout.addWidget(self.ribbon) - self.canvas = OCRCanvas(self, self.tracker) + self.canvas = BaseImageView(self, self.tracker) self.explorer = ImageExplorer(self, self.tracker.filepath) self.splitter = QSplitter() @@ -163,7 +163,7 @@ def captureExternal(self): externalWindow.setAttribute(Qt.WA_DeleteOnClose) externalWindow.setCentralWidget( - FullScreen(externalWindow, self.tracker)) + FullScreenOCRView(externalWindow, self.tracker)) fullScreen = externalWindow.centralWidget() screenIndex = fullScreen.getActiveScreenIndex() diff --git a/app/components/views/__init__.py b/app/components/views/__init__.py new file mode 100644 index 0000000..c780e57 --- /dev/null +++ b/app/components/views/__init__.py @@ -0,0 +1,21 @@ +""" +Poricom Views + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .image import BaseImageView +from .ocr import FullScreenOCRView diff --git a/app/components/views/image/__init__.py b/app/components/views/image/__init__.py new file mode 100644 index 0000000..fd0bfe0 --- /dev/null +++ b/app/components/views/image/__init__.py @@ -0,0 +1,20 @@ +""" +Poricom Views + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BaseImageView diff --git a/app/Views.py b/app/components/views/image/base.py similarity index 64% rename from app/Views.py rename to app/components/views/image/base.py index 8c74890..be20caf 100644 --- a/app/Views.py +++ b/app/components/views/image/base.py @@ -1,5 +1,5 @@ """ -Poricom View Components +Poricom Views Copyright (C) `2021-2022` `` @@ -19,116 +19,19 @@ from time import sleep -from PyQt5.QtCore import (Qt, QRect, QRectF, QSize, - QTimer, QThreadPool, pyqtSlot) -from PyQt5.QtWidgets import ( - QApplication, QGraphicsView, QGraphicsScene, QLabel) -from PyQt5.QtGui import QCursor +from PyQt5.QtCore import (Qt, QRect, QRectF, QSize, QThreadPool) +from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView, QMainWindow) +from ..ocr import BaseOCRView from components.services import BaseWorker -from utils.image_io import logText, pixboxToText +# TODO: This should be the other way around. OCRView should inherit from ImageView +class BaseImageView(BaseOCRView): + """ + Base image view to allow view/zoom/pan functions + """ -class BaseCanvas(QGraphicsView): - - def __init__(self, parent=None, tracker=None): - super(QGraphicsView, self).__init__(parent) - self.parent = parent - self.tracker = tracker - - self.timer_ = QTimer() - self.timer_.setInterval(300) - self.timer_.setSingleShot(True) - self.timer_.timeout.connect(self.rubberBandStopped) - - self.canvasText = QLabel("", self, Qt.WindowStaysOnTopHint) - self.canvasText.setWordWrap(True) - self.canvasText.hide() - self.canvasText.setObjectName("canvasText") - - self.scene = QGraphicsScene() - self.setScene(self.scene) - self.pixmap = self.scene.addPixmap(self.tracker.pixImage.scaledToWidth( - self.viewport().geometry().width(), Qt.SmoothTransformation)) - - self.setDragMode(QGraphicsView.RubberBandDrag) - - def mouseMoveEvent(self, event): - rubberBandVisible = not self.rubberBandRect().isNull() - if (event.buttons() & Qt.LeftButton) and rubberBandVisible: - self.timer_.start() - QGraphicsView.mouseMoveEvent(self, event) - - def mouseReleaseEvent(self, event): - logPath = self.tracker.filepath + "/log.txt" - logToFile = self.tracker.writeMode - text = self.canvasText.text() - logText(text, mode=logToFile, path=logPath) - try: - if not self.parent.config["PERSIST_TEXT_MODE"]: - self.canvasText.hide() - except AttributeError: - pass - super().mouseReleaseEvent(event) - - def handleTextResult(self, result): - try: - self.canvasText.setText(result) - except RuntimeError: - pass - - def handleTextFinished(self): - try: - self.canvasText.adjustSize() - except RuntimeError: - pass - - @pyqtSlot() - def rubberBandStopped(self): - - if (self.canvasText.isHidden()): - self.canvasText.setText("") - self.canvasText.adjustSize() - self.canvasText.show() - - lang = self.tracker.language + self.tracker.orientation - pixbox = self.grab(self.rubberBandRect()) - - worker = BaseWorker(pixboxToText, pixbox, lang, self.tracker.ocrModel) - worker.signals.result.connect(self.handleTextResult) - worker.signals.finished.connect(self.handleTextFinished) - self.timer_.timeout.disconnect(self.rubberBandStopped) - worker.signals.finished.connect( - lambda: self.timer_.timeout.connect(self.rubberBandStopped)) - QThreadPool.globalInstance().start(worker) - - -class FullScreen(BaseCanvas): - - def __init__(self, parent=None, tracker=None): - super().__init__(parent, tracker) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - - def takeScreenshot(self, screenIndex): - screen = QApplication.screens()[screenIndex] - s = screen.size() - self.pixmap.setPixmap(screen.grabWindow( - 0).scaled(s.width(), s.height())) - self.scene.setSceneRect(QRectF(self.pixmap.pixmap().rect())) - - def getActiveScreenIndex(self): - cursor = QCursor.pos() - return QApplication.desktop().screenNumber(cursor) - - def mouseReleaseEvent(self, event): - BaseCanvas.mouseReleaseEvent(self, event) - self.parent.close() - - -class OCRCanvas(BaseCanvas): - - def __init__(self, parent=None, tracker=None): + def __init__(self, parent: QMainWindow, tracker=None): super().__init__(parent, tracker) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) @@ -166,6 +69,7 @@ def viewImage(self, factor=1): w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)) self.scene.setSceneRect(QRectF(self.pixmap.pixmap().rect())) + # TODO: Cloe settings component might be useful def setViewImageMode(self, mode): self._viewImageMode = mode self.parent.config["VIEW_IMAGE_MODE"] = mode @@ -198,13 +102,14 @@ def toggleZoomPanMode(self): def resizeEvent(self, event): self.viewImage() - QGraphicsView.resizeEvent(self, event) + super().resizeEvent(event) def wheelEvent(self, event): pressedKey = QApplication.keyboardModifiers() zoomMode = pressedKey == Qt.ControlModifier or self._zoomPanMode # self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) + # TODO: Rewrite individual event handlers as separate functions if zoomMode: if event.angleDelta().y() > 0: isZoomIn = True @@ -268,7 +173,7 @@ def suppressScroll(): return else: self._scrollAtMin += 1 - QGraphicsView.wheelEvent(self, event) + super().wheelEvent(event) def mouseMoveEvent(self, event): pressedKey = QApplication.keyboardModifiers() @@ -279,13 +184,14 @@ def mouseMoveEvent(self, event): else: self.setDragMode(QGraphicsView.RubberBandDrag) - BaseCanvas.mouseMoveEvent(self, event) + super().mouseMoveEvent(event) def mouseDoubleClickEvent(self, event): self.currentScale = 1 self.viewImage(self.currentScale) - QGraphicsView.mouseDoubleClickEvent(self, event) + super().mouseDoubleClickEvent(event) + # TODO: Keyboard shortcuts should be in another class def keyPressEvent(self, event): if event.key() == Qt.Key_Left: self.parent.loadPrevImage() diff --git a/app/components/views/ocr/__init__.py b/app/components/views/ocr/__init__.py new file mode 100644 index 0000000..e26fef0 --- /dev/null +++ b/app/components/views/ocr/__init__.py @@ -0,0 +1,21 @@ +""" +Poricom Views + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BaseOCRView +from .fullscreen import FullScreenOCRView diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py new file mode 100644 index 0000000..191393a --- /dev/null +++ b/app/components/views/ocr/base.py @@ -0,0 +1,105 @@ +""" +Poricom View Components + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (pyqtSlot, Qt, QThreadPool, QTimer) +from PyQt5.QtWidgets import (QGraphicsScene, QGraphicsView, QLabel, QMainWindow) + +from components.services import BaseWorker +from utils.image_io import logText, pixboxToText + + +class BaseOCRView(QGraphicsView): + """Base view with OCR capabilities + + Args: + parent (QMainWindow): View parent. Set to main window + tracker (Any, optional): State tracker. Defaults to None. + """ + def __init__(self, parent: QMainWindow, tracker=None): + # TODO: Remove references to tracker + super().__init__(parent) + self.parent = parent + self.tracker = tracker + + self.timer = QTimer() + self.timer.setInterval(300) + self.timer.setSingleShot(True) + self.timer.timeout.connect(self.rubberBandStopped) + + self.canvasText = QLabel("", self, Qt.WindowStaysOnTopHint) + self.canvasText.setWordWrap(True) + self.canvasText.hide() + self.canvasText.setObjectName("canvasText") + + # TODO: Set scene and pixmap should be on BaseImageView + self.scene = QGraphicsScene() + self.setScene(self.scene) + self.pixmap = self.scene.addPixmap(self.tracker.pixImage.scaledToWidth( + self.viewport().geometry().width(), Qt.SmoothTransformation)) + + self.setDragMode(QGraphicsView.RubberBandDrag) + + def mouseMoveEvent(self, event): + rubberBandVisible = not self.rubberBandRect().isNull() + if (event.buttons() & Qt.LeftButton) and rubberBandVisible: + self.timer.start() + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + logPath = self.tracker.filepath + "/log.txt" + logToFile = self.tracker.writeMode + text = self.canvasText.text() + logText(text, mode=logToFile, path=logPath) + try: + if not self.parent.config["PERSIST_TEXT_MODE"]: + self.canvasText.hide() + except AttributeError: + pass + super().mouseReleaseEvent(event) + + def handleTextResult(self, result): + try: + self.canvasText.setText(result) + except RuntimeError: + pass + + def handleTextFinished(self): + try: + self.canvasText.adjustSize() + except RuntimeError: + pass + + @pyqtSlot() + def rubberBandStopped(self): + + if (self.canvasText.isHidden()): + self.canvasText.setText("") + self.canvasText.adjustSize() + self.canvasText.show() + + lang = self.tracker.language + self.tracker.orientation + pixbox = self.grab(self.rubberBandRect()) + + worker = BaseWorker(pixboxToText, pixbox, lang, self.tracker.ocrModel) + worker.signals.result.connect(self.handleTextResult) + worker.signals.finished.connect(self.handleTextFinished) + self.timer.timeout.disconnect(self.rubberBandStopped) + worker.signals.finished.connect( + lambda: self.timer.timeout.connect(self.rubberBandStopped)) + QThreadPool.globalInstance().start(worker) diff --git a/app/components/views/ocr/fullscreen.py b/app/components/views/ocr/fullscreen.py new file mode 100644 index 0000000..2ea93cb --- /dev/null +++ b/app/components/views/ocr/fullscreen.py @@ -0,0 +1,49 @@ +""" +Poricom Views + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (Qt, QRectF) +from PyQt5.QtWidgets import (QApplication, QMainWindow) +from PyQt5.QtGui import QCursor, QMouseEvent + +from .base import BaseOCRView + + +class FullScreenOCRView(BaseOCRView): + """ + Fullscreen view with OCR capabilities + """ + def __init__(self, parent: QMainWindow, tracker=None): + super().__init__(parent, tracker) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + def takeScreenshot(self, screenIndex: int): + screen = QApplication.screens()[screenIndex] + s = screen.size() + self.pixmap.setPixmap(screen.grabWindow( + 0).scaled(s.width(), s.height())) + self.scene.setSceneRect(QRectF(self.pixmap.pixmap().rect())) + + def getActiveScreenIndex(self): + cursor = QCursor.pos() + return QApplication.desktop().screenNumber(cursor) + + def mouseReleaseEvent(self, event: QMouseEvent): + super().mouseReleaseEvent(event) + self.parent.close() From 9cc597dcf85310a295e76400a12ee006f516de71 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 14 Jan 2023 19:35:08 +0800 Subject: [PATCH 029/137] Update view inheritance tree --- app/components/views/image/base.py | 18 ++++++----- app/components/views/ocr/base.py | 51 +++++++++++++----------------- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index be20caf..61a8837 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -22,17 +22,17 @@ from PyQt5.QtCore import (Qt, QRect, QRectF, QSize, QThreadPool) from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView, QMainWindow) -from ..ocr import BaseOCRView from components.services import BaseWorker -# TODO: This should be the other way around. OCRView should inherit from ImageView -class BaseImageView(BaseOCRView): +class BaseImageView(QGraphicsView): """ Base image view to allow view/zoom/pan functions """ def __init__(self, parent: QMainWindow, tracker=None): - super().__init__(parent, tracker) + super().__init__(parent) + self.parent = parent + self.tracker = tracker self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) @@ -48,9 +48,11 @@ def __init__(self, parent: QMainWindow, tracker=None): self._trackPadAtMax = 0 self._scrollSuppressed = False - self.scene = QGraphicsScene() - self.setScene(self.scene) - self.pixmap = self.scene.addPixmap(self.tracker.pixImage.scaledToWidth( + self.initializePixmapItem() + + def initializePixmapItem(self): + self.setScene(QGraphicsScene()) + self.pixmap = self.scene().addPixmap(self.tracker.pixImage.scaledToWidth( self.viewport().geometry().width(), Qt.SmoothTransformation)) def viewImage(self, factor=1): @@ -67,7 +69,7 @@ def viewImage(self, factor=1): elif self._viewImageMode == 2: self.pixmap.setPixmap(self.tracker.pixImage.scaled( w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)) - self.scene.setSceneRect(QRectF(self.pixmap.pixmap().rect())) + self.scene().setSceneRect(QRectF(self.pixmap.pixmap().rect())) # TODO: Cloe settings component might be useful def setViewImageMode(self, mode): diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 191393a..382f38b 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -18,13 +18,14 @@ """ from PyQt5.QtCore import (pyqtSlot, Qt, QThreadPool, QTimer) -from PyQt5.QtWidgets import (QGraphicsScene, QGraphicsView, QLabel, QMainWindow) +from PyQt5.QtWidgets import (QGraphicsView, QLabel, QMainWindow) +from ..image import BaseImageView from components.services import BaseWorker from utils.image_io import logText, pixboxToText -class BaseOCRView(QGraphicsView): +class BaseOCRView(BaseImageView): """Base view with OCR capabilities Args: @@ -33,9 +34,7 @@ class BaseOCRView(QGraphicsView): """ def __init__(self, parent: QMainWindow, tracker=None): # TODO: Remove references to tracker - super().__init__(parent) - self.parent = parent - self.tracker = tracker + super().__init__(parent, tracker) self.timer = QTimer() self.timer.setInterval(300) @@ -47,32 +46,8 @@ def __init__(self, parent: QMainWindow, tracker=None): self.canvasText.hide() self.canvasText.setObjectName("canvasText") - # TODO: Set scene and pixmap should be on BaseImageView - self.scene = QGraphicsScene() - self.setScene(self.scene) - self.pixmap = self.scene.addPixmap(self.tracker.pixImage.scaledToWidth( - self.viewport().geometry().width(), Qt.SmoothTransformation)) - self.setDragMode(QGraphicsView.RubberBandDrag) - def mouseMoveEvent(self, event): - rubberBandVisible = not self.rubberBandRect().isNull() - if (event.buttons() & Qt.LeftButton) and rubberBandVisible: - self.timer.start() - super().mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - logPath = self.tracker.filepath + "/log.txt" - logToFile = self.tracker.writeMode - text = self.canvasText.text() - logText(text, mode=logToFile, path=logPath) - try: - if not self.parent.config["PERSIST_TEXT_MODE"]: - self.canvasText.hide() - except AttributeError: - pass - super().mouseReleaseEvent(event) - def handleTextResult(self, result): try: self.canvasText.setText(result) @@ -103,3 +78,21 @@ def rubberBandStopped(self): worker.signals.finished.connect( lambda: self.timer.timeout.connect(self.rubberBandStopped)) QThreadPool.globalInstance().start(worker) + + def mouseMoveEvent(self, event): + rubberBandVisible = not self.rubberBandRect().isNull() + if (event.buttons() & Qt.LeftButton) and rubberBandVisible: + self.timer.start() + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + logPath = self.tracker.filepath + "/log.txt" + logToFile = self.tracker.writeMode + text = self.canvasText.text() + logText(text, mode=logToFile, path=logPath) + try: + if not self.parent.config["PERSIST_TEXT_MODE"]: + self.canvasText.hide() + except AttributeError: + pass + super().mouseReleaseEvent(event) From 44ba97b2aa23170e7ac23e1c17cb47fb899dbfd8 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 14 Jan 2023 19:43:29 +0800 Subject: [PATCH 030/137] Integrate explorer and ocr view into main view --- app/MainWindow.py | 87 ++------------- .../toolbar/tabs/containers/base.py | 12 +- app/components/views/__init__.py | 2 +- app/components/views/main.py | 103 ++++++++++++++++++ app/utils/constants.py | 3 + 5 files changed, 127 insertions(+), 80 deletions(-) create mode 100644 app/components/views/main.py diff --git a/app/MainWindow.py b/app/MainWindow.py index 584f7e6..e4c952f 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -24,14 +24,13 @@ from manga_ocr import MangaOcr from PyQt5.QtCore import (Qt, QAbstractNativeEventFilter, QThreadPool) from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QDesktopWidget, QMainWindow, QApplication, - QPushButton, QFileDialog, QInputDialog, QSplitter) + QPushButton, QFileDialog) from utils.image_io import mangaFileToImageDir from utils.config import config, saveOnClose -from components.explorers import ImageExplorer from components.services import BaseWorker from components.toolbar import BaseToolbar -from components.views import BaseImageView, FullScreenOCRView +from components.views import MainView, FullScreenOCRView from Popups import (FontPicker, LanguagePicker, ScaleImagePicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) @@ -54,43 +53,25 @@ def __init__(self, parent=None, tracker=None): self.config = config self.vLayout = QVBoxLayout() + + self.mainView = MainView(self, self.tracker) self.ribbon = BaseToolbar(self) self.vLayout.addWidget(self.ribbon) - self.canvas = BaseImageView(self, self.tracker) - self.explorer = ImageExplorer(self, self.tracker.filepath) - - self.splitter = QSplitter() - self.splitter.addWidget(self.explorer) - self.splitter.addWidget(self.canvas) - self.splitter.setChildrenCollapsible(False) - for i, s in enumerate(config["NAV_VIEW_RATIO"]): - self.splitter.setStretchFactor(i, s) - self.vLayout.addWidget(self.splitter) + self.vLayout.addWidget(self.mainView) _mainWidget = QWidget() _mainWidget.setLayout(self.vLayout) self.setCentralWidget(_mainWidget) self.threadpool = QThreadPool() - def viewImageFromExplorer(self, filename, filenext): - if not self.canvas.splitViewMode(): - self.tracker.pixImage = filename - if self.canvas.splitViewMode(): - self.tracker.pixImage = (filename, filenext) - if not self.tracker.pixImage.isValid(): - return False - self.canvas.resetTransform() - self.canvas.currentScale = 1 - self.canvas.verticalScrollBar().setSliderPosition(0) - self.canvas.viewImage() - self.canvas.setFocus() - return True - - def resizeEvent(self, event): - self.explorer.setMinimumWidth(0.1*self.width()) - self.canvas.setMinimumWidth(0.6*self.width()) - return super().resizeEvent(event) + @property + def canvas(self): + return self.mainView.canvas + + @property + def explorer(self): + return self.mainView.explorer def closeEvent(self, event): try: @@ -328,47 +309,3 @@ def modifyTesseract(self): def toggleLogging(self): self.tracker.switchWriteMode() - -# --------------------------- Always On Functions ---------------------------- # - - def loadPrevImage(self): - index = self.explorer.indexAbove(self.explorer.currentIndex()) - if self.canvas.splitViewMode(): - tempIndex = self.explorer.indexAbove(index) - if tempIndex.isValid(): - index = tempIndex - if (not index.isValid()): - return - self.explorer.setCurrentIndex(index) - - def loadNextImage(self): - index = self.explorer.indexBelow(self.explorer.currentIndex()) - if self.canvas.splitViewMode(): - tempIndex = self.explorer.indexBelow(index) - if tempIndex.isValid(): - index = tempIndex - if (not index.isValid()): - return - self.explorer.setCurrentIndex(index) - - def loadImageAtIndex(self): - rowCount = self.explorer.model.rowCount(self.explorer.rootIndex()) - i, _ = QInputDialog.getInt( - self, - 'Jump to', - f'Enter page number: (max is {rowCount})', - value=-1, - min=1, - max=rowCount, - flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint) - if (i == -1): - return - - index = self.explorer.model.index(i-1, 0, self.explorer.rootIndex()) - self.explorer.setCurrentIndex(index) - - def zoomIn(self): - self.canvas.zoomView(True, usingButton=True) - - def zoomOut(self): - self.canvas.zoomView(False, usingButton=True) diff --git a/app/components/toolbar/tabs/containers/base.py b/app/components/toolbar/tabs/containers/base.py index db05f82..d666c7b 100644 --- a/app/components/toolbar/tabs/containers/base.py +++ b/app/components/toolbar/tabs/containers/base.py @@ -75,9 +75,13 @@ def initializeButton(self, name: str, config: ButtonConfig): button.setCheckable(config["toggle"]) # Connect button to main window function - if hasattr(self.mainWindow, name): + try: button.clicked.connect( getattr(self.mainWindow, name)) - else: - button.clicked.connect( - getattr(self.mainWindow, 'poricomNoop')) + except AttributeError: + try: + button.clicked.connect( + getattr(self.mainWindow.mainView, name)) + except AttributeError: + button.clicked.connect( + getattr(self.mainWindow, 'poricomNoop')) diff --git a/app/components/views/__init__.py b/app/components/views/__init__.py index c780e57..963217c 100644 --- a/app/components/views/__init__.py +++ b/app/components/views/__init__.py @@ -17,5 +17,5 @@ along with this program. If not, see . """ -from .image import BaseImageView +from .main import MainView from .ocr import FullScreenOCRView diff --git a/app/components/views/main.py b/app/components/views/main.py new file mode 100644 index 0000000..1e07012 --- /dev/null +++ b/app/components/views/main.py @@ -0,0 +1,103 @@ +""" +Poricom Main Window Component + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (Qt) +from PyQt5.QtWidgets import (QInputDialog, QMainWindow, QSplitter) + +from .ocr import BaseOCRView +from components.explorers import ImageExplorer +from utils.config import config +from utils.constants import MAIN_VIEW_RATIO + +class MainView(QSplitter): + + def __init__(self, parent: QMainWindow, tracker=None): + super().__init__(parent) + self.tracker = tracker + self.config = config + + self.canvas = BaseOCRView(self, self.tracker) + self.explorer = ImageExplorer(self, self.tracker.filepath) + + self.addWidget(self.explorer) + self.addWidget(self.canvas) + self.setChildrenCollapsible(False) + for i, s in enumerate(MAIN_VIEW_RATIO): + self.setStretchFactor(i, s) + + def viewImageFromExplorer(self, filename, filenext): + if not self.canvas.splitViewMode: + self.tracker.pixImage = filename + if self.canvas.splitViewMode: + self.tracker.pixImage = (filename, filenext) + if not self.tracker.pixImage.isValid(): + return False + self.canvas.resetTransform() + self.canvas.currentScale = 1 + self.canvas.verticalScrollBar().setSliderPosition(0) + self.canvas.viewImage() + # self.canvas.setFocus() + return True + + def loadPrevImage(self): + index = self.explorer.indexAbove(self.explorer.currentIndex()) + if self.canvas.splitViewMode: + tempIndex = self.explorer.indexAbove(index) + if tempIndex.isValid(): + index = tempIndex + if (not index.isValid()): + return + self.explorer.setCurrentIndex(index) + + def loadNextImage(self): + index = self.explorer.indexBelow(self.explorer.currentIndex()) + if self.canvas.splitViewMode: + tempIndex = self.explorer.indexBelow(index) + if tempIndex.isValid(): + index = tempIndex + if (not index.isValid()): + return + self.explorer.setCurrentIndex(index) + + def loadImageAtIndex(self): + rowCount = self.explorer.model().rowCount(self.explorer.rootIndex()) + i, _ = QInputDialog.getInt( + self, + 'Jump to', + f'Enter page number: (max is {rowCount})', + value=-1, + min=1, + max=rowCount, + flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint) + if (i == -1): + return + + index = self.explorer.model().index(i-1, 0, self.explorer.rootIndex()) + self.explorer.setCurrentIndex(index) + + def zoomIn(self): + self.canvas.zoomView(True, usingButton=True) + + def zoomOut(self): + self.canvas.zoomView(False, usingButton=True) + + def resizeEvent(self, event): + self.explorer.setMinimumWidth(0.1*self.width()) + self.canvas.setMinimumWidth(0.6*self.width()) + return super().resizeEvent(event) diff --git a/app/utils/constants.py b/app/utils/constants.py index 25a2684..e40a4d9 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -30,6 +30,9 @@ # --------------------------------------- UI ---------------------------------------- # +# Main view +MAIN_VIEW_RATIO = [1, 9] + # Toolbar TOOLBAR_ICON_SIZE = 0.05 # Fraction of primary screen height From 08110ede7ff0d366c050ba462f4054d2793e8753 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 14 Jan 2023 21:30:29 +0800 Subject: [PATCH 031/137] Update image view to use base settings Remove the need to access parent config --- .gitignore | 5 +- app/MainWindow.py | 4 +- app/components/popups/__init__.py | 20 +++++ app/components/popups/base.py | 43 +++++++++++ app/components/settings/__init__.py | 20 +++++ app/components/settings/base.py | 114 ++++++++++++++++++++++++++++ app/components/views/image/base.py | 58 +++++++------- app/utils/constants.py | 16 ++++ 8 files changed, 251 insertions(+), 29 deletions(-) create mode 100644 app/components/popups/__init__.py create mode 100644 app/components/popups/base.py create mode 100644 app/components/settings/__init__.py create mode 100644 app/components/settings/base.py diff --git a/.gitignore b/.gitignore index b5a6c65..b55b78c 100644 --- a/.gitignore +++ b/.gitignore @@ -143,4 +143,7 @@ dmypy.json cython_debug/ # Documentation -*.mp4 \ No newline at end of file +*.mp4 + +# Settings +*.ini diff --git a/app/MainWindow.py b/app/MainWindow.py index e4c952f..75c168f 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -191,11 +191,11 @@ def modifyFontSettings(self): def toggleSplitView(self): self.canvas.toggleSplitView() - if self.canvas.splitViewMode(): + if self.canvas.splitViewMode: self.canvas.setViewImageMode(2) index = self.explorer.currentIndex() self.explorer.currentChanged(index, index) - elif not self.canvas.splitViewMode(): + elif not self.canvas.splitViewMode: index = self.explorer.currentIndex() self.explorer.currentChanged(index, index) diff --git a/app/components/popups/__init__.py b/app/components/popups/__init__.py new file mode 100644 index 0000000..121e868 --- /dev/null +++ b/app/components/popups/__init__.py @@ -0,0 +1,20 @@ +""" +Poricom Popups + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BasePopup diff --git a/app/components/popups/base.py b/app/components/popups/base.py new file mode 100644 index 0000000..6e9ea48 --- /dev/null +++ b/app/components/popups/base.py @@ -0,0 +1,43 @@ +""" +Cloe Popups + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMessageBox + + +class BasePopup(QMessageBox): + """Base popup object to display info + + Args: + title (str): Text to show on the title bar + message (str): Text to show on the main area + buttons (StandardButtons, optional): Buttons to show below the popup. + Defaults to Ok button + """ + + def __init__( + self, + title: str, + message: str, + buttons: QMessageBox.StandardButtons = QMessageBox.Ok, + *args, + **kwargs + ): + super().__init__(QMessageBox.NoIcon, title, message, buttons, *args, **kwargs) + self.setAttribute(Qt.WA_DeleteOnClose) diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py new file mode 100644 index 0000000..11facfd --- /dev/null +++ b/app/components/settings/__init__.py @@ -0,0 +1,20 @@ +""" +Poricom Settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BaseSettings diff --git a/app/components/settings/base.py b/app/components/settings/base.py new file mode 100644 index 0000000..e053c15 --- /dev/null +++ b/app/components/settings/base.py @@ -0,0 +1,114 @@ +""" +Poricom Settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from typing import Any, Callable + +from PyQt5.QtCore import QSettings +from PyQt5.QtWidgets import QWidget + +from components.popups import BasePopup +from utils.constants import SETTINGS_FILE_DEFAULT + + +class BaseSettings(QWidget): + """Base settings widget to allow save/load/reset of settings + + Args: + parent (QWidget): Parent widget. Set to SettingsMenu object. + file (str): Path to configuration file. Must be in ini format. Defaults to SETTINGS_FILE_DEFAULT. + prefix (str, optional): Text added to the saved property. Defaults to "". + """ + + def __init__(self, parent: QWidget, file: str = SETTINGS_FILE_DEFAULT, prefix: str = ""): + super().__init__(parent) + self.settings = QSettings(file, QSettings.IniFormat) + + # Settings widgets may sometimes share the same configuration file. + # Set the prefix to a unique value to avoid this. + self._prefix = prefix + + self.setDefaults({}) + self.setTypes({}) + + def setDefaults(self, defaults: dict[str, Any]): + """Set the default dictionary + + `self._defaults` contains the default values for ALL properties. + Any property that is saved/loaded from settings should have a default. + Otherwise, the property will not be saved/loaded. + """ + self._defaults = defaults + + def setTypes(self, types: dict[str, Callable]): + """Set the types dictionary + + By default, if the value is a non-QVariant, it is read as a str. + Use `self._types` to set the correct property type. + """ + self._types = types + + def getProperty(self, prop: str): + return getattr(self, prop) + + def setProperty(self, prop: str, value: Any): + try: + t = self._types[prop] + if t == bool: + return setattr(self, prop, value.lower() == "true") + return setattr(self, prop, t(value)) + except KeyError: + return setattr(self, prop, value) + + def addProperty(self, prop: str, value: Any, t: Callable = str): + self._defaults[prop] = value + self._types[prop] = t + + def removeProperty(self, prop: str): + del self._defaults[prop] + del self._types[prop] + + def saveSettings(self, hasMessage=True): + for propName, _ in self._defaults.items(): + self.settings.setValue( + f"{self._prefix}{propName}", self.getProperty(propName) + ) + if hasMessage: + BasePopup("Save Settings", "Configuration has been saved.").exec() + + def loadSettings(self): + for propName, propDefault in self._defaults.items(): + prop = self.settings.value(f"{self._prefix}{propName}", propDefault) + self.setProperty(propName, prop) + + def confirmResetSettings(self): + confirm = BasePopup( + "Reset Settings", + "Are you sure? This will delete the current configuration.", + BasePopup.Ok | BasePopup.Cancel, + ) + response = confirm.exec() + if response == BasePopup.Ok: + self.resetSettings() + + def resetSettings(self): + try: + self.settings.clear() + except Exception: + pass + self.loadSettings() diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index 61a8837..7b92c7c 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -23,8 +23,10 @@ from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView, QMainWindow) from components.services import BaseWorker +from components.settings import BaseSettings +from utils.constants import IMAGE_VIEW_DEFAULT, IMAGE_VIEW_TYPES -class BaseImageView(QGraphicsView): +class BaseImageView(QGraphicsView, BaseSettings): """ Base image view to allow view/zoom/pan functions """ @@ -37,9 +39,6 @@ def __init__(self, parent: QMainWindow, tracker=None): self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self._viewImageMode = parent.config["VIEW_IMAGE_MODE"] - self._splitViewMode = parent.config["SPLIT_VIEW_MODE"] - self._zoomPanMode = False self.currentScale = 1 self._scrollAtMin = 0 @@ -48,8 +47,30 @@ def __init__(self, parent: QMainWindow, tracker=None): self._trackPadAtMax = 0 self._scrollSuppressed = False + self.setDefaults(IMAGE_VIEW_DEFAULT) + self.setTypes(IMAGE_VIEW_TYPES) + self.loadSettings() + self.initializePixmapItem() +# ------------------------------------ Settings ------------------------------------- # + + def setViewImageMode(self, mode: int): + # TODO: This should be an enum not an int + self.setProperty('viewImageMode', mode) + self.saveSettings(hasMessage=False) + self.viewImage() + + def toggleSplitView(self): + self.setProperty('splitViewMode', "false" if self.splitViewMode else "true") + self.saveSettings(hasMessage=False) + + def toggleZoomPanMode(self): + self.setProperty('zoomPanMode', "false" if self.zoomPanMode else "true") + self.saveSettings(hasMessage=False) + +# -------------------------------------- View --------------------------------------- # + def initializePixmapItem(self): self.setScene(QGraphicsScene()) self.pixmap = self.scene().addPixmap(self.tracker.pixImage.scaledToWidth( @@ -60,31 +81,17 @@ def viewImage(self, factor=1): factor = self.currentScale w = factor*self.viewport().geometry().width() h = factor*self.viewport().geometry().height() - if self._viewImageMode == 0: + if self.viewImageMode == 0: self.pixmap.setPixmap( self.tracker.pixImage.scaledToWidth(w, Qt.SmoothTransformation)) - elif self._viewImageMode == 1: + elif self.viewImageMode == 1: self.pixmap.setPixmap( self.tracker.pixImage.scaledToHeight(h, Qt.SmoothTransformation)) - elif self._viewImageMode == 2: + elif self.viewImageMode == 2: self.pixmap.setPixmap(self.tracker.pixImage.scaled( w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)) self.scene().setSceneRect(QRectF(self.pixmap.pixmap().rect())) - # TODO: Cloe settings component might be useful - def setViewImageMode(self, mode): - self._viewImageMode = mode - self.parent.config["VIEW_IMAGE_MODE"] = mode - self.parent.config["SELECTED_INDEX"]['imageScaling'] = mode - self.viewImage() - - def splitViewMode(self): - return self._splitViewMode - - def toggleSplitView(self): - self._splitViewMode = not self._splitViewMode - self.parent.config["SPLIT_VIEW_MODE"] = self._splitViewMode - def zoomView(self, isZoomIn, usingButton=False): factor = 1.1 if usingButton: @@ -99,16 +106,13 @@ def zoomView(self, isZoomIn, usingButton=False): self.currentScale /= factor self.viewImage(self.currentScale) - def toggleZoomPanMode(self): - self._zoomPanMode = not self._zoomPanMode - def resizeEvent(self, event): self.viewImage() super().resizeEvent(event) def wheelEvent(self, event): pressedKey = QApplication.keyboardModifiers() - zoomMode = pressedKey == Qt.ControlModifier or self._zoomPanMode + zoomMode = pressedKey == Qt.ControlModifier or self.zoomPanMode # self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) # TODO: Rewrite individual event handlers as separate functions @@ -179,7 +183,7 @@ def suppressScroll(): def mouseMoveEvent(self, event): pressedKey = QApplication.keyboardModifiers() - panMode = pressedKey == Qt.ControlModifier or self._zoomPanMode + panMode = pressedKey == Qt.ControlModifier or self.zoomPanMode if panMode: self.setDragMode(QGraphicsView.ScrollHandDrag) @@ -193,6 +197,8 @@ def mouseDoubleClickEvent(self, event): self.viewImage(self.currentScale) super().mouseDoubleClickEvent(event) +# ------------------------------------ Shortcut ------------------------------------- # + # TODO: Keyboard shortcuts should be in another class def keyPressEvent(self, event): if event.key() == Qt.Key_Left: diff --git a/app/utils/constants.py b/app/utils/constants.py index e40a4d9..c6ed233 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -28,6 +28,22 @@ EXPLORER_ROOT_DEFAULT = './assets/images/' +# ------------------------------------ Settings ------------------------------------- # + +SETTINGS_FILE_DEFAULT = './utils/poricom-config.ini' + +# View +IMAGE_VIEW_DEFAULT = { + "viewImageMode": 0, + "splitViewMode": "false", + "zoomPanMode": "false" +} +IMAGE_VIEW_TYPES = { + "viewImageMode": int, + "splitViewMode": bool, + "zoomPanMode": bool +} + # --------------------------------------- UI ---------------------------------------- # # Main view From 9466004fc4a92ba24f6d7ecee79477efea5773df Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 00:22:28 +0800 Subject: [PATCH 032/137] Fix issue where external capture is not showing Fix resizeEvent conflict with OCRView, inherits base ocr and image views --- app/components/views/main.py | 4 +-- app/components/views/ocr/__init__.py | 2 +- app/components/views/ocr/base.py | 16 ++++++------ app/components/views/ocr/fullscreen.py | 12 ++++++--- app/components/views/ocr/ocr.py | 34 ++++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 app/components/views/ocr/ocr.py diff --git a/app/components/views/main.py b/app/components/views/main.py index 1e07012..3f35322 100644 --- a/app/components/views/main.py +++ b/app/components/views/main.py @@ -20,7 +20,7 @@ from PyQt5.QtCore import (Qt) from PyQt5.QtWidgets import (QInputDialog, QMainWindow, QSplitter) -from .ocr import BaseOCRView +from .ocr import OCRView from components.explorers import ImageExplorer from utils.config import config from utils.constants import MAIN_VIEW_RATIO @@ -32,7 +32,7 @@ def __init__(self, parent: QMainWindow, tracker=None): self.tracker = tracker self.config = config - self.canvas = BaseOCRView(self, self.tracker) + self.canvas = OCRView(self, self.tracker) self.explorer = ImageExplorer(self, self.tracker.filepath) self.addWidget(self.explorer) diff --git a/app/components/views/ocr/__init__.py b/app/components/views/ocr/__init__.py index e26fef0..a39b64a 100644 --- a/app/components/views/ocr/__init__.py +++ b/app/components/views/ocr/__init__.py @@ -17,5 +17,5 @@ along with this program. If not, see . """ -from .base import BaseOCRView from .fullscreen import FullScreenOCRView +from .ocr import OCRView diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 382f38b..34a2199 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -1,5 +1,5 @@ """ -Poricom View Components +Poricom Views Copyright (C) `2021-2022` `` @@ -20,12 +20,11 @@ from PyQt5.QtCore import (pyqtSlot, Qt, QThreadPool, QTimer) from PyQt5.QtWidgets import (QGraphicsView, QLabel, QMainWindow) -from ..image import BaseImageView from components.services import BaseWorker from utils.image_io import logText, pixboxToText -class BaseOCRView(BaseImageView): +class BaseOCRView(QGraphicsView): """Base view with OCR capabilities Args: @@ -34,7 +33,8 @@ class BaseOCRView(BaseImageView): """ def __init__(self, parent: QMainWindow, tracker=None): # TODO: Remove references to tracker - super().__init__(parent, tracker) + super().__init__(parent) + self.tracker = tracker self.timer = QTimer() self.timer.setInterval(300) @@ -59,6 +59,10 @@ def handleTextFinished(self): self.canvasText.adjustSize() except RuntimeError: pass + try: + self.timer.timeout.connect(self.rubberBandStopped) + except TypeError: + pass @pyqtSlot() def rubberBandStopped(self): @@ -75,8 +79,6 @@ def rubberBandStopped(self): worker.signals.result.connect(self.handleTextResult) worker.signals.finished.connect(self.handleTextFinished) self.timer.timeout.disconnect(self.rubberBandStopped) - worker.signals.finished.connect( - lambda: self.timer.timeout.connect(self.rubberBandStopped)) QThreadPool.globalInstance().start(worker) def mouseMoveEvent(self, event): @@ -91,7 +93,7 @@ def mouseReleaseEvent(self, event): text = self.canvasText.text() logText(text, mode=logToFile, path=logPath) try: - if not self.parent.config["PERSIST_TEXT_MODE"]: + if not self.parent().config["PERSIST_TEXT_MODE"]: self.canvasText.hide() except AttributeError: pass diff --git a/app/components/views/ocr/fullscreen.py b/app/components/views/ocr/fullscreen.py index 2ea93cb..37f36f2 100644 --- a/app/components/views/ocr/fullscreen.py +++ b/app/components/views/ocr/fullscreen.py @@ -18,7 +18,7 @@ """ from PyQt5.QtCore import (Qt, QRectF) -from PyQt5.QtWidgets import (QApplication, QMainWindow) +from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QMainWindow) from PyQt5.QtGui import QCursor, QMouseEvent from .base import BaseOCRView @@ -30,15 +30,19 @@ class FullScreenOCRView(BaseOCRView): """ def __init__(self, parent: QMainWindow, tracker=None): super().__init__(parent, tracker) + self.externalWindow = parent + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setScene(QGraphicsScene()) + def takeScreenshot(self, screenIndex: int): screen = QApplication.screens()[screenIndex] s = screen.size() - self.pixmap.setPixmap(screen.grabWindow( + self.pixmap = self.scene().addPixmap(screen.grabWindow( 0).scaled(s.width(), s.height())) - self.scene.setSceneRect(QRectF(self.pixmap.pixmap().rect())) + self.scene().setSceneRect(QRectF(self.pixmap.pixmap().rect())) def getActiveScreenIndex(self): cursor = QCursor.pos() @@ -46,4 +50,4 @@ def getActiveScreenIndex(self): def mouseReleaseEvent(self, event: QMouseEvent): super().mouseReleaseEvent(event) - self.parent.close() + self.externalWindow.close() diff --git a/app/components/views/ocr/ocr.py b/app/components/views/ocr/ocr.py new file mode 100644 index 0000000..fd830a6 --- /dev/null +++ b/app/components/views/ocr/ocr.py @@ -0,0 +1,34 @@ +""" +Poricom Views + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (pyqtSlot) +from PyQt5.QtWidgets import (QMainWindow) + +from ..image import BaseImageView +from .base import BaseOCRView + + +class OCRView(BaseImageView, BaseOCRView): + def __init__(self, parent: QMainWindow, tracker=None): + # TODO: Remove references to tracker + super().__init__(parent, tracker) + + @pyqtSlot() + def rubberBandStopped(self): + super().rubberBandStopped() From 671bb228a9b62bd44c5af15afa3bcbc3e0c3e6ba Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 00:24:56 +0800 Subject: [PATCH 033/137] Rename MainView to WorkspaceView Avoid confusion on main app file and main view --- app/MainWindow.py | 4 ++-- app/components/views/__init__.py | 2 +- app/components/views/{main.py => workspace.py} | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) rename app/components/views/{main.py => workspace.py} (96%) diff --git a/app/MainWindow.py b/app/MainWindow.py index 75c168f..523efb2 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -30,7 +30,7 @@ from utils.config import config, saveOnClose from components.services import BaseWorker from components.toolbar import BaseToolbar -from components.views import MainView, FullScreenOCRView +from components.views import WorkspaceView, FullScreenOCRView from Popups import (FontPicker, LanguagePicker, ScaleImagePicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) @@ -54,7 +54,7 @@ def __init__(self, parent=None, tracker=None): self.vLayout = QVBoxLayout() - self.mainView = MainView(self, self.tracker) + self.mainView = WorkspaceView(self, self.tracker) self.ribbon = BaseToolbar(self) self.vLayout.addWidget(self.ribbon) diff --git a/app/components/views/__init__.py b/app/components/views/__init__.py index 963217c..bc908b8 100644 --- a/app/components/views/__init__.py +++ b/app/components/views/__init__.py @@ -17,5 +17,5 @@ along with this program. If not, see . """ -from .main import MainView +from .workspace import WorkspaceView from .ocr import FullScreenOCRView diff --git a/app/components/views/main.py b/app/components/views/workspace.py similarity index 96% rename from app/components/views/main.py rename to app/components/views/workspace.py index 3f35322..2ebd28a 100644 --- a/app/components/views/main.py +++ b/app/components/views/workspace.py @@ -25,8 +25,10 @@ from utils.config import config from utils.constants import MAIN_VIEW_RATIO -class MainView(QSplitter): - +class WorkspaceView(QSplitter): + """ + Main view of the program. Includes the explorer and the view. + """ def __init__(self, parent: QMainWindow, tracker=None): super().__init__(parent) self.tracker = tracker From aee6bba50fa429d0ec6e35eb437d9c43f4064de8 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 00:30:14 +0800 Subject: [PATCH 034/137] Update parenting behavior in image view Remove manual parenting since it is not needed in QSplitter --- app/components/views/image/base.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index 7b92c7c..2ec1d6e 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -18,22 +18,25 @@ """ from time import sleep +from typing import TYPE_CHECKING from PyQt5.QtCore import (Qt, QRect, QRectF, QSize, QThreadPool) -from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView, QMainWindow) +from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView, QSplitter) from components.services import BaseWorker from components.settings import BaseSettings from utils.constants import IMAGE_VIEW_DEFAULT, IMAGE_VIEW_TYPES +if TYPE_CHECKING: + from ..workspace import WorkspaceView + class BaseImageView(QGraphicsView, BaseSettings): """ Base image view to allow view/zoom/pan functions """ - def __init__(self, parent: QMainWindow, tracker=None): + def __init__(self, parent: 'WorkspaceView', tracker=None): super().__init__(parent) - self.parent = parent self.tracker = tracker self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) @@ -146,7 +149,7 @@ def suppressScroll(): self.verticalScrollBar().value() == self.verticalScrollBar().maximum()): if (event.angleDelta().y() > -wheelDelta): if (self._trackPadAtMax == trackpadScrollLimit): - self.parent.loadNextImage() + self.parent().loadNextImage() self._trackPadAtMax = 0 suppressScroll() return @@ -154,7 +157,7 @@ def suppressScroll(): self._trackPadAtMax += 1 elif (event.angleDelta().y() <= -wheelDelta): if (self._scrollAtMax == mouseScrollLimit): - self.parent.loadNextImage() + self.parent().loadNextImage() self._scrollAtMax = 0 suppressScroll() return @@ -165,7 +168,7 @@ def suppressScroll(): self.verticalScrollBar().value() == self.verticalScrollBar().minimum()): if (event.angleDelta().y() < wheelDelta): if (self._trackPadAtMin == trackpadScrollLimit): - self.parent.loadPrevImage() + self.parent().loadPrevImage() self._trackPadAtMin = 0 suppressScroll() return @@ -173,7 +176,7 @@ def suppressScroll(): self._trackPadAtMin += 1 elif (event.angleDelta().y() >= wheelDelta): if (self._scrollAtMin == mouseScrollLimit): - self.parent.loadPrevImage() + self.parent().loadPrevImage() self._scrollAtMin = 0 suppressScroll() return @@ -202,10 +205,10 @@ def mouseDoubleClickEvent(self, event): # TODO: Keyboard shortcuts should be in another class def keyPressEvent(self, event): if event.key() == Qt.Key_Left: - self.parent.loadPrevImage() + self.parent().loadPrevImage() return if event.key() == Qt.Key_Right: - self.parent.loadNextImage() + self.parent().loadNextImage() return if event.key() == Qt.Key_Minus: self.zoomView(isZoomIn=False, usingButton=True) From d2d764e0a6a0568a0545bfe8cb0cb85cf9080e07 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 00:39:43 +0800 Subject: [PATCH 035/137] Update base ocr to inherit from settings Remove dependency on parent config file --- app/components/settings/base.py | 1 + app/components/views/ocr/base.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/components/settings/base.py b/app/components/settings/base.py index e053c15..8f069b1 100644 --- a/app/components/settings/base.py +++ b/app/components/settings/base.py @@ -78,6 +78,7 @@ def setProperty(self, prop: str, value: Any): def addProperty(self, prop: str, value: Any, t: Callable = str): self._defaults[prop] = value self._types[prop] = t + self.setProperty(prop, value) def removeProperty(self, prop: str): del self._defaults[prop] diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 34a2199..0cfe090 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -21,10 +21,11 @@ from PyQt5.QtWidgets import (QGraphicsView, QLabel, QMainWindow) from components.services import BaseWorker +from components.settings import BaseSettings from utils.image_io import logText, pixboxToText -class BaseOCRView(QGraphicsView): +class BaseOCRView(QGraphicsView, BaseSettings): """Base view with OCR capabilities Args: @@ -48,6 +49,8 @@ def __init__(self, parent: QMainWindow, tracker=None): self.setDragMode(QGraphicsView.RubberBandDrag) + self.addProperty('persistTextMode', "false", bool) + def handleTextResult(self, result): try: self.canvasText.setText(result) @@ -93,7 +96,7 @@ def mouseReleaseEvent(self, event): text = self.canvasText.text() logText(text, mode=logToFile, path=logPath) try: - if not self.parent().config["PERSIST_TEXT_MODE"]: + if not self.persistTextMode: self.canvasText.hide() except AttributeError: pass From 4401c4d949656432a1e6c1111b5b85ecca0ba7c8 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 16:37:50 +0800 Subject: [PATCH 036/137] Refactor utils file structure --- app/MainWindow.py | 2 +- app/components/views/ocr/base.py | 13 ++-- app/utils/constants.py | 2 + app/utils/scripts/__init__.py | 22 +++++++ app/utils/scripts/logText.py | 35 +++++++++++ .../mangaFileToImageDir.py} | 52 +++------------- app/utils/scripts/pixmapToText.py | 59 +++++++++++++++++++ 7 files changed, 133 insertions(+), 52 deletions(-) create mode 100644 app/utils/scripts/__init__.py create mode 100644 app/utils/scripts/logText.py rename app/utils/{image_io.py => scripts/mangaFileToImageDir.py} (58%) create mode 100644 app/utils/scripts/pixmapToText.py diff --git a/app/MainWindow.py b/app/MainWindow.py index 523efb2..0c07206 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -26,8 +26,8 @@ from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QDesktopWidget, QMainWindow, QApplication, QPushButton, QFileDialog) -from utils.image_io import mangaFileToImageDir from utils.config import config, saveOnClose +from utils.scripts import mangaFileToImageDir from components.services import BaseWorker from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 0cfe090..69f1a37 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -22,8 +22,7 @@ from components.services import BaseWorker from components.settings import BaseSettings -from utils.image_io import logText, pixboxToText - +from utils.scripts import logText, pixmapToText class BaseOCRView(QGraphicsView, BaseSettings): """Base view with OCR capabilities @@ -75,10 +74,10 @@ def rubberBandStopped(self): self.canvasText.adjustSize() self.canvasText.show() - lang = self.tracker.language + self.tracker.orientation + language = self.tracker.language + self.tracker.orientation pixbox = self.grab(self.rubberBandRect()) - worker = BaseWorker(pixboxToText, pixbox, lang, self.tracker.ocrModel) + worker = BaseWorker(pixmapToText, pixbox, language, self.tracker.ocrModel) worker.signals.result.connect(self.handleTextResult) worker.signals.finished.connect(self.handleTextFinished) self.timer.timeout.disconnect(self.rubberBandStopped) @@ -91,10 +90,10 @@ def mouseMoveEvent(self, event): super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): - logPath = self.tracker.filepath + "/log.txt" - logToFile = self.tracker.writeMode + logPath = self.tracker.filepath + "/text-log.txt" + isLogFile = self.tracker.writeMode text = self.canvasText.text() - logText(text, mode=logToFile, path=logPath) + logText(text, isLogFile=isLogFile, path=logPath) try: if not self.persistTextMode: self.canvasText.hide() diff --git a/app/utils/constants.py b/app/utils/constants.py index c6ed233..10ec6a3 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -23,6 +23,8 @@ IMAGE_EXTENSIONS = ["*.bmp", "*.gif", "*.jpeg", "*.jpg", "*.pbm", "*.pgm", "*.png", "*.ppm", "*.webp", "*.xbm", "*.xpm",] # Paths +TESSERACT_LANGUAGES = "./assets/languages/" + TOOLBAR_ICONS = './assets/images/icons/' TOOLBAR_ICON_DEFAULT = './assets/images/icons/default_icon.png' diff --git a/app/utils/scripts/__init__.py b/app/utils/scripts/__init__.py new file mode 100644 index 0000000..5b5328c --- /dev/null +++ b/app/utils/scripts/__init__.py @@ -0,0 +1,22 @@ +""" +Poricom Helper Functions + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .logText import logText +from .mangaFileToImageDir import mangaFileToImageDir +from .pixmapToText import pixmapToText diff --git a/app/utils/scripts/logText.py b/app/utils/scripts/logText.py new file mode 100644 index 0000000..08e5705 --- /dev/null +++ b/app/utils/scripts/logText.py @@ -0,0 +1,35 @@ +""" +Poricom Helper Functions + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtGui import QGuiApplication + +def logText(text: str, isLogFile: bool=False, path: str="."): + """Log text by copying to clipboard + + Args: + text (str): Text to log. + isLogFile (bool, optional): Set flag to save copied text to clipboard. Defaults to False. + path (str, optional): Path to log file. Defaults to ".". + """ + clipboard = QGuiApplication.clipboard() + clipboard.setText(text) + + if isLogFile: + with open(path, 'a', encoding="utf-8") as fh: + fh.write(text + "\n") diff --git a/app/utils/image_io.py b/app/utils/scripts/mangaFileToImageDir.py similarity index 58% rename from app/utils/image_io.py rename to app/utils/scripts/mangaFileToImageDir.py index fe98673..efbcef3 100644 --- a/app/utils/image_io.py +++ b/app/utils/scripts/mangaFileToImageDir.py @@ -1,5 +1,5 @@ """ -Poricom Image Processing Utility +Poricom Helper Functions Copyright (C) `2021-2022` `` @@ -17,22 +17,22 @@ along with this program. If not, see . """ -from io import BytesIO from os.path import splitext, basename from pathlib import Path -from PyQt5.QtCore import QBuffer -from PyQt5.QtGui import QGuiApplication -from tesserocr import PyTessBaseAPI -from PIL import Image import zipfile import rarfile import pdf2image -from utils.config import config +def mangaFileToImageDir(filepath: str): + """Converts a manga file to a directory of images + Args: + filepath (str): Path to manga file. -def mangaFileToImageDir(filepath): + Returns: + str: Path to directory of images. + """ extractPath, extension = splitext(filepath) cachePath = f"./poricom_cache/{basename(extractPath)}" @@ -58,39 +58,3 @@ def mangaFileToImageDir(filepath): f"{cachePath}/{i+1}_{filename}.png", 'PNG') return cachePath - - -def pixboxToText(pixmap, lang="jpn_vert", model=None): - - buffer = QBuffer() - buffer.open(QBuffer.ReadWrite) - pixmap.save(buffer, "PNG") - bytes = BytesIO(buffer.data()) - - if bytes.getbuffer().nbytes == 0: - return - - pillowImage = Image.open(bytes) - text = "" - - if model is not None: - text = model(pillowImage) - - # PSM = 1 works most of the time except on smaller bounding boxes. - # By smaller, we mean textboxes with less text. Usually these - # boxes have at most one vertical line of text. - else: - with PyTessBaseAPI(path=config["LANG_PATH"], lang=lang, oem=1, psm=1) as api: - api.SetImage(pillowImage) - text = api.GetUTF8Text() - - return text.strip() - - -def logText(text, mode=False, path="."): - clipboard = QGuiApplication.clipboard() - clipboard.setText(text) - - if mode: - with open(path, 'a', encoding="utf-8") as fh: - fh.write(text + "\n") diff --git a/app/utils/scripts/pixmapToText.py b/app/utils/scripts/pixmapToText.py new file mode 100644 index 0000000..bb3cc4d --- /dev/null +++ b/app/utils/scripts/pixmapToText.py @@ -0,0 +1,59 @@ +""" +Poricom Helper Functions + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from io import BytesIO +from typing import Optional + +from manga_ocr import MangaOcr +from PIL import Image +from PyQt5.QtCore import QBuffer +from PyQt5.QtGui import QPixmap +from tesserocr import PyTessBaseAPI + +from ..constants import TESSERACT_LANGUAGES + + +def pixmapToText(pixmap: QPixmap, language: str = "jpn_vert", model: Optional[MangaOcr] = None) -> str: + """ + Convert QPixmap object to text using the model + """ + + buffer = QBuffer() + buffer.open(QBuffer.ReadWrite) + pixmap.save(buffer, "PNG") + bytes = BytesIO(buffer.data()) + + if bytes.getbuffer().nbytes == 0: + return "" + + pillowImage = Image.open(bytes) + text = "" + + if model is not None: + text = model(pillowImage) + + # PSM = 1 works most of the time except on smaller bounding boxes. + # By smaller, we mean textboxes with less text. Usually these + # boxes have at most one vertical line of text. + else: + with PyTessBaseAPI(path=TESSERACT_LANGUAGES, lang=language, oem=1, psm=1) as api: + api.SetImage(pillowImage) + text = api.GetUTF8Text() + + return text.strip() \ No newline at end of file From e9ccbdd78b8b5c10ed4ad41a86dab0a3781bfdac Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 16:39:05 +0800 Subject: [PATCH 037/137] Move event filters to services --- app/components/services/__init__.py | 1 + app/components/services/filters.py | 30 +++++++++++++++++++++++++++++ app/main.py | 3 ++- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 app/components/services/filters.py diff --git a/app/components/services/__init__.py b/app/components/services/__init__.py index 96e5c75..28818b1 100644 --- a/app/components/services/__init__.py +++ b/app/components/services/__init__.py @@ -16,4 +16,5 @@ along with this program. If not, see . """ +from .filters import WinEventFilter from .workers import BaseWorker, BaseWorkerSignal diff --git a/app/components/services/filters.py b/app/components/services/filters.py new file mode 100644 index 0000000..3504795 --- /dev/null +++ b/app/components/services/filters.py @@ -0,0 +1,30 @@ +""" +Poricom Services + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (QAbstractNativeEventFilter) + + +class WinEventFilter(QAbstractNativeEventFilter): + def __init__(self, keybinder): + self.keybinder = keybinder + super().__init__() + + def nativeEventFilter(self, eventType, message): + ret = self.keybinder.handler(eventType, message) + return ret, 0 \ No newline at end of file diff --git a/app/main.py b/app/main.py index 6f122c4..1936c16 100644 --- a/app/main.py +++ b/app/main.py @@ -23,7 +23,8 @@ from PyQt5.QtCore import QAbstractEventDispatcher from pyqtkeybind import keybinder -from MainWindow import MainWindow, WinEventFilter +from components.services import WinEventFilter +from MainWindow import MainWindow from Trackers import Tracker from utils.config import config From aa30659b3342ac0ba99e0481e68366607ac48e3c Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 16:42:50 +0800 Subject: [PATCH 038/137] Use huggingface for connection error handling --- app/MainWindow.py | 78 ++++++++++-------------------------------- app/utils/constants.py | 8 +++++ 2 files changed, 27 insertions(+), 59 deletions(-) diff --git a/app/MainWindow.py b/app/MainWindow.py index 0c07206..1ac1c39 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -1,5 +1,5 @@ """ -Poricom Main Window Component +Poricom Windows Copyright (C) `2021-2022` `` @@ -22,27 +22,18 @@ import toml from manga_ocr import MangaOcr -from PyQt5.QtCore import (Qt, QAbstractNativeEventFilter, QThreadPool) +from PyQt5.QtCore import (Qt, QThreadPool) from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QDesktopWidget, QMainWindow, QApplication, QPushButton, QFileDialog) -from utils.config import config, saveOnClose -from utils.scripts import mangaFileToImageDir from components.services import BaseWorker from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView from Popups import (FontPicker, LanguagePicker, ScaleImagePicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) - - -class WinEventFilter(QAbstractNativeEventFilter): - def __init__(self, keybinder): - self.keybinder = keybinder - super().__init__() - - def nativeEventFilter(self, eventType, message): - ret = self.keybinder.handler(eventType, message) - return ret, 0 +from utils.config import config, saveOnClose +from utils.constants import LOAD_MODEL_MESSAGE +from utils.scripts import mangaFileToImageDir class MainWindow(QMainWindow): @@ -229,11 +220,7 @@ def loadModel(self): if loadModelButton.isChecked() and self.config["LOAD_MODEL_POPUP"]: confirmation = CheckboxPopup( "Load the MangaOCR model?", - "If you are running this for the first time, this will " + - "download the MangaOcr model which is about 400 MB in size. " + - "This will improve the accuracy of Japanese text detection " + - "in Poricom. If it is already in your cache, it will take a " + - "few seconds to load the model.", + LOAD_MODEL_MESSAGE, MessagePopup.Ok | MessagePopup.Cancel ) ret = confirmation.exec() @@ -247,56 +234,29 @@ def loadModel(self): def loadModelHelper(tracker): betterOCR = tracker.switchOCRMode() if betterOCR: - import http.client as httplib - - def isConnected(url=self.config["CHECK_INTERNET_URL"]): - if not self.config["CHECK_INTERNET_POPUP"]: - return True - connection = httplib.HTTPSConnection(url, timeout=2) - try: - connection.request("HEAD", "/") - return True - except Exception: - tracker.switchOCRMode() - return False - finally: - connection.close() - - connected = isConnected() - if connected: - try: - tracker.ocrModel = MangaOcr() - except ValueError: - return (betterOCR, False) - return (betterOCR, connected) + try: + tracker.ocrModel = MangaOcr() + return "success" + except Exception as e: + tracker.switchOCRMode() + return str(e) else: tracker.ocrModel = None - return (betterOCR, True) + return "success" - def modelLoadedConfirmation(typeConnectionTuple): - usingMangaOCR, connected = typeConnectionTuple - modelName = "MangaOCR" if usingMangaOCR else "Tesseract" - if connected: + def loadModelConfirm(message: str): + modelName = "MangaOCR" if self.tracker.ocrModel else "Tesseract" + if message == "success": MessagePopup( f"{modelName} model loaded", f"You are now using the {modelName} model for Japanese text detection." ).exec() - - elif not connected: - connectionErrorMessage = CheckboxPopup( - "Connection Error", - "Please try again or make sure your Internet connection is on.", - checkboxMessage=( - "Check this box if you keep getting this error even with connection on." - ) - ) - connectionErrorMessage.exec() - self.config["CHECK_INTERNET_POPUP"] = \ - not connectionErrorMessage.checkBox().isChecked() + else: + MessagePopup("Load Model Error", message).exec() loadModelButton.setChecked(False) worker = BaseWorker(loadModelHelper, self.tracker) - worker.signals.result.connect(modelLoadedConfirmation) + worker.signals.result.connect(loadModelConfirm) worker.signals.finished.connect(lambda: loadModelButton.setEnabled(True)) diff --git a/app/utils/constants.py b/app/utils/constants.py index 10ec6a3..b60d309 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -30,6 +30,14 @@ EXPLORER_ROOT_DEFAULT = './assets/images/' +# Messages +LOAD_MODEL_MESSAGE = ( + "If you are running this for the first time, this will download the MangaOcr model" + "which is about 400 MB in size. This will improve the accuracy of Japanese text" + "detection in Poricom. If it is already in your cache, it will take a few seconds" + "to load the model." +) + # ------------------------------------ Settings ------------------------------------- # SETTINGS_FILE_DEFAULT = './utils/poricom-config.ini' From 40a6e35fa0e13ec506323922f6d56ff5f239091d Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 22:44:35 +0800 Subject: [PATCH 039/137] Refactor options to use BaseSettings Remove widget dependency on config file --- app/MainWindow.py | 7 ++- app/Popups.py | 45 -------------- app/components/settings/__init__.py | 1 + app/components/settings/base.py | 12 ++++ app/components/settings/popups/__init__.py | 21 +++++++ app/components/settings/popups/base.py | 68 +++++++++++++++++++++ app/components/settings/popups/container.py | 44 +++++++++++++ app/components/settings/popups/tesseract.py | 54 ++++++++++++++++ app/utils/constants.py | 10 +++ 9 files changed, 215 insertions(+), 47 deletions(-) create mode 100644 app/components/settings/popups/__init__.py create mode 100644 app/components/settings/popups/base.py create mode 100644 app/components/settings/popups/container.py create mode 100644 app/components/settings/popups/tesseract.py diff --git a/app/MainWindow.py b/app/MainWindow.py index 1ac1c39..a7827bb 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -27,9 +27,10 @@ QPushButton, QFileDialog) from components.services import BaseWorker +from components.settings import OptionsContainer, TesseractOptions from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView -from Popups import (FontPicker, LanguagePicker, ScaleImagePicker, +from Popups import (FontPicker, ScaleImagePicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) from utils.config import config, saveOnClose from utils.constants import LOAD_MODEL_MESSAGE @@ -264,8 +265,10 @@ def loadModelConfirm(message: str): loadModelButton.setEnabled(False) def modifyTesseract(self): - confirmation = PickerPopup(LanguagePicker(self, self.tracker)) + confirmation = OptionsContainer(TesseractOptions(self)) confirmation.exec() + if confirmation: + self.canvas.loadSettings() def toggleLogging(self): self.tracker.switchWriteMode() diff --git a/app/Popups.py b/app/Popups.py index 6d1dfee..60e558e 100644 --- a/app/Popups.py +++ b/app/Popups.py @@ -71,51 +71,6 @@ def applySelections(self, selections): editSelectionConfig(index, selection) -class LanguagePicker(BasePicker): - def __init__(self, parent, tracker): - config = parent.config - listTop = config["LANGUAGE"] - listBot = config["ORIENTATION"] - optionLists = [listTop, listBot] - - super().__init__(parent, tracker, optionLists) - self.pickTop.currentIndexChanged.connect(self.changeLanguage) - self.pickTop.setCurrentIndex(config["SELECTED_INDEX"]["language"]) - self.nameTop.setText("Language: ") - self.pickBot.currentIndexChanged.connect(self.changeOrientation) - self.pickBot.setCurrentIndex(config["SELECTED_INDEX"]["orientation"]) - self.nameBot.setText("Orientation: ") - - self.languageIndex = self.pickTop.currentIndex() - self.orientationIndex = self.pickBot.currentIndex() - - def changeLanguage(self, i): - self.languageIndex = i - selectedLanguage = self.pickTop.currentText().strip() - if selectedLanguage == "Japanese": - self.tracker.language = "jpn" - if selectedLanguage == "Korean": - self.tracker.language = "kor" - if selectedLanguage == "Chinese SIM": - self.tracker.language = "chi_sim" - if selectedLanguage == "Chinese TRA": - self.tracker.language = "chi_tra" - if selectedLanguage == "English": - self.tracker.language = "eng" - - def changeOrientation(self, i): - self.orientationIndex = i - selectedOrientation = self.pickBot.currentText().strip() - if selectedOrientation == "Vertical": - self.tracker.orientation = "_vert" - if selectedOrientation == "Horizontal": - self.tracker.orientation = "" - - def applyChanges(self): - self.applySelections(['language', 'orientation']) - return True - - class FontPicker(BasePicker): def __init__(self, parent, tracker): config = parent.config diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py index 11facfd..74c43f9 100644 --- a/app/components/settings/__init__.py +++ b/app/components/settings/__init__.py @@ -18,3 +18,4 @@ """ from .base import BaseSettings +from .popups import OptionsContainer, TesseractOptions diff --git a/app/components/settings/base.py b/app/components/settings/base.py index 8f069b1..b0d7bf3 100644 --- a/app/components/settings/base.py +++ b/app/components/settings/base.py @@ -55,6 +55,12 @@ def setDefaults(self, defaults: dict[str, Any]): """ self._defaults = defaults + def addDefaults(self, defaults: dict[str, Any]): + """ + Extends the defaults dictionary + """ + self.setDefaults({**self._defaults, **defaults}) + def setTypes(self, types: dict[str, Callable]): """Set the types dictionary @@ -62,6 +68,12 @@ def setTypes(self, types: dict[str, Callable]): Use `self._types` to set the correct property type. """ self._types = types + + def setTypes(self, types: dict[str, Callable]): + """ + Extends the types dictionary + """ + self.setTypes({**self._types, **types}) def getProperty(self, prop: str): return getattr(self, prop) diff --git a/app/components/settings/popups/__init__.py b/app/components/settings/popups/__init__.py new file mode 100644 index 0000000..1f47a47 --- /dev/null +++ b/app/components/settings/popups/__init__.py @@ -0,0 +1,21 @@ +""" +Poricom Settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +# TODO: Most components here have no docstrings +from .container import OptionsContainer +from .tesseract import TesseractOptions diff --git a/app/components/settings/popups/base.py b/app/components/settings/popups/base.py new file mode 100644 index 0000000..b4d1ac8 --- /dev/null +++ b/app/components/settings/popups/base.py @@ -0,0 +1,68 @@ +""" +Poricom Settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from typing import Any, Callable + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import (QComboBox, QGridLayout, QLabel, QWidget) +from stringcase import titlecase, capitalcase + +from ..base import BaseSettings + + +class BaseOptions(BaseSettings): + def __init__(self, parent: QWidget, optionLists:list[list[str]]=[]): + super().__init__(parent) + self.setAttribute(Qt.WA_DeleteOnClose) + + self.setLayout(QGridLayout()) + self.layout().setContentsMargins(0, 0, 0, 0) + + self.comboBoxList: list[QComboBox] = [] + self.labelList: list[QLabel] = [] + + for i in range(len(optionLists)): + optionList = optionLists[i] + + self.comboBoxList.append(QComboBox()) + self.comboBoxList[i].addItems(optionList) + self.layout().addWidget(self.comboBoxList[i], i, 1) + self.labelList.append(QLabel("")) + self.layout().addWidget(self.labelList[i], i, 0) + + def setOptionIndex(self, option: str, index: int = 0): + optionIndex = self.settings.value(f"{option}Index", index, int) + comboBox = self.getProperty(f"{option}ComboBox") + comboBox.setCurrentIndex(optionIndex) + self.addProperty(f"{option}Index", optionIndex, int) + + def addOptionProperties(self, props: list[tuple[str, Any, Callable]]): + for i, p in enumerate(props): + # Property + prop, propDefault, propType = p + self.addProperty(prop, propDefault, propType) + + # Label + self.labelList[i].setText(f"{titlecase(prop)}: ") + + # Combo Box + comboBox = self.comboBoxList[i] + self.setProperty(f"{prop}ComboBox", comboBox) + comboBox.currentIndexChanged.connect(self.getProperty(f"change{capitalcase(prop)}")) + self.setOptionIndex(prop) \ No newline at end of file diff --git a/app/components/settings/popups/container.py b/app/components/settings/popups/container.py new file mode 100644 index 0000000..c770b59 --- /dev/null +++ b/app/components/settings/popups/container.py @@ -0,0 +1,44 @@ +""" +Poricom settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (Qt) +from PyQt5.QtWidgets import (QVBoxLayout, QDialog, QDialogButtonBox) + +from .base import BaseOptions + +class OptionsContainer(QDialog): + def __init__(self, options: BaseOptions): + super().__init__(None, Qt.WindowCloseButtonHint | Qt.WindowSystemMenuHint | Qt.WindowTitleHint) + self.options = options + self.setLayout(QVBoxLayout()) + self.layout().addWidget(options) + self.buttonBox = QDialogButtonBox( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.layout().addWidget(self.buttonBox) + + self.buttonBox.rejected.connect(self.cancelClickedEvent) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + def accept(self): + self.options.saveSettings(hasMessage=False) + return super().accept() + + def cancelClickedEvent(self): + self.close() diff --git a/app/components/settings/popups/tesseract.py b/app/components/settings/popups/tesseract.py new file mode 100644 index 0000000..23cea3f --- /dev/null +++ b/app/components/settings/popups/tesseract.py @@ -0,0 +1,54 @@ +""" +Poricom Popups + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (Qt) +from PyQt5.QtWidgets import (QGridLayout, QVBoxLayout, QWidget, QLabel, QCheckBox, + QLineEdit, QComboBox, QDialog, QDialogButtonBox, QMessageBox) + +from utils.constants import LANGUAGE, ORIENTATION + +from .base import BaseOptions + +class TesseractOptions(BaseOptions): + def __init__(self, parent: QWidget): + super().__init__(parent, [LANGUAGE, ORIENTATION]) + # TODO: Use constants here + self.addOptionProperties([("language", "jpn", str), ("orientation", "_vert", str)]) + + def changeLanguage(self, i): + self.languageIndex = i + selectedLanguage = self.languageComboBox.currentText().strip() + if selectedLanguage == "Japanese": + self.language = "jpn" + if selectedLanguage == "Korean": + self.language = "kor" + if selectedLanguage == "Chinese SIM": + self.language = "chi_sim" + if selectedLanguage == "Chinese TRA": + self.language = "chi_tra" + if selectedLanguage == "English": + self.language = "eng" + + def changeOrientation(self, i): + self.orientationIndex = i + selectedOrientation = self.orientationComboBox.currentText().strip() + if selectedOrientation == "Vertical": + self.orientation = "_vert" + if selectedOrientation == "Horizontal": + self.orientation = "" diff --git a/app/utils/constants.py b/app/utils/constants.py index b60d309..b470827 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -22,6 +22,10 @@ IMAGE_EXTENSIONS = ["*.bmp", "*.gif", "*.jpeg", "*.jpg", "*.pbm", "*.pgm", "*.png", "*.ppm", "*.webp", "*.xbm", "*.xpm",] +# Settings +LANGUAGE = [ " Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English"] +ORIENTATION = [ " Vertical", " Horizontal"] + # Paths TESSERACT_LANGUAGES = "./assets/languages/" @@ -54,6 +58,12 @@ "zoomPanMode": bool } +# Tesseract +TESSERACT_DEFAULTS = { + "language": "jpn", + "orientation": "_vert" +} + # --------------------------------------- UI ---------------------------------------- # # Main view From 057714b6e14fdd984470634bb2c2211b6c9d4859 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 22:46:34 +0800 Subject: [PATCH 040/137] Update view to read saved settings --- app/components/settings/base.py | 16 +++++++++++----- app/components/views/image/base.py | 4 ++-- app/components/views/ocr/base.py | 9 +++++---- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/components/settings/base.py b/app/components/settings/base.py index b0d7bf3..46c49f8 100644 --- a/app/components/settings/base.py +++ b/app/components/settings/base.py @@ -57,9 +57,12 @@ def setDefaults(self, defaults: dict[str, Any]): def addDefaults(self, defaults: dict[str, Any]): """ - Extends the defaults dictionary + Extends the defaults dictionary, if it exists """ - self.setDefaults({**self._defaults, **defaults}) + try: + self.setDefaults({**self._defaults, **defaults}) + except AttributeError: + self.setDefaults(defaults) def setTypes(self, types: dict[str, Callable]): """Set the types dictionary @@ -69,11 +72,14 @@ def setTypes(self, types: dict[str, Callable]): """ self._types = types - def setTypes(self, types: dict[str, Callable]): + def addTypes(self, types: dict[str, Callable]): """ - Extends the types dictionary + Extends the types dictionary, if it exists """ - self.setTypes({**self._types, **types}) + try: + self.setTypes({**self._types, **types}) + except AttributeError: + self.setTypes(types) def getProperty(self, prop: str): return getattr(self, prop) diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index 2ec1d6e..414bf3c 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -50,8 +50,8 @@ def __init__(self, parent: 'WorkspaceView', tracker=None): self._trackPadAtMax = 0 self._scrollSuppressed = False - self.setDefaults(IMAGE_VIEW_DEFAULT) - self.setTypes(IMAGE_VIEW_TYPES) + self.addDefaults(IMAGE_VIEW_DEFAULT) + self.addTypes(IMAGE_VIEW_TYPES) self.loadSettings() self.initializePixmapItem() diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 69f1a37..120535f 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -22,6 +22,7 @@ from components.services import BaseWorker from components.settings import BaseSettings +from utils.constants import TESSERACT_DEFAULTS from utils.scripts import logText, pixmapToText class BaseOCRView(QGraphicsView, BaseSettings): @@ -48,6 +49,7 @@ def __init__(self, parent: QMainWindow, tracker=None): self.setDragMode(QGraphicsView.RubberBandDrag) + self.addDefaults(TESSERACT_DEFAULTS) self.addProperty('persistTextMode', "false", bool) def handleTextResult(self, result): @@ -68,16 +70,15 @@ def handleTextFinished(self): @pyqtSlot() def rubberBandStopped(self): - if (self.canvasText.isHidden()): self.canvasText.setText("") self.canvasText.adjustSize() self.canvasText.show() - language = self.tracker.language + self.tracker.orientation - pixbox = self.grab(self.rubberBandRect()) + language = self.language + self.orientation + pixmap = self.grab(self.rubberBandRect()) - worker = BaseWorker(pixmapToText, pixbox, language, self.tracker.ocrModel) + worker = BaseWorker(pixmapToText, pixmap, language, self.tracker.ocrModel) worker.signals.result.connect(self.handleTextResult) worker.signals.finished.connect(self.handleTextFinished) self.timer.timeout.disconnect(self.rubberBandStopped) From e565f0adff544e8e337b6b8a04cbe70fa1825239 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 22:48:39 +0800 Subject: [PATCH 041/137] Add docstrings to options module --- app/components/settings/popups/__init__.py | 2 +- app/components/settings/popups/base.py | 25 +++++++++++++++++++-- app/components/settings/popups/container.py | 11 +++++++++ app/components/settings/popups/tesseract.py | 6 ++--- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/app/components/settings/popups/__init__.py b/app/components/settings/popups/__init__.py index 1f47a47..d78e704 100644 --- a/app/components/settings/popups/__init__.py +++ b/app/components/settings/popups/__init__.py @@ -16,6 +16,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -# TODO: Most components here have no docstrings + from .container import OptionsContainer from .tesseract import TesseractOptions diff --git a/app/components/settings/popups/base.py b/app/components/settings/popups/base.py index b4d1ac8..9cb52e7 100644 --- a/app/components/settings/popups/base.py +++ b/app/components/settings/popups/base.py @@ -19,14 +19,17 @@ from typing import Any, Callable +from stringcase import titlecase, capitalcase from PyQt5.QtCore import Qt from PyQt5.QtWidgets import (QComboBox, QGridLayout, QLabel, QWidget) -from stringcase import titlecase, capitalcase from ..base import BaseSettings class BaseOptions(BaseSettings): + """ + Allows saving/selecting options + """ def __init__(self, parent: QWidget, optionLists:list[list[str]]=[]): super().__init__(parent) self.setAttribute(Qt.WA_DeleteOnClose) @@ -47,12 +50,28 @@ def __init__(self, parent: QWidget, optionLists:list[list[str]]=[]): self.layout().addWidget(self.labelList[i], i, 0) def setOptionIndex(self, option: str, index: int = 0): + """Set the combo box index based on the option name + + Args: + option (str): Option name in camelcase + index (int, optional): Combo box index. Defaults to 0. + """ optionIndex = self.settings.value(f"{option}Index", index, int) comboBox = self.getProperty(f"{option}ComboBox") comboBox.setCurrentIndex(optionIndex) self.addProperty(f"{option}Index", optionIndex, int) - def addOptionProperties(self, props: list[tuple[str, Any, Callable]]): + def initializeProperties(self, props: list[tuple[str, Any, Callable]]): + """Initialize property values and names + + Args: + props (list[tuple[str, Any, Callable]]): List of props. \ + Each prop must have the following format: (name, default, type). + It is recommended that the name is in camelcase. + + Note: + Child classes must implement change{PropName} method + """ for i, p in enumerate(props): # Property prop, propDefault, propType = p @@ -64,5 +83,7 @@ def addOptionProperties(self, props: list[tuple[str, Any, Callable]]): # Combo Box comboBox = self.comboBoxList[i] self.setProperty(f"{prop}ComboBox", comboBox) + + # Child classes must implement change{PropName} method comboBox.currentIndexChanged.connect(self.getProperty(f"change{capitalcase(prop)}")) self.setOptionIndex(prop) \ No newline at end of file diff --git a/app/components/settings/popups/container.py b/app/components/settings/popups/container.py index c770b59..c29a6a7 100644 --- a/app/components/settings/popups/container.py +++ b/app/components/settings/popups/container.py @@ -23,8 +23,15 @@ from .base import BaseOptions class OptionsContainer(QDialog): + """Dialog to contain option widgets + + Args: + options (BaseOptions): Child option widget + """ def __init__(self, options: BaseOptions): super().__init__(None, Qt.WindowCloseButtonHint | Qt.WindowSystemMenuHint | Qt.WindowTitleHint) + self.setAttribute(Qt.WA_DeleteOnClose) + self.options = options self.setLayout(QVBoxLayout()) self.layout().addWidget(options) @@ -42,3 +49,7 @@ def accept(self): def cancelClickedEvent(self): self.close() + + def closeEvent(self, event): + self.options.close() + return super().closeEvent(event) diff --git a/app/components/settings/popups/tesseract.py b/app/components/settings/popups/tesseract.py index 23cea3f..5ef0741 100644 --- a/app/components/settings/popups/tesseract.py +++ b/app/components/settings/popups/tesseract.py @@ -17,9 +17,7 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (Qt) -from PyQt5.QtWidgets import (QGridLayout, QVBoxLayout, QWidget, QLabel, QCheckBox, - QLineEdit, QComboBox, QDialog, QDialogButtonBox, QMessageBox) +from PyQt5.QtWidgets import (QWidget) from utils.constants import LANGUAGE, ORIENTATION @@ -29,7 +27,7 @@ class TesseractOptions(BaseOptions): def __init__(self, parent: QWidget): super().__init__(parent, [LANGUAGE, ORIENTATION]) # TODO: Use constants here - self.addOptionProperties([("language", "jpn", str), ("orientation", "_vert", str)]) + self.initializeProperties([("language", "jpn", str), ("orientation", "_vert", str)]) def changeLanguage(self, i): self.languageIndex = i From fa9e28868a1dd2ee3987c14cb4790a715f734c9f Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 16:29:05 +0800 Subject: [PATCH 042/137] Refactor ImageScalingOptions to use BaseOptions --- app/MainWindow.py | 7 ++-- app/Popups.py | 22 ----------- app/components/settings/__init__.py | 2 +- app/components/settings/popups/__init__.py | 1 + app/components/settings/popups/base.py | 1 + .../settings/popups/imageScaling.py | 38 +++++++++++++++++++ app/utils/constants.py | 2 + 7 files changed, 46 insertions(+), 27 deletions(-) create mode 100644 app/components/settings/popups/imageScaling.py diff --git a/app/MainWindow.py b/app/MainWindow.py index a7827bb..a874dd2 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -27,11 +27,10 @@ QPushButton, QFileDialog) from components.services import BaseWorker -from components.settings import OptionsContainer, TesseractOptions +from components.settings import ImageScalingOptions, OptionsContainer, TesseractOptions from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView -from Popups import (FontPicker, ScaleImagePicker, - ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) +from Popups import (FontPicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) from utils.config import config, saveOnClose from utils.constants import LOAD_MODEL_MESSAGE from utils.scripts import mangaFileToImageDir @@ -192,7 +191,7 @@ def toggleSplitView(self): self.explorer.currentChanged(index, index) def scaleImage(self): - confirmation = PickerPopup(ScaleImagePicker(self, self.tracker)) + confirmation = OptionsContainer(ImageScalingOptions(self)) confirmation.exec() def hideExplorer(self): diff --git a/app/Popups.py b/app/Popups.py index 60e558e..c3ca146 100644 --- a/app/Popups.py +++ b/app/Popups.py @@ -122,28 +122,6 @@ def applyChanges(self): return True -class ScaleImagePicker(BasePicker): - def __init__(self, parent, tracker): - config = parent.config - listTop = config["IMAGE_SCALING"] - optionLists = [listTop] - - super().__init__(parent, tracker, optionLists) - self.pickTop.currentIndexChanged.connect(self.changeScaling) - self.pickTop.setCurrentIndex(config["SELECTED_INDEX"]["imageScaling"]) - self.nameTop.setText("Image Scaling: ") - - self.imageScalingIndex = self.pickTop.currentIndex() - - def changeScaling(self, i): - self.imageScalingIndex = i - - def applyChanges(self): - self.applySelections(['imageScaling']) - self.parent.canvas.setViewImageMode(self.imageScalingIndex) - return True - - class ShortcutPicker(BasePicker): def __init__(self, parent, tracker): config = parent.config diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py index 74c43f9..75ec14c 100644 --- a/app/components/settings/__init__.py +++ b/app/components/settings/__init__.py @@ -18,4 +18,4 @@ """ from .base import BaseSettings -from .popups import OptionsContainer, TesseractOptions +from .popups import ImageScalingOptions, OptionsContainer, TesseractOptions diff --git a/app/components/settings/popups/__init__.py b/app/components/settings/popups/__init__.py index d78e704..e5d716b 100644 --- a/app/components/settings/popups/__init__.py +++ b/app/components/settings/popups/__init__.py @@ -18,4 +18,5 @@ """ from .container import OptionsContainer +from .imageScaling import ImageScalingOptions from .tesseract import TesseractOptions diff --git a/app/components/settings/popups/base.py b/app/components/settings/popups/base.py index 9cb52e7..a3de2f8 100644 --- a/app/components/settings/popups/base.py +++ b/app/components/settings/popups/base.py @@ -32,6 +32,7 @@ class BaseOptions(BaseSettings): """ def __init__(self, parent: QWidget, optionLists:list[list[str]]=[]): super().__init__(parent) + self.mainWindow = parent self.setAttribute(Qt.WA_DeleteOnClose) self.setLayout(QGridLayout()) diff --git a/app/components/settings/popups/imageScaling.py b/app/components/settings/popups/imageScaling.py new file mode 100644 index 0000000..0369310 --- /dev/null +++ b/app/components/settings/popups/imageScaling.py @@ -0,0 +1,38 @@ +""" +Poricom Settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtWidgets import (QWidget) + +from .base import BaseOptions +from utils.constants import IMAGE_SCALING + + +class ImageScalingOptions(BaseOptions): + def __init__(self, parent: QWidget): + super().__init__(parent, [IMAGE_SCALING]) + # TODO: Use constants here + # TODO: Image scaling must be an enum not an int + self.initializeProperties([("imageScaling", 0, int)]) + + def changeImageScaling(self, i): + self.imageScalingIndex = i + + def saveSettings(self, hasMessage=False): + self.mainWindow.canvas.setViewImageMode(self.imageScalingIndex) + return super().saveSettings(hasMessage) diff --git a/app/utils/constants.py b/app/utils/constants.py index b470827..c16e3bc 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -26,6 +26,8 @@ LANGUAGE = [ " Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English"] ORIENTATION = [ " Vertical", " Horizontal"] +IMAGE_SCALING = [ " Fit to Width", " Fit to Height", " Fit to Screen"] + # Paths TESSERACT_LANGUAGES = "./assets/languages/" From 7e2715ff0136737b08bedd60e9d0c8ca868114e6 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 16:33:26 +0800 Subject: [PATCH 043/137] Refactor FontOptions to inherit BaseOptions --- app/MainWindow.py | 6 +-- app/Popups.py | 53 +------------------- app/components/settings/__init__.py | 2 +- app/components/settings/popups/__init__.py | 1 + app/components/settings/popups/font.py | 58 ++++++++++++++++++++++ app/components/views/ocr/base.py | 4 +- app/utils/config.py | 13 ----- app/utils/constants.py | 8 +++ app/utils/scripts/__init__.py | 1 + app/utils/scripts/editStylesheet.py | 33 ++++++++++++ 10 files changed, 108 insertions(+), 71 deletions(-) create mode 100644 app/components/settings/popups/font.py create mode 100644 app/utils/scripts/editStylesheet.py diff --git a/app/MainWindow.py b/app/MainWindow.py index a874dd2..1df4f35 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -27,10 +27,10 @@ QPushButton, QFileDialog) from components.services import BaseWorker -from components.settings import ImageScalingOptions, OptionsContainer, TesseractOptions +from components.settings import FontOptions, ImageScalingOptions, OptionsContainer, TesseractOptions from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView -from Popups import (FontPicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) +from Popups import (ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) from utils.config import config, saveOnClose from utils.constants import LOAD_MODEL_MESSAGE from utils.scripts import mangaFileToImageDir @@ -169,7 +169,7 @@ def toggleStylesheet(self): app.setStyleSheet(fh.read()) def modifyFontSettings(self): - confirmation = PickerPopup(FontPicker(self, self.tracker)) + confirmation = OptionsContainer(FontOptions(self)) ret = confirmation.exec() if ret: diff --git a/app/Popups.py b/app/Popups.py index c3ca146..45c0d91 100644 --- a/app/Popups.py +++ b/app/Popups.py @@ -21,7 +21,7 @@ from PyQt5.QtWidgets import (QGridLayout, QVBoxLayout, QWidget, QLabel, QCheckBox, QLineEdit, QComboBox, QDialog, QDialogButtonBox, QMessageBox) -from utils.config import (editSelectionConfig, editStylesheet) +from utils.config import (editSelectionConfig) class MessagePopup(QMessageBox): @@ -71,57 +71,6 @@ def applySelections(self, selections): editSelectionConfig(index, selection) -class FontPicker(BasePicker): - def __init__(self, parent, tracker): - config = parent.config - listTop = config["FONT_STYLE"] - listBot = config["FONT_SIZE"] - optionLists = [listTop, listBot] - - super().__init__(parent, tracker, optionLists) - self.pickTop.currentIndexChanged.connect(self.changeFontStyle) - self.pickTop.setCurrentIndex(config["SELECTED_INDEX"]["fontStyle"]) - self.nameTop.setText("Font Style: ") - self.pickBot.currentIndexChanged.connect(self.changeFontSize) - self.pickBot.setCurrentIndex(config["SELECTED_INDEX"]["fontSize"]) - self.nameBot.setText("Font Size: ") - - self.persistText = QComboBox() - self.persistText.addItems(["Disabled", "Enabled"]) - self.layout.addWidget(QLabel("Persist Text: "), 2, 0) - self.layout.addWidget(self.persistText, 2, 1) - self.persistText.setCurrentIndex(config["PERSIST_TEXT_MODE"]) - self.persistText.currentIndexChanged.connect(self.changePersistText) - - self.fontStyleText = f" font-family: '{self.pickTop.currentText().strip()}';\n" - self.fontSizeText = f" font-size: {self.pickBot.currentText().strip()}pt;\n" - self.fontStyleIndex = self.pickTop.currentIndex() - self.fontSizeIndex = self.pickBot.currentIndex() - self.persistTextIndex = self.persistText.currentIndex() - - def changeFontStyle(self, i): - self.fontStyleIndex = i - selectedFontStyle = self.pickTop.currentText().strip() - replacementText = f" font-family: '{selectedFontStyle}';\n" - self.fontStyleText = replacementText - - def changeFontSize(self, i): - self.fontSizeIndex = i - selectedFontSize = int(self.pickBot.currentText().strip()) - replacementText = f" font-size: {selectedFontSize}pt;\n" - self.fontSizeText = replacementText - - def changePersistText(self, i): - self.persistTextIndex = i - - def applyChanges(self): - self.applySelections(['fontStyle', 'fontSize']) - editStylesheet(41, self.fontStyleText) - editStylesheet(42, self.fontSizeText) - self.parent.config["PERSIST_TEXT_MODE"] = self.persistTextIndex - return True - - class ShortcutPicker(BasePicker): def __init__(self, parent, tracker): config = parent.config diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py index 75ec14c..e3dad24 100644 --- a/app/components/settings/__init__.py +++ b/app/components/settings/__init__.py @@ -18,4 +18,4 @@ """ from .base import BaseSettings -from .popups import ImageScalingOptions, OptionsContainer, TesseractOptions +from .popups import FontOptions, ImageScalingOptions, OptionsContainer, TesseractOptions diff --git a/app/components/settings/popups/__init__.py b/app/components/settings/popups/__init__.py index e5d716b..e6cc9c4 100644 --- a/app/components/settings/popups/__init__.py +++ b/app/components/settings/popups/__init__.py @@ -18,5 +18,6 @@ """ from .container import OptionsContainer +from .font import FontOptions from .imageScaling import ImageScalingOptions from .tesseract import TesseractOptions diff --git a/app/components/settings/popups/font.py b/app/components/settings/popups/font.py new file mode 100644 index 0000000..cbc3e43 --- /dev/null +++ b/app/components/settings/popups/font.py @@ -0,0 +1,58 @@ +""" +Poricom Settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtWidgets import (QWidget) + +from .base import BaseOptions +from utils.constants import FONT_SIZE, FONT_STYLE, TOGGLE_CHOICES +from utils.scripts import editStylesheet + + +class FontOptions(BaseOptions): + def __init__(self, parent: QWidget): + super().__init__(parent, [FONT_STYLE, FONT_SIZE, TOGGLE_CHOICES]) + self.initializeProperties([ + ("fontStyle", " font-family: 'Helvetica';\n", str), + ("fontSize", " font-size: 16pt;\n", str), + ("persistText", "true", bool), + ]) + self.setOptionIndex("fontSize", 2) + self.setOptionIndex("persistText", 1) + + def changeFontStyle(self, i): + self.fontStyleIndex = i + selectedFontStyle = self.fontStyleComboBox.currentText().strip() + replacementText = f" font-family: '{selectedFontStyle}';\n" + self.fontStyle = replacementText + + def changeFontSize(self, i): + self.fontSizeIndex = i + selectedFontSize = int(self.fontSizeComboBox.currentText().strip()) + replacementText = f" font-size: {selectedFontSize}pt;\n" + self.fontSize = replacementText + + def changePersistText(self, i): + self.persistTextIndex = i + self.persistText = True if i else False + + def saveSettings(self, hasMessage=False): + editStylesheet(41, self.fontStyle) + editStylesheet(42, self.fontSize) + self.mainWindow.canvas.setProperty('persistText', "true" if self.persistText else "false") + return super().saveSettings(hasMessage) diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 120535f..ff7b956 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -50,7 +50,7 @@ def __init__(self, parent: QMainWindow, tracker=None): self.setDragMode(QGraphicsView.RubberBandDrag) self.addDefaults(TESSERACT_DEFAULTS) - self.addProperty('persistTextMode', "false", bool) + self.addProperty('persistText', "true", bool) def handleTextResult(self, result): try: @@ -96,7 +96,7 @@ def mouseReleaseEvent(self, event): text = self.canvasText.text() logText(text, isLogFile=isLogFile, path=logPath) try: - if not self.persistTextMode: + if not self.persistText: self.canvasText.hide() except AttributeError: pass diff --git a/app/utils/config.py b/app/utils/config.py index a283705..7d06220 100644 --- a/app/utils/config.py +++ b/app/utils/config.py @@ -38,16 +38,3 @@ def editSelectionConfig(index, cBoxName, config="utils/config.toml"): data["SELECTED_INDEX"][cBoxName] = index with open(config, 'w') as fh: toml.dump(data, fh) - - -def editStylesheet(index, replacementText): - sheetLight = './assets/styles.qss' - sheetDark = './assets/styles-dark.qss' - with open(sheetLight, 'r') as slFh, open(sheetDark, 'r') as sdFh: - lineLight = slFh.readlines() - linesDark = sdFh.readlines() - lineLight[index] = replacementText - linesDark[index] = replacementText - with open(sheetLight, 'w') as slFh, open(sheetDark, 'w') as sdFh: - slFh.writelines(lineLight) - sdFh.writelines(linesDark) diff --git a/app/utils/constants.py b/app/utils/constants.py index c16e3bc..37ef2c1 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -23,12 +23,20 @@ IMAGE_EXTENSIONS = ["*.bmp", "*.gif", "*.jpeg", "*.jpg", "*.pbm", "*.pgm", "*.png", "*.ppm", "*.webp", "*.xbm", "*.xpm",] # Settings +TOGGLE_CHOICES = [ " Disabled", " Enabled"] + LANGUAGE = [ " Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English"] ORIENTATION = [ " Vertical", " Horizontal"] IMAGE_SCALING = [ " Fit to Width", " Fit to Height", " Fit to Screen"] +FONT_SIZE = [ " 12", " 14", " 16", " 20", " 24", " 32", " 40", " 56", " 72"] +FONT_STYLE = [ " Helvetica", " Poppins", " Arial", " Verdana", " Times New Roman"] + # Paths +STYLESHEET_LIGHT = './assets/styles.qss' +STYLESHEET_DARK = './assets/styles-dark.qss' + TESSERACT_LANGUAGES = "./assets/languages/" TOOLBAR_ICONS = './assets/images/icons/' diff --git a/app/utils/scripts/__init__.py b/app/utils/scripts/__init__.py index 5b5328c..603452d 100644 --- a/app/utils/scripts/__init__.py +++ b/app/utils/scripts/__init__.py @@ -17,6 +17,7 @@ along with this program. If not, see . """ +from .editStylesheet import editStylesheet from .logText import logText from .mangaFileToImageDir import mangaFileToImageDir from .pixmapToText import pixmapToText diff --git a/app/utils/scripts/editStylesheet.py b/app/utils/scripts/editStylesheet.py new file mode 100644 index 0000000..a0c09e5 --- /dev/null +++ b/app/utils/scripts/editStylesheet.py @@ -0,0 +1,33 @@ +""" +Poricom Helper Functions + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from ..constants import STYLESHEET_LIGHT, STYLESHEET_DARK + +def editStylesheet(index: int, style: str): + """ + Replace stylesheet at line `index` with input `style` + """ + with open(STYLESHEET_LIGHT, 'r') as slFh, open(STYLESHEET_DARK, 'r') as sdFh: + lineLight = slFh.readlines() + linesDark = sdFh.readlines() + lineLight[index] = style + linesDark[index] = style + with open(STYLESHEET_LIGHT, 'w') as slFh, open(STYLESHEET_DARK, 'w') as sdFh: + slFh.writelines(lineLight) + sdFh.writelines(linesDark) From 6d1e410799930f97b49085f24d9bc98233c424a3 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 16:41:12 +0800 Subject: [PATCH 044/137] Rename FontOptions to PreviewOptions --- app/MainWindow.py | 4 ++-- app/components/settings/__init__.py | 2 +- app/components/settings/popups/__init__.py | 2 +- app/components/settings/popups/{font.py => preview.py} | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename app/components/settings/popups/{font.py => preview.py} (98%) diff --git a/app/MainWindow.py b/app/MainWindow.py index 1df4f35..5229798 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -27,7 +27,7 @@ QPushButton, QFileDialog) from components.services import BaseWorker -from components.settings import FontOptions, ImageScalingOptions, OptionsContainer, TesseractOptions +from components.settings import PreviewOptions, ImageScalingOptions, OptionsContainer, TesseractOptions from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView from Popups import (ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) @@ -169,7 +169,7 @@ def toggleStylesheet(self): app.setStyleSheet(fh.read()) def modifyFontSettings(self): - confirmation = OptionsContainer(FontOptions(self)) + confirmation = OptionsContainer(PreviewOptions(self)) ret = confirmation.exec() if ret: diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py index e3dad24..96307a7 100644 --- a/app/components/settings/__init__.py +++ b/app/components/settings/__init__.py @@ -18,4 +18,4 @@ """ from .base import BaseSettings -from .popups import FontOptions, ImageScalingOptions, OptionsContainer, TesseractOptions +from .popups import ImageScalingOptions, OptionsContainer, PreviewOptions, TesseractOptions diff --git a/app/components/settings/popups/__init__.py b/app/components/settings/popups/__init__.py index e6cc9c4..1df3628 100644 --- a/app/components/settings/popups/__init__.py +++ b/app/components/settings/popups/__init__.py @@ -18,6 +18,6 @@ """ from .container import OptionsContainer -from .font import FontOptions from .imageScaling import ImageScalingOptions +from .preview import PreviewOptions from .tesseract import TesseractOptions diff --git a/app/components/settings/popups/font.py b/app/components/settings/popups/preview.py similarity index 98% rename from app/components/settings/popups/font.py rename to app/components/settings/popups/preview.py index cbc3e43..0e59f26 100644 --- a/app/components/settings/popups/font.py +++ b/app/components/settings/popups/preview.py @@ -24,7 +24,7 @@ from utils.scripts import editStylesheet -class FontOptions(BaseOptions): +class PreviewOptions(BaseOptions): def __init__(self, parent: QWidget): super().__init__(parent, [FONT_STYLE, FONT_SIZE, TOGGLE_CHOICES]) self.initializeProperties([ From 4390ddc5e3cff687fedf09ba49cf012fb08877ff Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 16:41:48 +0800 Subject: [PATCH 045/137] Load preview settings on app start --- app/components/views/ocr/ocr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/views/ocr/ocr.py b/app/components/views/ocr/ocr.py index fd830a6..ebb1358 100644 --- a/app/components/views/ocr/ocr.py +++ b/app/components/views/ocr/ocr.py @@ -28,6 +28,7 @@ class OCRView(BaseImageView, BaseOCRView): def __init__(self, parent: QMainWindow, tracker=None): # TODO: Remove references to tracker super().__init__(parent, tracker) + self.loadSettings() @pyqtSlot() def rubberBandStopped(self): From aefd9ac546d23dd2f713874bc3e66f820ffddd4a Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 21:27:12 +0800 Subject: [PATCH 046/137] Refactor ShortcutOptions to inherit BaseOptions --- app/MainWindow.py | 12 +--- app/Popups.py | 52 ----------------- app/components/settings/__init__.py | 2 +- app/components/settings/popups/__init__.py | 1 + app/components/settings/popups/shortcut.py | 66 ++++++++++++++++++++++ app/main.py | 10 ++-- app/utils/constants.py | 4 +- 7 files changed, 80 insertions(+), 67 deletions(-) create mode 100644 app/components/settings/popups/shortcut.py diff --git a/app/MainWindow.py b/app/MainWindow.py index 5229798..ba805dd 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -27,10 +27,10 @@ QPushButton, QFileDialog) from components.services import BaseWorker -from components.settings import PreviewOptions, ImageScalingOptions, OptionsContainer, TesseractOptions +from components.settings import PreviewOptions, ImageScalingOptions, OptionsContainer, ShortcutOptions, TesseractOptions from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView -from Popups import (ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) +from Popups import (MessagePopup, CheckboxPopup) from utils.config import config, saveOnClose from utils.constants import LOAD_MODEL_MESSAGE from utils.scripts import mangaFileToImageDir @@ -203,13 +203,7 @@ def toggleMouseMode(self): self.canvas.toggleZoomPanMode() def modifyHotkeys(self): - confirmation = PickerPopup(ShortcutPicker(self, self.tracker)) - ret = confirmation.exec() - if ret: - MessagePopup( - "Shortcut Remapped", - "Close the app to apply changes." - ).exec() + OptionsContainer(ShortcutOptions(self)).exec() # ------------------------------ Misc Functions ------------------------------ # diff --git a/app/Popups.py b/app/Popups.py index 45c0d91..416a6cc 100644 --- a/app/Popups.py +++ b/app/Popups.py @@ -71,58 +71,6 @@ def applySelections(self, selections): editSelectionConfig(index, selection) -class ShortcutPicker(BasePicker): - def __init__(self, parent, tracker): - config = parent.config - listTop = config["MODIFIER"] - optionLists = [listTop] - - super().__init__(parent, tracker, optionLists) - self.pickTop.currentIndexChanged.connect(self.changeModifier) - self.pickTop.setCurrentIndex(config["SELECTED_INDEX"]["modifier"]) - self.nameTop.setText("Modifier: ") - - self.pickBot = QLineEdit(config["SHORTCUT"]["captureExternalKey"]) - self.layout.addWidget(self.pickBot, 1, 1) - self.nameBot = QLabel("Key: ") - self.layout.addWidget(self.nameBot, 1, 0) - - self.modifierIndex = self.pickTop.currentIndex() - - def keyInvalidError(self): - MessagePopup( - "Invalid Key", - "Please select an alphanumeric key." - ).exec() - - def changeModifier(self, i): - self.modifierIndex = i - - def setShortcut(self, keyName, modifierText, keyText): - - tooltip = f"{self.parent.config['SHORTCUT'][f'{keyName}Tip']}{modifierText}{keyText}." - self.parent.config["SHORTCUT"][keyName] = f"{modifierText}{keyText}" - self.parent.config["SHORTCUT"][f"{keyName}Key"] = keyText - self.parent.config["TBAR_FUNCS"]["FILE"][f"{keyName}Helper"]["helpMsg"] = tooltip - - def applyChanges(self): - selectedModifier = self.pickTop.currentText().strip() + "+" - if selectedModifier == "No Modifier+": - selectedModifier = "" - - if not self.pickBot.text().isalnum(): - self.keyInvalidError() - return False - if len(self.pickBot.text()) != 1: - self.keyInvalidError() - return False - - self.setShortcut('captureExternal', selectedModifier, - self.pickBot.text()) - self.applySelections(['modifier']) - return True - - class PickerPopup(QDialog): def __init__(self, widget): super(QDialog, self).__init__(None, diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py index 96307a7..32784d0 100644 --- a/app/components/settings/__init__.py +++ b/app/components/settings/__init__.py @@ -18,4 +18,4 @@ """ from .base import BaseSettings -from .popups import ImageScalingOptions, OptionsContainer, PreviewOptions, TesseractOptions +from .popups import ImageScalingOptions, OptionsContainer, PreviewOptions, ShortcutOptions, TesseractOptions diff --git a/app/components/settings/popups/__init__.py b/app/components/settings/popups/__init__.py index 1df3628..1b9a45c 100644 --- a/app/components/settings/popups/__init__.py +++ b/app/components/settings/popups/__init__.py @@ -20,4 +20,5 @@ from .container import OptionsContainer from .imageScaling import ImageScalingOptions from .preview import PreviewOptions +from .shortcut import ShortcutOptions from .tesseract import TesseractOptions diff --git a/app/components/settings/popups/shortcut.py b/app/components/settings/popups/shortcut.py new file mode 100644 index 0000000..8d57ee7 --- /dev/null +++ b/app/components/settings/popups/shortcut.py @@ -0,0 +1,66 @@ +""" +Poricom Settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtWidgets import (QLabel, QLineEdit, QWidget) + +from .base import BaseOptions +from components.popups import BasePopup +from utils.constants import MODIFIER + +class ShortcutOptions(BaseOptions): + def __init__(self, parent: QWidget): + super().__init__(parent, [MODIFIER]) + self.initializeProperties([("modifier", "Alt", str)]) + self.setOptionIndex("modifier", 2) + self.addDefaults({ + "captureExternalKey": "Q", + "captureExternalShortcut": "Alt+Q" + }) + self.loadSettings() + + self.keyLineEdit = QLineEdit(self.captureExternalKey) + self.layout().addWidget(self.keyLineEdit, 1, 1) + self.layout().addWidget(QLabel("Key: "), 1, 0) + + def raiseKeyInvalidError(self, message: str): + BasePopup("Invalid Key", message).exec() + + def changeModifier(self, i): + self.modifierIndex = i + self.modifier = self.modifierComboBox.currentText().strip() + "+" + if self.modifier == "No Modifier+": + self.modifier = "" + + def changeShortcut(self): + self.captureExternalShortcut = self.modifier + self.captureExternalKey + + def saveSettings(self, hasMessage=False): + if not self.keyLineEdit.text().isalnum(): + self.raiseKeyInvalidError("Please select an alphanumeric key.") + return + if len(self.keyLineEdit.text()) != 1: + self.raiseKeyInvalidError("Please select exactly one key.") + return + self.captureExternalKey = self.keyLineEdit.text() + + self.changeShortcut() + + super().saveSettings(hasMessage) + + BasePopup("Shortcut Remapped", "Close the app to apply changes.").exec() diff --git a/app/main.py b/app/main.py index 1936c16..f863470 100644 --- a/app/main.py +++ b/app/main.py @@ -20,13 +20,14 @@ from PyQt5.QtWidgets import QApplication from PyQt5.QtGui import QIcon -from PyQt5.QtCore import QAbstractEventDispatcher +from PyQt5.QtCore import QAbstractEventDispatcher, QSettings from pyqtkeybind import keybinder from components.services import WinEventFilter from MainWindow import MainWindow from Trackers import Tracker from utils.config import config +from utils.constants import SETTINGS_FILE_DEFAULT if __name__ == '__main__': @@ -41,10 +42,11 @@ with open(styles, 'r') as fh: app.setStyleSheet(fh.read()) + settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) + shortcut = settings.value("captureExternalShortcut", "Alt+Q") keybinder.init() - previousShortcut = config["SHORTCUT"]["captureExternal"] keybinder.register_hotkey( - widget.winId(), config["SHORTCUT"]["captureExternal"], widget.captureExternal) + widget.winId(), shortcut, widget.captureExternal) winEventFilter = WinEventFilter(keybinder) eventDispatcher = QAbstractEventDispatcher.instance() eventDispatcher.installNativeEventFilter(winEventFilter) @@ -53,5 +55,5 @@ widget.loadModel() app.exec_() - # keybinder.unregister_hotkey(widget.winId(), previousShortcut) + # keybinder.unregister_hotkey(widget.winId(), shortcut) sys.exit() diff --git a/app/utils/constants.py b/app/utils/constants.py index 37ef2c1..1826091 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -33,6 +33,8 @@ FONT_SIZE = [ " 12", " 14", " 16", " 20", " 24", " 32", " 40", " 56", " 72"] FONT_STYLE = [ " Helvetica", " Poppins", " Arial", " Verdana", " Times New Roman"] +MODIFIER = [ " Ctrl", " Shift", " Alt", " Ctrl+Alt", " Shift+Alt", " Shift+Ctrl", " Shift+Alt+Ctrl", " No Modifier"] + # Paths STYLESHEET_LIGHT = './assets/styles.qss' STYLESHEET_DARK = './assets/styles-dark.qss' @@ -151,7 +153,7 @@ }, "captureExternalHelper": { "title": "External capture", - "message": "This will minimize the app and perform OCR on the current screen. Alternatively, you may use the shortcut Alt+Q.", + "message": "This will minimize the app and perform OCR on the current screen. Alternatively, you may use the shortcut Alt+Q (default).", "path": "captureExternalHelper.png", "toggle": False, "align": "AlignLeft", From 91ac4299dd920a62bd5bc87a3316506d9cba4ef6 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 21:29:41 +0800 Subject: [PATCH 047/137] Load tesseract settings on fullscreen view --- app/components/settings/base.py | 6 ++++-- app/components/views/ocr/fullscreen.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/components/settings/base.py b/app/components/settings/base.py index 46c49f8..78aeb13 100644 --- a/app/components/settings/base.py +++ b/app/components/settings/base.py @@ -110,8 +110,10 @@ def saveSettings(self, hasMessage=True): if hasMessage: BasePopup("Save Settings", "Configuration has been saved.").exec() - def loadSettings(self): - for propName, propDefault in self._defaults.items(): + def loadSettings(self, settings: dict[str, Any] = {}): + if not settings: + settings = self._defaults + for propName, propDefault in settings.items(): prop = self.settings.value(f"{self._prefix}{propName}", propDefault) self.setProperty(propName, prop) diff --git a/app/components/views/ocr/fullscreen.py b/app/components/views/ocr/fullscreen.py index 37f36f2..90a42d3 100644 --- a/app/components/views/ocr/fullscreen.py +++ b/app/components/views/ocr/fullscreen.py @@ -22,6 +22,7 @@ from PyQt5.QtGui import QCursor, QMouseEvent from .base import BaseOCRView +from utils.constants import TESSERACT_DEFAULTS class FullScreenOCRView(BaseOCRView): @@ -36,6 +37,7 @@ def __init__(self, parent: QMainWindow, tracker=None): self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setScene(QGraphicsScene()) + self.loadSettings(TESSERACT_DEFAULTS) def takeScreenshot(self, screenIndex: int): screen = QApplication.screens()[screenIndex] From dcde80492875e39d4623483dfb867702954810cd Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 21:30:43 +0800 Subject: [PATCH 048/137] Update CheckboxPopup to use QSettings --- app/MainWindow.py | 58 ++++++------ app/Popups.py | 94 ------------------- app/components/popups/__init__.py | 1 + app/components/popups/checkbox.py | 46 +++++++++ .../toolbar/tabs/containers/base.py | 2 +- 5 files changed, 79 insertions(+), 122 deletions(-) delete mode 100644 app/Popups.py create mode 100644 app/components/popups/checkbox.py diff --git a/app/MainWindow.py b/app/MainWindow.py index ba805dd..4932fe1 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -26,33 +26,38 @@ from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QDesktopWidget, QMainWindow, QApplication, QPushButton, QFileDialog) +from components.popups import BasePopup, CheckboxPopup from components.services import BaseWorker -from components.settings import PreviewOptions, ImageScalingOptions, OptionsContainer, ShortcutOptions, TesseractOptions +from components.settings import BaseSettings, PreviewOptions, ImageScalingOptions, OptionsContainer, ShortcutOptions, TesseractOptions from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView -from Popups import (MessagePopup, CheckboxPopup) from utils.config import config, saveOnClose from utils.constants import LOAD_MODEL_MESSAGE from utils.scripts import mangaFileToImageDir -class MainWindow(QMainWindow): - +class MainWindow(QMainWindow, BaseSettings): def __init__(self, parent=None, tracker=None): - super(QWidget, self).__init__(parent) + super().__init__(parent) self.tracker = tracker self.config = config self.vLayout = QVBoxLayout() self.mainView = WorkspaceView(self, self.tracker) - self.ribbon = BaseToolbar(self) - self.vLayout.addWidget(self.ribbon) + self.toolbar = BaseToolbar(self) + self.vLayout.addWidget(self.toolbar) self.vLayout.addWidget(self.mainView) - _mainWidget = QWidget() - _mainWidget.setLayout(self.vLayout) - self.setCentralWidget(_mainWidget) + mainWidget = QWidget() + mainWidget.setLayout(self.vLayout) + self.setCentralWidget(mainWidget) + + self.setDefaults({"hasLoadModelPopup": "true"}) + self.setTypes({"hasLoadModelPopup": bool}) + self.loadSettings() + print(self.hasLoadModelPopup) + self.threadpool = QThreadPool() @@ -73,9 +78,9 @@ def closeEvent(self, event): saveOnClose(self.config) return QMainWindow.closeEvent(self, event) - def poricomNoop(self): - MessagePopup( - "WIP", + def noop(self): + BasePopup( + "Not Implemented", "This function is not yet implemented." ).exec() @@ -93,9 +98,9 @@ def openDir(self): self.tracker.filepath = filepath self.explorer.setDirectory(filepath) except FileNotFoundError: - MessagePopup( - f"No images found in the directory", - f"Please select a directory with images." + BasePopup( + "No images found in the directory", + "Please select a directory with images." ).exec() def openManga(self): @@ -111,7 +116,7 @@ def setDirectory(filepath): self.tracker.filepath = filepath self.explorer.setDirectory(filepath) - openMangaButton = self.ribbon.findChild( + openMangaButton = self.toolbar.findChild( QPushButton, "openManga") worker = BaseWorker(mangaFileToImageDir, filename) @@ -208,18 +213,17 @@ def modifyHotkeys(self): # ------------------------------ Misc Functions ------------------------------ # def loadModel(self): - loadModelButton = self.ribbon.findChild(QPushButton, "loadModel") + loadModelButton = self.toolbar.findChild(QPushButton, "loadModel") loadModelButton.setChecked(not self.tracker.ocrModel) - if loadModelButton.isChecked() and self.config["LOAD_MODEL_POPUP"]: - confirmation = CheckboxPopup( + if loadModelButton.isChecked() and self.hasLoadModelPopup: + ret = CheckboxPopup( + "hasLoadModelPopup", "Load the MangaOCR model?", LOAD_MODEL_MESSAGE, - MessagePopup.Ok | MessagePopup.Cancel - ) - ret = confirmation.exec() - self.config["LOAD_MODEL_POPUP"] = not confirmation.checkBox().isChecked() - if (ret == MessagePopup.Ok): + CheckboxPopup.Ok | CheckboxPopup.Cancel + ).exec() + if (ret == CheckboxPopup.Ok): pass else: loadModelButton.setChecked(False) @@ -241,12 +245,12 @@ def loadModelHelper(tracker): def loadModelConfirm(message: str): modelName = "MangaOCR" if self.tracker.ocrModel else "Tesseract" if message == "success": - MessagePopup( + BasePopup( f"{modelName} model loaded", f"You are now using the {modelName} model for Japanese text detection." ).exec() else: - MessagePopup("Load Model Error", message).exec() + BasePopup("Load Model Error", message).exec() loadModelButton.setChecked(False) worker = BaseWorker(loadModelHelper, self.tracker) diff --git a/app/Popups.py b/app/Popups.py deleted file mode 100644 index 416a6cc..0000000 --- a/app/Popups.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -Poricom Popup Components - -Copyright (C) `2021-2022` `` - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -from PyQt5.QtCore import (Qt) -from PyQt5.QtWidgets import (QGridLayout, QVBoxLayout, QWidget, QLabel, QCheckBox, - QLineEdit, QComboBox, QDialog, QDialogButtonBox, QMessageBox) - -from utils.config import (editSelectionConfig) - - -class MessagePopup(QMessageBox): - def __init__(self, title, message, flags=QMessageBox.Ok): - super(QMessageBox, self).__init__( - QMessageBox.NoIcon, title, message, flags) - - -class CheckboxPopup(MessagePopup): - def __init__(self, title, message, flags=QMessageBox.Ok, - checkboxMessage="Don't show this dialog again"): - super(MessagePopup, self).__init__( - MessagePopup.NoIcon, title, message, flags) - self.checkbox = QCheckBox(checkboxMessage) - self.setCheckBox(self.checkbox) - -class BasePicker(QWidget): - def __init__(self, parent, tracker, optionLists=[]): - super(QWidget, self).__init__() - self.parent = parent - self.tracker = tracker - - self.layout = QGridLayout(self) - self.layout.setContentsMargins(0, 0, 0, 0) - - _comboBoxList = [] - _labelList = [] - - for i in range(len(optionLists)): - optionList = optionLists[i] - - _comboBoxList.append(QComboBox()) - _comboBoxList[i].addItems(optionList) - self.layout.addWidget(_comboBoxList[i], i, 1) - _labelList.append(QLabel("")) - self.layout.addWidget(_labelList[i], i, 0) - - self.pickTop = _comboBoxList[0] - self.pickBot = _comboBoxList[-1] - self.nameTop = _labelList[0] - self.nameBot = _labelList[-1] - - def applySelections(self, selections): - for selection in selections: - index = getattr(self, f"{selection}Index") - self.parent.config["SELECTED_INDEX"][selection] = index - editSelectionConfig(index, selection) - - -class PickerPopup(QDialog): - def __init__(self, widget): - super(QDialog, self).__init__(None, - Qt.WindowCloseButtonHint | Qt.WindowSystemMenuHint | Qt.WindowTitleHint) - self.widget = widget - self.setLayout(QVBoxLayout()) - self.layout().addWidget(widget) - self.buttonBox = QDialogButtonBox( - QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.layout().addWidget(self.buttonBox) - - self.buttonBox.rejected.connect(self.cancelClickedEvent) - self.buttonBox.accepted.connect(self.accept) - self.buttonBox.rejected.connect(self.reject) - - def accept(self): - if self.widget.applyChanges(): - return super().accept() - - def cancelClickedEvent(self): - self.close() diff --git a/app/components/popups/__init__.py b/app/components/popups/__init__.py index 121e868..1563758 100644 --- a/app/components/popups/__init__.py +++ b/app/components/popups/__init__.py @@ -18,3 +18,4 @@ """ from .base import BasePopup +from .checkbox import CheckboxPopup diff --git a/app/components/popups/checkbox.py b/app/components/popups/checkbox.py new file mode 100644 index 0000000..bdd4c92 --- /dev/null +++ b/app/components/popups/checkbox.py @@ -0,0 +1,46 @@ +""" +Poricom Popups + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import QSettings +from PyQt5.QtWidgets import (QCheckBox) + +from components.popups import BasePopup +from utils.constants import SETTINGS_FILE_DEFAULT + +class CheckboxPopup(BasePopup): + """Popup message with a checkbox + + Args: + prop (str): Name of the boolean property to be saved. + checkboxMessage (str, optional): Checkbox label. + Defaults to "Don't show this dialog again". + """ + def __init__(self, prop: str, title: str, message: str, + buttons: BasePopup.StandardButtons = BasePopup.Ok, + checkboxMessage="Don't show this dialog again"): + super().__init__(title, message, buttons) + + self.setCheckBox(QCheckBox(checkboxMessage, self)) + + self.prop = prop + self.accepted.connect(self.saveSettings) + + def saveSettings(self): + settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) + settings.setValue(self.prop, not self.checkBox().isChecked()) diff --git a/app/components/toolbar/tabs/containers/base.py b/app/components/toolbar/tabs/containers/base.py index d666c7b..d1ab13f 100644 --- a/app/components/toolbar/tabs/containers/base.py +++ b/app/components/toolbar/tabs/containers/base.py @@ -84,4 +84,4 @@ def initializeButton(self, name: str, config: ButtonConfig): getattr(self.mainWindow.mainView, name)) except AttributeError: button.clicked.connect( - getattr(self.mainWindow, 'poricomNoop')) + getattr(self.mainWindow, 'noop')) From 577cde7ed7a5f32f8cb5072e8731735fa1e99fab Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 23:36:46 +0800 Subject: [PATCH 049/137] Use QSettings to save/load settings Remove toml as dependency --- app/MainWindow.py | 38 +-- app/Trackers.py | 59 ++--- app/components/popups/base.py | 2 +- app/components/settings/popups/container.py | 2 +- app/components/views/image/base.py | 2 +- app/components/views/workspace.py | 5 +- app/main.py | 12 +- app/utils/config.py | 40 --- app/utils/config.toml | 256 -------------------- app/utils/constants.py | 13 + environment/base.yaml | 1 - 11 files changed, 51 insertions(+), 379 deletions(-) delete mode 100644 app/utils/config.py delete mode 100644 app/utils/config.toml diff --git a/app/MainWindow.py b/app/MainWindow.py index 4932fe1..0a7bb79 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -20,7 +20,6 @@ from shutil import rmtree from time import sleep -import toml from manga_ocr import MangaOcr from PyQt5.QtCore import (Qt, QThreadPool) from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QDesktopWidget, QMainWindow, QApplication, @@ -31,8 +30,7 @@ from components.settings import BaseSettings, PreviewOptions, ImageScalingOptions, OptionsContainer, ShortcutOptions, TesseractOptions from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView -from utils.config import config, saveOnClose -from utils.constants import LOAD_MODEL_MESSAGE +from utils.constants import LOAD_MODEL_MESSAGE, MAIN_WINDOW_DEFAULTS, MAIN_WINDOW_TYPES, STYLESHEET_DARK, STYLESHEET_LIGHT from utils.scripts import mangaFileToImageDir @@ -40,7 +38,6 @@ class MainWindow(QMainWindow, BaseSettings): def __init__(self, parent=None, tracker=None): super().__init__(parent) self.tracker = tracker - self.config = config self.vLayout = QVBoxLayout() @@ -53,11 +50,9 @@ def __init__(self, parent=None, tracker=None): mainWidget.setLayout(self.vLayout) self.setCentralWidget(mainWidget) - self.setDefaults({"hasLoadModelPopup": "true"}) - self.setTypes({"hasLoadModelPopup": bool}) + self.setDefaults(MAIN_WINDOW_DEFAULTS) + self.setTypes(MAIN_WINDOW_TYPES) self.loadSettings() - print(self.hasLoadModelPopup) - self.threadpool = QThreadPool() @@ -74,9 +69,8 @@ def closeEvent(self, event): rmtree("./poricom_cache") except FileNotFoundError: pass - self.config["NAV_ROOT"] = self.tracker.filepath - saveOnClose(self.config) - return QMainWindow.closeEvent(self, event) + self.saveSettings(False) + return super().closeEvent(event) def noop(self): BasePopup( @@ -97,6 +91,7 @@ def openDir(self): try: self.tracker.filepath = filepath self.explorer.setDirectory(filepath) + self.explorerPath = filepath except FileNotFoundError: BasePopup( "No images found in the directory", @@ -152,25 +147,16 @@ def captureExternal(self): # ------------------------------ View Functions ------------------------------ # def toggleStylesheet(self): - config = "./utils/config.toml" - lightMode = "./assets/styles.qss" - darkMode = "./assets/styles-dark.qss" - - data = toml.load(config) - if data["STYLES_DEFAULT"] == lightMode: - data["STYLES_DEFAULT"] = darkMode - elif data["STYLES_DEFAULT"] == darkMode: - data["STYLES_DEFAULT"] = lightMode - with open(config, 'w') as fh: - toml.dump(data, fh) + if self.stylesheetPath == STYLESHEET_LIGHT: + self.stylesheetPath = STYLESHEET_DARK + elif self.stylesheetPath == STYLESHEET_DARK: + self.stylesheetPath = STYLESHEET_LIGHT app = QApplication.instance() if app is None: raise RuntimeError("No Qt Application found.") - styles = data["STYLES_DEFAULT"] - self.config["STYLES_DEFAULT"] = data["STYLES_DEFAULT"] - with open(styles, 'r') as fh: + with open(self.stylesheetPath, 'r') as fh: app.setStyleSheet(fh.read()) def modifyFontSettings(self): @@ -182,7 +168,7 @@ def modifyFontSettings(self): if app is None: raise RuntimeError("No Qt Application found.") - with open(config["STYLES_DEFAULT"], 'r') as fh: + with open(self.stylesheetPath, 'r') as fh: app.setStyleSheet(fh.read()) def toggleSplitView(self): diff --git a/app/Trackers.py b/app/Trackers.py index f489067..9654efe 100644 --- a/app/Trackers.py +++ b/app/Trackers.py @@ -20,25 +20,34 @@ from os.path import isfile, join, splitext, normpath, abspath, exists, dirname from os import listdir +from PyQt5.QtCore import QSettings from PyQt5.QtGui import QPixmap, QPainter -from utils.config import config +from utils.constants import EXPLORER_ROOT_DEFAULT, IMAGE_EXTENSIONS, SETTINGS_FILE_DEFAULT +settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) +split = settings.value("splitViewMode").lower() == "true" +explorerPath = settings.value("explorerPath", EXPLORER_ROOT_DEFAULT) +# TODO: This needs refactoring +# 1. Rename the module to `states` +# 2. Instead of one object there should be several state objects i.e. image states, filepath states +# 3. It might also be better if images states is an object tracked by WorkspaceView (parent) +# then pass the references to its children class Tracker: def __init__(self): try: - self.filepath = abspath(config["NAV_ROOT"]) + self.filepath = abspath(explorerPath) except FileNotFoundError: - self.filepath = abspath(config["DEFAULT_NAV_ROOT"]) + self.filepath = abspath(explorerPath) try: filename, filenext, *_ = self._imageList except ValueError: filename, *_ = self._imageList filenext = None - if not config["SPLIT_VIEW_MODE"]: + if not split: self._pixImage = PImage(filename) - if config["SPLIT_VIEW_MODE"]: + if split: splitImage = self.twoFileToImage(filename, filenext) self._pixImage = PImage(splitImage, filename) self._pixMask = PImage(filename) @@ -47,31 +56,9 @@ def __init__(self): self._imageList = [] - selectedLanguage = config["LANGUAGE"][config["SELECTED_INDEX"]["language"]] - self._language = self.selectionToLangCode(selectedLanguage.strip()) - selectedOrientation = config["ORIENTATION"][config["SELECTED_INDEX"]["orientation"]] - self._orientation = self.selectionToOrientCode(selectedOrientation.strip()) - self._betterOCR = False self._ocrModel = None - def selectionToLangCode(self, selectedLanguage): - if selectedLanguage == "Japanese": - langCode = "jpn" - if selectedLanguage == "Korean": - langCode = "kor" - if selectedLanguage == "Chinese SIM": - langCode = "chi_sim" - if selectedLanguage == "Chinese TRA": - langCode = "chi_tra" - if selectedLanguage == "English": - langCode = "eng" - return langCode - - def selectionToOrientCode(self, selectedOrientation): - isVert = selectedOrientation == "Vertical" - return "_vert" if isVert else "" - def twoFileToImage(self, fileLeft, fileRight): imageLeft, imageRight = PImage(fileRight), PImage(fileLeft) if not (imageLeft.isValid()): @@ -132,29 +119,13 @@ def filepath(self): def filepath(self, filepath): fileList = filter(lambda f: isfile(join(filepath, f)), listdir(filepath)) imageList = list(map(lambda p: normpath(join(filepath, p)), filter( - (lambda f: ('*'+splitext(f)[1]) in config["IMAGE_EXTENSIONS"]), fileList))) + (lambda f: ('*'+splitext(f)[1]) in IMAGE_EXTENSIONS), fileList))) if len(imageList) <= 0: raise FileNotFoundError("Empty directory") self._filepath = filepath self._imageList = imageList - @property - def language(self): - return self._language - - @language.setter - def language(self, language): - self._language = language - - @property - def orientation(self): - return self._orientation - - @orientation.setter - def orientation(self, orientation): - self._orientation = orientation - @property def ocrModel(self): return self._ocrModel diff --git a/app/components/popups/base.py b/app/components/popups/base.py index 6e9ea48..cda9e26 100644 --- a/app/components/popups/base.py +++ b/app/components/popups/base.py @@ -1,5 +1,5 @@ """ -Cloe Popups +Poricom Popups Copyright (C) `2021-2022` `` diff --git a/app/components/settings/popups/container.py b/app/components/settings/popups/container.py index c29a6a7..5c037b4 100644 --- a/app/components/settings/popups/container.py +++ b/app/components/settings/popups/container.py @@ -18,7 +18,7 @@ """ from PyQt5.QtCore import (Qt) -from PyQt5.QtWidgets import (QVBoxLayout, QDialog, QDialogButtonBox) +from PyQt5.QtWidgets import (QDialog, QDialogButtonBox, QVBoxLayout) from .base import BaseOptions diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index 414bf3c..7ae0e2b 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -21,7 +21,7 @@ from typing import TYPE_CHECKING from PyQt5.QtCore import (Qt, QRect, QRectF, QSize, QThreadPool) -from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView, QSplitter) +from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView) from components.services import BaseWorker from components.settings import BaseSettings diff --git a/app/components/views/workspace.py b/app/components/views/workspace.py index 2ebd28a..2aa6f73 100644 --- a/app/components/views/workspace.py +++ b/app/components/views/workspace.py @@ -1,5 +1,5 @@ """ -Poricom Main Window Component +Poricom Workspace View Component Copyright (C) `2021-2022` `` @@ -22,9 +22,9 @@ from .ocr import OCRView from components.explorers import ImageExplorer -from utils.config import config from utils.constants import MAIN_VIEW_RATIO +# TODO: Move view settings here class WorkspaceView(QSplitter): """ Main view of the program. Includes the explorer and the view. @@ -32,7 +32,6 @@ class WorkspaceView(QSplitter): def __init__(self, parent: QMainWindow, tracker=None): super().__init__(parent) self.tracker = tracker - self.config = config self.canvas = OCRView(self, self.tracker) self.explorer = ImageExplorer(self, self.tracker.filepath) diff --git a/app/main.py b/app/main.py index f863470..2dcdc5c 100644 --- a/app/main.py +++ b/app/main.py @@ -26,23 +26,23 @@ from components.services import WinEventFilter from MainWindow import MainWindow from Trackers import Tracker -from utils.config import config -from utils.constants import SETTINGS_FILE_DEFAULT +from utils.constants import APP_NAME, APP_LOGO, SETTINGS_FILE_DEFAULT, STYLESHEET_LIGHT if __name__ == '__main__': app = QApplication(sys.argv) - app.setApplicationName("Poricom") - app.setWindowIcon(QIcon(config["LOGO"])) + app.setApplicationName(APP_NAME) + app.setWindowIcon(QIcon(APP_LOGO)) tracker = Tracker() widget = MainWindow(parent=None, tracker=tracker) - styles = config["STYLES_DEFAULT"] + settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) + + styles = settings.value("stylesheetPath", STYLESHEET_LIGHT) with open(styles, 'r') as fh: app.setStyleSheet(fh.read()) - settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) shortcut = settings.value("captureExternalShortcut", "Alt+Q") keybinder.init() keybinder.register_hotkey( diff --git a/app/utils/config.py b/app/utils/config.py deleted file mode 100644 index 7d06220..0000000 --- a/app/utils/config.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Poricom Configuration Utilities - -Copyright (C) `2021-2022` `` - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -import toml -config = toml.load("./utils/config.toml") - - -def saveOnClose(data, config="utils/config.toml"): - with open(config, 'w') as fh: - toml.dump(data, fh) - - -def editConfig(index, replacementText, config="utils/config.toml"): - data = toml.load(config) - data[index] = replacementText - with open(config, 'w') as fh: - toml.dump(data, fh) - - -def editSelectionConfig(index, cBoxName, config="utils/config.toml"): - data = toml.load(config) - data["SELECTED_INDEX"][cBoxName] = index - with open(config, 'w') as fh: - toml.dump(data, fh) diff --git a/app/utils/config.toml b/app/utils/config.toml deleted file mode 100644 index d03227e..0000000 --- a/app/utils/config.toml +++ /dev/null @@ -1,256 +0,0 @@ -# Poricom Default Configuration File -# -# Copyright (C) `2021-2022` `` -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# Lists -IMAGE_EXTENSIONS = [ "*.bmp", "*.gif", "*.jpg", "*.jpeg", "*.png", "*.pbm", "*.pgm", "*.ppm", "*.webp", "*.xbm", "*.xpm",] -LANGUAGE = [ " Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English",] -ORIENTATION = [ " Vertical", " Horizontal",] -FONT_STYLE = [ " Helvetica", " Poppins", " Arial", " Verdana", " Times New Roman",] -FONT_SIZE = [ " 12", " 14", " 16", " 20", " 24", " 32", " 40", " 56", " 72",] -IMAGE_SCALING = [ " Fit to Width", " Fit to Height", " Fit to Screen",] -MODIFIER = [ " Ctrl", " Shift", " Alt", " Ctrl+Alt", " Shift+Alt", " Shift+Ctrl", " Shift+Alt+Ctrl", " No Modifier",] - -# Filepath -STYLES_PATH = "./assets/" -DEFAULT_NAV_ROOT = "./assets/images/" -NAV_ROOT = "./assets/images/" -TBAR_ICONS = "./assets/images/icons/" -TBAR_ICONS_LIGHT = "./assets/images/icons/" -LANG_PATH = "./assets/languages/" - -STYLES_DEFAULT = "./assets/styles.qss" -LOGO = "./assets/images/icons/logo.ico" -HOME_IMAGE = "./assets/images/home.png" -ABOUT_IMAGE = "./assets/images/about.png" -TBAR_ICON_DEFAULT = "./assets/images/icons/default_icon.png" - -# Sizes -RBN_HEIGHT = 2.4 -TBAR_ISIZE_REL = 0.1 -TBAR_ISIZE_MARGIN = 1.3 -NAV_VIEW_RATIO = [ 1, 9,] - -# Mode -VIEW_IMAGE_MODE = 0 -SPLIT_VIEW_MODE = false -PERSIST_TEXT_MODE = 1 - -# Popups -LOAD_MODEL_POPUP = true -CHECK_INTERNET_POPUP = true -CHECK_INTERNET_URL = "8.8.8.8" - -[SELECTED_INDEX] -language = 0 -orientation = 0 -fontStyle = 0 -fontSize = 2 -imageScaling = 0 -modifier = 2 - -[PICKER_INDEX] -language = 49 -orientation = 50 -fontStyle = 51 -fontSize = 52 -imageScaling = 53 -modifier = 54 - -[SHORTCUT] -captureExternal = "Alt+Q" -captureExternalKey = "Q" -captureExternalTip = "This will minimize the app and perform OCR on the current screen. Alternatively, you may use the shortcut: " - -[NAV_FUNCS] -pathChanged = "viewImageFromFDialog" -navClicked = "viewImageFromExplorer" - -# Ribbon buttons (always on) -[MODE_FUNCS] - - [MODE_FUNCS.zoomIn] - helpTitle = "Zoom in" - helpMsg = "Hint: Double click the image to reset zoom." - path = "zoomIn.png" - toggle = false - align = "AlignRight" - iconH = 0.45 - iconW = 0.45 - - [MODE_FUNCS.zoomOut] - helpTitle = "Zoom out" - helpMsg = "Hint: Double click the image to reset zoom." - path = "zoomOut.png" - toggle = false - align = "AlignRight" - iconH = 0.45 - iconW = 0.45 - - [MODE_FUNCS.loadImageAtIndex] - helpTitle = "" - helpMsg = "Jump to page" - path = "loadImageAtIndex.png" - toggle = false - align = "AlignRight" - iconH = 0.45 - iconW = 1.3 - - [MODE_FUNCS.loadPrevImage] - helpTitle = "" - helpMsg = "Show previous image" - path = "loadPrevImage.png" - toggle = false - align = "AlignRight" - iconH = 0.45 - iconW = 0.6 - - [MODE_FUNCS.loadNextImage] - helpTitle = "" - helpMsg = "Show next image" - path = "loadNextImage.png" - toggle = false - align = "AlignRight" - iconH = 0.45 - iconW = 0.6 - -# Ribbon buttons -[TBAR_FUNCS] - - [TBAR_FUNCS.FILE] - - [TBAR_FUNCS.FILE.openDir] - helpTitle = "Open manga directory" - helpMsg = "Open a directory containing images." - path = "openDir.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.FILE.openManga] - helpTitle = "Open manga file" - helpMsg = "Supports the following formats: cbr, cbz, pdf." - path = "openManga.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.FILE.captureExternalHelper] - helpTitle = "External capture" - helpMsg = "This will minimize the app and perform OCR on the current screen. Alternatively, you may use the shortcut Alt+Q." - path = "captureExternalHelper.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.VIEW] - - [TBAR_FUNCS.VIEW.toggleStylesheet] - helpTitle = "Change theme" - helpMsg = "Switch between light and dark mode." - path = "toggleStylesheet.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.VIEW.hideExplorer] - helpTitle = "Hide explorer" - helpMsg = "Hide the file explorer from view" - path = "hideExplorer.png" - toggle = true - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.VIEW.modifyFontSettings] - helpTitle = "Modify preview text" - helpMsg = "Change font style and font size of preview text." - path = "modifyFontSettings.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.VIEW.toggleSplitView] - helpTitle = "Turn on split view" - helpMsg = "View two images at once." - path = "toggleSplitView.png" - toggle = true - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.VIEW.scaleImage] - helpTitle = "Adjust image scaling" - helpMsg = "Fit an image according to the available options: fit to width, fit to height, fit to screen" - path = "scaleImage.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.CONTROLS] - - [TBAR_FUNCS.CONTROLS.toggleMouseMode] - helpTitle = "Change mouse behavior" - helpMsg = "This will disable text detection. Turn this on only if do not want to hold CTRL key to zoom and pan on an image." - path = "toggleMouseMode.png" - toggle = true - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.CONTROLS.modifyHotkeys] - helpTitle = "Remap hotkeys" - helpMsg = "Change shortcut for external captures." - path = "modifyHotkeys.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.MISC] - - [TBAR_FUNCS.MISC.loadModel] - helpTitle = "Switch detection model" - helpMsg = "Switch between MangaOCR and Tesseract models." - path = "loadModel.png" - toggle = true - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.MISC.modifyTesseract] - helpTitle = "Tesseract settings" - helpMsg = "Set the language and orientation for the Tesseract model." - path = "modifyTesseract.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.MISC.toggleLogging] - helpTitle = "Enable text logging" - helpMsg = "Save detected text to a text file located in the current project directory." - path = "toggleLogging.png" - toggle = true - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 \ No newline at end of file diff --git a/app/utils/constants.py b/app/utils/constants.py index 1826091..582b83e 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -20,6 +20,9 @@ # ------------------------------------- General ------------------------------------- # +APP_NAME = "Poricom" +APP_LOGO = "./assets/images/icons/logo.ico" + IMAGE_EXTENSIONS = ["*.bmp", "*.gif", "*.jpeg", "*.jpg", "*.pbm", "*.pgm", "*.png", "*.ppm", "*.webp", "*.xbm", "*.xpm",] # Settings @@ -58,6 +61,16 @@ SETTINGS_FILE_DEFAULT = './utils/poricom-config.ini' +# Window +MAIN_WINDOW_DEFAULTS = { + "hasLoadModelPopup": "true", + "explorerPath": "./assets/images/", + "stylesheetPath": "./assets/styles.qss" +} +MAIN_WINDOW_TYPES = { + "hasLoadModelPopup": bool +} + # View IMAGE_VIEW_DEFAULT = { "viewImageMode": 0, diff --git a/environment/base.yaml b/environment/base.yaml index b97fb86..59cb598 100644 --- a/environment/base.yaml +++ b/environment/base.yaml @@ -15,5 +15,4 @@ dependencies: - pyqtkeybind - rarfile - pdf2image - - toml - huggingface-hub==0.7.0 From e1c65318c165b473028b40d1327361951064071d Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 23:39:13 +0800 Subject: [PATCH 050/137] Refactor windows component file structure --- app/components/windows/__init__.py | 20 ++++++ .../windows/base.py} | 22 ++---- app/components/windows/external.py | 70 +++++++++++++++++++ app/main.py | 2 +- 4 files changed, 96 insertions(+), 18 deletions(-) create mode 100644 app/components/windows/__init__.py rename app/{MainWindow.py => components/windows/base.py} (90%) create mode 100644 app/components/windows/external.py diff --git a/app/components/windows/__init__.py b/app/components/windows/__init__.py new file mode 100644 index 0000000..639b764 --- /dev/null +++ b/app/components/windows/__init__.py @@ -0,0 +1,20 @@ +""" +Poricom Windows + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import MainWindow diff --git a/app/MainWindow.py b/app/components/windows/base.py similarity index 90% rename from app/MainWindow.py rename to app/components/windows/base.py index 0a7bb79..075e76d 100644 --- a/app/MainWindow.py +++ b/app/components/windows/base.py @@ -21,15 +21,16 @@ from time import sleep from manga_ocr import MangaOcr -from PyQt5.QtCore import (Qt, QThreadPool) -from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QDesktopWidget, QMainWindow, QApplication, +from PyQt5.QtCore import (QThreadPool) +from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QMainWindow, QApplication, QPushButton, QFileDialog) +from .external import ExternalWindow from components.popups import BasePopup, CheckboxPopup from components.services import BaseWorker from components.settings import BaseSettings, PreviewOptions, ImageScalingOptions, OptionsContainer, ShortcutOptions, TesseractOptions from components.toolbar import BaseToolbar -from components.views import WorkspaceView, FullScreenOCRView +from components.views import WorkspaceView from utils.constants import LOAD_MODEL_MESSAGE, MAIN_WINDOW_DEFAULTS, MAIN_WINDOW_TYPES, STYLESHEET_DARK, STYLESHEET_LIGHT from utils.scripts import mangaFileToImageDir @@ -129,20 +130,7 @@ def captureExternalHelper(self): self.captureExternal() def captureExternal(self): - externalWindow = QMainWindow() - externalWindow.layout().setContentsMargins(0, 0, 0, 0) - externalWindow.setStyleSheet("border:0px; margin:0px") - externalWindow.setAttribute(Qt.WA_DeleteOnClose) - - externalWindow.setCentralWidget( - FullScreenOCRView(externalWindow, self.tracker)) - fullScreen = externalWindow.centralWidget() - - screenIndex = fullScreen.getActiveScreenIndex() - screen = QDesktopWidget().screenGeometry(screenIndex) - fullScreen.takeScreenshot(screenIndex) - externalWindow.move(screen.left(), screen.top()) - externalWindow.showFullScreen() + ExternalWindow(self).showFullScreen() # ------------------------------ View Functions ------------------------------ # diff --git a/app/components/windows/external.py b/app/components/windows/external.py new file mode 100644 index 0000000..272bace --- /dev/null +++ b/app/components/windows/external.py @@ -0,0 +1,70 @@ +""" +Poricom Windows + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from typing import TYPE_CHECKING + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QCloseEvent, QCursor +from PyQt5.QtWidgets import QApplication, QDesktopWidget, QMainWindow + +from components.views import FullScreenOCRView + +if TYPE_CHECKING: + from .base import MainWindow + + +class ExternalWindow(QMainWindow): + """ + External window widget to enclose FullScreenOCRView + """ + def __init__(self, parent: "MainWindow"): + super().__init__() + self.mainWindow = parent + + # By setting the border thickness and margin to zero, + # we ensure that the whole screen is captured. + self.layout().setContentsMargins(0, 0, 0, 0) + self.setStyleSheet("border:0px; margin:0px") + + # Delete external window on close + self.setAttribute(Qt.WA_DeleteOnClose) + + # WindowStaysOnTopHint & Popup flags ensures that the widget is the top window. + self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.Popup) + + self.setCentralWidget(FullScreenOCRView(self, parent.tracker)) + # self.ocrModel = parent.ocrModel + + def showFullScreen(self): + # Overridden to show on the active screen + fullscreen: FullScreenOCRView = self.centralWidget() + screenIndex = fullscreen.getActiveScreenIndex() + + # TODO: Find an alternative way to show the active screen, + # since QDesktopWidget is obsolete according to Qt docs + screen = QDesktopWidget().screenGeometry(screenIndex) + fullscreen.takeScreenshot(screenIndex) + self.move(screen.left(), screen.top()) + + return super().showFullScreen() + + def closeEvent(self, event: QCloseEvent): + # Ensure that object is deleted before closing + self.deleteLater() + return super().closeEvent(event) \ No newline at end of file diff --git a/app/main.py b/app/main.py index 2dcdc5c..d3f35c6 100644 --- a/app/main.py +++ b/app/main.py @@ -24,7 +24,7 @@ from pyqtkeybind import keybinder from components.services import WinEventFilter -from MainWindow import MainWindow +from components.windows import MainWindow from Trackers import Tracker from utils.constants import APP_NAME, APP_LOGO, SETTINGS_FILE_DEFAULT, STYLESHEET_LIGHT From 916c31a3b9fbc66b9ee9dcf1ad1de44a0bc56407 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 23:42:04 +0800 Subject: [PATCH 051/137] Move binaries and settings to bin --- app/Trackers.py | 2 +- app/{utils => bin}/unrar.exe | Bin app/utils/constants.py | 2 +- app/utils/scripts/mangaFileToImageDir.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename app/{utils => bin}/unrar.exe (100%) diff --git a/app/Trackers.py b/app/Trackers.py index 9654efe..d68bb09 100644 --- a/app/Trackers.py +++ b/app/Trackers.py @@ -26,7 +26,7 @@ from utils.constants import EXPLORER_ROOT_DEFAULT, IMAGE_EXTENSIONS, SETTINGS_FILE_DEFAULT settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) -split = settings.value("splitViewMode").lower() == "true" +split = settings.value("splitViewMode", "false").lower() == "true" explorerPath = settings.value("explorerPath", EXPLORER_ROOT_DEFAULT) # TODO: This needs refactoring diff --git a/app/utils/unrar.exe b/app/bin/unrar.exe similarity index 100% rename from app/utils/unrar.exe rename to app/bin/unrar.exe diff --git a/app/utils/constants.py b/app/utils/constants.py index 582b83e..95ad056 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -59,7 +59,7 @@ # ------------------------------------ Settings ------------------------------------- # -SETTINGS_FILE_DEFAULT = './utils/poricom-config.ini' +SETTINGS_FILE_DEFAULT = './bin/poricom-config.ini' # Window MAIN_WINDOW_DEFAULTS = { diff --git a/app/utils/scripts/mangaFileToImageDir.py b/app/utils/scripts/mangaFileToImageDir.py index efbcef3..45947ce 100644 --- a/app/utils/scripts/mangaFileToImageDir.py +++ b/app/utils/scripts/mangaFileToImageDir.py @@ -40,7 +40,7 @@ def mangaFileToImageDir(filepath: str): with zipfile.ZipFile(filepath, 'r') as zipRef: zipRef.extractall(cachePath) - rarfile.UNRAR_TOOL = "utils/unrar.exe" + rarfile.UNRAR_TOOL = "bin/unrar.exe" if extension in [".cbr", ".rar"]: with rarfile.RarFile(filepath) as zipRef: zipRef.extractall(cachePath) From e061e8bb638472ef8ca709a93297394916176a5c Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 22 Jan 2023 00:25:30 +0800 Subject: [PATCH 052/137] Format with black --- app/Trackers.py | 29 +++-- app/components/explorers/image.py | 6 +- app/components/explorers/models/image.py | 6 +- app/components/misc/screenAware.py | 3 +- app/components/popups/checkbox.py | 15 ++- app/components/services/filters.py | 4 +- app/components/services/workers/base.py | 3 +- app/components/services/workers/signal.py | 2 +- app/components/settings/__init__.py | 8 +- app/components/settings/base.py | 6 +- app/components/settings/popups/base.py | 13 +- app/components/settings/popups/container.py | 16 ++- .../settings/popups/imageScaling.py | 2 +- app/components/settings/popups/preview.py | 20 +-- app/components/settings/popups/shortcut.py | 10 +- app/components/settings/popups/tesseract.py | 7 +- app/components/toolbar/base.py | 7 +- app/components/toolbar/tabs/base.py | 6 +- .../toolbar/tabs/containers/base.py | 23 ++-- .../toolbar/tabs/containers/navigate.py | 3 +- app/components/views/image/base.py | 82 ++++++++----- app/components/views/ocr/base.py | 12 +- app/components/views/ocr/fullscreen.py | 10 +- app/components/views/ocr/ocr.py | 4 +- app/components/views/workspace.py | 24 ++-- app/components/windows/base.py | 70 ++++++----- app/components/windows/external.py | 5 +- app/main.py | 7 +- app/utils/constants.py | 116 ++++++++++-------- app/utils/scripts/editStylesheet.py | 5 +- app/utils/scripts/logText.py | 5 +- app/utils/scripts/mangaFileToImageDir.py | 9 +- app/utils/scripts/pixmapToText.py | 12 +- app/utils/types.py | 2 + 34 files changed, 328 insertions(+), 224 deletions(-) diff --git a/app/Trackers.py b/app/Trackers.py index d68bb09..58fe95c 100644 --- a/app/Trackers.py +++ b/app/Trackers.py @@ -23,7 +23,11 @@ from PyQt5.QtCore import QSettings from PyQt5.QtGui import QPixmap, QPainter -from utils.constants import EXPLORER_ROOT_DEFAULT, IMAGE_EXTENSIONS, SETTINGS_FILE_DEFAULT +from utils.constants import ( + EXPLORER_ROOT_DEFAULT, + IMAGE_EXTENSIONS, + SETTINGS_FILE_DEFAULT, +) settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) split = settings.value("splitViewMode", "false").lower() == "true" @@ -71,10 +75,10 @@ def twoFileToImage(self, fileLeft, fileRight): h = imageLeft.height() splitImage = QPixmap(w, h) painter = QPainter(splitImage) - painter.drawPixmap(0, 0, imageLeft.width(), imageLeft.height(), - imageLeft) - painter.drawPixmap(imageLeft.width(), 0, imageRight.width(), - imageRight.height(), imageRight) + painter.drawPixmap(0, 0, imageLeft.width(), imageLeft.height(), imageLeft) + painter.drawPixmap( + imageLeft.width(), 0, imageRight.width(), imageRight.height(), imageRight + ) painter.end() return splitImage @@ -85,11 +89,11 @@ def pixImage(self): @pixImage.setter def pixImage(self, image): - if (type(image) is str and PImage(image).isValid()): + if type(image) is str and PImage(image).isValid(): self._pixImage = PImage(image) self._pixImage.filename = abspath(image) self._filepath = abspath(dirname(image)) - if (type(image) is tuple): + if type(image) is tuple: fileLeft, fileRight = image if not fileRight: if fileLeft: @@ -118,8 +122,14 @@ def filepath(self): @filepath.setter def filepath(self, filepath): fileList = filter(lambda f: isfile(join(filepath, f)), listdir(filepath)) - imageList = list(map(lambda p: normpath(join(filepath, p)), filter( - (lambda f: ('*'+splitext(f)[1]) in IMAGE_EXTENSIONS), fileList))) + imageList = list( + map( + lambda p: normpath(join(filepath, p)), + filter( + (lambda f: ("*" + splitext(f)[1]) in IMAGE_EXTENSIONS), fileList + ), + ) + ) if len(imageList) <= 0: raise FileNotFoundError("Empty directory") @@ -152,7 +162,6 @@ def switchOCRMode(self): class PImage(QPixmap): - def __init__(self, *args): super(QPixmap, self).__init__(args[0]) diff --git a/app/components/explorers/image.py b/app/components/explorers/image.py index 2ac7ba8..6ed016b 100644 --- a/app/components/explorers/image.py +++ b/app/components/explorers/image.py @@ -16,12 +16,13 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (Qt) -from PyQt5.QtWidgets import (QMainWindow, QTreeView) +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMainWindow, QTreeView from .models import ImageModel from utils.constants import EXPLORER_ROOT_DEFAULT + class ImageExplorer(QTreeView): """View to allow exploring images @@ -29,6 +30,7 @@ class ImageExplorer(QTreeView): parent (QMainWindow): Image explorer parent. Set to main window. initialDir (str, optional): Initial directory. Defaults to EXPLORER_ROOT_DEFAULT. """ + def __init__(self, parent: QMainWindow, initialDir: str = EXPLORER_ROOT_DEFAULT): super().__init__(parent) # TODO: It might be better if the parent is set to the QSplitter diff --git a/app/components/explorers/models/image.py b/app/components/explorers/models/image.py index d676676..ccda546 100644 --- a/app/components/explorers/models/image.py +++ b/app/components/explorers/models/image.py @@ -16,15 +16,17 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (QModelIndex) -from PyQt5.QtWidgets import (QFileSystemModel) +from PyQt5.QtCore import QModelIndex +from PyQt5.QtWidgets import QFileSystemModel from utils.constants import IMAGE_EXTENSIONS + class ImageModel(QFileSystemModel): """ Image model based on the native file system """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setNameFilterDisables(False) diff --git a/app/components/misc/screenAware.py b/app/components/misc/screenAware.py index 2b4372a..fd1da21 100644 --- a/app/components/misc/screenAware.py +++ b/app/components/misc/screenAware.py @@ -17,13 +17,14 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QApplication, QWidget) +from PyQt5.QtWidgets import QApplication, QWidget class ScreenAwareWidget(QWidget): """ Screen-aware widget. Allows retrieving desktop screen dimensions """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/app/components/popups/checkbox.py b/app/components/popups/checkbox.py index bdd4c92..67971f2 100644 --- a/app/components/popups/checkbox.py +++ b/app/components/popups/checkbox.py @@ -18,11 +18,12 @@ """ from PyQt5.QtCore import QSettings -from PyQt5.QtWidgets import (QCheckBox) +from PyQt5.QtWidgets import QCheckBox from components.popups import BasePopup from utils.constants import SETTINGS_FILE_DEFAULT + class CheckboxPopup(BasePopup): """Popup message with a checkbox @@ -31,9 +32,15 @@ class CheckboxPopup(BasePopup): checkboxMessage (str, optional): Checkbox label. Defaults to "Don't show this dialog again". """ - def __init__(self, prop: str, title: str, message: str, - buttons: BasePopup.StandardButtons = BasePopup.Ok, - checkboxMessage="Don't show this dialog again"): + + def __init__( + self, + prop: str, + title: str, + message: str, + buttons: BasePopup.StandardButtons = BasePopup.Ok, + checkboxMessage="Don't show this dialog again", + ): super().__init__(title, message, buttons) self.setCheckBox(QCheckBox(checkboxMessage, self)) diff --git a/app/components/services/filters.py b/app/components/services/filters.py index 3504795..2764923 100644 --- a/app/components/services/filters.py +++ b/app/components/services/filters.py @@ -17,7 +17,7 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (QAbstractNativeEventFilter) +from PyQt5.QtCore import QAbstractNativeEventFilter class WinEventFilter(QAbstractNativeEventFilter): @@ -27,4 +27,4 @@ def __init__(self, keybinder): def nativeEventFilter(self, eventType, message): ret = self.keybinder.handler(eventType, message) - return ret, 0 \ No newline at end of file + return ret, 0 diff --git a/app/components/services/workers/base.py b/app/components/services/workers/base.py index 486a17b..d122b85 100644 --- a/app/components/services/workers/base.py +++ b/app/components/services/workers/base.py @@ -19,10 +19,11 @@ from typing import Callable -from PyQt5.QtCore import (pyqtSlot, QRunnable) +from PyQt5.QtCore import pyqtSlot, QRunnable from .signal import BaseWorkerSignal + class BaseWorker(QRunnable): """Runnable object to support multithreading diff --git a/app/components/services/workers/signal.py b/app/components/services/workers/signal.py index a13446a..26e7f89 100644 --- a/app/components/services/workers/signal.py +++ b/app/components/services/workers/signal.py @@ -17,7 +17,7 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (pyqtSignal, QObject) +from PyQt5.QtCore import pyqtSignal, QObject class BaseWorkerSignal(QObject): diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py index 32784d0..f6f8e58 100644 --- a/app/components/settings/__init__.py +++ b/app/components/settings/__init__.py @@ -18,4 +18,10 @@ """ from .base import BaseSettings -from .popups import ImageScalingOptions, OptionsContainer, PreviewOptions, ShortcutOptions, TesseractOptions +from .popups import ( + ImageScalingOptions, + OptionsContainer, + PreviewOptions, + ShortcutOptions, + TesseractOptions, +) diff --git a/app/components/settings/base.py b/app/components/settings/base.py index 78aeb13..527b18c 100644 --- a/app/components/settings/base.py +++ b/app/components/settings/base.py @@ -35,7 +35,9 @@ class BaseSettings(QWidget): prefix (str, optional): Text added to the saved property. Defaults to "". """ - def __init__(self, parent: QWidget, file: str = SETTINGS_FILE_DEFAULT, prefix: str = ""): + def __init__( + self, parent: QWidget, file: str = SETTINGS_FILE_DEFAULT, prefix: str = "" + ): super().__init__(parent) self.settings = QSettings(file, QSettings.IniFormat) @@ -71,7 +73,7 @@ def setTypes(self, types: dict[str, Callable]): Use `self._types` to set the correct property type. """ self._types = types - + def addTypes(self, types: dict[str, Callable]): """ Extends the types dictionary, if it exists diff --git a/app/components/settings/popups/base.py b/app/components/settings/popups/base.py index a3de2f8..6e06c88 100644 --- a/app/components/settings/popups/base.py +++ b/app/components/settings/popups/base.py @@ -21,7 +21,7 @@ from stringcase import titlecase, capitalcase from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import (QComboBox, QGridLayout, QLabel, QWidget) +from PyQt5.QtWidgets import QComboBox, QGridLayout, QLabel, QWidget from ..base import BaseSettings @@ -30,7 +30,8 @@ class BaseOptions(BaseSettings): """ Allows saving/selecting options """ - def __init__(self, parent: QWidget, optionLists:list[list[str]]=[]): + + def __init__(self, parent: QWidget, optionLists: list[list[str]] = []): super().__init__(parent) self.mainWindow = parent self.setAttribute(Qt.WA_DeleteOnClose) @@ -80,11 +81,13 @@ def initializeProperties(self, props: list[tuple[str, Any, Callable]]): # Label self.labelList[i].setText(f"{titlecase(prop)}: ") - + # Combo Box comboBox = self.comboBoxList[i] self.setProperty(f"{prop}ComboBox", comboBox) # Child classes must implement change{PropName} method - comboBox.currentIndexChanged.connect(self.getProperty(f"change{capitalcase(prop)}")) - self.setOptionIndex(prop) \ No newline at end of file + comboBox.currentIndexChanged.connect( + self.getProperty(f"change{capitalcase(prop)}") + ) + self.setOptionIndex(prop) diff --git a/app/components/settings/popups/container.py b/app/components/settings/popups/container.py index 5c037b4..369ca4c 100644 --- a/app/components/settings/popups/container.py +++ b/app/components/settings/popups/container.py @@ -17,26 +17,30 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (Qt) -from PyQt5.QtWidgets import (QDialog, QDialogButtonBox, QVBoxLayout) +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout from .base import BaseOptions + class OptionsContainer(QDialog): """Dialog to contain option widgets Args: options (BaseOptions): Child option widget """ + def __init__(self, options: BaseOptions): - super().__init__(None, Qt.WindowCloseButtonHint | Qt.WindowSystemMenuHint | Qt.WindowTitleHint) + super().__init__( + None, + Qt.WindowCloseButtonHint | Qt.WindowSystemMenuHint | Qt.WindowTitleHint, + ) self.setAttribute(Qt.WA_DeleteOnClose) self.options = options self.setLayout(QVBoxLayout()) self.layout().addWidget(options) - self.buttonBox = QDialogButtonBox( - QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.layout().addWidget(self.buttonBox) self.buttonBox.rejected.connect(self.cancelClickedEvent) @@ -49,7 +53,7 @@ def accept(self): def cancelClickedEvent(self): self.close() - + def closeEvent(self, event): self.options.close() return super().closeEvent(event) diff --git a/app/components/settings/popups/imageScaling.py b/app/components/settings/popups/imageScaling.py index 0369310..c348763 100644 --- a/app/components/settings/popups/imageScaling.py +++ b/app/components/settings/popups/imageScaling.py @@ -17,7 +17,7 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QWidget) +from PyQt5.QtWidgets import QWidget from .base import BaseOptions from utils.constants import IMAGE_SCALING diff --git a/app/components/settings/popups/preview.py b/app/components/settings/popups/preview.py index 0e59f26..bf8b9be 100644 --- a/app/components/settings/popups/preview.py +++ b/app/components/settings/popups/preview.py @@ -17,7 +17,7 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QWidget) +from PyQt5.QtWidgets import QWidget from .base import BaseOptions from utils.constants import FONT_SIZE, FONT_STYLE, TOGGLE_CHOICES @@ -27,11 +27,13 @@ class PreviewOptions(BaseOptions): def __init__(self, parent: QWidget): super().__init__(parent, [FONT_STYLE, FONT_SIZE, TOGGLE_CHOICES]) - self.initializeProperties([ - ("fontStyle", " font-family: 'Helvetica';\n", str), - ("fontSize", " font-size: 16pt;\n", str), - ("persistText", "true", bool), - ]) + self.initializeProperties( + [ + ("fontStyle", " font-family: 'Helvetica';\n", str), + ("fontSize", " font-size: 16pt;\n", str), + ("persistText", "true", bool), + ] + ) self.setOptionIndex("fontSize", 2) self.setOptionIndex("persistText", 1) @@ -46,7 +48,7 @@ def changeFontSize(self, i): selectedFontSize = int(self.fontSizeComboBox.currentText().strip()) replacementText = f" font-size: {selectedFontSize}pt;\n" self.fontSize = replacementText - + def changePersistText(self, i): self.persistTextIndex = i self.persistText = True if i else False @@ -54,5 +56,7 @@ def changePersistText(self, i): def saveSettings(self, hasMessage=False): editStylesheet(41, self.fontStyle) editStylesheet(42, self.fontSize) - self.mainWindow.canvas.setProperty('persistText', "true" if self.persistText else "false") + self.mainWindow.canvas.setProperty( + "persistText", "true" if self.persistText else "false" + ) return super().saveSettings(hasMessage) diff --git a/app/components/settings/popups/shortcut.py b/app/components/settings/popups/shortcut.py index 8d57ee7..eaad453 100644 --- a/app/components/settings/popups/shortcut.py +++ b/app/components/settings/popups/shortcut.py @@ -17,21 +17,21 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QLabel, QLineEdit, QWidget) +from PyQt5.QtWidgets import QLabel, QLineEdit, QWidget from .base import BaseOptions from components.popups import BasePopup from utils.constants import MODIFIER + class ShortcutOptions(BaseOptions): def __init__(self, parent: QWidget): super().__init__(parent, [MODIFIER]) self.initializeProperties([("modifier", "Alt", str)]) self.setOptionIndex("modifier", 2) - self.addDefaults({ - "captureExternalKey": "Q", - "captureExternalShortcut": "Alt+Q" - }) + self.addDefaults( + {"captureExternalKey": "Q", "captureExternalShortcut": "Alt+Q"} + ) self.loadSettings() self.keyLineEdit = QLineEdit(self.captureExternalKey) diff --git a/app/components/settings/popups/tesseract.py b/app/components/settings/popups/tesseract.py index 5ef0741..ff2ff94 100644 --- a/app/components/settings/popups/tesseract.py +++ b/app/components/settings/popups/tesseract.py @@ -17,17 +17,20 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QWidget) +from PyQt5.QtWidgets import QWidget from utils.constants import LANGUAGE, ORIENTATION from .base import BaseOptions + class TesseractOptions(BaseOptions): def __init__(self, parent: QWidget): super().__init__(parent, [LANGUAGE, ORIENTATION]) # TODO: Use constants here - self.initializeProperties([("language", "jpn", str), ("orientation", "_vert", str)]) + self.initializeProperties( + [("language", "jpn", str), ("orientation", "_vert", str)] + ) def changeLanguage(self, i): self.languageIndex = i diff --git a/app/components/toolbar/base.py b/app/components/toolbar/base.py index 73362ca..2da74e9 100644 --- a/app/components/toolbar/base.py +++ b/app/components/toolbar/base.py @@ -17,11 +17,12 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QMainWindow, QSizePolicy, QTabWidget) +from PyQt5.QtWidgets import QMainWindow, QSizePolicy, QTabWidget from .tabs import BaseToolbarTab, NavigateToolbarContainer from utils.constants import TOOLBAR_FUNCTIONS + class BaseToolbar(QTabWidget): """ Toolbar widget @@ -31,6 +32,7 @@ class BaseToolbar(QTabWidget): Notes: Parent must be passed to children to call main window functions. """ + def __init__(self, parent: QMainWindow): super(QTabWidget, self).__init__(parent) self.parent = parent @@ -40,6 +42,5 @@ def __init__(self, parent: QMainWindow): for tabName, funcs in TOOLBAR_FUNCTIONS.items(): tab = BaseToolbarTab(parent=self.parent, funcs=funcs) tab.layout().addStretch() - tab.layout().addWidget( - NavigateToolbarContainer(self.parent)) + tab.layout().addWidget(NavigateToolbarContainer(self.parent)) self.addTab(tab, tabName.upper()) diff --git a/app/components/toolbar/tabs/base.py b/app/components/toolbar/tabs/base.py index 63df709..9285868 100644 --- a/app/components/toolbar/tabs/base.py +++ b/app/components/toolbar/tabs/base.py @@ -17,11 +17,12 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QHBoxLayout, QMainWindow) +from PyQt5.QtWidgets import QHBoxLayout, QMainWindow from .containers import BaseToolbarContainer from utils.types import ButtonConfigDict + class BaseToolbarTab(BaseToolbarContainer): """Tab widget to arrange toolbar tab containers @@ -29,7 +30,8 @@ class BaseToolbarTab(BaseToolbarContainer): parent (QMainWindow): Toolbar tab parent. Set to main window. funcs (ButtonConfigDict, optional): Toolbar function configuration. Defaults to {}. """ - def __init__(self, parent: QMainWindow, funcs: ButtonConfigDict={}): + + def __init__(self, parent: QMainWindow, funcs: ButtonConfigDict = {}): super().__init__(parent) self.initializeButtons(funcs) diff --git a/app/components/toolbar/tabs/containers/base.py b/app/components/toolbar/tabs/containers/base.py index d1ab13f..f889f04 100644 --- a/app/components/toolbar/tabs/containers/base.py +++ b/app/components/toolbar/tabs/containers/base.py @@ -19,20 +19,22 @@ from os.path import exists -from PyQt5.QtCore import (QSize) -from PyQt5.QtGui import (QIcon) -from PyQt5.QtWidgets import (QMainWindow, QPushButton) +from PyQt5.QtCore import QSize +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QMainWindow, QPushButton from components.misc import ScreenAwareWidget from utils.constants import TOOLBAR_ICON_DEFAULT, TOOLBAR_ICON_SIZE, TOOLBAR_ICONS from utils.types import ButtonConfig + class BaseToolbarContainer(ScreenAwareWidget): """Widget that contains the toolbar functions Args: parent (QMainWindow): Container parent. Set to main window. """ + def __init__(self, parent: QMainWindow): super().__init__(parent) @@ -57,13 +59,13 @@ def initializeButton(self, name: str, config: ButtonConfig): # Set button icon and size path = TOOLBAR_ICONS + config["path"] - if (exists(path)): + if exists(path): icon = QIcon(path) else: icon = QIcon(TOOLBAR_ICON_DEFAULT) button.setIcon(icon) - w = self.primaryScreenHeight()*TOOLBAR_ICON_SIZE*config["iconWidth"] - h = self.primaryScreenHeight()*TOOLBAR_ICON_SIZE*config["iconHeight"] + w = self.primaryScreenHeight() * TOOLBAR_ICON_SIZE * config["iconWidth"] + h = self.primaryScreenHeight() * TOOLBAR_ICON_SIZE * config["iconHeight"] button.setIconSize(QSize(w, h)) tooltip = f"\ @@ -76,12 +78,9 @@ def initializeButton(self, name: str, config: ButtonConfig): # Connect button to main window function try: - button.clicked.connect( - getattr(self.mainWindow, name)) + button.clicked.connect(getattr(self.mainWindow, name)) except AttributeError: try: - button.clicked.connect( - getattr(self.mainWindow.mainView, name)) + button.clicked.connect(getattr(self.mainWindow.mainView, name)) except AttributeError: - button.clicked.connect( - getattr(self.mainWindow, 'noop')) + button.clicked.connect(getattr(self.mainWindow, "noop")) diff --git a/app/components/toolbar/tabs/containers/navigate.py b/app/components/toolbar/tabs/containers/navigate.py index df04d29..f9f441c 100644 --- a/app/components/toolbar/tabs/containers/navigate.py +++ b/app/components/toolbar/tabs/containers/navigate.py @@ -17,11 +17,12 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QGridLayout, QMainWindow) +from PyQt5.QtWidgets import QGridLayout, QMainWindow from .base import BaseToolbarContainer from utils.constants import NAVIGATION_FUNCTIONS + class NavigateToolbarContainer(BaseToolbarContainer): """Widget that contains the toolbar navigation functions diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index 7ae0e2b..e76d6cd 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -20,8 +20,8 @@ from time import sleep from typing import TYPE_CHECKING -from PyQt5.QtCore import (Qt, QRect, QRectF, QSize, QThreadPool) -from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView) +from PyQt5.QtCore import Qt, QRect, QRectF, QSize, QThreadPool +from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsView from components.services import BaseWorker from components.settings import BaseSettings @@ -30,12 +30,13 @@ if TYPE_CHECKING: from ..workspace import WorkspaceView + class BaseImageView(QGraphicsView, BaseSettings): """ - Base image view to allow view/zoom/pan functions + Base image view to allow view/zoom/pan functions """ - def __init__(self, parent: 'WorkspaceView', tracker=None): + def __init__(self, parent: "WorkspaceView", tracker=None): super().__init__(parent) self.tracker = tracker @@ -56,43 +57,51 @@ def __init__(self, parent: 'WorkspaceView', tracker=None): self.initializePixmapItem() -# ------------------------------------ Settings ------------------------------------- # + # ------------------------------------ Settings ------------------------------------- # def setViewImageMode(self, mode: int): # TODO: This should be an enum not an int - self.setProperty('viewImageMode', mode) + self.setProperty("viewImageMode", mode) self.saveSettings(hasMessage=False) self.viewImage() def toggleSplitView(self): - self.setProperty('splitViewMode', "false" if self.splitViewMode else "true") + self.setProperty("splitViewMode", "false" if self.splitViewMode else "true") self.saveSettings(hasMessage=False) def toggleZoomPanMode(self): - self.setProperty('zoomPanMode', "false" if self.zoomPanMode else "true") + self.setProperty("zoomPanMode", "false" if self.zoomPanMode else "true") self.saveSettings(hasMessage=False) -# -------------------------------------- View --------------------------------------- # + # -------------------------------------- View --------------------------------------- # def initializePixmapItem(self): self.setScene(QGraphicsScene()) - self.pixmap = self.scene().addPixmap(self.tracker.pixImage.scaledToWidth( - self.viewport().geometry().width(), Qt.SmoothTransformation)) + self.pixmap = self.scene().addPixmap( + self.tracker.pixImage.scaledToWidth( + self.viewport().geometry().width(), Qt.SmoothTransformation + ) + ) def viewImage(self, factor=1): # self.verticalScrollBar().setSliderPosition(0) factor = self.currentScale - w = factor*self.viewport().geometry().width() - h = factor*self.viewport().geometry().height() + w = factor * self.viewport().geometry().width() + h = factor * self.viewport().geometry().height() if self.viewImageMode == 0: self.pixmap.setPixmap( - self.tracker.pixImage.scaledToWidth(w, Qt.SmoothTransformation)) + self.tracker.pixImage.scaledToWidth(w, Qt.SmoothTransformation) + ) elif self.viewImageMode == 1: self.pixmap.setPixmap( - self.tracker.pixImage.scaledToHeight(h, Qt.SmoothTransformation)) + self.tracker.pixImage.scaledToHeight(h, Qt.SmoothTransformation) + ) elif self.viewImageMode == 2: - self.pixmap.setPixmap(self.tracker.pixImage.scaled( - w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)) + self.pixmap.setPixmap( + self.tracker.pixImage.scaled( + w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation + ) + ) self.scene().setSceneRect(QRectF(self.pixmap.pixmap().rect())) def zoomView(self, isZoomIn, usingButton=False): @@ -101,11 +110,11 @@ def zoomView(self, isZoomIn, usingButton=False): factor = 1.4 if isZoomIn and self.currentScale < 15: - #self.scale(factor, factor) + # self.scale(factor, factor) self.currentScale *= factor self.viewImage(self.currentScale) elif not isZoomIn and self.currentScale > 0.35: - #self.scale(1/factor, 1/factor) + # self.scale(1/factor, 1/factor) self.currentScale /= factor self.viewImage(self.currentScale) @@ -142,21 +151,25 @@ def suppressScroll(): self._scrollSuppressed = True worker = BaseWorker(sleep, 0.3) worker.signals.finished.connect( - lambda: setattr(self, "_scrollSuppressed", False)) + lambda: setattr(self, "_scrollSuppressed", False) + ) QThreadPool.globalInstance().start(worker) - if (event.angleDelta().y() < 0 and - self.verticalScrollBar().value() == self.verticalScrollBar().maximum()): - if (event.angleDelta().y() > -wheelDelta): - if (self._trackPadAtMax == trackpadScrollLimit): + if ( + event.angleDelta().y() < 0 + and self.verticalScrollBar().value() + == self.verticalScrollBar().maximum() + ): + if event.angleDelta().y() > -wheelDelta: + if self._trackPadAtMax == trackpadScrollLimit: self.parent().loadNextImage() self._trackPadAtMax = 0 suppressScroll() return else: self._trackPadAtMax += 1 - elif (event.angleDelta().y() <= -wheelDelta): - if (self._scrollAtMax == mouseScrollLimit): + elif event.angleDelta().y() <= -wheelDelta: + if self._scrollAtMax == mouseScrollLimit: self.parent().loadNextImage() self._scrollAtMax = 0 suppressScroll() @@ -164,18 +177,21 @@ def suppressScroll(): else: self._scrollAtMax += 1 - if (event.angleDelta().y() > 0 and - self.verticalScrollBar().value() == self.verticalScrollBar().minimum()): - if (event.angleDelta().y() < wheelDelta): - if (self._trackPadAtMin == trackpadScrollLimit): + if ( + event.angleDelta().y() > 0 + and self.verticalScrollBar().value() + == self.verticalScrollBar().minimum() + ): + if event.angleDelta().y() < wheelDelta: + if self._trackPadAtMin == trackpadScrollLimit: self.parent().loadPrevImage() self._trackPadAtMin = 0 suppressScroll() return else: self._trackPadAtMin += 1 - elif (event.angleDelta().y() >= wheelDelta): - if (self._scrollAtMin == mouseScrollLimit): + elif event.angleDelta().y() >= wheelDelta: + if self._scrollAtMin == mouseScrollLimit: self.parent().loadPrevImage() self._scrollAtMin = 0 suppressScroll() @@ -200,7 +216,7 @@ def mouseDoubleClickEvent(self, event): self.viewImage(self.currentScale) super().mouseDoubleClickEvent(event) -# ------------------------------------ Shortcut ------------------------------------- # + # ------------------------------------ Shortcut ------------------------------------- # # TODO: Keyboard shortcuts should be in another class def keyPressEvent(self, event): diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index ff7b956..02ee5b1 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -17,14 +17,15 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (pyqtSlot, Qt, QThreadPool, QTimer) -from PyQt5.QtWidgets import (QGraphicsView, QLabel, QMainWindow) +from PyQt5.QtCore import pyqtSlot, Qt, QThreadPool, QTimer +from PyQt5.QtWidgets import QGraphicsView, QLabel, QMainWindow from components.services import BaseWorker from components.settings import BaseSettings from utils.constants import TESSERACT_DEFAULTS from utils.scripts import logText, pixmapToText + class BaseOCRView(QGraphicsView, BaseSettings): """Base view with OCR capabilities @@ -32,6 +33,7 @@ class BaseOCRView(QGraphicsView, BaseSettings): parent (QMainWindow): View parent. Set to main window tracker (Any, optional): State tracker. Defaults to None. """ + def __init__(self, parent: QMainWindow, tracker=None): # TODO: Remove references to tracker super().__init__(parent) @@ -50,14 +52,14 @@ def __init__(self, parent: QMainWindow, tracker=None): self.setDragMode(QGraphicsView.RubberBandDrag) self.addDefaults(TESSERACT_DEFAULTS) - self.addProperty('persistText', "true", bool) + self.addProperty("persistText", "true", bool) def handleTextResult(self, result): try: self.canvasText.setText(result) except RuntimeError: pass - + def handleTextFinished(self): try: self.canvasText.adjustSize() @@ -70,7 +72,7 @@ def handleTextFinished(self): @pyqtSlot() def rubberBandStopped(self): - if (self.canvasText.isHidden()): + if self.canvasText.isHidden(): self.canvasText.setText("") self.canvasText.adjustSize() self.canvasText.show() diff --git a/app/components/views/ocr/fullscreen.py b/app/components/views/ocr/fullscreen.py index 90a42d3..53c3dd9 100644 --- a/app/components/views/ocr/fullscreen.py +++ b/app/components/views/ocr/fullscreen.py @@ -17,8 +17,8 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (Qt, QRectF) -from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QMainWindow) +from PyQt5.QtCore import Qt, QRectF +from PyQt5.QtWidgets import QApplication, QGraphicsScene, QMainWindow from PyQt5.QtGui import QCursor, QMouseEvent from .base import BaseOCRView @@ -29,6 +29,7 @@ class FullScreenOCRView(BaseOCRView): """ Fullscreen view with OCR capabilities """ + def __init__(self, parent: QMainWindow, tracker=None): super().__init__(parent, tracker) self.externalWindow = parent @@ -42,8 +43,9 @@ def __init__(self, parent: QMainWindow, tracker=None): def takeScreenshot(self, screenIndex: int): screen = QApplication.screens()[screenIndex] s = screen.size() - self.pixmap = self.scene().addPixmap(screen.grabWindow( - 0).scaled(s.width(), s.height())) + self.pixmap = self.scene().addPixmap( + screen.grabWindow(0).scaled(s.width(), s.height()) + ) self.scene().setSceneRect(QRectF(self.pixmap.pixmap().rect())) def getActiveScreenIndex(self): diff --git a/app/components/views/ocr/ocr.py b/app/components/views/ocr/ocr.py index ebb1358..ac2931a 100644 --- a/app/components/views/ocr/ocr.py +++ b/app/components/views/ocr/ocr.py @@ -17,8 +17,8 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (pyqtSlot) -from PyQt5.QtWidgets import (QMainWindow) +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtWidgets import QMainWindow from ..image import BaseImageView from .base import BaseOCRView diff --git a/app/components/views/workspace.py b/app/components/views/workspace.py index 2aa6f73..3103fae 100644 --- a/app/components/views/workspace.py +++ b/app/components/views/workspace.py @@ -17,8 +17,8 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (Qt) -from PyQt5.QtWidgets import (QInputDialog, QMainWindow, QSplitter) +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QInputDialog, QMainWindow, QSplitter from .ocr import OCRView from components.explorers import ImageExplorer @@ -29,6 +29,7 @@ class WorkspaceView(QSplitter): """ Main view of the program. Includes the explorer and the view. """ + def __init__(self, parent: QMainWindow, tracker=None): super().__init__(parent) self.tracker = tracker @@ -62,7 +63,7 @@ def loadPrevImage(self): tempIndex = self.explorer.indexAbove(index) if tempIndex.isValid(): index = tempIndex - if (not index.isValid()): + if not index.isValid(): return self.explorer.setCurrentIndex(index) @@ -72,7 +73,7 @@ def loadNextImage(self): tempIndex = self.explorer.indexBelow(index) if tempIndex.isValid(): index = tempIndex - if (not index.isValid()): + if not index.isValid(): return self.explorer.setCurrentIndex(index) @@ -80,16 +81,17 @@ def loadImageAtIndex(self): rowCount = self.explorer.model().rowCount(self.explorer.rootIndex()) i, _ = QInputDialog.getInt( self, - 'Jump to', - f'Enter page number: (max is {rowCount})', + "Jump to", + f"Enter page number: (max is {rowCount})", value=-1, min=1, max=rowCount, - flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint) - if (i == -1): + flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint, + ) + if i == -1: return - index = self.explorer.model().index(i-1, 0, self.explorer.rootIndex()) + index = self.explorer.model().index(i - 1, 0, self.explorer.rootIndex()) self.explorer.setCurrentIndex(index) def zoomIn(self): @@ -99,6 +101,6 @@ def zoomOut(self): self.canvas.zoomView(False, usingButton=True) def resizeEvent(self, event): - self.explorer.setMinimumWidth(0.1*self.width()) - self.canvas.setMinimumWidth(0.6*self.width()) + self.explorer.setMinimumWidth(0.1 * self.width()) + self.canvas.setMinimumWidth(0.6 * self.width()) return super().resizeEvent(event) diff --git a/app/components/windows/base.py b/app/components/windows/base.py index 075e76d..b8ed22c 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -21,17 +21,36 @@ from time import sleep from manga_ocr import MangaOcr -from PyQt5.QtCore import (QThreadPool) -from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QMainWindow, QApplication, - QPushButton, QFileDialog) +from PyQt5.QtCore import QThreadPool +from PyQt5.QtWidgets import ( + QVBoxLayout, + QWidget, + QMainWindow, + QApplication, + QPushButton, + QFileDialog, +) from .external import ExternalWindow from components.popups import BasePopup, CheckboxPopup from components.services import BaseWorker -from components.settings import BaseSettings, PreviewOptions, ImageScalingOptions, OptionsContainer, ShortcutOptions, TesseractOptions +from components.settings import ( + BaseSettings, + PreviewOptions, + ImageScalingOptions, + OptionsContainer, + ShortcutOptions, + TesseractOptions, +) from components.toolbar import BaseToolbar from components.views import WorkspaceView -from utils.constants import LOAD_MODEL_MESSAGE, MAIN_WINDOW_DEFAULTS, MAIN_WINDOW_TYPES, STYLESHEET_DARK, STYLESHEET_LIGHT +from utils.constants import ( + LOAD_MODEL_MESSAGE, + MAIN_WINDOW_DEFAULTS, + MAIN_WINDOW_TYPES, + STYLESHEET_DARK, + STYLESHEET_LIGHT, +) from utils.scripts import mangaFileToImageDir @@ -60,7 +79,7 @@ def __init__(self, parent=None, tracker=None): @property def canvas(self): return self.mainView.canvas - + @property def explorer(self): return self.mainView.explorer @@ -74,18 +93,15 @@ def closeEvent(self, event): return super().closeEvent(event) def noop(self): - BasePopup( - "Not Implemented", - "This function is not yet implemented." - ).exec() + BasePopup("Not Implemented", "This function is not yet implemented.").exec() -# ------------------------------ File Functions ------------------------------ # + # ------------------------------ File Functions ------------------------------ # def openDir(self): filepath = QFileDialog.getExistingDirectory( self, "Open Directory", - self.tracker.filepath # , QFileDialog.DontUseNativeDialog + self.tracker.filepath, # , QFileDialog.DontUseNativeDialog ) if filepath: @@ -96,7 +112,7 @@ def openDir(self): except FileNotFoundError: BasePopup( "No images found in the directory", - "Please select a directory with images." + "Please select a directory with images.", ).exec() def openManga(self): @@ -104,21 +120,20 @@ def openManga(self): self, "Open Manga File", self.tracker.filepath, - "Manga (*.cbz *.cbr *.zip *.rar *.pdf)" + "Manga (*.cbz *.cbr *.zip *.rar *.pdf)", ) if filename: + def setDirectory(filepath): self.tracker.filepath = filepath self.explorer.setDirectory(filepath) - openMangaButton = self.toolbar.findChild( - QPushButton, "openManga") + openMangaButton = self.toolbar.findChild(QPushButton, "openManga") worker = BaseWorker(mangaFileToImageDir, filename) worker.signals.result.connect(setDirectory) - worker.signals.finished.connect( - lambda: openMangaButton.setEnabled(True)) + worker.signals.finished.connect(lambda: openMangaButton.setEnabled(True)) self.threadpool.start(worker) openMangaButton.setEnabled(False) @@ -132,7 +147,7 @@ def captureExternalHelper(self): def captureExternal(self): ExternalWindow(self).showFullScreen() -# ------------------------------ View Functions ------------------------------ # + # ------------------------------ View Functions ------------------------------ # def toggleStylesheet(self): if self.stylesheetPath == STYLESHEET_LIGHT: @@ -144,7 +159,7 @@ def toggleStylesheet(self): if app is None: raise RuntimeError("No Qt Application found.") - with open(self.stylesheetPath, 'r') as fh: + with open(self.stylesheetPath, "r") as fh: app.setStyleSheet(fh.read()) def modifyFontSettings(self): @@ -156,7 +171,7 @@ def modifyFontSettings(self): if app is None: raise RuntimeError("No Qt Application found.") - with open(self.stylesheetPath, 'r') as fh: + with open(self.stylesheetPath, "r") as fh: app.setStyleSheet(fh.read()) def toggleSplitView(self): @@ -176,7 +191,7 @@ def scaleImage(self): def hideExplorer(self): self.explorer.setVisible(not self.explorer.isVisible()) -# ----------------------------- Control Functions ---------------------------- # + # ----------------------------- Control Functions ---------------------------- # def toggleMouseMode(self): self.canvas.toggleZoomPanMode() @@ -184,7 +199,7 @@ def toggleMouseMode(self): def modifyHotkeys(self): OptionsContainer(ShortcutOptions(self)).exec() -# ------------------------------ Misc Functions ------------------------------ # + # ------------------------------ Misc Functions ------------------------------ # def loadModel(self): loadModelButton = self.toolbar.findChild(QPushButton, "loadModel") @@ -195,9 +210,9 @@ def loadModel(self): "hasLoadModelPopup", "Load the MangaOCR model?", LOAD_MODEL_MESSAGE, - CheckboxPopup.Ok | CheckboxPopup.Cancel + CheckboxPopup.Ok | CheckboxPopup.Cancel, ).exec() - if (ret == CheckboxPopup.Ok): + if ret == CheckboxPopup.Ok: pass else: loadModelButton.setChecked(False) @@ -221,7 +236,7 @@ def loadModelConfirm(message: str): if message == "success": BasePopup( f"{modelName} model loaded", - f"You are now using the {modelName} model for Japanese text detection." + f"You are now using the {modelName} model for Japanese text detection.", ).exec() else: BasePopup("Load Model Error", message).exec() @@ -229,8 +244,7 @@ def loadModelConfirm(message: str): worker = BaseWorker(loadModelHelper, self.tracker) worker.signals.result.connect(loadModelConfirm) - worker.signals.finished.connect(lambda: - loadModelButton.setEnabled(True)) + worker.signals.finished.connect(lambda: loadModelButton.setEnabled(True)) self.threadpool.start(worker) loadModelButton.setEnabled(False) diff --git a/app/components/windows/external.py b/app/components/windows/external.py index 272bace..c2c53b0 100644 --- a/app/components/windows/external.py +++ b/app/components/windows/external.py @@ -33,6 +33,7 @@ class ExternalWindow(QMainWindow): """ External window widget to enclose FullScreenOCRView """ + def __init__(self, parent: "MainWindow"): super().__init__() self.mainWindow = parent @@ -41,7 +42,7 @@ def __init__(self, parent: "MainWindow"): # we ensure that the whole screen is captured. self.layout().setContentsMargins(0, 0, 0, 0) self.setStyleSheet("border:0px; margin:0px") - + # Delete external window on close self.setAttribute(Qt.WA_DeleteOnClose) @@ -67,4 +68,4 @@ def showFullScreen(self): def closeEvent(self, event: QCloseEvent): # Ensure that object is deleted before closing self.deleteLater() - return super().closeEvent(event) \ No newline at end of file + return super().closeEvent(event) diff --git a/app/main.py b/app/main.py index d3f35c6..d791db3 100644 --- a/app/main.py +++ b/app/main.py @@ -28,7 +28,7 @@ from Trackers import Tracker from utils.constants import APP_NAME, APP_LOGO, SETTINGS_FILE_DEFAULT, STYLESHEET_LIGHT -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) app.setApplicationName(APP_NAME) @@ -40,13 +40,12 @@ settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) styles = settings.value("stylesheetPath", STYLESHEET_LIGHT) - with open(styles, 'r') as fh: + with open(styles, "r") as fh: app.setStyleSheet(fh.read()) shortcut = settings.value("captureExternalShortcut", "Alt+Q") keybinder.init() - keybinder.register_hotkey( - widget.winId(), shortcut, widget.captureExternal) + keybinder.register_hotkey(widget.winId(), shortcut, widget.captureExternal) winEventFilter = WinEventFilter(keybinder) eventDispatcher = QAbstractEventDispatcher.instance() eventDispatcher.installNativeEventFilter(winEventFilter) diff --git a/app/utils/constants.py b/app/utils/constants.py index 95ad056..5cf72fd 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -23,31 +23,52 @@ APP_NAME = "Poricom" APP_LOGO = "./assets/images/icons/logo.ico" -IMAGE_EXTENSIONS = ["*.bmp", "*.gif", "*.jpeg", "*.jpg", "*.pbm", "*.pgm", "*.png", "*.ppm", "*.webp", "*.xbm", "*.xpm",] +IMAGE_EXTENSIONS = [ + "*.bmp", + "*.gif", + "*.jpeg", + "*.jpg", + "*.pbm", + "*.pgm", + "*.png", + "*.ppm", + "*.webp", + "*.xbm", + "*.xpm", +] # Settings -TOGGLE_CHOICES = [ " Disabled", " Enabled"] +TOGGLE_CHOICES = [" Disabled", " Enabled"] -LANGUAGE = [ " Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English"] -ORIENTATION = [ " Vertical", " Horizontal"] +LANGUAGE = [" Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English"] +ORIENTATION = [" Vertical", " Horizontal"] -IMAGE_SCALING = [ " Fit to Width", " Fit to Height", " Fit to Screen"] +IMAGE_SCALING = [" Fit to Width", " Fit to Height", " Fit to Screen"] -FONT_SIZE = [ " 12", " 14", " 16", " 20", " 24", " 32", " 40", " 56", " 72"] -FONT_STYLE = [ " Helvetica", " Poppins", " Arial", " Verdana", " Times New Roman"] +FONT_SIZE = [" 12", " 14", " 16", " 20", " 24", " 32", " 40", " 56", " 72"] +FONT_STYLE = [" Helvetica", " Poppins", " Arial", " Verdana", " Times New Roman"] -MODIFIER = [ " Ctrl", " Shift", " Alt", " Ctrl+Alt", " Shift+Alt", " Shift+Ctrl", " Shift+Alt+Ctrl", " No Modifier"] +MODIFIER = [ + " Ctrl", + " Shift", + " Alt", + " Ctrl+Alt", + " Shift+Alt", + " Shift+Ctrl", + " Shift+Alt+Ctrl", + " No Modifier", +] # Paths -STYLESHEET_LIGHT = './assets/styles.qss' -STYLESHEET_DARK = './assets/styles-dark.qss' +STYLESHEET_LIGHT = "./assets/styles.qss" +STYLESHEET_DARK = "./assets/styles-dark.qss" TESSERACT_LANGUAGES = "./assets/languages/" -TOOLBAR_ICONS = './assets/images/icons/' -TOOLBAR_ICON_DEFAULT = './assets/images/icons/default_icon.png' +TOOLBAR_ICONS = "./assets/images/icons/" +TOOLBAR_ICON_DEFAULT = "./assets/images/icons/default_icon.png" -EXPLORER_ROOT_DEFAULT = './assets/images/' +EXPLORER_ROOT_DEFAULT = "./assets/images/" # Messages LOAD_MODEL_MESSAGE = ( @@ -59,35 +80,26 @@ # ------------------------------------ Settings ------------------------------------- # -SETTINGS_FILE_DEFAULT = './bin/poricom-config.ini' +SETTINGS_FILE_DEFAULT = "./bin/poricom-config.ini" # Window MAIN_WINDOW_DEFAULTS = { "hasLoadModelPopup": "true", "explorerPath": "./assets/images/", - "stylesheetPath": "./assets/styles.qss" -} -MAIN_WINDOW_TYPES = { - "hasLoadModelPopup": bool + "stylesheetPath": "./assets/styles.qss", } +MAIN_WINDOW_TYPES = {"hasLoadModelPopup": bool} # View IMAGE_VIEW_DEFAULT = { "viewImageMode": 0, "splitViewMode": "false", - "zoomPanMode": "false" -} -IMAGE_VIEW_TYPES = { - "viewImageMode": int, - "splitViewMode": bool, - "zoomPanMode": bool + "zoomPanMode": "false", } +IMAGE_VIEW_TYPES = {"viewImageMode": int, "splitViewMode": bool, "zoomPanMode": bool} # Tesseract -TESSERACT_DEFAULTS = { - "language": "jpn", - "orientation": "_vert" -} +TESSERACT_DEFAULTS = {"language": "jpn", "orientation": "_vert"} # --------------------------------------- UI ---------------------------------------- # @@ -105,7 +117,7 @@ "toggle": False, "align": "AlignRight", "iconHeight": 0.45, - "iconWidth": 0.45 + "iconWidth": 0.45, }, "zoomOut": { "title": "Zoom out", @@ -114,7 +126,7 @@ "toggle": False, "align": "AlignRight", "iconHeight": 0.45, - "iconWidth": 0.45 + "iconWidth": 0.45, }, "loadImageAtIndex": { "title": "", @@ -123,7 +135,7 @@ "toggle": False, "align": "AlignRight", "iconHeight": 0.45, - "iconWidth": 1.3 + "iconWidth": 1.3, }, "loadPrevImage": { "title": "", @@ -132,7 +144,7 @@ "toggle": False, "align": "AlignRight", "iconHeight": 0.45, - "iconWidth": 0.6 + "iconWidth": 0.6, }, "loadNextImage": { "title": "", @@ -141,8 +153,8 @@ "toggle": False, "align": "AlignRight", "iconHeight": 0.45, - "iconWidth": 0.6 - } + "iconWidth": 0.6, + }, } TOOLBAR_FUNCTIONS: dict[str, ButtonConfigDict] = { "file": { @@ -153,7 +165,7 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "openManga": { "title": "Open manga file", @@ -162,7 +174,7 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "captureExternalHelper": { "title": "External capture", @@ -171,8 +183,8 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 - } + "iconWidth": 1.0, + }, }, "view": { "toggleStylesheet": { @@ -182,7 +194,7 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "hideExplorer": { "title": "Hide explorer", @@ -191,7 +203,7 @@ "toggle": True, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "modifyFontSettings": { "title": "Modify preview text", @@ -200,7 +212,7 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "toggleSplitView": { "title": "Turn on split view", @@ -209,7 +221,7 @@ "toggle": True, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "scaleImage": { "title": "Adjust image scaling", @@ -218,8 +230,8 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 - } + "iconWidth": 1.0, + }, }, "controls": { "toggleMouseMode": { @@ -229,7 +241,7 @@ "toggle": True, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "modifyHotkeys": { "title": "Remap hotkeys", @@ -238,8 +250,8 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 - } + "iconWidth": 1.0, + }, }, "misc": { "loadModel": { @@ -249,7 +261,7 @@ "toggle": True, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "modifyTesseract": { "title": "Tesseract settings", @@ -258,7 +270,7 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "toggleLogging": { "title": "Enable text logging", @@ -267,7 +279,7 @@ "toggle": True, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 - } - } + "iconWidth": 1.0, + }, + }, } diff --git a/app/utils/scripts/editStylesheet.py b/app/utils/scripts/editStylesheet.py index a0c09e5..8b9adbd 100644 --- a/app/utils/scripts/editStylesheet.py +++ b/app/utils/scripts/editStylesheet.py @@ -19,15 +19,16 @@ from ..constants import STYLESHEET_LIGHT, STYLESHEET_DARK + def editStylesheet(index: int, style: str): """ Replace stylesheet at line `index` with input `style` """ - with open(STYLESHEET_LIGHT, 'r') as slFh, open(STYLESHEET_DARK, 'r') as sdFh: + with open(STYLESHEET_LIGHT, "r") as slFh, open(STYLESHEET_DARK, "r") as sdFh: lineLight = slFh.readlines() linesDark = sdFh.readlines() lineLight[index] = style linesDark[index] = style - with open(STYLESHEET_LIGHT, 'w') as slFh, open(STYLESHEET_DARK, 'w') as sdFh: + with open(STYLESHEET_LIGHT, "w") as slFh, open(STYLESHEET_DARK, "w") as sdFh: slFh.writelines(lineLight) sdFh.writelines(linesDark) diff --git a/app/utils/scripts/logText.py b/app/utils/scripts/logText.py index 08e5705..592d78d 100644 --- a/app/utils/scripts/logText.py +++ b/app/utils/scripts/logText.py @@ -19,7 +19,8 @@ from PyQt5.QtGui import QGuiApplication -def logText(text: str, isLogFile: bool=False, path: str="."): + +def logText(text: str, isLogFile: bool = False, path: str = "."): """Log text by copying to clipboard Args: @@ -31,5 +32,5 @@ def logText(text: str, isLogFile: bool=False, path: str="."): clipboard.setText(text) if isLogFile: - with open(path, 'a', encoding="utf-8") as fh: + with open(path, "a", encoding="utf-8") as fh: fh.write(text + "\n") diff --git a/app/utils/scripts/mangaFileToImageDir.py b/app/utils/scripts/mangaFileToImageDir.py index 45947ce..ef018c7 100644 --- a/app/utils/scripts/mangaFileToImageDir.py +++ b/app/utils/scripts/mangaFileToImageDir.py @@ -24,6 +24,7 @@ import rarfile import pdf2image + def mangaFileToImageDir(filepath: str): """Converts a manga file to a directory of images @@ -37,7 +38,7 @@ def mangaFileToImageDir(filepath: str): cachePath = f"./poricom_cache/{basename(extractPath)}" if extension in [".cbz", ".zip"]: - with zipfile.ZipFile(filepath, 'r') as zipRef: + with zipfile.ZipFile(filepath, "r") as zipRef: zipRef.extractall(cachePath) rarfile.UNRAR_TOOL = "bin/unrar.exe" @@ -50,11 +51,11 @@ def mangaFileToImageDir(filepath: str): images = pdf2image.convert_from_path(filepath) except pdf2image.exceptions.PDFInfoNotInstalledError: images = pdf2image.convert_from_path( - filepath, poppler_path="poppler/Library/bin") + filepath, poppler_path="poppler/Library/bin" + ) for i in range(len(images)): filename = basename(extractPath) Path(cachePath).mkdir(parents=True, exist_ok=True) - images[i].save( - f"{cachePath}/{i+1}_{filename}.png", 'PNG') + images[i].save(f"{cachePath}/{i+1}_{filename}.png", "PNG") return cachePath diff --git a/app/utils/scripts/pixmapToText.py b/app/utils/scripts/pixmapToText.py index bb3cc4d..723fc86 100644 --- a/app/utils/scripts/pixmapToText.py +++ b/app/utils/scripts/pixmapToText.py @@ -29,7 +29,9 @@ from ..constants import TESSERACT_LANGUAGES -def pixmapToText(pixmap: QPixmap, language: str = "jpn_vert", model: Optional[MangaOcr] = None) -> str: +def pixmapToText( + pixmap: QPixmap, language: str = "jpn_vert", model: Optional[MangaOcr] = None +) -> str: """ Convert QPixmap object to text using the model """ @@ -47,13 +49,15 @@ def pixmapToText(pixmap: QPixmap, language: str = "jpn_vert", model: Optional[Ma if model is not None: text = model(pillowImage) - + # PSM = 1 works most of the time except on smaller bounding boxes. # By smaller, we mean textboxes with less text. Usually these # boxes have at most one vertical line of text. else: - with PyTessBaseAPI(path=TESSERACT_LANGUAGES, lang=language, oem=1, psm=1) as api: + with PyTessBaseAPI( + path=TESSERACT_LANGUAGES, lang=language, oem=1, psm=1 + ) as api: api.SetImage(pillowImage) text = api.GetUTF8Text() - return text.strip() \ No newline at end of file + return text.strip() diff --git a/app/utils/types.py b/app/utils/types.py index 8db2d63..72de520 100644 --- a/app/utils/types.py +++ b/app/utils/types.py @@ -18,6 +18,7 @@ from typing import TypedDict + class ButtonConfig(TypedDict): title: str message: str @@ -27,4 +28,5 @@ class ButtonConfig(TypedDict): iconHeight: float iconWidth: float + ButtonConfigDict = dict[str, ButtonConfig] From 90e23284491431bea2a68249dbd6aaa671370cb7 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 22 Jan 2023 00:27:19 +0800 Subject: [PATCH 053/137] Move services outside components --- app/components/views/image/base.py | 2 +- app/components/views/ocr/base.py | 2 +- app/components/windows/base.py | 2 +- app/main.py | 2 +- app/{components => }/services/__init__.py | 0 app/{components => }/services/filters.py | 0 app/{components => }/services/workers/__init__.py | 0 app/{components => }/services/workers/base.py | 0 app/{components => }/services/workers/signal.py | 0 9 files changed, 4 insertions(+), 4 deletions(-) rename app/{components => }/services/__init__.py (100%) rename app/{components => }/services/filters.py (100%) rename app/{components => }/services/workers/__init__.py (100%) rename app/{components => }/services/workers/base.py (100%) rename app/{components => }/services/workers/signal.py (100%) diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index e76d6cd..4f357e9 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -23,8 +23,8 @@ from PyQt5.QtCore import Qt, QRect, QRectF, QSize, QThreadPool from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsView -from components.services import BaseWorker from components.settings import BaseSettings +from services import BaseWorker from utils.constants import IMAGE_VIEW_DEFAULT, IMAGE_VIEW_TYPES if TYPE_CHECKING: diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 02ee5b1..3317a3b 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -20,8 +20,8 @@ from PyQt5.QtCore import pyqtSlot, Qt, QThreadPool, QTimer from PyQt5.QtWidgets import QGraphicsView, QLabel, QMainWindow -from components.services import BaseWorker from components.settings import BaseSettings +from services import BaseWorker from utils.constants import TESSERACT_DEFAULTS from utils.scripts import logText, pixmapToText diff --git a/app/components/windows/base.py b/app/components/windows/base.py index b8ed22c..cefd39c 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -33,7 +33,6 @@ from .external import ExternalWindow from components.popups import BasePopup, CheckboxPopup -from components.services import BaseWorker from components.settings import ( BaseSettings, PreviewOptions, @@ -44,6 +43,7 @@ ) from components.toolbar import BaseToolbar from components.views import WorkspaceView +from services import BaseWorker from utils.constants import ( LOAD_MODEL_MESSAGE, MAIN_WINDOW_DEFAULTS, diff --git a/app/main.py b/app/main.py index d791db3..cbc250e 100644 --- a/app/main.py +++ b/app/main.py @@ -23,8 +23,8 @@ from PyQt5.QtCore import QAbstractEventDispatcher, QSettings from pyqtkeybind import keybinder -from components.services import WinEventFilter from components.windows import MainWindow +from services import WinEventFilter from Trackers import Tracker from utils.constants import APP_NAME, APP_LOGO, SETTINGS_FILE_DEFAULT, STYLESHEET_LIGHT diff --git a/app/components/services/__init__.py b/app/services/__init__.py similarity index 100% rename from app/components/services/__init__.py rename to app/services/__init__.py diff --git a/app/components/services/filters.py b/app/services/filters.py similarity index 100% rename from app/components/services/filters.py rename to app/services/filters.py diff --git a/app/components/services/workers/__init__.py b/app/services/workers/__init__.py similarity index 100% rename from app/components/services/workers/__init__.py rename to app/services/workers/__init__.py diff --git a/app/components/services/workers/base.py b/app/services/workers/base.py similarity index 100% rename from app/components/services/workers/base.py rename to app/services/workers/base.py diff --git a/app/components/services/workers/signal.py b/app/services/workers/signal.py similarity index 100% rename from app/components/services/workers/signal.py rename to app/services/workers/signal.py From 5a6c9bdf492da2cd5abc667b44645c021c27cfa8 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 22 Jan 2023 17:09:04 +0800 Subject: [PATCH 054/137] Move view and explorer functions to workspace --- app/components/views/workspace.py | 104 ++++++++++++++++++++++++------ app/components/windows/base.py | 77 +--------------------- 2 files changed, 88 insertions(+), 93 deletions(-) diff --git a/app/components/views/workspace.py b/app/components/views/workspace.py index 3103fae..0378be9 100644 --- a/app/components/views/workspace.py +++ b/app/components/views/workspace.py @@ -17,32 +17,84 @@ along with this program. If not, see . """ -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QInputDialog, QMainWindow, QSplitter +from PyQt5.QtCore import Qt, QThreadPool +from PyQt5.QtWidgets import QFileDialog, QInputDialog, QMainWindow, QSplitter from .ocr import OCRView from components.explorers import ImageExplorer -from utils.constants import MAIN_VIEW_RATIO +from components.popups import BasePopup +from components.settings import BaseSettings, ImageScalingOptions, OptionsContainer +from services import BaseWorker +from utils.constants import EXPLORER_ROOT_DEFAULT, MAIN_VIEW_DEFAULTS, MAIN_VIEW_RATIO +from utils.scripts import mangaFileToImageDir -# TODO: Move view settings here -class WorkspaceView(QSplitter): + +class WorkspaceView(QSplitter, BaseSettings): """ Main view of the program. Includes the explorer and the view. """ def __init__(self, parent: QMainWindow, tracker=None): super().__init__(parent) + self.mainWindow = parent self.tracker = tracker - self.canvas = OCRView(self, self.tracker) - self.explorer = ImageExplorer(self, self.tracker.filepath) + self.setDefaults(MAIN_VIEW_DEFAULTS) + self.loadSettings() + self.canvas = OCRView(self, self.tracker) + self.explorer = ImageExplorer(self, self.explorerPath) self.addWidget(self.explorer) self.addWidget(self.canvas) self.setChildrenCollapsible(False) for i, s in enumerate(MAIN_VIEW_RATIO): self.setStretchFactor(i, s) + def resizeEvent(self, event): + self.explorer.setMinimumWidth(0.1 * self.width()) + self.canvas.setMinimumWidth(0.7 * self.width()) + return super().resizeEvent(event) + + # ------------------------------------ Explorer ------------------------------------- # + + def openDir(self): + filepath = QFileDialog.getExistingDirectory( + self, "Open Directory", self.explorerPath + ) + + if filepath: + try: + self.explorer.setDirectory(filepath) + self.explorerPath = filepath + except FileNotFoundError: + BasePopup( + "No images found in the directory", + "Please select a directory with images.", + ).exec() + + def openManga(self): + filename, _ = QFileDialog.getOpenFileName( + self, + "Open Manga File", + self.explorerPath, + "Manga (*.cbz *.cbr *.zip *.rar *.pdf)", + ) + + if filename: + def setDirectory(filepath): + self.explorer.setDirectory(filepath) + self.explorerPath = EXPLORER_ROOT_DEFAULT + + worker = BaseWorker(mangaFileToImageDir, filename) + worker.signals.result.connect(setDirectory) + + QThreadPool.globalInstance().start(worker) + + def hideExplorer(self): + self.explorer.setVisible(not self.explorer.isVisible()) + + # -------------------------------------- View --------------------------------------- # + def viewImageFromExplorer(self, filename, filenext): if not self.canvas.splitViewMode: self.tracker.pixImage = filename @@ -54,9 +106,34 @@ def viewImageFromExplorer(self, filename, filenext): self.canvas.currentScale = 1 self.canvas.verticalScrollBar().setSliderPosition(0) self.canvas.viewImage() - # self.canvas.setFocus() return True + def toggleSplitView(self): + self.canvas.toggleSplitView() + if self.canvas.splitViewMode: + self.canvas.modifyViewImageMode(2) + index = self.explorer.currentIndex() + self.explorer.currentChanged(index, index) + elif not self.canvas.splitViewMode: + index = self.explorer.currentIndex() + self.explorer.currentChanged(index, index) + + def scaleImage(self): + OptionsContainer(ImageScalingOptions(self)).exec() + + # -------------------------------------- Zoom --------------------------------------- # + + def toggleMouseMode(self): + self.canvas.toggleZoomPanMode() + + def zoomIn(self): + self.canvas.zoomView(True, usingButton=True) + + def zoomOut(self): + self.canvas.zoomView(False, usingButton=True) + + # ----------------------------------- Navigation ------------------------------------ # + def loadPrevImage(self): index = self.explorer.indexAbove(self.explorer.currentIndex()) if self.canvas.splitViewMode: @@ -93,14 +170,3 @@ def loadImageAtIndex(self): index = self.explorer.model().index(i - 1, 0, self.explorer.rootIndex()) self.explorer.setCurrentIndex(index) - - def zoomIn(self): - self.canvas.zoomView(True, usingButton=True) - - def zoomOut(self): - self.canvas.zoomView(False, usingButton=True) - - def resizeEvent(self, event): - self.explorer.setMinimumWidth(0.1 * self.width()) - self.canvas.setMinimumWidth(0.6 * self.width()) - return super().resizeEvent(event) diff --git a/app/components/windows/base.py b/app/components/windows/base.py index cefd39c..dde8e1c 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -22,21 +22,13 @@ from manga_ocr import MangaOcr from PyQt5.QtCore import QThreadPool -from PyQt5.QtWidgets import ( - QVBoxLayout, - QWidget, - QMainWindow, - QApplication, - QPushButton, - QFileDialog, -) +from PyQt5.QtWidgets import QVBoxLayout, QWidget, QMainWindow, QApplication, QPushButton from .external import ExternalWindow from components.popups import BasePopup, CheckboxPopup from components.settings import ( BaseSettings, PreviewOptions, - ImageScalingOptions, OptionsContainer, ShortcutOptions, TesseractOptions, @@ -51,7 +43,6 @@ STYLESHEET_DARK, STYLESHEET_LIGHT, ) -from utils.scripts import mangaFileToImageDir class MainWindow(QMainWindow, BaseSettings): @@ -90,6 +81,7 @@ def closeEvent(self, event): except FileNotFoundError: pass self.saveSettings(False) + self.mainView.saveSettings(False) return super().closeEvent(event) def noop(self): @@ -97,47 +89,6 @@ def noop(self): # ------------------------------ File Functions ------------------------------ # - def openDir(self): - filepath = QFileDialog.getExistingDirectory( - self, - "Open Directory", - self.tracker.filepath, # , QFileDialog.DontUseNativeDialog - ) - - if filepath: - try: - self.tracker.filepath = filepath - self.explorer.setDirectory(filepath) - self.explorerPath = filepath - except FileNotFoundError: - BasePopup( - "No images found in the directory", - "Please select a directory with images.", - ).exec() - - def openManga(self): - filename, _ = QFileDialog.getOpenFileName( - self, - "Open Manga File", - self.tracker.filepath, - "Manga (*.cbz *.cbr *.zip *.rar *.pdf)", - ) - - if filename: - - def setDirectory(filepath): - self.tracker.filepath = filepath - self.explorer.setDirectory(filepath) - - openMangaButton = self.toolbar.findChild(QPushButton, "openManga") - - worker = BaseWorker(mangaFileToImageDir, filename) - worker.signals.result.connect(setDirectory) - worker.signals.finished.connect(lambda: openMangaButton.setEnabled(True)) - - self.threadpool.start(worker) - openMangaButton.setEnabled(False) - def captureExternalHelper(self): self.showMinimized() sleep(0.5) @@ -174,28 +125,8 @@ def modifyFontSettings(self): with open(self.stylesheetPath, "r") as fh: app.setStyleSheet(fh.read()) - def toggleSplitView(self): - self.canvas.toggleSplitView() - if self.canvas.splitViewMode: - self.canvas.setViewImageMode(2) - index = self.explorer.currentIndex() - self.explorer.currentChanged(index, index) - elif not self.canvas.splitViewMode: - index = self.explorer.currentIndex() - self.explorer.currentChanged(index, index) - - def scaleImage(self): - confirmation = OptionsContainer(ImageScalingOptions(self)) - confirmation.exec() - - def hideExplorer(self): - self.explorer.setVisible(not self.explorer.isVisible()) - # ----------------------------- Control Functions ---------------------------- # - def toggleMouseMode(self): - self.canvas.toggleZoomPanMode() - def modifyHotkeys(self): OptionsContainer(ShortcutOptions(self)).exec() @@ -212,9 +143,7 @@ def loadModel(self): LOAD_MODEL_MESSAGE, CheckboxPopup.Ok | CheckboxPopup.Cancel, ).exec() - if ret == CheckboxPopup.Ok: - pass - else: + if ret == CheckboxPopup.Cancel: loadModelButton.setChecked(False) return From 21674af9875a5d10c854ab9173bce12ac41b1443 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 22 Jan 2023 17:09:57 +0800 Subject: [PATCH 055/137] Rename image scaling option method --- .../{scaleImage.png => modifyImageScaling.png} | Bin app/components/settings/popups/imageScaling.py | 2 +- app/components/views/image/base.py | 7 ++++--- app/utils/constants.py | 9 +++++---- 4 files changed, 10 insertions(+), 8 deletions(-) rename app/assets/images/icons/{scaleImage.png => modifyImageScaling.png} (100%) diff --git a/app/assets/images/icons/scaleImage.png b/app/assets/images/icons/modifyImageScaling.png similarity index 100% rename from app/assets/images/icons/scaleImage.png rename to app/assets/images/icons/modifyImageScaling.png diff --git a/app/components/settings/popups/imageScaling.py b/app/components/settings/popups/imageScaling.py index c348763..6731c88 100644 --- a/app/components/settings/popups/imageScaling.py +++ b/app/components/settings/popups/imageScaling.py @@ -34,5 +34,5 @@ def changeImageScaling(self, i): self.imageScalingIndex = i def saveSettings(self, hasMessage=False): - self.mainWindow.canvas.setViewImageMode(self.imageScalingIndex) + self.mainWindow.canvas.modifyViewImageMode(self.imageScalingIndex) return super().saveSettings(hasMessage) diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index 4f357e9..8ba3e7a 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -25,7 +25,7 @@ from components.settings import BaseSettings from services import BaseWorker -from utils.constants import IMAGE_VIEW_DEFAULT, IMAGE_VIEW_TYPES +from utils.constants import IMAGE_VIEW_DEFAULTS, IMAGE_VIEW_TYPES if TYPE_CHECKING: from ..workspace import WorkspaceView @@ -51,7 +51,7 @@ def __init__(self, parent: "WorkspaceView", tracker=None): self._trackPadAtMax = 0 self._scrollSuppressed = False - self.addDefaults(IMAGE_VIEW_DEFAULT) + self.addDefaults(IMAGE_VIEW_DEFAULTS) self.addTypes(IMAGE_VIEW_TYPES) self.loadSettings() @@ -59,9 +59,10 @@ def __init__(self, parent: "WorkspaceView", tracker=None): # ------------------------------------ Settings ------------------------------------- # - def setViewImageMode(self, mode: int): + def modifyViewImageMode(self, mode: int): # TODO: This should be an enum not an int self.setProperty("viewImageMode", mode) + self.setProperty("imageScalingIndex", mode) self.saveSettings(hasMessage=False) self.viewImage() diff --git a/app/utils/constants.py b/app/utils/constants.py index 5cf72fd..eedd2ae 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -85,14 +85,15 @@ # Window MAIN_WINDOW_DEFAULTS = { "hasLoadModelPopup": "true", - "explorerPath": "./assets/images/", "stylesheetPath": "./assets/styles.qss", } MAIN_WINDOW_TYPES = {"hasLoadModelPopup": bool} # View -IMAGE_VIEW_DEFAULT = { +MAIN_VIEW_DEFAULTS = {"explorerPath": "./assets/images/"} +IMAGE_VIEW_DEFAULTS = { "viewImageMode": 0, + "imageScalingIndex": 0, "splitViewMode": "false", "zoomPanMode": "false", } @@ -223,10 +224,10 @@ "iconHeight": 1.0, "iconWidth": 1.0, }, - "scaleImage": { + "modifyImageScaling": { "title": "Adjust image scaling", "message": "Fit an image according to the available options: fit to width, fit to height, fit to screen", - "path": "scaleImage.png", + "path": "modifyImageScaling.png", "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, From 336c2241d40f2feb2cc5c70a355aa96705366145 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 22 Jan 2023 19:28:23 +0800 Subject: [PATCH 056/137] Move get directory logic to ImageExplorer --- app/Trackers.py | 5 +-- app/components/explorers/image.py | 56 +++++++++++++++++++++++-------- app/components/views/workspace.py | 37 +++++++------------- 3 files changed, 56 insertions(+), 42 deletions(-) diff --git a/app/Trackers.py b/app/Trackers.py index 58fe95c..186e901 100644 --- a/app/Trackers.py +++ b/app/Trackers.py @@ -40,10 +40,7 @@ # then pass the references to its children class Tracker: def __init__(self): - try: - self.filepath = abspath(explorerPath) - except FileNotFoundError: - self.filepath = abspath(explorerPath) + self.filepath = explorerPath try: filename, filenext, *_ = self._imageList except ValueError: diff --git a/app/components/explorers/image.py b/app/components/explorers/image.py index 6ed016b..66ca09e 100644 --- a/app/components/explorers/image.py +++ b/app/components/explorers/image.py @@ -16,28 +16,31 @@ along with this program. If not, see . """ +from os import listdir, path as ospath +from typing import TYPE_CHECKING + from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QMainWindow, QTreeView +from PyQt5.QtWidgets import QFileDialog, QTreeView from .models import ImageModel -from utils.constants import EXPLORER_ROOT_DEFAULT +from utils.constants import EXPLORER_ROOT_DEFAULT, IMAGE_EXTENSIONS + +if TYPE_CHECKING: + from components.views import WorkspaceView class ImageExplorer(QTreeView): """View to allow exploring images Args: - parent (QMainWindow): Image explorer parent. Set to main window. + parent (WorkspaceView): Image explorer parent. Set to workspace view. initialDir (str, optional): Initial directory. Defaults to EXPLORER_ROOT_DEFAULT. """ - def __init__(self, parent: QMainWindow, initialDir: str = EXPLORER_ROOT_DEFAULT): + def __init__( + self, parent: "WorkspaceView", initialDir: str = EXPLORER_ROOT_DEFAULT + ): super().__init__(parent) - # TODO: It might be better if the parent is set to the QSplitter - # Then add property getter methods to main window to access its children - # Manually set parent since `addWidget` method will reparent the widget - self.mainWindow = parent - self.setModel(ImageModel()) for i in range(1, 4): @@ -45,17 +48,46 @@ def __init__(self, parent: QMainWindow, initialDir: str = EXPLORER_ROOT_DEFAULT) self.setIndentation(0) self.layoutCheck = False + if not ospath.exists(initialDir): + initialDir = EXPLORER_ROOT_DEFAULT self.setDirectory(initialDir) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + def setDirectory(self, path: str): + self.setRootIndex(self.model().setRootPath(path)) + self.setTopIndex() + + def getDirectory(self, startPath: str, isManga=False): + if isManga: + filename, _ = QFileDialog.getOpenFileName( + self.parent(), + "Open Manga File", + startPath, + "Manga (*.cbz *.cbr *.zip *.rar *.pdf)", + ) + return filename + filepath = QFileDialog.getExistingDirectory( + self.parent(), "Open Directory", startPath + ) + if not filepath: + return filepath + for file in listdir(filepath): + try: + _, extension = file.split(".") + if "*." + extension in IMAGE_EXTENSIONS: + return filepath + except ValueError: + continue + return None + def currentChanged(self, current, previous): if not current.isValid(): current = self.model().index(self.getTopIndex(), 0, self.rootIndex()) filename = self.model().fileInfo(current).absoluteFilePath() nextIndex = self.indexBelow(current) filenext = self.model().fileInfo(nextIndex).absoluteFilePath() - self.mainWindow.viewImageFromExplorer(filename, filenext) + self.parent().viewImageFromExplorer(filename, filenext) super().currentChanged(current, previous) def getTopIndex(self): @@ -72,7 +104,3 @@ def setTopIndex(self): if not self.layoutCheck: self.model().layoutChanged.connect(self.setTopIndex) self.layoutCheck = True - - def setDirectory(self, path: str): - self.setRootIndex(self.model().setRootPath(path)) - self.setTopIndex() diff --git a/app/components/views/workspace.py b/app/components/views/workspace.py index 0378be9..d7ec56c 100644 --- a/app/components/views/workspace.py +++ b/app/components/views/workspace.py @@ -18,7 +18,7 @@ """ from PyQt5.QtCore import Qt, QThreadPool -from PyQt5.QtWidgets import QFileDialog, QInputDialog, QMainWindow, QSplitter +from PyQt5.QtWidgets import QInputDialog, QMainWindow, QSplitter from .ocr import OCRView from components.explorers import ImageExplorer @@ -58,36 +58,25 @@ def resizeEvent(self, event): # ------------------------------------ Explorer ------------------------------------- # def openDir(self): - filepath = QFileDialog.getExistingDirectory( - self, "Open Directory", self.explorerPath - ) + filepath = self.explorer.getDirectory(self.explorerPath) if filepath: - try: - self.explorer.setDirectory(filepath) - self.explorerPath = filepath - except FileNotFoundError: - BasePopup( - "No images found in the directory", - "Please select a directory with images.", - ).exec() + self.explorer.setDirectory(filepath) + self.explorerPath = filepath + else: + BasePopup( + "No images found", + "Please select a directory with images.", + ).exec() def openManga(self): - filename, _ = QFileDialog.getOpenFileName( - self, - "Open Manga File", - self.explorerPath, - "Manga (*.cbz *.cbr *.zip *.rar *.pdf)", - ) + filename = self.explorer.getDirectory(self.explorerPath, True) if filename: - def setDirectory(filepath): - self.explorer.setDirectory(filepath) - self.explorerPath = EXPLORER_ROOT_DEFAULT + self.explorerPath = EXPLORER_ROOT_DEFAULT worker = BaseWorker(mangaFileToImageDir, filename) - worker.signals.result.connect(setDirectory) - + worker.signals.result.connect(self.explorer.setDirectory) QThreadPool.globalInstance().start(worker) def hideExplorer(self): @@ -118,7 +107,7 @@ def toggleSplitView(self): index = self.explorer.currentIndex() self.explorer.currentChanged(index, index) - def scaleImage(self): + def modifyImageScaling(self): OptionsContainer(ImageScalingOptions(self)).exec() # -------------------------------------- Zoom --------------------------------------- # From 45f90798b56e3a0727b037187dc120bda4ef6dbb Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 22 Jan 2023 19:30:45 +0800 Subject: [PATCH 057/137] Refactor text logging to use BaseSettings --- app/components/views/ocr/base.py | 15 +++++++++++---- app/components/views/ocr/fullscreen.py | 4 ++-- app/components/windows/base.py | 3 ++- app/utils/constants.py | 10 ++++++++-- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 3317a3b..4bc78e2 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -17,12 +17,18 @@ along with this program. If not, see . """ +from os.path import join + from PyQt5.QtCore import pyqtSlot, Qt, QThreadPool, QTimer from PyQt5.QtWidgets import QGraphicsView, QLabel, QMainWindow from components.settings import BaseSettings from services import BaseWorker -from utils.constants import TESSERACT_DEFAULTS +from utils.constants import ( + TESSERACT_DEFAULTS, + TEXT_LOGGING_DEFAULTS, + TEXT_LOGGING_TYPES, +) from utils.scripts import logText, pixmapToText @@ -52,6 +58,8 @@ def __init__(self, parent: QMainWindow, tracker=None): self.setDragMode(QGraphicsView.RubberBandDrag) self.addDefaults(TESSERACT_DEFAULTS) + self.addDefaults(TEXT_LOGGING_DEFAULTS) + self.addTypes(TEXT_LOGGING_TYPES) self.addProperty("persistText", "true", bool) def handleTextResult(self, result): @@ -93,10 +101,9 @@ def mouseMoveEvent(self, event): super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): - logPath = self.tracker.filepath + "/text-log.txt" - isLogFile = self.tracker.writeMode + logPath = join(self.explorerPath, "text-log.txt") text = self.canvasText.text() - logText(text, isLogFile=isLogFile, path=logPath) + logText(text, isLogFile=self.logToFile, path=logPath) try: if not self.persistText: self.canvasText.hide() diff --git a/app/components/views/ocr/fullscreen.py b/app/components/views/ocr/fullscreen.py index 53c3dd9..e76ab2b 100644 --- a/app/components/views/ocr/fullscreen.py +++ b/app/components/views/ocr/fullscreen.py @@ -22,7 +22,7 @@ from PyQt5.QtGui import QCursor, QMouseEvent from .base import BaseOCRView -from utils.constants import TESSERACT_DEFAULTS +from utils.constants import TESSERACT_DEFAULTS, TEXT_LOGGING_DEFAULTS class FullScreenOCRView(BaseOCRView): @@ -38,7 +38,7 @@ def __init__(self, parent: QMainWindow, tracker=None): self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setScene(QGraphicsScene()) - self.loadSettings(TESSERACT_DEFAULTS) + self.loadSettings({**TESSERACT_DEFAULTS, **TEXT_LOGGING_DEFAULTS}) def takeScreenshot(self, screenIndex: int): screen = QApplication.screens()[screenIndex] diff --git a/app/components/windows/base.py b/app/components/windows/base.py index dde8e1c..cf7f850 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -185,4 +185,5 @@ def modifyTesseract(self): self.canvas.loadSettings() def toggleLogging(self): - self.tracker.switchWriteMode() + self.logToFile = not self.logToFile + self.canvas.loadSettings() diff --git a/app/utils/constants.py b/app/utils/constants.py index eedd2ae..2464baf 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -85,12 +85,13 @@ # Window MAIN_WINDOW_DEFAULTS = { "hasLoadModelPopup": "true", + "logToFile": "false", "stylesheetPath": "./assets/styles.qss", } -MAIN_WINDOW_TYPES = {"hasLoadModelPopup": bool} +MAIN_WINDOW_TYPES = {"hasLoadModelPopup": bool, "logToFile": bool} # View -MAIN_VIEW_DEFAULTS = {"explorerPath": "./assets/images/"} +MAIN_VIEW_DEFAULTS = {"explorerPath": EXPLORER_ROOT_DEFAULT} IMAGE_VIEW_DEFAULTS = { "viewImageMode": 0, "imageScalingIndex": 0, @@ -102,6 +103,11 @@ # Tesseract TESSERACT_DEFAULTS = {"language": "jpn", "orientation": "_vert"} +# Text Logging +TEXT_LOGGING_DEFAULTS = {"explorerPath": EXPLORER_ROOT_DEFAULT, "logToFile": "false"} +TEXT_LOGGING_TYPES = {"logToFile": bool} + + # --------------------------------------- UI ---------------------------------------- # # Main view From a6fa1d3c787f30cff40650fed749c6ef7d0e0658 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 22 Jan 2023 19:58:20 +0800 Subject: [PATCH 058/137] Rename state tracker and remove unused properties --- app/Trackers.py | 182 ------------------------- app/components/views/image/base.py | 18 +-- app/components/views/ocr/base.py | 16 +-- app/components/views/ocr/fullscreen.py | 5 +- app/components/views/ocr/ocr.py | 6 +- app/components/views/workspace.py | 16 +-- app/components/windows/base.py | 24 ++-- app/components/windows/external.py | 2 +- app/main.py | 4 +- app/services/__init__.py | 1 + app/services/states.py | 86 ++++++++++++ app/utils/scripts/__init__.py | 1 + app/utils/scripts/combineTwoImages.py | 46 +++++++ 13 files changed, 175 insertions(+), 232 deletions(-) delete mode 100644 app/Trackers.py create mode 100644 app/services/states.py create mode 100644 app/utils/scripts/combineTwoImages.py diff --git a/app/Trackers.py b/app/Trackers.py deleted file mode 100644 index 186e901..0000000 --- a/app/Trackers.py +++ /dev/null @@ -1,182 +0,0 @@ -""" -Poricom State-Tracking Logic - -Copyright (C) `2021-2022` `` - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -from os.path import isfile, join, splitext, normpath, abspath, exists, dirname -from os import listdir - -from PyQt5.QtCore import QSettings -from PyQt5.QtGui import QPixmap, QPainter - -from utils.constants import ( - EXPLORER_ROOT_DEFAULT, - IMAGE_EXTENSIONS, - SETTINGS_FILE_DEFAULT, -) - -settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) -split = settings.value("splitViewMode", "false").lower() == "true" -explorerPath = settings.value("explorerPath", EXPLORER_ROOT_DEFAULT) - -# TODO: This needs refactoring -# 1. Rename the module to `states` -# 2. Instead of one object there should be several state objects i.e. image states, filepath states -# 3. It might also be better if images states is an object tracked by WorkspaceView (parent) -# then pass the references to its children -class Tracker: - def __init__(self): - self.filepath = explorerPath - try: - filename, filenext, *_ = self._imageList - except ValueError: - filename, *_ = self._imageList - filenext = None - if not split: - self._pixImage = PImage(filename) - if split: - splitImage = self.twoFileToImage(filename, filenext) - self._pixImage = PImage(splitImage, filename) - self._pixMask = PImage(filename) - - self._writeMode = False - - self._imageList = [] - - self._betterOCR = False - self._ocrModel = None - - def twoFileToImage(self, fileLeft, fileRight): - imageLeft, imageRight = PImage(fileRight), PImage(fileLeft) - if not (imageLeft.isValid()): - return - - w = imageLeft.width() + imageRight.width() - h = max(imageLeft.height(), imageRight.height()) - if imageRight.isNull(): - w = imageLeft.width() * 2 - h = imageLeft.height() - splitImage = QPixmap(w, h) - painter = QPainter(splitImage) - painter.drawPixmap(0, 0, imageLeft.width(), imageLeft.height(), imageLeft) - painter.drawPixmap( - imageLeft.width(), 0, imageRight.width(), imageRight.height(), imageRight - ) - painter.end() - - return splitImage - - @property - def pixImage(self): - return self._pixImage - - @pixImage.setter - def pixImage(self, image): - if type(image) is str and PImage(image).isValid(): - self._pixImage = PImage(image) - self._pixImage.filename = abspath(image) - self._filepath = abspath(dirname(image)) - if type(image) is tuple: - fileLeft, fileRight = image - if not fileRight: - if fileLeft: - self._pixImage = PImage(fileLeft) - self._pixImage.filename = abspath(fileLeft) - self._filepath = abspath(dirname(fileLeft)) - return - splitImage = self.twoFileToImage(fileLeft, fileRight) - - self._pixImage = PImage(splitImage, fileLeft) - self._pixImage.filename = abspath(fileLeft) - self._filepath = abspath(dirname(fileLeft)) - - @property - def pixMask(self): - return self._pixMask - - @pixMask.setter - def pixMask(self, image): - self._pixMask = image - - @property - def filepath(self): - return self._filepath - - @filepath.setter - def filepath(self, filepath): - fileList = filter(lambda f: isfile(join(filepath, f)), listdir(filepath)) - imageList = list( - map( - lambda p: normpath(join(filepath, p)), - filter( - (lambda f: ("*" + splitext(f)[1]) in IMAGE_EXTENSIONS), fileList - ), - ) - ) - if len(imageList) <= 0: - raise FileNotFoundError("Empty directory") - - self._filepath = filepath - self._imageList = imageList - - @property - def ocrModel(self): - return self._ocrModel - - @ocrModel.setter - def ocrModel(self, ocrModel): - self._ocrModel = ocrModel - - @property - def writeMode(self): - return self._writeMode - - @writeMode.setter - def writeMode(self, writeMode): - self._writeMode = writeMode - - def switchWriteMode(self): - self._writeMode = not self._writeMode - return self._writeMode - - def switchOCRMode(self): - self._betterOCR = not self._betterOCR - return self._betterOCR - - -class PImage(QPixmap): - def __init__(self, *args): - super(QPixmap, self).__init__(args[0]) - - # Current directory + filename - if type(args[0]) == str: - self._filename = args[0] - if type(args[0]) == QPixmap: - self._filename = args[1] - # Current directory - self._filepath = None - - @property - def filename(self): - return self._filename - - @filename.setter - def filename(self, filename): - self._filename = filename - - def isValid(self): - return exists(self._filename) and isfile(self._filename) diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index 8ba3e7a..6df2671 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -24,7 +24,7 @@ from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsView from components.settings import BaseSettings -from services import BaseWorker +from services import BaseWorker, State from utils.constants import IMAGE_VIEW_DEFAULTS, IMAGE_VIEW_TYPES if TYPE_CHECKING: @@ -36,9 +36,9 @@ class BaseImageView(QGraphicsView, BaseSettings): Base image view to allow view/zoom/pan functions """ - def __init__(self, parent: "WorkspaceView", tracker=None): + def __init__(self, parent: "WorkspaceView", state: State = None): super().__init__(parent) - self.tracker = tracker + self.state = state self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) @@ -78,11 +78,7 @@ def toggleZoomPanMode(self): def initializePixmapItem(self): self.setScene(QGraphicsScene()) - self.pixmap = self.scene().addPixmap( - self.tracker.pixImage.scaledToWidth( - self.viewport().geometry().width(), Qt.SmoothTransformation - ) - ) + self.pixmap = self.scene().addPixmap(self.state.baseImage) def viewImage(self, factor=1): # self.verticalScrollBar().setSliderPosition(0) @@ -91,15 +87,15 @@ def viewImage(self, factor=1): h = factor * self.viewport().geometry().height() if self.viewImageMode == 0: self.pixmap.setPixmap( - self.tracker.pixImage.scaledToWidth(w, Qt.SmoothTransformation) + self.state.baseImage.scaledToWidth(w, Qt.SmoothTransformation) ) elif self.viewImageMode == 1: self.pixmap.setPixmap( - self.tracker.pixImage.scaledToHeight(h, Qt.SmoothTransformation) + self.state.baseImage.scaledToHeight(h, Qt.SmoothTransformation) ) elif self.viewImageMode == 2: self.pixmap.setPixmap( - self.tracker.pixImage.scaled( + self.state.baseImage.scaled( w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation ) ) diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 4bc78e2..df715e4 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -23,7 +23,7 @@ from PyQt5.QtWidgets import QGraphicsView, QLabel, QMainWindow from components.settings import BaseSettings -from services import BaseWorker +from services import BaseWorker, State from utils.constants import ( TESSERACT_DEFAULTS, TEXT_LOGGING_DEFAULTS, @@ -33,17 +33,13 @@ class BaseOCRView(QGraphicsView, BaseSettings): - """Base view with OCR capabilities - - Args: - parent (QMainWindow): View parent. Set to main window - tracker (Any, optional): State tracker. Defaults to None. + """ + Base view with OCR capabilities """ - def __init__(self, parent: QMainWindow, tracker=None): - # TODO: Remove references to tracker + def __init__(self, parent: QMainWindow, state: State = None): super().__init__(parent) - self.tracker = tracker + self.state = state self.timer = QTimer() self.timer.setInterval(300) @@ -88,7 +84,7 @@ def rubberBandStopped(self): language = self.language + self.orientation pixmap = self.grab(self.rubberBandRect()) - worker = BaseWorker(pixmapToText, pixmap, language, self.tracker.ocrModel) + worker = BaseWorker(pixmapToText, pixmap, language, self.state.ocrModel) worker.signals.result.connect(self.handleTextResult) worker.signals.finished.connect(self.handleTextFinished) self.timer.timeout.disconnect(self.rubberBandStopped) diff --git a/app/components/views/ocr/fullscreen.py b/app/components/views/ocr/fullscreen.py index e76ab2b..89e39a5 100644 --- a/app/components/views/ocr/fullscreen.py +++ b/app/components/views/ocr/fullscreen.py @@ -22,6 +22,7 @@ from PyQt5.QtGui import QCursor, QMouseEvent from .base import BaseOCRView +from services import State from utils.constants import TESSERACT_DEFAULTS, TEXT_LOGGING_DEFAULTS @@ -30,8 +31,8 @@ class FullScreenOCRView(BaseOCRView): Fullscreen view with OCR capabilities """ - def __init__(self, parent: QMainWindow, tracker=None): - super().__init__(parent, tracker) + def __init__(self, parent: QMainWindow, state: State = None): + super().__init__(parent, state) self.externalWindow = parent self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) diff --git a/app/components/views/ocr/ocr.py b/app/components/views/ocr/ocr.py index ac2931a..e1f8e44 100644 --- a/app/components/views/ocr/ocr.py +++ b/app/components/views/ocr/ocr.py @@ -22,12 +22,12 @@ from ..image import BaseImageView from .base import BaseOCRView +from services import State class OCRView(BaseImageView, BaseOCRView): - def __init__(self, parent: QMainWindow, tracker=None): - # TODO: Remove references to tracker - super().__init__(parent, tracker) + def __init__(self, parent: QMainWindow, state: State = None): + super().__init__(parent, state) self.loadSettings() @pyqtSlot() diff --git a/app/components/views/workspace.py b/app/components/views/workspace.py index d7ec56c..78f9e5e 100644 --- a/app/components/views/workspace.py +++ b/app/components/views/workspace.py @@ -24,7 +24,7 @@ from components.explorers import ImageExplorer from components.popups import BasePopup from components.settings import BaseSettings, ImageScalingOptions, OptionsContainer -from services import BaseWorker +from services import BaseWorker, State from utils.constants import EXPLORER_ROOT_DEFAULT, MAIN_VIEW_DEFAULTS, MAIN_VIEW_RATIO from utils.scripts import mangaFileToImageDir @@ -34,15 +34,15 @@ class WorkspaceView(QSplitter, BaseSettings): Main view of the program. Includes the explorer and the view. """ - def __init__(self, parent: QMainWindow, tracker=None): + def __init__(self, parent: QMainWindow, state: State): super().__init__(parent) self.mainWindow = parent - self.tracker = tracker + self.state = state self.setDefaults(MAIN_VIEW_DEFAULTS) self.loadSettings() - self.canvas = OCRView(self, self.tracker) + self.canvas = OCRView(self, self.state) self.explorer = ImageExplorer(self, self.explorerPath) self.addWidget(self.explorer) self.addWidget(self.canvas) @@ -63,7 +63,7 @@ def openDir(self): if filepath: self.explorer.setDirectory(filepath) self.explorerPath = filepath - else: + elif filepath == None: BasePopup( "No images found", "Please select a directory with images.", @@ -86,10 +86,10 @@ def hideExplorer(self): def viewImageFromExplorer(self, filename, filenext): if not self.canvas.splitViewMode: - self.tracker.pixImage = filename + self.state.baseImage = filename if self.canvas.splitViewMode: - self.tracker.pixImage = (filename, filenext) - if not self.tracker.pixImage.isValid(): + self.state.baseImage = (filename, filenext) + if not self.state.baseImage.isValid(): return False self.canvas.resetTransform() self.canvas.currentScale = 1 diff --git a/app/components/windows/base.py b/app/components/windows/base.py index cf7f850..6da437f 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -35,7 +35,7 @@ ) from components.toolbar import BaseToolbar from components.views import WorkspaceView -from services import BaseWorker +from services import BaseWorker, State from utils.constants import ( LOAD_MODEL_MESSAGE, MAIN_WINDOW_DEFAULTS, @@ -46,13 +46,13 @@ class MainWindow(QMainWindow, BaseSettings): - def __init__(self, parent=None, tracker=None): + def __init__(self, parent: QWidget = None): super().__init__(parent) - self.tracker = tracker + self.state = State() self.vLayout = QVBoxLayout() - self.mainView = WorkspaceView(self, self.tracker) + self.mainView = WorkspaceView(self, self.state) self.toolbar = BaseToolbar(self) self.vLayout.addWidget(self.toolbar) @@ -134,7 +134,7 @@ def modifyHotkeys(self): def loadModel(self): loadModelButton = self.toolbar.findChild(QPushButton, "loadModel") - loadModelButton.setChecked(not self.tracker.ocrModel) + loadModelButton.setChecked(not self.state.ocrModel) if loadModelButton.isChecked() and self.hasLoadModelPopup: ret = CheckboxPopup( @@ -147,21 +147,21 @@ def loadModel(self): loadModelButton.setChecked(False) return - def loadModelHelper(tracker): - betterOCR = tracker.switchOCRMode() + def loadModelHelper(state): + betterOCR = state.switchOCRMode() if betterOCR: try: - tracker.ocrModel = MangaOcr() + state.ocrModel = MangaOcr() return "success" except Exception as e: - tracker.switchOCRMode() + state.switchOCRMode() return str(e) else: - tracker.ocrModel = None + state.ocrModel = None return "success" def loadModelConfirm(message: str): - modelName = "MangaOCR" if self.tracker.ocrModel else "Tesseract" + modelName = "MangaOCR" if self.state.ocrModel else "Tesseract" if message == "success": BasePopup( f"{modelName} model loaded", @@ -171,7 +171,7 @@ def loadModelConfirm(message: str): BasePopup("Load Model Error", message).exec() loadModelButton.setChecked(False) - worker = BaseWorker(loadModelHelper, self.tracker) + worker = BaseWorker(loadModelHelper, self.state) worker.signals.result.connect(loadModelConfirm) worker.signals.finished.connect(lambda: loadModelButton.setEnabled(True)) diff --git a/app/components/windows/external.py b/app/components/windows/external.py index c2c53b0..76a826b 100644 --- a/app/components/windows/external.py +++ b/app/components/windows/external.py @@ -49,7 +49,7 @@ def __init__(self, parent: "MainWindow"): # WindowStaysOnTopHint & Popup flags ensures that the widget is the top window. self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.Popup) - self.setCentralWidget(FullScreenOCRView(self, parent.tracker)) + self.setCentralWidget(FullScreenOCRView(self, parent.state)) # self.ocrModel = parent.ocrModel def showFullScreen(self): diff --git a/app/main.py b/app/main.py index cbc250e..91d7af9 100644 --- a/app/main.py +++ b/app/main.py @@ -25,7 +25,6 @@ from components.windows import MainWindow from services import WinEventFilter -from Trackers import Tracker from utils.constants import APP_NAME, APP_LOGO, SETTINGS_FILE_DEFAULT, STYLESHEET_LIGHT if __name__ == "__main__": @@ -34,8 +33,7 @@ app.setApplicationName(APP_NAME) app.setWindowIcon(QIcon(APP_LOGO)) - tracker = Tracker() - widget = MainWindow(parent=None, tracker=tracker) + widget = MainWindow() settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) diff --git a/app/services/__init__.py b/app/services/__init__.py index 28818b1..f82fb96 100644 --- a/app/services/__init__.py +++ b/app/services/__init__.py @@ -17,4 +17,5 @@ """ from .filters import WinEventFilter +from .states import State from .workers import BaseWorker, BaseWorkerSignal diff --git a/app/services/states.py b/app/services/states.py new file mode 100644 index 0000000..29b45be --- /dev/null +++ b/app/services/states.py @@ -0,0 +1,86 @@ +""" +Poricom States + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from os.path import isfile, exists + +from PyQt5.QtGui import QPixmap + +from utils.scripts import combineTwoImages + + +class Pixmap(QPixmap): + def __init__(self, *args): + super().__init__(args[0]) + + # Current directory + filename + if type(args[0]) == str: + self._filename = args[0] + if type(args[0]) == QPixmap: + self._filename = args[1] + # Current directory + self._filepath = None + + @property + def filename(self): + return self._filename + + @filename.setter + def filename(self, filename): + self._filename = filename + + def isValid(self): + return exists(self._filename) and isfile(self._filename) + + +class State: + def __init__(self): + self._baseImage = Pixmap("") + + self._betterOCR = False + self._ocrModel = None + + @property + def baseImage(self): + return self._baseImage + + @baseImage.setter + def baseImage(self, image): + if type(image) is str and Pixmap(image).isValid(): + self._baseImage = Pixmap(image) + if type(image) is tuple: + fileLeft, fileRight = image + if not fileRight: + if fileLeft: + self._baseImage = Pixmap(fileLeft) + return + splitImage = combineTwoImages(fileLeft, fileRight) + + self._baseImage = Pixmap(splitImage, fileLeft) + + @property + def ocrModel(self): + return self._ocrModel + + @ocrModel.setter + def ocrModel(self, ocrModel): + self._ocrModel = ocrModel + + def switchOCRMode(self): + self._betterOCR = not self._betterOCR + return self._betterOCR diff --git a/app/utils/scripts/__init__.py b/app/utils/scripts/__init__.py index 603452d..7124755 100644 --- a/app/utils/scripts/__init__.py +++ b/app/utils/scripts/__init__.py @@ -17,6 +17,7 @@ along with this program. If not, see . """ +from .combineTwoImages import combineTwoImages from .editStylesheet import editStylesheet from .logText import logText from .mangaFileToImageDir import mangaFileToImageDir diff --git a/app/utils/scripts/combineTwoImages.py b/app/utils/scripts/combineTwoImages.py new file mode 100644 index 0000000..6ab12dd --- /dev/null +++ b/app/utils/scripts/combineTwoImages.py @@ -0,0 +1,46 @@ +""" +Poricom Helper Functions + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from typing import Union + +from PyQt5.QtGui import QPixmap, QPainter + + +def combineTwoImages(fileLeft: Union[str, QPixmap], fileRight: Union[str, QPixmap]): + """ + Combines two image files or pixmaps to one pixmap + """ + imageLeft, imageRight = QPixmap(fileRight), QPixmap(fileLeft) + if imageRight.isNull(): + raise FileNotFoundError("The first file is null.") + + w = imageLeft.width() + imageRight.width() + h = max(imageLeft.height(), imageRight.height()) + if imageLeft.isNull(): + w = imageRight.width() * 2 + h = imageRight.height() + combinedImage = QPixmap(w, h) + painter = QPainter(combinedImage) + painter.drawPixmap(0, 0, imageLeft.width(), imageLeft.height(), imageLeft) + painter.drawPixmap( + imageLeft.width(), 0, imageRight.width(), imageRight.height(), imageRight + ) + painter.end() + + return combinedImage From bd9b14be9fbed51ba8e3fc8124e6098df1b4389b Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 22 Jan 2023 22:06:04 +0800 Subject: [PATCH 059/137] Fix performance issue on zoom in --- code/MainWindow.py | 4 ++-- code/Views.py | 55 +++++++++++++++++++++------------------------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/code/MainWindow.py b/code/MainWindow.py index 4f0be31..6896c06 100644 --- a/code/MainWindow.py +++ b/code/MainWindow.py @@ -368,7 +368,7 @@ def loadImageAtIndex(self): self.explorer.setCurrentIndex(index) def zoomIn(self): - self.canvas.zoomView(True, usingButton=True) + self.canvas.zoomView(True) def zoomOut(self): - self.canvas.zoomView(False, usingButton=True) + self.canvas.zoomView(False) diff --git a/code/Views.py b/code/Views.py index de0704e..54701d5 100644 --- a/code/Views.py +++ b/code/Views.py @@ -23,7 +23,7 @@ QTimer, QThreadPool, pyqtSlot) from PyQt5.QtWidgets import ( QApplication, QGraphicsView, QGraphicsScene, QLabel) -from PyQt5.QtGui import QCursor +from PyQt5.QtGui import QCursor, QTransform from Workers import BaseWorker from utils.image_io import logText, pixboxToText @@ -137,7 +137,7 @@ def __init__(self, parent=None, tracker=None): self._viewImageMode = parent.config["VIEW_IMAGE_MODE"] self._splitViewMode = parent.config["SPLIT_VIEW_MODE"] self._zoomPanMode = False - self.currentScale = 1 + self.currentScale = 0 self._scrollAtMin = 0 self._scrollAtMax = 0 @@ -150,11 +150,11 @@ def __init__(self, parent=None, tracker=None): self.pixmap = self.scene.addPixmap(self.tracker.pixImage.scaledToWidth( self.viewport().geometry().width(), Qt.SmoothTransformation)) - def viewImage(self, factor=1): + def viewImage(self): # self.verticalScrollBar().setSliderPosition(0) - factor = self.currentScale - w = factor*self.viewport().geometry().width() - h = factor*self.viewport().geometry().height() + self.currentScale = 0 + w = self.viewport().geometry().width() + h = self.viewport().geometry().height() if self._viewImageMode == 0: self.pixmap.setPixmap( self.tracker.pixImage.scaledToWidth(w, Qt.SmoothTransformation)) @@ -179,40 +179,34 @@ def toggleSplitView(self): self._splitViewMode = not self._splitViewMode self.parent.config["SPLIT_VIEW_MODE"] = self._splitViewMode - def zoomView(self, isZoomIn, usingButton=False): - factor = 1.1 - if usingButton: - factor = 1.4 - - if isZoomIn and self.currentScale < 15: - #self.scale(factor, factor) - self.currentScale *= factor - self.viewImage(self.currentScale) - elif not isZoomIn and self.currentScale > 0.35: - #self.scale(1/factor, 1/factor) - self.currentScale /= factor - self.viewImage(self.currentScale) + def zoomView(self, isZoomIn): + if isZoomIn and self.currentScale < 8: + factor = 1.25 + self.currentScale += 1 + self.scale(factor, factor) + elif not isZoomIn and self.currentScale > -8: + factor = 0.8 + self.currentScale -= 1 + self.scale(factor, factor) def toggleZoomPanMode(self): self._zoomPanMode = not self._zoomPanMode def resizeEvent(self, event): self.viewImage() - QGraphicsView.resizeEvent(self, event) + super().resizeEvent(event) def wheelEvent(self, event): pressedKey = QApplication.keyboardModifiers() zoomMode = pressedKey == Qt.ControlModifier or self._zoomPanMode - # self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) + self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) + self.setResizeAnchor(QGraphicsView.NoAnchor) if zoomMode: if event.angleDelta().y() > 0: isZoomIn = True elif event.angleDelta().y() < 0: isZoomIn = False - scenePos = self.mapToScene(event.pos()) - truePos = QRect(scenePos.toPoint(), QSize(2, 2)).center() - self.centerOn(truePos) self.zoomView(isZoomIn) if self._scrollSuppressed: @@ -268,7 +262,7 @@ def suppressScroll(): return else: self._scrollAtMin += 1 - QGraphicsView.wheelEvent(self, event) + super().wheelEvent(event) def mouseMoveEvent(self, event): pressedKey = QApplication.keyboardModifiers() @@ -282,9 +276,10 @@ def mouseMoveEvent(self, event): BaseCanvas.mouseMoveEvent(self, event) def mouseDoubleClickEvent(self, event): - self.currentScale = 1 - self.viewImage(self.currentScale) - QGraphicsView.mouseDoubleClickEvent(self, event) + self.setTransform(QTransform()) + self.viewImage() + self.verticalScrollBar().setSliderPosition(0) + super().mouseDoubleClickEvent(event) def keyPressEvent(self, event): if event.key() == Qt.Key_Left: @@ -294,9 +289,9 @@ def keyPressEvent(self, event): self.parent.loadNextImage() return if event.key() == Qt.Key_Minus: - self.zoomView(isZoomIn=False, usingButton=True) + self.zoomView(isZoomIn=False) return if event.key() == Qt.Key_Plus: - self.zoomView(isZoomIn=True, usingButton=True) + self.zoomView(isZoomIn=True) return super().keyPressEvent(event) From c54dc08cc8548cfe1f97d20260197c780434f65b Mon Sep 17 00:00:00 2001 From: quanticism <> Date: Mon, 23 Jan 2023 21:30:32 +1100 Subject: [PATCH 060/137] fix: Use utf-8 encoding when saving config.toml file --- code/utils/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/utils/config.py b/code/utils/config.py index a283705..6010300 100644 --- a/code/utils/config.py +++ b/code/utils/config.py @@ -22,21 +22,21 @@ def saveOnClose(data, config="utils/config.toml"): - with open(config, 'w') as fh: + with open(config, 'w', encoding='utf-8') as fh: toml.dump(data, fh) def editConfig(index, replacementText, config="utils/config.toml"): data = toml.load(config) data[index] = replacementText - with open(config, 'w') as fh: + with open(config, 'w', encoding='utf-8') as fh: toml.dump(data, fh) def editSelectionConfig(index, cBoxName, config="utils/config.toml"): data = toml.load(config) data["SELECTED_INDEX"][cBoxName] = index - with open(config, 'w') as fh: + with open(config, 'w', encoding='utf-8') as fh: toml.dump(data, fh) From cf7abdf29d3f1c48641375f541959a1e1388f65b Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 28 Jan 2023 18:51:25 +0800 Subject: [PATCH 061/137] Handle UnicodeDecodeError on tesseract import --- code/Views.py | 7 +++++++ code/utils/image_io.py | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/code/Views.py b/code/Views.py index de0704e..22c7278 100644 --- a/code/Views.py +++ b/code/Views.py @@ -25,6 +25,7 @@ QApplication, QGraphicsView, QGraphicsScene, QLabel) from PyQt5.QtGui import QCursor +from Popups import MessagePopup from Workers import BaseWorker from utils.image_io import logText, pixboxToText @@ -72,6 +73,12 @@ def mouseReleaseEvent(self, event): super().mouseReleaseEvent(event) def handleTextResult(self, result): + if result == None: + MessagePopup( + "Tesseract not loaded", + "Tesseract model cannot be loaded in your machine, please use the MangaOcr instead." + ).exec() + return try: self.canvasText.setText(result) except RuntimeError: diff --git a/code/utils/image_io.py b/code/utils/image_io.py index fe98673..73ba8d8 100644 --- a/code/utils/image_io.py +++ b/code/utils/image_io.py @@ -23,11 +23,15 @@ from PyQt5.QtCore import QBuffer from PyQt5.QtGui import QGuiApplication -from tesserocr import PyTessBaseAPI from PIL import Image import zipfile import rarfile import pdf2image +try: + from tesserocr import PyTessBaseAPI +except UnicodeDecodeError: + pass + from utils.config import config @@ -80,9 +84,13 @@ def pixboxToText(pixmap, lang="jpn_vert", model=None): # By smaller, we mean textboxes with less text. Usually these # boxes have at most one vertical line of text. else: - with PyTessBaseAPI(path=config["LANG_PATH"], lang=lang, oem=1, psm=1) as api: - api.SetImage(pillowImage) - text = api.GetUTF8Text() + try: + with PyTessBaseAPI(path=config["LANG_PATH"], lang=lang, oem=1, psm=1) as api: + api.SetImage(pillowImage) + text = api.GetUTF8Text() + except NameError: + # PyTessBaseAPI is undefined and there is no fallback model + return None return text.strip() From 3be9aeb4d95ce0bdfa248f20e3e5e5abab38252e Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 15:31:42 +0800 Subject: [PATCH 062/137] Rename source code directory to app --- {code => app}/Explorers.py | 0 {code => app}/MainWindow.py | 0 {code => app}/Popups.py | 0 {code => app}/Ribbon.py | 0 {code => app}/Trackers.py | 0 {code => app}/Views.py | 0 {code => app}/Workers.py | 0 {code => app}/assets/images/home.png | Bin .../assets/images/icons/captureExternalHelper.png | Bin {code => app}/assets/images/icons/cb_contract.png | Bin {code => app}/assets/images/icons/cb_expand.png | Bin {code => app}/assets/images/icons/default_icon.png | Bin {code => app}/assets/images/icons/hideExplorer.png | Bin .../assets/images/icons/input_dialog_down.png | Bin .../assets/images/icons/input_dialog_up.png | Bin .../assets/images/icons/loadImageAtIndex.png | Bin {code => app}/assets/images/icons/loadModel.png | Bin {code => app}/assets/images/icons/loadNextImage.png | Bin {code => app}/assets/images/icons/loadPrevImage.png | Bin {code => app}/assets/images/icons/logo.ico | Bin .../assets/images/icons/modifyFontSettings.png | Bin {code => app}/assets/images/icons/modifyHotkeys.png | Bin .../assets/images/icons/modifyTesseract.png | Bin {code => app}/assets/images/icons/openDir.png | Bin {code => app}/assets/images/icons/openManga.png | Bin {code => app}/assets/images/icons/scaleImage.png | Bin {code => app}/assets/images/icons/toggleLogging.png | Bin .../assets/images/icons/toggleMouseMode.png | Bin .../assets/images/icons/toggleSplitView.png | Bin .../assets/images/icons/toggleStylesheet.png | Bin {code => app}/assets/images/icons/zoomIn.png | Bin {code => app}/assets/images/icons/zoomOut.png | Bin {code => app}/assets/images/poricom-about.png | Bin {code => app}/assets/languages/chi_sim.traineddata | Bin .../assets/languages/chi_sim_vert.traineddata | Bin {code => app}/assets/languages/chi_tra.traineddata | Bin .../assets/languages/chi_tra_vert.traineddata | Bin {code => app}/assets/languages/eng.traineddata | Bin {code => app}/assets/languages/eng_vert.traineddata | Bin {code => app}/assets/languages/jpn.traineddata | Bin {code => app}/assets/languages/jpn_vert.traineddata | Bin {code => app}/assets/languages/kor.traineddata | Bin {code => app}/assets/languages/kor_vert.traineddata | Bin {code => app}/assets/styles-dark.qss | 0 {code => app}/assets/styles.qss | 0 {code => app}/main.py | 0 {code => app}/old/config.py | 0 {code => app}/old/memory.py | 0 {code => app}/old/ribbon.py | 0 {code => app}/old/viewer.py | 0 {code => app}/utils/config.py | 0 {code => app}/utils/config.toml | 0 {code => app}/utils/image_io.py | 0 {code => app}/utils/unrar.exe | Bin 54 files changed, 0 insertions(+), 0 deletions(-) rename {code => app}/Explorers.py (100%) rename {code => app}/MainWindow.py (100%) rename {code => app}/Popups.py (100%) rename {code => app}/Ribbon.py (100%) rename {code => app}/Trackers.py (100%) rename {code => app}/Views.py (100%) rename {code => app}/Workers.py (100%) rename {code => app}/assets/images/home.png (100%) rename {code => app}/assets/images/icons/captureExternalHelper.png (100%) rename {code => app}/assets/images/icons/cb_contract.png (100%) rename {code => app}/assets/images/icons/cb_expand.png (100%) rename {code => app}/assets/images/icons/default_icon.png (100%) rename {code => app}/assets/images/icons/hideExplorer.png (100%) rename {code => app}/assets/images/icons/input_dialog_down.png (100%) rename {code => app}/assets/images/icons/input_dialog_up.png (100%) rename {code => app}/assets/images/icons/loadImageAtIndex.png (100%) rename {code => app}/assets/images/icons/loadModel.png (100%) rename {code => app}/assets/images/icons/loadNextImage.png (100%) rename {code => app}/assets/images/icons/loadPrevImage.png (100%) rename {code => app}/assets/images/icons/logo.ico (100%) rename {code => app}/assets/images/icons/modifyFontSettings.png (100%) rename {code => app}/assets/images/icons/modifyHotkeys.png (100%) rename {code => app}/assets/images/icons/modifyTesseract.png (100%) rename {code => app}/assets/images/icons/openDir.png (100%) rename {code => app}/assets/images/icons/openManga.png (100%) rename {code => app}/assets/images/icons/scaleImage.png (100%) rename {code => app}/assets/images/icons/toggleLogging.png (100%) rename {code => app}/assets/images/icons/toggleMouseMode.png (100%) rename {code => app}/assets/images/icons/toggleSplitView.png (100%) rename {code => app}/assets/images/icons/toggleStylesheet.png (100%) rename {code => app}/assets/images/icons/zoomIn.png (100%) rename {code => app}/assets/images/icons/zoomOut.png (100%) rename {code => app}/assets/images/poricom-about.png (100%) rename {code => app}/assets/languages/chi_sim.traineddata (100%) rename {code => app}/assets/languages/chi_sim_vert.traineddata (100%) rename {code => app}/assets/languages/chi_tra.traineddata (100%) rename {code => app}/assets/languages/chi_tra_vert.traineddata (100%) rename {code => app}/assets/languages/eng.traineddata (100%) rename {code => app}/assets/languages/eng_vert.traineddata (100%) rename {code => app}/assets/languages/jpn.traineddata (100%) rename {code => app}/assets/languages/jpn_vert.traineddata (100%) rename {code => app}/assets/languages/kor.traineddata (100%) rename {code => app}/assets/languages/kor_vert.traineddata (100%) rename {code => app}/assets/styles-dark.qss (100%) rename {code => app}/assets/styles.qss (100%) rename {code => app}/main.py (100%) rename {code => app}/old/config.py (100%) rename {code => app}/old/memory.py (100%) rename {code => app}/old/ribbon.py (100%) rename {code => app}/old/viewer.py (100%) rename {code => app}/utils/config.py (100%) rename {code => app}/utils/config.toml (100%) rename {code => app}/utils/image_io.py (100%) rename {code => app}/utils/unrar.exe (100%) diff --git a/code/Explorers.py b/app/Explorers.py similarity index 100% rename from code/Explorers.py rename to app/Explorers.py diff --git a/code/MainWindow.py b/app/MainWindow.py similarity index 100% rename from code/MainWindow.py rename to app/MainWindow.py diff --git a/code/Popups.py b/app/Popups.py similarity index 100% rename from code/Popups.py rename to app/Popups.py diff --git a/code/Ribbon.py b/app/Ribbon.py similarity index 100% rename from code/Ribbon.py rename to app/Ribbon.py diff --git a/code/Trackers.py b/app/Trackers.py similarity index 100% rename from code/Trackers.py rename to app/Trackers.py diff --git a/code/Views.py b/app/Views.py similarity index 100% rename from code/Views.py rename to app/Views.py diff --git a/code/Workers.py b/app/Workers.py similarity index 100% rename from code/Workers.py rename to app/Workers.py diff --git a/code/assets/images/home.png b/app/assets/images/home.png similarity index 100% rename from code/assets/images/home.png rename to app/assets/images/home.png diff --git a/code/assets/images/icons/captureExternalHelper.png b/app/assets/images/icons/captureExternalHelper.png similarity index 100% rename from code/assets/images/icons/captureExternalHelper.png rename to app/assets/images/icons/captureExternalHelper.png diff --git a/code/assets/images/icons/cb_contract.png b/app/assets/images/icons/cb_contract.png similarity index 100% rename from code/assets/images/icons/cb_contract.png rename to app/assets/images/icons/cb_contract.png diff --git a/code/assets/images/icons/cb_expand.png b/app/assets/images/icons/cb_expand.png similarity index 100% rename from code/assets/images/icons/cb_expand.png rename to app/assets/images/icons/cb_expand.png diff --git a/code/assets/images/icons/default_icon.png b/app/assets/images/icons/default_icon.png similarity index 100% rename from code/assets/images/icons/default_icon.png rename to app/assets/images/icons/default_icon.png diff --git a/code/assets/images/icons/hideExplorer.png b/app/assets/images/icons/hideExplorer.png similarity index 100% rename from code/assets/images/icons/hideExplorer.png rename to app/assets/images/icons/hideExplorer.png diff --git a/code/assets/images/icons/input_dialog_down.png b/app/assets/images/icons/input_dialog_down.png similarity index 100% rename from code/assets/images/icons/input_dialog_down.png rename to app/assets/images/icons/input_dialog_down.png diff --git a/code/assets/images/icons/input_dialog_up.png b/app/assets/images/icons/input_dialog_up.png similarity index 100% rename from code/assets/images/icons/input_dialog_up.png rename to app/assets/images/icons/input_dialog_up.png diff --git a/code/assets/images/icons/loadImageAtIndex.png b/app/assets/images/icons/loadImageAtIndex.png similarity index 100% rename from code/assets/images/icons/loadImageAtIndex.png rename to app/assets/images/icons/loadImageAtIndex.png diff --git a/code/assets/images/icons/loadModel.png b/app/assets/images/icons/loadModel.png similarity index 100% rename from code/assets/images/icons/loadModel.png rename to app/assets/images/icons/loadModel.png diff --git a/code/assets/images/icons/loadNextImage.png b/app/assets/images/icons/loadNextImage.png similarity index 100% rename from code/assets/images/icons/loadNextImage.png rename to app/assets/images/icons/loadNextImage.png diff --git a/code/assets/images/icons/loadPrevImage.png b/app/assets/images/icons/loadPrevImage.png similarity index 100% rename from code/assets/images/icons/loadPrevImage.png rename to app/assets/images/icons/loadPrevImage.png diff --git a/code/assets/images/icons/logo.ico b/app/assets/images/icons/logo.ico similarity index 100% rename from code/assets/images/icons/logo.ico rename to app/assets/images/icons/logo.ico diff --git a/code/assets/images/icons/modifyFontSettings.png b/app/assets/images/icons/modifyFontSettings.png similarity index 100% rename from code/assets/images/icons/modifyFontSettings.png rename to app/assets/images/icons/modifyFontSettings.png diff --git a/code/assets/images/icons/modifyHotkeys.png b/app/assets/images/icons/modifyHotkeys.png similarity index 100% rename from code/assets/images/icons/modifyHotkeys.png rename to app/assets/images/icons/modifyHotkeys.png diff --git a/code/assets/images/icons/modifyTesseract.png b/app/assets/images/icons/modifyTesseract.png similarity index 100% rename from code/assets/images/icons/modifyTesseract.png rename to app/assets/images/icons/modifyTesseract.png diff --git a/code/assets/images/icons/openDir.png b/app/assets/images/icons/openDir.png similarity index 100% rename from code/assets/images/icons/openDir.png rename to app/assets/images/icons/openDir.png diff --git a/code/assets/images/icons/openManga.png b/app/assets/images/icons/openManga.png similarity index 100% rename from code/assets/images/icons/openManga.png rename to app/assets/images/icons/openManga.png diff --git a/code/assets/images/icons/scaleImage.png b/app/assets/images/icons/scaleImage.png similarity index 100% rename from code/assets/images/icons/scaleImage.png rename to app/assets/images/icons/scaleImage.png diff --git a/code/assets/images/icons/toggleLogging.png b/app/assets/images/icons/toggleLogging.png similarity index 100% rename from code/assets/images/icons/toggleLogging.png rename to app/assets/images/icons/toggleLogging.png diff --git a/code/assets/images/icons/toggleMouseMode.png b/app/assets/images/icons/toggleMouseMode.png similarity index 100% rename from code/assets/images/icons/toggleMouseMode.png rename to app/assets/images/icons/toggleMouseMode.png diff --git a/code/assets/images/icons/toggleSplitView.png b/app/assets/images/icons/toggleSplitView.png similarity index 100% rename from code/assets/images/icons/toggleSplitView.png rename to app/assets/images/icons/toggleSplitView.png diff --git a/code/assets/images/icons/toggleStylesheet.png b/app/assets/images/icons/toggleStylesheet.png similarity index 100% rename from code/assets/images/icons/toggleStylesheet.png rename to app/assets/images/icons/toggleStylesheet.png diff --git a/code/assets/images/icons/zoomIn.png b/app/assets/images/icons/zoomIn.png similarity index 100% rename from code/assets/images/icons/zoomIn.png rename to app/assets/images/icons/zoomIn.png diff --git a/code/assets/images/icons/zoomOut.png b/app/assets/images/icons/zoomOut.png similarity index 100% rename from code/assets/images/icons/zoomOut.png rename to app/assets/images/icons/zoomOut.png diff --git a/code/assets/images/poricom-about.png b/app/assets/images/poricom-about.png similarity index 100% rename from code/assets/images/poricom-about.png rename to app/assets/images/poricom-about.png diff --git a/code/assets/languages/chi_sim.traineddata b/app/assets/languages/chi_sim.traineddata similarity index 100% rename from code/assets/languages/chi_sim.traineddata rename to app/assets/languages/chi_sim.traineddata diff --git a/code/assets/languages/chi_sim_vert.traineddata b/app/assets/languages/chi_sim_vert.traineddata similarity index 100% rename from code/assets/languages/chi_sim_vert.traineddata rename to app/assets/languages/chi_sim_vert.traineddata diff --git a/code/assets/languages/chi_tra.traineddata b/app/assets/languages/chi_tra.traineddata similarity index 100% rename from code/assets/languages/chi_tra.traineddata rename to app/assets/languages/chi_tra.traineddata diff --git a/code/assets/languages/chi_tra_vert.traineddata b/app/assets/languages/chi_tra_vert.traineddata similarity index 100% rename from code/assets/languages/chi_tra_vert.traineddata rename to app/assets/languages/chi_tra_vert.traineddata diff --git a/code/assets/languages/eng.traineddata b/app/assets/languages/eng.traineddata similarity index 100% rename from code/assets/languages/eng.traineddata rename to app/assets/languages/eng.traineddata diff --git a/code/assets/languages/eng_vert.traineddata b/app/assets/languages/eng_vert.traineddata similarity index 100% rename from code/assets/languages/eng_vert.traineddata rename to app/assets/languages/eng_vert.traineddata diff --git a/code/assets/languages/jpn.traineddata b/app/assets/languages/jpn.traineddata similarity index 100% rename from code/assets/languages/jpn.traineddata rename to app/assets/languages/jpn.traineddata diff --git a/code/assets/languages/jpn_vert.traineddata b/app/assets/languages/jpn_vert.traineddata similarity index 100% rename from code/assets/languages/jpn_vert.traineddata rename to app/assets/languages/jpn_vert.traineddata diff --git a/code/assets/languages/kor.traineddata b/app/assets/languages/kor.traineddata similarity index 100% rename from code/assets/languages/kor.traineddata rename to app/assets/languages/kor.traineddata diff --git a/code/assets/languages/kor_vert.traineddata b/app/assets/languages/kor_vert.traineddata similarity index 100% rename from code/assets/languages/kor_vert.traineddata rename to app/assets/languages/kor_vert.traineddata diff --git a/code/assets/styles-dark.qss b/app/assets/styles-dark.qss similarity index 100% rename from code/assets/styles-dark.qss rename to app/assets/styles-dark.qss diff --git a/code/assets/styles.qss b/app/assets/styles.qss similarity index 100% rename from code/assets/styles.qss rename to app/assets/styles.qss diff --git a/code/main.py b/app/main.py similarity index 100% rename from code/main.py rename to app/main.py diff --git a/code/old/config.py b/app/old/config.py similarity index 100% rename from code/old/config.py rename to app/old/config.py diff --git a/code/old/memory.py b/app/old/memory.py similarity index 100% rename from code/old/memory.py rename to app/old/memory.py diff --git a/code/old/ribbon.py b/app/old/ribbon.py similarity index 100% rename from code/old/ribbon.py rename to app/old/ribbon.py diff --git a/code/old/viewer.py b/app/old/viewer.py similarity index 100% rename from code/old/viewer.py rename to app/old/viewer.py diff --git a/code/utils/config.py b/app/utils/config.py similarity index 100% rename from code/utils/config.py rename to app/utils/config.py diff --git a/code/utils/config.toml b/app/utils/config.toml similarity index 100% rename from code/utils/config.toml rename to app/utils/config.toml diff --git a/code/utils/image_io.py b/app/utils/image_io.py similarity index 100% rename from code/utils/image_io.py rename to app/utils/image_io.py diff --git a/code/utils/unrar.exe b/app/utils/unrar.exe similarity index 100% rename from code/utils/unrar.exe rename to app/utils/unrar.exe From 212e562883743d0c19e2768dfcf4f1c2f64fb078 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 15:44:19 +0800 Subject: [PATCH 063/137] Remove legacy code --- app/old/config.py | 219 ---------------------------------------------- app/old/memory.py | 62 ------------- app/old/ribbon.py | 120 ------------------------- app/old/viewer.py | 116 ------------------------ 4 files changed, 517 deletions(-) delete mode 100644 app/old/config.py delete mode 100644 app/old/memory.py delete mode 100644 app/old/ribbon.py delete mode 100644 app/old/viewer.py diff --git a/app/old/config.py b/app/old/config.py deleted file mode 100644 index 604307c..0000000 --- a/app/old/config.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -Poricom -Copyright (C) `2021-2022` `` - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -stylesheet_path = './assets/styles.qss' -combobox_selected_index = { - 'language': 0, - 'orientation': 0, - 'font_style': 0, - 'font_size': 2, -} -picker_index = { - 'language': 20, - 'orientation': 21, - 'font_style': 22, - 'font_size': 23 -} -cfg = { - "IMAGE_EXTENSIONS": ["*.bmp", "*.gif", "*.jpg", "*.jpeg", "*.png", - "*.pbm", "*.pgm", "*.ppm", "*.webp", "*.xbm", "*.xpm"], - - "LANGUAGE": [" Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English"], - "ORIENTATION": [" Vertical", " Horizontal"], - "LANG_PATH": "./assets/languages/", - - "FONT_STYLE": [" Poppins", " Arial", " Verdana", " Helvetica", " Times New Roman"], - "FONT_SIZE": [" 12", " 14", " 16", " 20", " 24", " 32", " 40", " 56", " 72"], - - "SELECTED_INDEX": combobox_selected_index, - "PICKER_INDEX": picker_index, - - "STYLES_PATH": "./assets/", - "STYLES_DEFAULT": stylesheet_path, - - "NAV_VIEW_RATIO": [3,11], - "NAV_ROOT": "./assets/images/", - - "NAV_FUNCS": { - "path_changed": "view_image_from_fdialog", - "nav_clicked": "view_image_from_explorer" - }, - - "LOGO": "./assets/images/icons/logo.ico", - "HOME_IMAGE": "./assets/images/home.png", - - "RBN_HEIGHT": 2.4, - - "TBAR_ISIZE_REL": 0.1, - "TBAR_ISIZE_MARGIN": 1.3, - - "TBAR_ICONS": "./assets/images/icons/", - "TBAR_ICONS_LIGHT": "./assets/images/icons/", - "TBAR_ICON_DEFAULT": "./assets/images/icons/default_icon.png", - - "TBAR_FUNCS": { - "FILE": { - "open_dir": { - "help_title": "Open manga directory", - "help_msg": "Open a directory containing images.", - "path": "open_dir.png", - "toggle": False, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - }, - "open_manga": { - "help_title": "Open manga file", - "help_msg": "Supports the following formats: cbr, cbz, pdf.", - "path": "open_manga.png", - "toggle": False, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - } - }, - "VIEW": { - "toggle_stylesheet": { - "help_title": "Change theme", - "help_msg": "Switch between light and dark mode.", - "path": "toggle_stylesheet.png", - "toggle": False, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - }, - "modify_font_settings": { - "help_title": "Modify preview text", - "help_msg": "Change font style and font size of preview text.", - "path": "modify_font_settings.png", - "toggle": False, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - }, - "fit_horizontally": { - "help_title": "Fit image horizontally", - "help_msg": "", - "path": "fit_horizontally.png", - "toggle": True, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - }, - "fit_vertically": { - "help_title": "Fit image vertically", - "help_msg": "", - "path": "fit_vertically.png", - "toggle": True, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - } - }, - "SETTINGS": { - "load_model": { - "help_title": "Switch detection model", - "help_msg": "Switch between MangaOCR and Tesseract models.", - "path": "load_model.png", - "toggle": True, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - }, - "modify_tesseract": { - "help_title": "Tesseract settings", - "help_msg": "Set the language and orientation for the \ - Tesseract model.", - "path": "modify_tesseract.png", - "toggle": False, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - }, - "toggle_logging": { - "help_title": "Enable text logging", - "help_msg": "Save detected text to a text file located in the \ - current project directory.", - "path": "toggle_logging.png", - "toggle": True, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - }, - "toggle_mouse_mode": { - "help_title": "Change mouse behavior", - "help_msg": "This will disable text detection. Turn this on \ - only if do not want to hold CTRL key to zoom and pan \ - on an image.", - "path": "toggle_mouse_mode.png", - "toggle": True, - "align": "AlignLeft", - "icon_h": 1.0, - "icon_w": 1.0 - } - } - }, - - "MODE_FUNCS": { - "zoom_in": { - "help_title": "Zoom in", - "help_msg": "Hint: Double click the image to reset zoom.", - "path": "zoom_in.png", - "toggle": False, - "align": "AlignRight", - "icon_h": 0.45, - "icon_w": 0.45 - }, - "zoom_out": { - "help_title": "Zoom out", - "help_msg": "Hint: Double click the image to reset zoom.", - "path": "zoom_out.png", - "toggle": False, - "align": "AlignRight", - "icon_h": 0.45, - "icon_w": 0.45 - }, - "load_image_at_idx": { - "help_title": "", - "help_msg": "Jump to page", - "path": "load_image_at_idx.png", - "toggle": False, - "align": "AlignRight", - "icon_h": 0.45, - "icon_w": 1.3 - }, - "load_prev_image": { - "help_title": "", - "help_msg": "Show previous image", - "path": "load_prev_image.png", - "toggle": False, - "align": "AlignRight", - "icon_h": 0.45, - "icon_w": 0.6 - }, - "load_next_image": { - "help_title": "", - "help_msg": "Show next image", - "path": "load_next_image.png", - "toggle": False, - "align": "AlignRight", - "icon_h": 0.45, - "icon_w": 0.6 - } - } -} \ No newline at end of file diff --git a/app/old/memory.py b/app/old/memory.py deleted file mode 100644 index fc4781f..0000000 --- a/app/old/memory.py +++ /dev/null @@ -1,62 +0,0 @@ -# TODO: Rewrite this as a Tracker object that will -# save image paths, pixmaps of original images and -# masks, button states, and window state -# Use decorators - -class Tracker: - pass - -from default import cfg -from os import listdir -from os.path import isfile, join, splitext, normpath - -img_index = 0 -img_paths = [] -mask_paths = [] -curr_dir = cfg["NAV_ROOT"] -curr_img = cfg["HOME_IMAGE"] - -def get_img_path(): - #global curr_dir - return curr_dir - -def set_img_path(path): - global curr_dir, img_paths - curr_dir = normpath(path) - filelist = filter(lambda f: isfile(join(path, f)), listdir(path)) - img_paths = list(map(lambda p: normpath(join(path, p)), - filter((lambda f: ('*'+splitext(f)[1]) in - cfg["IMAGE_EXTENSIONS"]), filelist))) - -def get_img_list(): - #global curr_dir - return img_paths - -def get_curr_img(): - #global curr_img - return curr_img - -def get_prev_img(): - pass - -def set_curr_img(filepath): - global curr_img - curr_img = filepath - -def set_prev_img(): - pass - -def get_curr_mask(): - pass - -def get_prev_mask(): - pass - -def set_curr_mask(): - pass - -def set_prev_mask(): - pass - -def get_img_index(): - pass \ No newline at end of file diff --git a/app/old/ribbon.py b/app/old/ribbon.py deleted file mode 100644 index 8bf17ca..0000000 --- a/app/old/ribbon.py +++ /dev/null @@ -1,120 +0,0 @@ -from PyQt5.QtWidgets import (QMainWindow, QApplication, QPushButton, - QWidget, QAction, QTabWidget, QVBoxLayout, QGridLayout, - QLabel, QHBoxLayout, QFileDialog) - -from PyQt5.QtGui import QIcon -from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot - -import memory as mem -from viewer import ImageViewer, ImageNavigator - -from default import cfg -from os.path import exists - -class PMainWindow(QWidget): - def __init__(self, parent=None): - super(QWidget, self).__init__(parent) - self.layout = QVBoxLayout(self) - - self.ribbon = QTabWidget() - self.createRibbon() - self.layout.addWidget(self.ribbon) - - self.img_viewer = ImageNavigator() - self.layout.addWidget(self.img_viewer) - - def createRibbon(self): - h = self.frameGeometry().height() * cfg["TBAR_ISIZE_REL"] * cfg["RBN_HEIGHT"] - self.ribbon.setFixedHeight(h) - for tab_name, tools in cfg["TBAR_FUNCS"].items(): - self.ribbon.addTab(Toolbar(parent=self, fxns=tools), tab_name) - - def update_window(self, mode=0): - self.img_viewer.set_proj_path(mem.get_img_path()) - - def open_dir(self): - path = str(QFileDialog.getExistingDirectory(self, "Select Directory")) - if path: - mem.set_img_path(path) - self.update_window() - - def save_img(self): - print("Image saved to ", mem.get_img_path()) - pass - - def delete_img(self): - print("Image deleted in ", mem.get_img_path()) - pass - - def get_mask(self): - print("Generating mask for ", mem.get_img_path()) - pass - - def delete_text(self): - print("Text deleted from mask ", mem.get_img_path()) - pass - - def edit_mask_(self): - #TODO - pass - - def edit_mask(self): - # Connect the trigger signal to a slot. - self.img_viewer.some_signal.connect(self.handle_trigger) - - # Emit the signal. - self.img_viewer.some_signal.emit("1") - - @pyqtSlot(str) - def handle_trigger(self, r): - #print("I got a signal" + r) - pass - - def compare_img(self): - #self.img_viewer.mask_viewer.setHidden( - # not self.img_viewer.mask_viewer.isHidden()) - pass - -class Toolbar(QWidget): - - acquire_index = pyqtSignal(int) - - def __init__(self, parent=None, fxns=None): - super(QWidget, self).__init__() - self.layout = QHBoxLayout(self) - self.buttons = [] - self.parent = parent - - s = self.parent.frameGeometry().height() * cfg["TBAR_ISIZE_REL"] - m = s * cfg["TBAR_ISIZE_MARGIN"] - self.layout.setAlignment(Qt.AlignLeft) - count = 0 - for fxn in fxns: - icon = QIcon() - path = cfg["TBAR_IMG_ASSETS"] + fxn + ".png" - if (exists(path)): - icon = QIcon(path) - else: icon = QIcon(cfg["TBAR_ICON_IMG"]) - - self.buttons.append(QPushButton(self)) - self.buttons[-1].setIcon(icon) - self.buttons[-1].setIconSize(QSize(s,s)) - self.buttons[-1].setFixedSize(QSize(m,m)) - #if count == 0: - # self.buttons.append(QPushButton(self)) - # self.buttons[-1].setIcon(QIcon("../assets/images/" + fxn + ".png")) - # self.buttons[-1].setIconSize(QSize(s,s)) - # self.buttons[-1].setFixedSize(QSize(m,m)) - #r = cfg[fxn]["button_code"] - #print(r) - #self.buttons[-1].clicked.connect(lambda s=r: self.on_click(s)) - #count += 1 - #else: - # self.buttons.append(QPushButton("", self)) - # self.buttons[-1].setFixedSize(QSize(m,m)) - self.buttons[-1].clicked.connect(getattr(self.parent, fxn)) - self.layout.addWidget(self.buttons[-1]) - - @pyqtSlot(str) - def on_click(self, index): - print("hahahaha", index) \ No newline at end of file diff --git a/app/old/viewer.py b/app/old/viewer.py deleted file mode 100644 index 5a8048c..0000000 --- a/app/old/viewer.py +++ /dev/null @@ -1,116 +0,0 @@ -from PyQt5.QtCore import Qt, QDir, pyqtSignal, pyqtSlot -from PyQt5.QtGui import (QImage, QPixmap) -from PyQt5.QtWidgets import (QLabel, QScrollArea, QSizePolicy) -from PyQt5.QtWidgets import (QWidget, QFileSystemModel, QTreeView, - QHBoxLayout) - -import memory as mem -from default import cfg - -class ImageNavigator(QWidget): - - some_signal = pyqtSignal(str) - - def __init__(self, parent=None): - super(QWidget, self).__init__(parent) - - _layout = QHBoxLayout(self) - _layout.setContentsMargins(0,0,2,0) - - self.model = QFileSystemModel() - self.init_fs_model() - - self.treeview = QTreeView() - self.treeview.setModel(self.model) - self.init_treeview() - _layout.addWidget(self.treeview, cfg["NAV_VIEW_RATIO"][0]) - - self.image_viewer = ImageViewer() - _layout.addWidget(self.image_viewer, cfg["NAV_VIEW_RATIO"][1]) - - self.mask_viewer = ImageViewer() - _layout.addWidget(self.mask_viewer, cfg["NAV_VIEW_RATIO"][1]) - self.mask_viewer.setHidden(True) - - self.set_proj_path(mem.get_img_path()) - - def init_fs_model(self): - self.model.setFilter(QDir.Files) - self.model.setNameFilterDisables(False) - self.model.setNameFilters(cfg["IMAGE_EXTENSIONS"]) - - self.model.directoryLoaded.connect(self.load_default_img) - #self.model.rootPathChanged.connect(self.set_proj_path) - - def init_treeview(self): - for i in range(1,4): - self.treeview.hideColumn(i) - self.treeview.setIndentation(5) - - self.treeview.clicked.connect(self.view_image_from_explorer) - - def view_image_from_explorer(self, index): - fp = self.model.fileInfo(index).absoluteFilePath() - mem.set_curr_img(fp) - self.image_viewer.view_image() - - def view_image_from_toolbar(self, mode=0): - fp = mem.get_curr_img() - mem.set_curr_img(fp) - self.image_viewer.view_image() - pass - - def set_proj_path(self, path): - if path is None: - #TODO: Error Handling - pass - mem.set_img_path(path) - self.treeview.setRootIndex(self.model.setRootPath(path)) - - def load_default_img(self): - fp = self.model.index(0, 0, self.model.index(self.model.rootPath())) - mem.set_curr_img(self.model.rootPath()+"/"+self.model.data(fp)) - self.image_viewer.view_image() - - - -class ImageViewer(QScrollArea): - def __init__(self, parent = None): - super(QScrollArea, self).__init__(parent) - - self._img_label = QLabel() - self.setWidget(self._img_label) - self.init_img_label() - - self.setWidgetResizable(True) - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - - self.verticalScrollBar().valueChanged.connect(lambda idx: self.kekw(idx)) - - def init_img_label(self): - self._img_label.setContentsMargins(10,10,10,0) - - def view_image(self, filepath=None, q_image=None, mode=0): - - w = self.frameGeometry().width() - h = self.frameGeometry().height() - - filepath = mem.get_curr_img() - image = q_image - if filepath: - image = QImage(filepath) - if image is None: - #TODO: Error Handling - return - pixmap_img = QPixmap.fromImage(image) - self._img_label.setPixmap(pixmap_img.scaledToWidth( - w-20, Qt.SmoothTransformation)) - self._img_label.adjustSize() - - def resizeEvent(self, event): - self.view_image() - QScrollArea.resizeEvent(self, event) - - def kekw(self,idx): - print(idx) \ No newline at end of file From bd7f0034e76a2176a281916dbf9757329b60581d Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 15:46:17 +0800 Subject: [PATCH 064/137] Refactor services file structure --- app/MainWindow.py | 2 +- app/Views.py | 2 +- app/__init__.py | 17 ++++++++++ app/components/__init__.py | 17 ++++++++++ app/components/services/__init__.py | 19 +++++++++++ app/components/services/workers/__init__.py | 20 ++++++++++++ .../services/workers/base.py} | 23 +++++++------ app/components/services/workers/signal.py | 32 +++++++++++++++++++ 8 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 app/__init__.py create mode 100644 app/components/__init__.py create mode 100644 app/components/services/__init__.py create mode 100644 app/components/services/workers/__init__.py rename app/{Workers.py => components/services/workers/base.py} (70%) create mode 100644 app/components/services/workers/signal.py diff --git a/app/MainWindow.py b/app/MainWindow.py index 6896c06..4d44937 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -28,7 +28,7 @@ from utils.image_io import mangaFileToImageDir from utils.config import config, saveOnClose -from Workers import BaseWorker +from components.services import BaseWorker from Ribbon import (Ribbon) from Explorers import (ImageExplorer) from Views import (OCRCanvas, FullScreen) diff --git a/app/Views.py b/app/Views.py index 8ccfee7..17b1c07 100644 --- a/app/Views.py +++ b/app/Views.py @@ -26,7 +26,7 @@ from PyQt5.QtGui import QCursor, QTransform from Popups import MessagePopup -from Workers import BaseWorker +from components.services import BaseWorker from utils.image_io import logText, pixboxToText diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..b1e2d50 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,17 @@ +""" +Poricom +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" diff --git a/app/components/__init__.py b/app/components/__init__.py new file mode 100644 index 0000000..d8be93f --- /dev/null +++ b/app/components/__init__.py @@ -0,0 +1,17 @@ +""" +Poricom Components +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" diff --git a/app/components/services/__init__.py b/app/components/services/__init__.py new file mode 100644 index 0000000..96e5c75 --- /dev/null +++ b/app/components/services/__init__.py @@ -0,0 +1,19 @@ +""" +Poricom Services +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .workers import BaseWorker, BaseWorkerSignal diff --git a/app/components/services/workers/__init__.py b/app/components/services/workers/__init__.py new file mode 100644 index 0000000..9b002c5 --- /dev/null +++ b/app/components/services/workers/__init__.py @@ -0,0 +1,20 @@ +""" +Poricom Services +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BaseWorker +from .signal import BaseWorkerSignal diff --git a/app/Workers.py b/app/components/services/workers/base.py similarity index 70% rename from app/Workers.py rename to app/components/services/workers/base.py index 64142c7..486a17b 100644 --- a/app/Workers.py +++ b/app/components/services/workers/base.py @@ -1,5 +1,5 @@ """ -Poricom Multithreaded Workers +Poricom Services Copyright (C) `2021-2022` `` @@ -17,24 +17,29 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (QRunnable, QObject, pyqtSignal, pyqtSlot) +from typing import Callable +from PyQt5.QtCore import (pyqtSlot, QRunnable) + +from .signal import BaseWorkerSignal class BaseWorker(QRunnable): - def __init__(self, fn, *args, **kwargs): + """Runnable object to support multithreading + + Args: + fn (Callable): Long running task or function + *Note: args/kwargs passed onto the BaseWorker are passed onto fn + """ + + def __init__(self, fn: Callable, *args, **kwargs): super(BaseWorker, self).__init__() self.fn = fn self.args = args self.kwargs = kwargs - self.signals = WorkerSignal() + self.signals = BaseWorkerSignal() @pyqtSlot() def run(self): output = self.fn(*self.args, **self.kwargs) self.signals.result.emit(output) self.signals.finished.emit() - - -class WorkerSignal(QObject): - finished = pyqtSignal() - result = pyqtSignal(object) diff --git a/app/components/services/workers/signal.py b/app/components/services/workers/signal.py new file mode 100644 index 0000000..a13446a --- /dev/null +++ b/app/components/services/workers/signal.py @@ -0,0 +1,32 @@ +""" +Poricom Services + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (pyqtSignal, QObject) + + +class BaseWorkerSignal(QObject): + """Base signal object + + Signals: + finished: Emit when thread finished the task + result: Emit the result of the task + """ + + finished = pyqtSignal() + result = pyqtSignal(object) From aa5ec8b4024c8628cf40aed48df39b6ee0260382 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 17:54:33 +0800 Subject: [PATCH 065/137] Refactor toolbar file structure --- app/MainWindow.py | 4 +- app/components/toolbar/__init__.py | 19 ++++ app/components/toolbar/base.py | 47 +++++++++ app/components/toolbar/tabs/__init__.py | 20 ++++ .../toolbar/tabs/base.py} | 95 ++++++++----------- app/components/toolbar/tabs/navigate.py | 50 ++++++++++ 6 files changed, 175 insertions(+), 60 deletions(-) create mode 100644 app/components/toolbar/__init__.py create mode 100644 app/components/toolbar/base.py create mode 100644 app/components/toolbar/tabs/__init__.py rename app/{Ribbon.py => components/toolbar/tabs/base.py} (50%) create mode 100644 app/components/toolbar/tabs/navigate.py diff --git a/app/MainWindow.py b/app/MainWindow.py index 4d44937..e3878ed 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -29,7 +29,7 @@ from utils.image_io import mangaFileToImageDir from utils.config import config, saveOnClose from components.services import BaseWorker -from Ribbon import (Ribbon) +from components.toolbar import BaseToolbar from Explorers import (ImageExplorer) from Views import (OCRCanvas, FullScreen) from Popups import (FontPicker, LanguagePicker, ScaleImagePicker, @@ -54,7 +54,7 @@ def __init__(self, parent=None, tracker=None): self.config = config self.vLayout = QVBoxLayout() - self.ribbon = Ribbon(self, self.tracker) + self.ribbon = BaseToolbar(self, self.tracker) self.vLayout.addWidget(self.ribbon) self.canvas = OCRCanvas(self, self.tracker) self.explorer = ImageExplorer(self, self.tracker) diff --git a/app/components/toolbar/__init__.py b/app/components/toolbar/__init__.py new file mode 100644 index 0000000..f5d9ef7 --- /dev/null +++ b/app/components/toolbar/__init__.py @@ -0,0 +1,19 @@ +""" +Poricom Toolbar +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BaseToolbar diff --git a/app/components/toolbar/base.py b/app/components/toolbar/base.py new file mode 100644 index 0000000..d167b1f --- /dev/null +++ b/app/components/toolbar/base.py @@ -0,0 +1,47 @@ +""" +Poricom Toolbar + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from os.path import exists + +from PyQt5.QtGui import (QIcon) +from PyQt5.QtCore import (Qt, QSize) +from PyQt5.QtWidgets import ( + QGridLayout, QHBoxLayout, QWidget, QTabWidget, QPushButton) + +from .tabs import BaseToolbarTab, NavigateToolbarContainer +from utils.config import config + + +class BaseToolbar(QTabWidget): + def __init__(self, parent=None, tracker=None): + super(QTabWidget, self).__init__(parent) + self.parent = parent + self.tracker = tracker + + h = self.parent.frameGeometry().height( + ) * config["TBAR_ISIZE_REL"] * config["RBN_HEIGHT"] + self.setFixedHeight(h) + + for tabName, tools in config["TBAR_FUNCS"].items(): + tab = BaseToolbarTab(parent=self.parent, funcs=tools, + tracker=self.tracker, tabName=tabName) + tab.layout().addStretch() + tab.layout().addWidget( + NavigateToolbarContainer(self.parent, self.tracker)) + self.addTab(tab, tabName) diff --git a/app/components/toolbar/tabs/__init__.py b/app/components/toolbar/tabs/__init__.py new file mode 100644 index 0000000..8dc745e --- /dev/null +++ b/app/components/toolbar/tabs/__init__.py @@ -0,0 +1,20 @@ +""" +Poricom Toolbar +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BaseToolbarTab +from .navigate import NavigateToolbarContainer diff --git a/app/Ribbon.py b/app/components/toolbar/tabs/base.py similarity index 50% rename from app/Ribbon.py rename to app/components/toolbar/tabs/base.py index 73c6669..bbece5f 100644 --- a/app/Ribbon.py +++ b/app/components/toolbar/tabs/base.py @@ -1,5 +1,5 @@ """ -Poricom Ribbon Components +Poricom Toolbar Copyright (C) `2021-2022` `` @@ -19,42 +19,53 @@ from os.path import exists -from PyQt5.QtGui import (QIcon) from PyQt5.QtCore import (Qt, QSize) -from PyQt5.QtWidgets import ( - QGridLayout, QHBoxLayout, QWidget, QTabWidget, QPushButton) +from PyQt5.QtGui import (QIcon) +from PyQt5.QtWidgets import (QHBoxLayout, QMainWindow, QPushButton, QWidget) from utils.config import config - -class RibbonTab(QWidget): - - def __init__(self, parent=None, funcs=None, tracker=None, tabName=""): +# TODO: Refactor this as a container +class BaseToolbarTab(QWidget): + """Widget that contains the toolbar functions + + Args: + parent (QWidget, optional): Toolbar tab parent. Set to main window. + funcs (Any, optional): Toolbar function configuration. Defaults to {}. + tracker (Any, optional): State tracker. Defaults to None. + tabName (str, optional): Toolbar tab name. Defaults to "". + """ + def __init__(self, parent: QMainWindow, funcs={}, tracker=None, tabName=""): + # TODO: Add type to funcs and tracker + # TODO: tracker and tabName might not be needed super(QWidget, self).__init__() - self.parent = parent + + # Manually set parent since `addTab` method will reparent the widget + self.mainWindow = parent self.tracker = tracker self.tabName = tabName - self.buttonList = [] - self.layout = QHBoxLayout(self) - self.layout.setAlignment(Qt.AlignLeft) + self.buttonList: list[QPushButton] = [] + self.setLayout(QHBoxLayout()) + # self.layout().setAlignment(Qt.AlignLeft) - self.initButtons(funcs) + self.initializeButtons(funcs) - def initButtons(self, funcs): + def initializeButtons(self, funcs): for funcName, funcConfig in funcs.items(): self.loadButtonConfig(funcName, funcConfig) - self.layout.addWidget(self.buttonList[-1], + # TODO: Alignment might be obsolete + self.layout().addWidget(self.buttonList[-1], alignment=getattr(Qt, funcConfig["align"])) - self.layout.addStretch() - self.layout.addWidget(PageNavigator(self.parent)) + # self.layout.addStretch() + # self.layout.addWidget(PageNavigator(self.mainWindow)) def loadButtonConfig(self, buttonName, buttonConfig): - - w = self.parent.frameGeometry().height( + # TODO: Base icon size on screen height instead of parent height + w = self.mainWindow.frameGeometry().height( )*config["TBAR_ISIZE_REL"]*buttonConfig["iconW"] - h = self.parent.frameGeometry().height( + h = self.mainWindow.frameGeometry().height( )*config["TBAR_ISIZE_REL"]*buttonConfig["iconH"] m = config["TBAR_ISIZE_MARGIN"] @@ -66,10 +77,13 @@ def loadButtonConfig(self, buttonName, buttonConfig): icon = QIcon(config["TBAR_ICON_DEFAULT"]) self.buttonList.append(QPushButton(self)) + + # Allows to programmatically interact with buttons self.buttonList[-1].setObjectName(buttonName) self.buttonList[-1].setIcon(icon) self.buttonList[-1].setIconSize(QSize(w, h)) + # TODO: Do not set fixed size self.buttonList[-1].setFixedSize(QSize(w*m, h*m)) tooltip = f"

{buttonConfig['helpTitle']}\ @@ -77,44 +91,9 @@ def loadButtonConfig(self, buttonName, buttonConfig): self.buttonList[-1].setToolTip(tooltip) self.buttonList[-1].setCheckable(buttonConfig["toggle"]) - if hasattr(self.parent, buttonName): + if hasattr(self.mainWindow, buttonName): self.buttonList[-1].clicked.connect( - getattr(self.parent, buttonName)) + getattr(self.mainWindow, buttonName)) else: self.buttonList[-1].clicked.connect( - getattr(self.parent, 'poricomNoop')) - - -class PageNavigator(RibbonTab): - - def __init__(self, parent=None, tracker=None): - super(QWidget, self).__init__() - self.parent = parent - self.tracker = tracker - self.buttonList = [] - - self.layout = QGridLayout(self) - self.layout.setContentsMargins(0, 0, 0, 0) - for funcName, funcConfig in config["MODE_FUNCS"].items(): - self.loadButtonConfig(funcName, funcConfig) - - self.layout.addWidget(self.buttonList[0], 0, 0, 1, 1) - self.layout.addWidget(self.buttonList[1], 1, 0, 1, 1) - self.layout.addWidget(self.buttonList[2], 0, 1, 1, 2) - self.layout.addWidget(self.buttonList[3], 1, 1, 1, 1) - self.layout.addWidget(self.buttonList[4], 1, 2, 1, 1) - - -class Ribbon(QTabWidget): - def __init__(self, parent=None, tracker=None): - super(QTabWidget, self).__init__(parent) - self.parent = parent - self.tracker = tracker - - h = self.parent.frameGeometry().height( - ) * config["TBAR_ISIZE_REL"] * config["RBN_HEIGHT"] - self.setFixedHeight(h) - - for tabName, tools in config["TBAR_FUNCS"].items(): - self.addTab(RibbonTab(parent=self.parent, funcs=tools, - tracker=self.tracker, tabName=tabName), tabName) + getattr(self.mainWindow, 'poricomNoop')) diff --git a/app/components/toolbar/tabs/navigate.py b/app/components/toolbar/tabs/navigate.py new file mode 100644 index 0000000..f5060c6 --- /dev/null +++ b/app/components/toolbar/tabs/navigate.py @@ -0,0 +1,50 @@ +""" +Poricom Toolbar + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtWidgets import ( + QGridLayout, QPushButton) + +from .base import BaseToolbarTab +from utils.config import config + +class NavigateToolbarContainer(BaseToolbarTab): + """Widget that contains the toolbar navigation functions + + Args: + parent (QWidget, optional): Toolbar tab parent. Set to main window. Defaults to None. + tracker (Any, optional): State tracker. Defaults to None. + """ + + def __init__(self, parent=None, tracker=None): + super().__init__(parent) + self.parent = parent + self.tracker = tracker + self.buttonList: QPushButton = [] + + self.layout = QGridLayout(self) + self.layout.setContentsMargins(0, 0, 0, 0) + + for funcName, funcConfig in config["MODE_FUNCS"].items(): + self.loadButtonConfig(funcName, funcConfig) + + self.layout.addWidget(self.buttonList[0], 0, 0, 1, 1) + self.layout.addWidget(self.buttonList[1], 1, 0, 1, 1) + self.layout.addWidget(self.buttonList[2], 0, 1, 1, 2) + self.layout.addWidget(self.buttonList[3], 1, 1, 1, 1) + self.layout.addWidget(self.buttonList[4], 1, 2, 1, 1) From fa3bedc9a25ba79e896899ee4646b6a524fa712d Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 18:05:15 +0800 Subject: [PATCH 066/137] Refactor tab to separate button initialization --- app/MainWindow.py | 2 +- app/components/toolbar/base.py | 23 ++--- app/components/toolbar/tabs/__init__.py | 2 +- app/components/toolbar/tabs/base.py | 79 +++-------------- .../toolbar/tabs/containers/__init__.py | 21 +++++ .../toolbar/tabs/containers/base.py | 86 +++++++++++++++++++ .../toolbar/tabs/containers/navigate.py | 48 +++++++++++ app/components/toolbar/tabs/navigate.py | 50 ----------- 8 files changed, 179 insertions(+), 132 deletions(-) create mode 100644 app/components/toolbar/tabs/containers/__init__.py create mode 100644 app/components/toolbar/tabs/containers/base.py create mode 100644 app/components/toolbar/tabs/containers/navigate.py delete mode 100644 app/components/toolbar/tabs/navigate.py diff --git a/app/MainWindow.py b/app/MainWindow.py index e3878ed..2f22b98 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -54,7 +54,7 @@ def __init__(self, parent=None, tracker=None): self.config = config self.vLayout = QVBoxLayout() - self.ribbon = BaseToolbar(self, self.tracker) + self.ribbon = BaseToolbar(self) self.vLayout.addWidget(self.ribbon) self.canvas = OCRCanvas(self, self.tracker) self.explorer = ImageExplorer(self, self.tracker) diff --git a/app/components/toolbar/base.py b/app/components/toolbar/base.py index d167b1f..61b43b2 100644 --- a/app/components/toolbar/base.py +++ b/app/components/toolbar/base.py @@ -17,31 +17,32 @@ along with this program. If not, see . """ -from os.path import exists - -from PyQt5.QtGui import (QIcon) -from PyQt5.QtCore import (Qt, QSize) -from PyQt5.QtWidgets import ( - QGridLayout, QHBoxLayout, QWidget, QTabWidget, QPushButton) +from PyQt5.QtWidgets import (QMainWindow, QTabWidget) from .tabs import BaseToolbarTab, NavigateToolbarContainer from utils.config import config class BaseToolbar(QTabWidget): - def __init__(self, parent=None, tracker=None): + """ + Toolbar widget + + Args: + parent (QWidget, optional): Toolbar parent. Set to main window. + Notes: + Parent must be passed to children to call main window functions. + """ + def __init__(self, parent: QMainWindow): super(QTabWidget, self).__init__(parent) self.parent = parent - self.tracker = tracker h = self.parent.frameGeometry().height( ) * config["TBAR_ISIZE_REL"] * config["RBN_HEIGHT"] self.setFixedHeight(h) for tabName, tools in config["TBAR_FUNCS"].items(): - tab = BaseToolbarTab(parent=self.parent, funcs=tools, - tracker=self.tracker, tabName=tabName) + tab = BaseToolbarTab(parent=self.parent, funcs=tools) tab.layout().addStretch() tab.layout().addWidget( - NavigateToolbarContainer(self.parent, self.tracker)) + NavigateToolbarContainer(self.parent)) self.addTab(tab, tabName) diff --git a/app/components/toolbar/tabs/__init__.py b/app/components/toolbar/tabs/__init__.py index 8dc745e..ab83bc8 100644 --- a/app/components/toolbar/tabs/__init__.py +++ b/app/components/toolbar/tabs/__init__.py @@ -17,4 +17,4 @@ """ from .base import BaseToolbarTab -from .navigate import NavigateToolbarContainer +from .containers import NavigateToolbarContainer diff --git a/app/components/toolbar/tabs/base.py b/app/components/toolbar/tabs/base.py index bbece5f..2290a03 100644 --- a/app/components/toolbar/tabs/base.py +++ b/app/components/toolbar/tabs/base.py @@ -17,83 +17,24 @@ along with this program. If not, see . """ -from os.path import exists +from PyQt5.QtWidgets import (QHBoxLayout, QMainWindow) -from PyQt5.QtCore import (Qt, QSize) -from PyQt5.QtGui import (QIcon) -from PyQt5.QtWidgets import (QHBoxLayout, QMainWindow, QPushButton, QWidget) +from .containers import BaseToolbarContainer -from utils.config import config - -# TODO: Refactor this as a container -class BaseToolbarTab(QWidget): - """Widget that contains the toolbar functions +class BaseToolbarTab(BaseToolbarContainer): + """Widget to contain all toolbar tab functions Args: parent (QWidget, optional): Toolbar tab parent. Set to main window. funcs (Any, optional): Toolbar function configuration. Defaults to {}. - tracker (Any, optional): State tracker. Defaults to None. - tabName (str, optional): Toolbar tab name. Defaults to "". """ - def __init__(self, parent: QMainWindow, funcs={}, tracker=None, tabName=""): - # TODO: Add type to funcs and tracker - # TODO: tracker and tabName might not be needed - super(QWidget, self).__init__() - - # Manually set parent since `addTab` method will reparent the widget - self.mainWindow = parent - self.tracker = tracker - self.tabName = tabName - - self.buttonList: list[QPushButton] = [] - self.setLayout(QHBoxLayout()) - # self.layout().setAlignment(Qt.AlignLeft) + def __init__(self, parent: QMainWindow, funcs={}): + super().__init__(parent) self.initializeButtons(funcs) def initializeButtons(self, funcs): - - for funcName, funcConfig in funcs.items(): - self.loadButtonConfig(funcName, funcConfig) - # TODO: Alignment might be obsolete - self.layout().addWidget(self.buttonList[-1], - alignment=getattr(Qt, funcConfig["align"])) - # self.layout.addStretch() - # self.layout.addWidget(PageNavigator(self.mainWindow)) - - def loadButtonConfig(self, buttonName, buttonConfig): - # TODO: Base icon size on screen height instead of parent height - w = self.mainWindow.frameGeometry().height( - )*config["TBAR_ISIZE_REL"]*buttonConfig["iconW"] - h = self.mainWindow.frameGeometry().height( - )*config["TBAR_ISIZE_REL"]*buttonConfig["iconH"] - m = config["TBAR_ISIZE_MARGIN"] - - icon = QIcon() - path = config["TBAR_ICONS"] + buttonConfig["path"] - if (exists(path)): - icon = QIcon(path) - else: - icon = QIcon(config["TBAR_ICON_DEFAULT"]) - - self.buttonList.append(QPushButton(self)) - - # Allows to programmatically interact with buttons - self.buttonList[-1].setObjectName(buttonName) - - self.buttonList[-1].setIcon(icon) - self.buttonList[-1].setIconSize(QSize(w, h)) - # TODO: Do not set fixed size - self.buttonList[-1].setFixedSize(QSize(w*m, h*m)) - - tooltip = f"

{buttonConfig['helpTitle']}\ -

{buttonConfig['helpMsg']}

" - self.buttonList[-1].setToolTip(tooltip) - self.buttonList[-1].setCheckable(buttonConfig["toggle"]) - - if hasattr(self.mainWindow, buttonName): - self.buttonList[-1].clicked.connect( - getattr(self.mainWindow, buttonName)) - else: - self.buttonList[-1].clicked.connect( - getattr(self.mainWindow, 'poricomNoop')) + self.setLayout(QHBoxLayout()) + for name, config in funcs.items(): + self.initializeButton(name, config) + self.layout().addWidget(self.buttonList[-1]) diff --git a/app/components/toolbar/tabs/containers/__init__.py b/app/components/toolbar/tabs/containers/__init__.py new file mode 100644 index 0000000..09f3348 --- /dev/null +++ b/app/components/toolbar/tabs/containers/__init__.py @@ -0,0 +1,21 @@ +""" +Poricom Toolbar + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BaseToolbarContainer +from .navigate import NavigateToolbarContainer diff --git a/app/components/toolbar/tabs/containers/base.py b/app/components/toolbar/tabs/containers/base.py new file mode 100644 index 0000000..09ba4de --- /dev/null +++ b/app/components/toolbar/tabs/containers/base.py @@ -0,0 +1,86 @@ +""" +Poricom Toolbar + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from os.path import exists + +from PyQt5.QtCore import (QSize) +from PyQt5.QtGui import (QIcon) +from PyQt5.QtWidgets import (QMainWindow, QPushButton, QWidget) + +# TODO: config should be a constant +from utils.config import config + +class BaseToolbarContainer(QWidget): + """Widget that contains the toolbar functions + + Args: + parent (QMainWindow, optional): Container parent. Set to main window. + """ + def __init__(self, parent: QMainWindow): + super().__init__(parent) + + # Manually set parent since `addTab` method will reparent the widget + self.mainWindow = parent + self.buttonList: list[QPushButton] = [] + + def addButton(self): + """Adds a QPushButton object to `buttonList` + + Returns: + QPushButton: Recently added QPushButton + """ + self.buttonList.append(QPushButton(self)) + return self.buttonList[-1] + + def initializeButton(self, buttonName, buttonConfig): + # TODO: Base icon size on screen height instead of parent height + w = self.mainWindow.frameGeometry().height( + )*config["TBAR_ISIZE_REL"]*buttonConfig["iconW"] + h = self.mainWindow.frameGeometry().height( + )*config["TBAR_ISIZE_REL"]*buttonConfig["iconH"] + m = config["TBAR_ISIZE_MARGIN"] + + icon = QIcon() + path = config["TBAR_ICONS"] + buttonConfig["path"] + if (exists(path)): + icon = QIcon(path) + else: + icon = QIcon(config["TBAR_ICON_DEFAULT"]) + + button = self.addButton() + + # Allows to programmatically interact with buttons + button.setObjectName(buttonName) + + button.setIcon(icon) + button.setIconSize(QSize(w, h)) + # TODO: Do not set fixed size + button.setFixedSize(QSize(w*m, h*m)) + + tooltip = f"

{buttonConfig['helpTitle']}\ +

{buttonConfig['helpMsg']}

" + button.setToolTip(tooltip) + button.setCheckable(buttonConfig["toggle"]) + + if hasattr(self.mainWindow, buttonName): + button.clicked.connect( + getattr(self.mainWindow, buttonName)) + else: + button.clicked.connect( + getattr(self.mainWindow, 'poricomNoop')) diff --git a/app/components/toolbar/tabs/containers/navigate.py b/app/components/toolbar/tabs/containers/navigate.py new file mode 100644 index 0000000..d29e4a1 --- /dev/null +++ b/app/components/toolbar/tabs/containers/navigate.py @@ -0,0 +1,48 @@ +""" +Poricom Toolbar + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtWidgets import (QGridLayout, QMainWindow) + +from .base import BaseToolbarContainer +from utils.config import config + +class NavigateToolbarContainer(BaseToolbarContainer): + """Widget that contains the toolbar navigation functions + + Args: + parent (QWidget, optional): Container parent. Set to main window. + """ + + def __init__(self, parent: QMainWindow): + super().__init__(parent) + + self.initializeButtons() + + def initializeButtons(self): + self.setLayout(QGridLayout()) + self.layout().setContentsMargins(0, 0, 0, 0) + + for funcName, funcConfig in config["MODE_FUNCS"].items(): + self.initializeButton(funcName, funcConfig) + + self.layout().addWidget(self.buttonList[0], 0, 0, 1, 1) + self.layout().addWidget(self.buttonList[1], 1, 0, 1, 1) + self.layout().addWidget(self.buttonList[2], 0, 1, 1, 2) + self.layout().addWidget(self.buttonList[3], 1, 1, 1, 1) + self.layout().addWidget(self.buttonList[4], 1, 2, 1, 1) diff --git a/app/components/toolbar/tabs/navigate.py b/app/components/toolbar/tabs/navigate.py deleted file mode 100644 index f5060c6..0000000 --- a/app/components/toolbar/tabs/navigate.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Poricom Toolbar - -Copyright (C) `2021-2022` `` - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -from PyQt5.QtWidgets import ( - QGridLayout, QPushButton) - -from .base import BaseToolbarTab -from utils.config import config - -class NavigateToolbarContainer(BaseToolbarTab): - """Widget that contains the toolbar navigation functions - - Args: - parent (QWidget, optional): Toolbar tab parent. Set to main window. Defaults to None. - tracker (Any, optional): State tracker. Defaults to None. - """ - - def __init__(self, parent=None, tracker=None): - super().__init__(parent) - self.parent = parent - self.tracker = tracker - self.buttonList: QPushButton = [] - - self.layout = QGridLayout(self) - self.layout.setContentsMargins(0, 0, 0, 0) - - for funcName, funcConfig in config["MODE_FUNCS"].items(): - self.loadButtonConfig(funcName, funcConfig) - - self.layout.addWidget(self.buttonList[0], 0, 0, 1, 1) - self.layout.addWidget(self.buttonList[1], 1, 0, 1, 1) - self.layout.addWidget(self.buttonList[2], 0, 1, 1, 2) - self.layout.addWidget(self.buttonList[3], 1, 1, 1, 1) - self.layout.addWidget(self.buttonList[4], 1, 2, 1, 1) From 76716c36bff851f32926844ba47b44f21c64e1ab Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 19:01:35 +0800 Subject: [PATCH 067/137] Refactor toolbar to inherit screen aware widget Allow relative instead of fixed sizing --- app/components/misc/__init__.py | 20 ++ app/components/misc/screenAware.py | 37 ++++ app/components/toolbar/base.py | 15 +- .../toolbar/tabs/containers/base.py | 51 ++--- .../toolbar/tabs/containers/navigate.py | 6 +- app/utils/__init__.py | 17 ++ app/utils/constants.py | 205 ++++++++++++++++++ app/utils/types.py | 30 +++ 8 files changed, 342 insertions(+), 39 deletions(-) create mode 100644 app/components/misc/__init__.py create mode 100644 app/components/misc/screenAware.py create mode 100644 app/utils/__init__.py create mode 100644 app/utils/constants.py create mode 100644 app/utils/types.py diff --git a/app/components/misc/__init__.py b/app/components/misc/__init__.py new file mode 100644 index 0000000..f9553f0 --- /dev/null +++ b/app/components/misc/__init__.py @@ -0,0 +1,20 @@ +""" +Poricom Misc Components + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .screenAware import ScreenAwareWidget diff --git a/app/components/misc/screenAware.py b/app/components/misc/screenAware.py new file mode 100644 index 0000000..2b4372a --- /dev/null +++ b/app/components/misc/screenAware.py @@ -0,0 +1,37 @@ +""" +Poricom Screen Aware Component + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtWidgets import (QApplication, QWidget) + + +class ScreenAwareWidget(QWidget): + """ + Screen-aware widget. Allows retrieving desktop screen dimensions + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def primaryScreen(self): + return QApplication.primaryScreen() + + def primaryScreenWidth(self): + return self.primaryScreen().geometry().width() + + def primaryScreenHeight(self): + return self.primaryScreen().geometry().height() diff --git a/app/components/toolbar/base.py b/app/components/toolbar/base.py index 61b43b2..cacaed2 100644 --- a/app/components/toolbar/base.py +++ b/app/components/toolbar/base.py @@ -17,11 +17,10 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QMainWindow, QTabWidget) +from PyQt5.QtWidgets import (QMainWindow, QSizePolicy, QTabWidget) from .tabs import BaseToolbarTab, NavigateToolbarContainer -from utils.config import config - +from utils.constants import TOOLBAR_FUNCTIONS class BaseToolbar(QTabWidget): """ @@ -36,13 +35,11 @@ def __init__(self, parent: QMainWindow): super(QTabWidget, self).__init__(parent) self.parent = parent - h = self.parent.frameGeometry().height( - ) * config["TBAR_ISIZE_REL"] * config["RBN_HEIGHT"] - self.setFixedHeight(h) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - for tabName, tools in config["TBAR_FUNCS"].items(): - tab = BaseToolbarTab(parent=self.parent, funcs=tools) + for tabName, funcs in TOOLBAR_FUNCTIONS.items(): + tab = BaseToolbarTab(parent=self.parent, funcs=funcs) tab.layout().addStretch() tab.layout().addWidget( NavigateToolbarContainer(self.parent)) - self.addTab(tab, tabName) + self.addTab(tab, tabName.upper()) diff --git a/app/components/toolbar/tabs/containers/base.py b/app/components/toolbar/tabs/containers/base.py index 09ba4de..56b9e62 100644 --- a/app/components/toolbar/tabs/containers/base.py +++ b/app/components/toolbar/tabs/containers/base.py @@ -21,12 +21,13 @@ from PyQt5.QtCore import (QSize) from PyQt5.QtGui import (QIcon) -from PyQt5.QtWidgets import (QMainWindow, QPushButton, QWidget) +from PyQt5.QtWidgets import (QMainWindow, QPushButton) -# TODO: config should be a constant -from utils.config import config +from components.misc import ScreenAwareWidget +from utils.constants import TOOLBAR_ICON_DEFAULT, TOOLBAR_ICON_SIZE, TOOLBAR_ICONS +from utils.types import ButtonConfig -class BaseToolbarContainer(QWidget): +class BaseToolbarContainer(ScreenAwareWidget): """Widget that contains the toolbar functions Args: @@ -48,39 +49,35 @@ def addButton(self): self.buttonList.append(QPushButton(self)) return self.buttonList[-1] - def initializeButton(self, buttonName, buttonConfig): - # TODO: Base icon size on screen height instead of parent height - w = self.mainWindow.frameGeometry().height( - )*config["TBAR_ISIZE_REL"]*buttonConfig["iconW"] - h = self.mainWindow.frameGeometry().height( - )*config["TBAR_ISIZE_REL"]*buttonConfig["iconH"] - m = config["TBAR_ISIZE_MARGIN"] - - icon = QIcon() - path = config["TBAR_ICONS"] + buttonConfig["path"] - if (exists(path)): - icon = QIcon(path) - else: - icon = QIcon(config["TBAR_ICON_DEFAULT"]) - + def initializeButton(self, name: str, config: ButtonConfig): button = self.addButton() # Allows to programmatically interact with buttons - button.setObjectName(buttonName) + button.setObjectName(name) + # Set button icon and size + path = TOOLBAR_ICONS + config["path"] + if (exists(path)): + icon = QIcon(path) + else: + icon = QIcon(TOOLBAR_ICON_DEFAULT) button.setIcon(icon) + w = self.primaryScreenHeight()*TOOLBAR_ICON_SIZE*config["iconWidth"] + h = self.primaryScreenHeight()*TOOLBAR_ICON_SIZE*config["iconHeight"] button.setIconSize(QSize(w, h)) - # TODO: Do not set fixed size - button.setFixedSize(QSize(w*m, h*m)) - tooltip = f"

{buttonConfig['helpTitle']}\ -

{buttonConfig['helpMsg']}

" + tooltip = f"\ +

{config['title']}

\ +

{config['message']}

\ + " button.setToolTip(tooltip) - button.setCheckable(buttonConfig["toggle"]) - if hasattr(self.mainWindow, buttonName): + button.setCheckable(config["toggle"]) + + # Connect button to main window function + if hasattr(self.mainWindow, name): button.clicked.connect( - getattr(self.mainWindow, buttonName)) + getattr(self.mainWindow, name)) else: button.clicked.connect( getattr(self.mainWindow, 'poricomNoop')) diff --git a/app/components/toolbar/tabs/containers/navigate.py b/app/components/toolbar/tabs/containers/navigate.py index d29e4a1..3e985af 100644 --- a/app/components/toolbar/tabs/containers/navigate.py +++ b/app/components/toolbar/tabs/containers/navigate.py @@ -20,7 +20,7 @@ from PyQt5.QtWidgets import (QGridLayout, QMainWindow) from .base import BaseToolbarContainer -from utils.config import config +from utils.constants import NAVIGATION_FUNCTIONS class NavigateToolbarContainer(BaseToolbarContainer): """Widget that contains the toolbar navigation functions @@ -38,8 +38,8 @@ def initializeButtons(self): self.setLayout(QGridLayout()) self.layout().setContentsMargins(0, 0, 0, 0) - for funcName, funcConfig in config["MODE_FUNCS"].items(): - self.initializeButton(funcName, funcConfig) + for name, config in NAVIGATION_FUNCTIONS.items(): + self.initializeButton(name, config) self.layout().addWidget(self.buttonList[0], 0, 0, 1, 1) self.layout().addWidget(self.buttonList[1], 1, 0, 1, 1) diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..0e99b78 --- /dev/null +++ b/app/utils/__init__.py @@ -0,0 +1,17 @@ +""" +Poricom Utilities +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" diff --git a/app/utils/constants.py b/app/utils/constants.py new file mode 100644 index 0000000..7be1a43 --- /dev/null +++ b/app/utils/constants.py @@ -0,0 +1,205 @@ +""" +Poricom Constants +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .types import ButtonConfigDict + +# ------------------------------------- General ------------------------------------- # + +# Paths +TOOLBAR_ICONS = './assets/images/icons/' +TOOLBAR_ICON_DEFAULT = './assets/images/icons/default_icon.png' + +# --------------------------------------- UI ---------------------------------------- # + +# Toolbar +TOOLBAR_ICON_SIZE = 0.05 # Fraction of primary screen height + +NAVIGATION_FUNCTIONS: ButtonConfigDict = { + "zoomIn": { + "title": "Zoom in", + "message": "Hint: Double click the image to reset zoom.", + "path": "zoomIn.png", + "toggle": False, + "align": "AlignRight", + "iconHeight": 0.45, + "iconWidth": 0.45 + }, + "zoomOut": { + "title": "Zoom out", + "message": "Hint: Double click the image to reset zoom.", + "path": "zoomOut.png", + "toggle": False, + "align": "AlignRight", + "iconHeight": 0.45, + "iconWidth": 0.45 + }, + "loadImageAtIndex": { + "title": "", + "message": "Jump to page", + "path": "loadImageAtIndex.png", + "toggle": False, + "align": "AlignRight", + "iconHeight": 0.45, + "iconWidth": 1.3 + }, + "loadPrevImage": { + "title": "", + "message": "Show previous image", + "path": "loadPrevImage.png", + "toggle": False, + "align": "AlignRight", + "iconHeight": 0.45, + "iconWidth": 0.6 + }, + "loadNextImage": { + "title": "", + "message": "Show next image", + "path": "loadNextImage.png", + "toggle": False, + "align": "AlignRight", + "iconHeight": 0.45, + "iconWidth": 0.6 + } +} +TOOLBAR_FUNCTIONS: dict[str, ButtonConfigDict] = { + "file": { + "openDir": { + "title": "Open manga directory", + "message": "Open a directory containing images.", + "path": "openDir.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "openManga": { + "title": "Open manga file", + "message": "Supports the following formats: cbr, cbz, pdf.", + "path": "openManga.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "captureExternalHelper": { + "title": "External capture", + "message": "This will minimize the app and perform OCR on the current screen. Alternatively, you may use the shortcut Alt+Q.", + "path": "captureExternalHelper.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + } + }, + "view": { + "toggleStylesheet": { + "title": "Change theme", + "message": "Switch between light and dark mode.", + "path": "toggleStylesheet.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "hideExplorer": { + "title": "Hide explorer", + "message": "Hide the file explorer from view", + "path": "hideExplorer.png", + "toggle": True, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "modifyFontSettings": { + "title": "Modify preview text", + "message": "Change font style and font size of preview text.", + "path": "modifyFontSettings.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "toggleSplitView": { + "title": "Turn on split view", + "message": "View two images at once.", + "path": "toggleSplitView.png", + "toggle": True, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "scaleImage": { + "title": "Adjust image scaling", + "message": "Fit an image according to the available options: fit to width, fit to height, fit to screen", + "path": "scaleImage.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + } + }, + "controls": { + "toggleMouseMode": { + "title": "Change mouse behavior", + "message": "This will disable text detection. Turn this on only if do not want to hold CTRL key to zoom and pan on an image.", + "path": "toggleMouseMode.png", + "toggle": True, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "modifyHotkeys": { + "title": "Remap hotkeys", + "message": "Change shortcut for external captures.", + "path": "modifyHotkeys.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + } + }, + "misc": { + "loadModel": { + "title": "Switch detection model", + "message": "Switch between MangaOCR and Tesseract models.", + "path": "loadModel.png", + "toggle": True, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "modifyTesseract": { + "title": "Tesseract settings", + "message": "Set the language and orientation for the Tesseract model.", + "path": "modifyTesseract.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + }, + "toggleLogging": { + "title": "Enable text logging", + "message": "Save detected text to a text file located in the current project directory.", + "path": "toggleLogging.png", + "toggle": True, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0 + } + } +} diff --git a/app/utils/types.py b/app/utils/types.py new file mode 100644 index 0000000..8db2d63 --- /dev/null +++ b/app/utils/types.py @@ -0,0 +1,30 @@ +""" +Poricom Types +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from typing import TypedDict + +class ButtonConfig(TypedDict): + title: str + message: str + path: str + toggle: bool + align: str + iconHeight: float + iconWidth: float + +ButtonConfigDict = dict[str, ButtonConfig] From 682944579ae4097a06027e6037d9728dc10973de Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 19:07:41 +0800 Subject: [PATCH 068/137] Update types and docstring --- app/components/toolbar/base.py | 2 +- app/components/toolbar/tabs/base.py | 11 ++++++----- app/components/toolbar/tabs/containers/base.py | 2 +- app/components/toolbar/tabs/containers/navigate.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/components/toolbar/base.py b/app/components/toolbar/base.py index cacaed2..73362ca 100644 --- a/app/components/toolbar/base.py +++ b/app/components/toolbar/base.py @@ -27,7 +27,7 @@ class BaseToolbar(QTabWidget): Toolbar widget Args: - parent (QWidget, optional): Toolbar parent. Set to main window. + parent (QMainWindow): Toolbar parent. Set to main window. Notes: Parent must be passed to children to call main window functions. """ diff --git a/app/components/toolbar/tabs/base.py b/app/components/toolbar/tabs/base.py index 2290a03..63df709 100644 --- a/app/components/toolbar/tabs/base.py +++ b/app/components/toolbar/tabs/base.py @@ -20,20 +20,21 @@ from PyQt5.QtWidgets import (QHBoxLayout, QMainWindow) from .containers import BaseToolbarContainer +from utils.types import ButtonConfigDict class BaseToolbarTab(BaseToolbarContainer): - """Widget to contain all toolbar tab functions + """Tab widget to arrange toolbar tab containers Args: - parent (QWidget, optional): Toolbar tab parent. Set to main window. - funcs (Any, optional): Toolbar function configuration. Defaults to {}. + parent (QMainWindow): Toolbar tab parent. Set to main window. + funcs (ButtonConfigDict, optional): Toolbar function configuration. Defaults to {}. """ - def __init__(self, parent: QMainWindow, funcs={}): + def __init__(self, parent: QMainWindow, funcs: ButtonConfigDict={}): super().__init__(parent) self.initializeButtons(funcs) - def initializeButtons(self, funcs): + def initializeButtons(self, funcs: ButtonConfigDict): self.setLayout(QHBoxLayout()) for name, config in funcs.items(): self.initializeButton(name, config) diff --git a/app/components/toolbar/tabs/containers/base.py b/app/components/toolbar/tabs/containers/base.py index 56b9e62..db05f82 100644 --- a/app/components/toolbar/tabs/containers/base.py +++ b/app/components/toolbar/tabs/containers/base.py @@ -31,7 +31,7 @@ class BaseToolbarContainer(ScreenAwareWidget): """Widget that contains the toolbar functions Args: - parent (QMainWindow, optional): Container parent. Set to main window. + parent (QMainWindow): Container parent. Set to main window. """ def __init__(self, parent: QMainWindow): super().__init__(parent) diff --git a/app/components/toolbar/tabs/containers/navigate.py b/app/components/toolbar/tabs/containers/navigate.py index 3e985af..df04d29 100644 --- a/app/components/toolbar/tabs/containers/navigate.py +++ b/app/components/toolbar/tabs/containers/navigate.py @@ -26,7 +26,7 @@ class NavigateToolbarContainer(BaseToolbarContainer): """Widget that contains the toolbar navigation functions Args: - parent (QWidget, optional): Container parent. Set to main window. + parent (QWidget): Container parent. Set to main window. """ def __init__(self, parent: QMainWindow): From 6f7730dc4ec802d24621e727c87193ad18ae4c19 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 22:05:15 +0800 Subject: [PATCH 069/137] Refactor explorers file structure --- app/Explorers.py | 89 --------------------- app/MainWindow.py | 4 +- app/components/explorers/__init__.py | 19 +++++ app/components/explorers/image.py | 76 ++++++++++++++++++ app/components/explorers/models/__init__.py | 19 +++++ app/components/explorers/models/image.py | 58 ++++++++++++++ app/utils/constants.py | 4 + 7 files changed, 178 insertions(+), 91 deletions(-) delete mode 100644 app/Explorers.py create mode 100644 app/components/explorers/__init__.py create mode 100644 app/components/explorers/image.py create mode 100644 app/components/explorers/models/__init__.py create mode 100644 app/components/explorers/models/image.py diff --git a/app/Explorers.py b/app/Explorers.py deleted file mode 100644 index 68777fa..0000000 --- a/app/Explorers.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Poricom Explorer Components - -Copyright (C) `2021-2022` `` - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -from PyQt5.QtCore import (Qt, QDir) -from PyQt5.QtWidgets import (QTreeView, QFileSystemModel) - -from utils.config import config - - -class ImageExplorer(QTreeView): - layoutCheck = False - - def __init__(self, parent=None, tracker=None): - super(QTreeView, self).__init__() - self.parent = parent - self.tracker = tracker - - self.model = QFileSystemModel() - # self.model.setFilter(QDir.Files) - self.model.setNameFilterDisables(False) - self.model.setNameFilters(config["IMAGE_EXTENSIONS"]) - self.setModel(self.model) - - for i in range(1, 4): - self.hideColumn(i) - self.setIndentation(0) - - self.setDirectory(tracker.filepath) - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - - def currentChanged(self, current, previous): - if not current.isValid(): - current = self.model.index(self.getTopIndex(), 0, self.rootIndex()) - filename = self.model.fileInfo(current).absoluteFilePath() - nextIndex = self.indexBelow(current) - filenext = self.model.fileInfo(nextIndex).absoluteFilePath() - self.parent.viewImageFromExplorer(filename, filenext) - QTreeView.currentChanged(self, current, previous) - - def getTopIndex(self): - item = self.model.index(0, 0, self.rootIndex()) - if self.model.fileInfo(item).isFile(): - return 0 - - r = self.model.rowCount(self.rootIndex()) // 2 - while True: - item = self.model.index(r, 0, self.rootIndex()) - if not item.isValid(): - break - if self.model.fileInfo(item).isFile(): - r //= 2 - elif not self.model.fileInfo(item).isFile(): - r += 1 - item = self.model.index(r, 0, self.rootIndex()) - if self.model.fileInfo(item).isFile(): - break - return r - - def setTopIndex(self): - topIndex = self.model.index(self.getTopIndex(), 0, self.rootIndex()) - if topIndex.isValid(): - self.setCurrentIndex(topIndex) - if self.layoutCheck: - self.model.layoutChanged.disconnect(self.setTopIndex) - self.layoutCheck = False - else: - if not self.layoutCheck: - self.model.layoutChanged.connect(self.setTopIndex) - self.layoutCheck = True - - def setDirectory(self, path): - self.setRootIndex(self.model.setRootPath(path)) - self.setTopIndex() diff --git a/app/MainWindow.py b/app/MainWindow.py index 2f22b98..a2baf84 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -28,9 +28,9 @@ from utils.image_io import mangaFileToImageDir from utils.config import config, saveOnClose +from components.explorers import ImageExplorer from components.services import BaseWorker from components.toolbar import BaseToolbar -from Explorers import (ImageExplorer) from Views import (OCRCanvas, FullScreen) from Popups import (FontPicker, LanguagePicker, ScaleImagePicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) @@ -57,7 +57,7 @@ def __init__(self, parent=None, tracker=None): self.ribbon = BaseToolbar(self) self.vLayout.addWidget(self.ribbon) self.canvas = OCRCanvas(self, self.tracker) - self.explorer = ImageExplorer(self, self.tracker) + self.explorer = ImageExplorer(self, self.tracker.filepath) self.splitter = QSplitter() self.splitter.addWidget(self.explorer) diff --git a/app/components/explorers/__init__.py b/app/components/explorers/__init__.py new file mode 100644 index 0000000..7912357 --- /dev/null +++ b/app/components/explorers/__init__.py @@ -0,0 +1,19 @@ +""" +Poricom Explorers +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .image import ImageExplorer diff --git a/app/components/explorers/image.py b/app/components/explorers/image.py new file mode 100644 index 0000000..2ac7ba8 --- /dev/null +++ b/app/components/explorers/image.py @@ -0,0 +1,76 @@ +""" +Poricom Explorers +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (Qt) +from PyQt5.QtWidgets import (QMainWindow, QTreeView) + +from .models import ImageModel +from utils.constants import EXPLORER_ROOT_DEFAULT + +class ImageExplorer(QTreeView): + """View to allow exploring images + + Args: + parent (QMainWindow): Image explorer parent. Set to main window. + initialDir (str, optional): Initial directory. Defaults to EXPLORER_ROOT_DEFAULT. + """ + def __init__(self, parent: QMainWindow, initialDir: str = EXPLORER_ROOT_DEFAULT): + super().__init__(parent) + # TODO: It might be better if the parent is set to the QSplitter + # Then add property getter methods to main window to access its children + # Manually set parent since `addWidget` method will reparent the widget + self.mainWindow = parent + + self.setModel(ImageModel()) + + for i in range(1, 4): + self.hideColumn(i) + self.setIndentation(0) + + self.layoutCheck = False + self.setDirectory(initialDir) + + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + def currentChanged(self, current, previous): + if not current.isValid(): + current = self.model().index(self.getTopIndex(), 0, self.rootIndex()) + filename = self.model().fileInfo(current).absoluteFilePath() + nextIndex = self.indexBelow(current) + filenext = self.model().fileInfo(nextIndex).absoluteFilePath() + self.mainWindow.viewImageFromExplorer(filename, filenext) + super().currentChanged(current, previous) + + def getTopIndex(self): + return self.model().getTopIndex(self.rootIndex()) + + def setTopIndex(self): + topIndex = self.model().index(self.getTopIndex(), 0, self.rootIndex()) + if topIndex.isValid(): + self.setCurrentIndex(topIndex) + if self.layoutCheck: + self.model().layoutChanged.disconnect(self.setTopIndex) + self.layoutCheck = False + else: + if not self.layoutCheck: + self.model().layoutChanged.connect(self.setTopIndex) + self.layoutCheck = True + + def setDirectory(self, path: str): + self.setRootIndex(self.model().setRootPath(path)) + self.setTopIndex() diff --git a/app/components/explorers/models/__init__.py b/app/components/explorers/models/__init__.py new file mode 100644 index 0000000..2ab9145 --- /dev/null +++ b/app/components/explorers/models/__init__.py @@ -0,0 +1,19 @@ +""" +Poricom Explorers +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .image import ImageModel diff --git a/app/components/explorers/models/image.py b/app/components/explorers/models/image.py new file mode 100644 index 0000000..d676676 --- /dev/null +++ b/app/components/explorers/models/image.py @@ -0,0 +1,58 @@ +""" +Poricom Explorers +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (QModelIndex) +from PyQt5.QtWidgets import (QFileSystemModel) + +from utils.constants import IMAGE_EXTENSIONS + +class ImageModel(QFileSystemModel): + """ + Image model based on the native file system + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setNameFilterDisables(False) + self.setNameFilters(IMAGE_EXTENSIONS) + + def getTopIndex(self, parentIndex: QModelIndex): + """Get the index of the top most file in the view + + Args: + parentIndex (QModelIndex): Root index of the parent view + + Returns: + int: Index of the top most file + """ + item = self.index(0, 0, parentIndex) + if self.fileInfo(item).isFile(): + return 0 + + r = self.rowCount(parentIndex) // 2 + while True: + item = self.index(r, 0, parentIndex) + if not item.isValid(): + break + if self.fileInfo(item).isFile(): + r //= 2 + elif not self.fileInfo(item).isFile(): + r += 1 + item = self.index(r, 0, parentIndex) + if self.fileInfo(item).isFile(): + break + return r diff --git a/app/utils/constants.py b/app/utils/constants.py index 7be1a43..25a2684 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -20,10 +20,14 @@ # ------------------------------------- General ------------------------------------- # +IMAGE_EXTENSIONS = ["*.bmp", "*.gif", "*.jpeg", "*.jpg", "*.pbm", "*.pgm", "*.png", "*.ppm", "*.webp", "*.xbm", "*.xpm",] + # Paths TOOLBAR_ICONS = './assets/images/icons/' TOOLBAR_ICON_DEFAULT = './assets/images/icons/default_icon.png' +EXPLORER_ROOT_DEFAULT = './assets/images/' + # --------------------------------------- UI ---------------------------------------- # # Toolbar From a438351affff37854bfd31615ebb61f32de21751 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 8 Jan 2023 22:50:06 +0800 Subject: [PATCH 070/137] Refactor view file structure --- app/MainWindow.py | 6 +- app/components/views/__init__.py | 21 +++ app/components/views/image/__init__.py | 20 +++ .../views/image/base.py} | 136 +++--------------- app/components/views/ocr/__init__.py | 21 +++ app/components/views/ocr/base.py | 105 ++++++++++++++ app/components/views/ocr/fullscreen.py | 49 +++++++ 7 files changed, 236 insertions(+), 122 deletions(-) create mode 100644 app/components/views/__init__.py create mode 100644 app/components/views/image/__init__.py rename app/{Views.py => components/views/image/base.py} (61%) create mode 100644 app/components/views/ocr/__init__.py create mode 100644 app/components/views/ocr/base.py create mode 100644 app/components/views/ocr/fullscreen.py diff --git a/app/MainWindow.py b/app/MainWindow.py index a2baf84..deca0dc 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -31,7 +31,7 @@ from components.explorers import ImageExplorer from components.services import BaseWorker from components.toolbar import BaseToolbar -from Views import (OCRCanvas, FullScreen) +from components.views import BaseImageView, FullScreenOCRView from Popups import (FontPicker, LanguagePicker, ScaleImagePicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) @@ -56,7 +56,7 @@ def __init__(self, parent=None, tracker=None): self.vLayout = QVBoxLayout() self.ribbon = BaseToolbar(self) self.vLayout.addWidget(self.ribbon) - self.canvas = OCRCanvas(self, self.tracker) + self.canvas = BaseImageView(self, self.tracker) self.explorer = ImageExplorer(self, self.tracker.filepath) self.splitter = QSplitter() @@ -163,7 +163,7 @@ def captureExternal(self): externalWindow.setAttribute(Qt.WA_DeleteOnClose) externalWindow.setCentralWidget( - FullScreen(externalWindow, self.tracker)) + FullScreenOCRView(externalWindow, self.tracker)) fullScreen = externalWindow.centralWidget() screenIndex = fullScreen.getActiveScreenIndex() diff --git a/app/components/views/__init__.py b/app/components/views/__init__.py new file mode 100644 index 0000000..c780e57 --- /dev/null +++ b/app/components/views/__init__.py @@ -0,0 +1,21 @@ +""" +Poricom Views + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .image import BaseImageView +from .ocr import FullScreenOCRView diff --git a/app/components/views/image/__init__.py b/app/components/views/image/__init__.py new file mode 100644 index 0000000..fd0bfe0 --- /dev/null +++ b/app/components/views/image/__init__.py @@ -0,0 +1,20 @@ +""" +Poricom Views + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BaseImageView diff --git a/app/Views.py b/app/components/views/image/base.py similarity index 61% rename from app/Views.py rename to app/components/views/image/base.py index 17b1c07..afdd8a3 100644 --- a/app/Views.py +++ b/app/components/views/image/base.py @@ -1,5 +1,5 @@ """ -Poricom View Components +Poricom Views Copyright (C) `2021-2022` `` @@ -19,123 +19,20 @@ from time import sleep -from PyQt5.QtCore import (Qt, QRect, QRectF, QSize, - QTimer, QThreadPool, pyqtSlot) -from PyQt5.QtWidgets import ( - QApplication, QGraphicsView, QGraphicsScene, QLabel) -from PyQt5.QtGui import QCursor, QTransform +from PyQt5.QtCore import (Qt, QRect, QRectF, QSize, QThreadPool) +from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView, QMainWindow) +from ..ocr import BaseOCRView from Popups import MessagePopup from components.services import BaseWorker -from utils.image_io import logText, pixboxToText +# TODO: This should be the other way around. OCRView should inherit from ImageView +class BaseImageView(BaseOCRView): + """ + Base image view to allow view/zoom/pan functions + """ -class BaseCanvas(QGraphicsView): - - def __init__(self, parent=None, tracker=None): - super(QGraphicsView, self).__init__(parent) - self.parent = parent - self.tracker = tracker - - self.timer_ = QTimer() - self.timer_.setInterval(300) - self.timer_.setSingleShot(True) - self.timer_.timeout.connect(self.rubberBandStopped) - - self.canvasText = QLabel("", self, Qt.WindowStaysOnTopHint) - self.canvasText.setWordWrap(True) - self.canvasText.hide() - self.canvasText.setObjectName("canvasText") - - self.scene = QGraphicsScene() - self.setScene(self.scene) - self.pixmap = self.scene.addPixmap(self.tracker.pixImage.scaledToWidth( - self.viewport().geometry().width(), Qt.SmoothTransformation)) - - self.setDragMode(QGraphicsView.RubberBandDrag) - - def mouseMoveEvent(self, event): - rubberBandVisible = not self.rubberBandRect().isNull() - if (event.buttons() & Qt.LeftButton) and rubberBandVisible: - self.timer_.start() - QGraphicsView.mouseMoveEvent(self, event) - - def mouseReleaseEvent(self, event): - logPath = self.tracker.filepath + "/log.txt" - logToFile = self.tracker.writeMode - text = self.canvasText.text() - logText(text, mode=logToFile, path=logPath) - try: - if not self.parent.config["PERSIST_TEXT_MODE"]: - self.canvasText.hide() - except AttributeError: - pass - super().mouseReleaseEvent(event) - - def handleTextResult(self, result): - if result == None: - MessagePopup( - "Tesseract not loaded", - "Tesseract model cannot be loaded in your machine, please use the MangaOcr instead." - ).exec() - return - try: - self.canvasText.setText(result) - except RuntimeError: - pass - - def handleTextFinished(self): - try: - self.canvasText.adjustSize() - except RuntimeError: - pass - - @pyqtSlot() - def rubberBandStopped(self): - - if (self.canvasText.isHidden()): - self.canvasText.setText("") - self.canvasText.adjustSize() - self.canvasText.show() - - lang = self.tracker.language + self.tracker.orientation - pixbox = self.grab(self.rubberBandRect()) - - worker = BaseWorker(pixboxToText, pixbox, lang, self.tracker.ocrModel) - worker.signals.result.connect(self.handleTextResult) - worker.signals.finished.connect(self.handleTextFinished) - self.timer_.timeout.disconnect(self.rubberBandStopped) - worker.signals.finished.connect( - lambda: self.timer_.timeout.connect(self.rubberBandStopped)) - QThreadPool.globalInstance().start(worker) - - -class FullScreen(BaseCanvas): - - def __init__(self, parent=None, tracker=None): - super().__init__(parent, tracker) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - - def takeScreenshot(self, screenIndex): - screen = QApplication.screens()[screenIndex] - s = screen.size() - self.pixmap.setPixmap(screen.grabWindow( - 0).scaled(s.width(), s.height())) - self.scene.setSceneRect(QRectF(self.pixmap.pixmap().rect())) - - def getActiveScreenIndex(self): - cursor = QCursor.pos() - return QApplication.desktop().screenNumber(cursor) - - def mouseReleaseEvent(self, event): - BaseCanvas.mouseReleaseEvent(self, event) - self.parent.close() - - -class OCRCanvas(BaseCanvas): - - def __init__(self, parent=None, tracker=None): + def __init__(self, parent: QMainWindow, tracker=None): super().__init__(parent, tracker) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) @@ -173,6 +70,7 @@ def viewImage(self): w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)) self.scene.setSceneRect(QRectF(self.pixmap.pixmap().rect())) + # TODO: Cloe settings component might be useful def setViewImageMode(self, mode): self._viewImageMode = mode self.parent.config["VIEW_IMAGE_MODE"] = mode @@ -207,8 +105,8 @@ def wheelEvent(self, event): pressedKey = QApplication.keyboardModifiers() zoomMode = pressedKey == Qt.ControlModifier or self._zoomPanMode - self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) - self.setResizeAnchor(QGraphicsView.NoAnchor) + # self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) + # TODO: Rewrite individual event handlers as separate functions if zoomMode: if event.angleDelta().y() > 0: isZoomIn = True @@ -280,14 +178,14 @@ def mouseMoveEvent(self, event): else: self.setDragMode(QGraphicsView.RubberBandDrag) - BaseCanvas.mouseMoveEvent(self, event) + super().mouseMoveEvent(event) def mouseDoubleClickEvent(self, event): - self.setTransform(QTransform()) - self.viewImage() - self.verticalScrollBar().setSliderPosition(0) + self.currentScale = 1 + self.viewImage(self.currentScale) super().mouseDoubleClickEvent(event) + # TODO: Keyboard shortcuts should be in another class def keyPressEvent(self, event): if event.key() == Qt.Key_Left: self.parent.loadPrevImage() diff --git a/app/components/views/ocr/__init__.py b/app/components/views/ocr/__init__.py new file mode 100644 index 0000000..e26fef0 --- /dev/null +++ b/app/components/views/ocr/__init__.py @@ -0,0 +1,21 @@ +""" +Poricom Views + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BaseOCRView +from .fullscreen import FullScreenOCRView diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py new file mode 100644 index 0000000..191393a --- /dev/null +++ b/app/components/views/ocr/base.py @@ -0,0 +1,105 @@ +""" +Poricom View Components + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (pyqtSlot, Qt, QThreadPool, QTimer) +from PyQt5.QtWidgets import (QGraphicsScene, QGraphicsView, QLabel, QMainWindow) + +from components.services import BaseWorker +from utils.image_io import logText, pixboxToText + + +class BaseOCRView(QGraphicsView): + """Base view with OCR capabilities + + Args: + parent (QMainWindow): View parent. Set to main window + tracker (Any, optional): State tracker. Defaults to None. + """ + def __init__(self, parent: QMainWindow, tracker=None): + # TODO: Remove references to tracker + super().__init__(parent) + self.parent = parent + self.tracker = tracker + + self.timer = QTimer() + self.timer.setInterval(300) + self.timer.setSingleShot(True) + self.timer.timeout.connect(self.rubberBandStopped) + + self.canvasText = QLabel("", self, Qt.WindowStaysOnTopHint) + self.canvasText.setWordWrap(True) + self.canvasText.hide() + self.canvasText.setObjectName("canvasText") + + # TODO: Set scene and pixmap should be on BaseImageView + self.scene = QGraphicsScene() + self.setScene(self.scene) + self.pixmap = self.scene.addPixmap(self.tracker.pixImage.scaledToWidth( + self.viewport().geometry().width(), Qt.SmoothTransformation)) + + self.setDragMode(QGraphicsView.RubberBandDrag) + + def mouseMoveEvent(self, event): + rubberBandVisible = not self.rubberBandRect().isNull() + if (event.buttons() & Qt.LeftButton) and rubberBandVisible: + self.timer.start() + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + logPath = self.tracker.filepath + "/log.txt" + logToFile = self.tracker.writeMode + text = self.canvasText.text() + logText(text, mode=logToFile, path=logPath) + try: + if not self.parent.config["PERSIST_TEXT_MODE"]: + self.canvasText.hide() + except AttributeError: + pass + super().mouseReleaseEvent(event) + + def handleTextResult(self, result): + try: + self.canvasText.setText(result) + except RuntimeError: + pass + + def handleTextFinished(self): + try: + self.canvasText.adjustSize() + except RuntimeError: + pass + + @pyqtSlot() + def rubberBandStopped(self): + + if (self.canvasText.isHidden()): + self.canvasText.setText("") + self.canvasText.adjustSize() + self.canvasText.show() + + lang = self.tracker.language + self.tracker.orientation + pixbox = self.grab(self.rubberBandRect()) + + worker = BaseWorker(pixboxToText, pixbox, lang, self.tracker.ocrModel) + worker.signals.result.connect(self.handleTextResult) + worker.signals.finished.connect(self.handleTextFinished) + self.timer.timeout.disconnect(self.rubberBandStopped) + worker.signals.finished.connect( + lambda: self.timer.timeout.connect(self.rubberBandStopped)) + QThreadPool.globalInstance().start(worker) diff --git a/app/components/views/ocr/fullscreen.py b/app/components/views/ocr/fullscreen.py new file mode 100644 index 0000000..2ea93cb --- /dev/null +++ b/app/components/views/ocr/fullscreen.py @@ -0,0 +1,49 @@ +""" +Poricom Views + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (Qt, QRectF) +from PyQt5.QtWidgets import (QApplication, QMainWindow) +from PyQt5.QtGui import QCursor, QMouseEvent + +from .base import BaseOCRView + + +class FullScreenOCRView(BaseOCRView): + """ + Fullscreen view with OCR capabilities + """ + def __init__(self, parent: QMainWindow, tracker=None): + super().__init__(parent, tracker) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + def takeScreenshot(self, screenIndex: int): + screen = QApplication.screens()[screenIndex] + s = screen.size() + self.pixmap.setPixmap(screen.grabWindow( + 0).scaled(s.width(), s.height())) + self.scene.setSceneRect(QRectF(self.pixmap.pixmap().rect())) + + def getActiveScreenIndex(self): + cursor = QCursor.pos() + return QApplication.desktop().screenNumber(cursor) + + def mouseReleaseEvent(self, event: QMouseEvent): + super().mouseReleaseEvent(event) + self.parent.close() From 9b468e7bdeeb4dbeb721b4a309aa1120012c6456 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 14 Jan 2023 19:35:08 +0800 Subject: [PATCH 071/137] Update view inheritance tree --- app/components/views/image/base.py | 19 +++++------ app/components/views/ocr/base.py | 51 +++++++++++++----------------- 2 files changed, 32 insertions(+), 38 deletions(-) diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index afdd8a3..9c0f3c1 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -22,18 +22,17 @@ from PyQt5.QtCore import (Qt, QRect, QRectF, QSize, QThreadPool) from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView, QMainWindow) -from ..ocr import BaseOCRView -from Popups import MessagePopup from components.services import BaseWorker -# TODO: This should be the other way around. OCRView should inherit from ImageView -class BaseImageView(BaseOCRView): +class BaseImageView(QGraphicsView): """ Base image view to allow view/zoom/pan functions """ def __init__(self, parent: QMainWindow, tracker=None): - super().__init__(parent, tracker) + super().__init__(parent) + self.parent = parent + self.tracker = tracker self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) @@ -49,9 +48,11 @@ def __init__(self, parent: QMainWindow, tracker=None): self._trackPadAtMax = 0 self._scrollSuppressed = False - self.scene = QGraphicsScene() - self.setScene(self.scene) - self.pixmap = self.scene.addPixmap(self.tracker.pixImage.scaledToWidth( + self.initializePixmapItem() + + def initializePixmapItem(self): + self.setScene(QGraphicsScene()) + self.pixmap = self.scene().addPixmap(self.tracker.pixImage.scaledToWidth( self.viewport().geometry().width(), Qt.SmoothTransformation)) def viewImage(self): @@ -68,7 +69,7 @@ def viewImage(self): elif self._viewImageMode == 2: self.pixmap.setPixmap(self.tracker.pixImage.scaled( w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)) - self.scene.setSceneRect(QRectF(self.pixmap.pixmap().rect())) + self.scene().setSceneRect(QRectF(self.pixmap.pixmap().rect())) # TODO: Cloe settings component might be useful def setViewImageMode(self, mode): diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 191393a..382f38b 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -18,13 +18,14 @@ """ from PyQt5.QtCore import (pyqtSlot, Qt, QThreadPool, QTimer) -from PyQt5.QtWidgets import (QGraphicsScene, QGraphicsView, QLabel, QMainWindow) +from PyQt5.QtWidgets import (QGraphicsView, QLabel, QMainWindow) +from ..image import BaseImageView from components.services import BaseWorker from utils.image_io import logText, pixboxToText -class BaseOCRView(QGraphicsView): +class BaseOCRView(BaseImageView): """Base view with OCR capabilities Args: @@ -33,9 +34,7 @@ class BaseOCRView(QGraphicsView): """ def __init__(self, parent: QMainWindow, tracker=None): # TODO: Remove references to tracker - super().__init__(parent) - self.parent = parent - self.tracker = tracker + super().__init__(parent, tracker) self.timer = QTimer() self.timer.setInterval(300) @@ -47,32 +46,8 @@ def __init__(self, parent: QMainWindow, tracker=None): self.canvasText.hide() self.canvasText.setObjectName("canvasText") - # TODO: Set scene and pixmap should be on BaseImageView - self.scene = QGraphicsScene() - self.setScene(self.scene) - self.pixmap = self.scene.addPixmap(self.tracker.pixImage.scaledToWidth( - self.viewport().geometry().width(), Qt.SmoothTransformation)) - self.setDragMode(QGraphicsView.RubberBandDrag) - def mouseMoveEvent(self, event): - rubberBandVisible = not self.rubberBandRect().isNull() - if (event.buttons() & Qt.LeftButton) and rubberBandVisible: - self.timer.start() - super().mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - logPath = self.tracker.filepath + "/log.txt" - logToFile = self.tracker.writeMode - text = self.canvasText.text() - logText(text, mode=logToFile, path=logPath) - try: - if not self.parent.config["PERSIST_TEXT_MODE"]: - self.canvasText.hide() - except AttributeError: - pass - super().mouseReleaseEvent(event) - def handleTextResult(self, result): try: self.canvasText.setText(result) @@ -103,3 +78,21 @@ def rubberBandStopped(self): worker.signals.finished.connect( lambda: self.timer.timeout.connect(self.rubberBandStopped)) QThreadPool.globalInstance().start(worker) + + def mouseMoveEvent(self, event): + rubberBandVisible = not self.rubberBandRect().isNull() + if (event.buttons() & Qt.LeftButton) and rubberBandVisible: + self.timer.start() + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + logPath = self.tracker.filepath + "/log.txt" + logToFile = self.tracker.writeMode + text = self.canvasText.text() + logText(text, mode=logToFile, path=logPath) + try: + if not self.parent.config["PERSIST_TEXT_MODE"]: + self.canvasText.hide() + except AttributeError: + pass + super().mouseReleaseEvent(event) From d9bb4034523b4cb0c03b2af6d11fdfd3124322c3 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 14 Jan 2023 19:43:29 +0800 Subject: [PATCH 072/137] Integrate explorer and ocr view into main view --- app/MainWindow.py | 87 ++------------- .../toolbar/tabs/containers/base.py | 12 +- app/components/views/__init__.py | 2 +- app/components/views/main.py | 103 ++++++++++++++++++ app/utils/constants.py | 3 + 5 files changed, 127 insertions(+), 80 deletions(-) create mode 100644 app/components/views/main.py diff --git a/app/MainWindow.py b/app/MainWindow.py index deca0dc..e4c952f 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -24,14 +24,13 @@ from manga_ocr import MangaOcr from PyQt5.QtCore import (Qt, QAbstractNativeEventFilter, QThreadPool) from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QDesktopWidget, QMainWindow, QApplication, - QPushButton, QFileDialog, QInputDialog, QSplitter) + QPushButton, QFileDialog) from utils.image_io import mangaFileToImageDir from utils.config import config, saveOnClose -from components.explorers import ImageExplorer from components.services import BaseWorker from components.toolbar import BaseToolbar -from components.views import BaseImageView, FullScreenOCRView +from components.views import MainView, FullScreenOCRView from Popups import (FontPicker, LanguagePicker, ScaleImagePicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) @@ -54,43 +53,25 @@ def __init__(self, parent=None, tracker=None): self.config = config self.vLayout = QVBoxLayout() + + self.mainView = MainView(self, self.tracker) self.ribbon = BaseToolbar(self) self.vLayout.addWidget(self.ribbon) - self.canvas = BaseImageView(self, self.tracker) - self.explorer = ImageExplorer(self, self.tracker.filepath) - - self.splitter = QSplitter() - self.splitter.addWidget(self.explorer) - self.splitter.addWidget(self.canvas) - self.splitter.setChildrenCollapsible(False) - for i, s in enumerate(config["NAV_VIEW_RATIO"]): - self.splitter.setStretchFactor(i, s) - self.vLayout.addWidget(self.splitter) + self.vLayout.addWidget(self.mainView) _mainWidget = QWidget() _mainWidget.setLayout(self.vLayout) self.setCentralWidget(_mainWidget) self.threadpool = QThreadPool() - def viewImageFromExplorer(self, filename, filenext): - if not self.canvas.splitViewMode(): - self.tracker.pixImage = filename - if self.canvas.splitViewMode(): - self.tracker.pixImage = (filename, filenext) - if not self.tracker.pixImage.isValid(): - return False - self.canvas.resetTransform() - self.canvas.currentScale = 1 - self.canvas.verticalScrollBar().setSliderPosition(0) - self.canvas.viewImage() - self.canvas.setFocus() - return True - - def resizeEvent(self, event): - self.explorer.setMinimumWidth(0.1*self.width()) - self.canvas.setMinimumWidth(0.6*self.width()) - return super().resizeEvent(event) + @property + def canvas(self): + return self.mainView.canvas + + @property + def explorer(self): + return self.mainView.explorer def closeEvent(self, event): try: @@ -328,47 +309,3 @@ def modifyTesseract(self): def toggleLogging(self): self.tracker.switchWriteMode() - -# --------------------------- Always On Functions ---------------------------- # - - def loadPrevImage(self): - index = self.explorer.indexAbove(self.explorer.currentIndex()) - if self.canvas.splitViewMode(): - tempIndex = self.explorer.indexAbove(index) - if tempIndex.isValid(): - index = tempIndex - if (not index.isValid()): - return - self.explorer.setCurrentIndex(index) - - def loadNextImage(self): - index = self.explorer.indexBelow(self.explorer.currentIndex()) - if self.canvas.splitViewMode(): - tempIndex = self.explorer.indexBelow(index) - if tempIndex.isValid(): - index = tempIndex - if (not index.isValid()): - return - self.explorer.setCurrentIndex(index) - - def loadImageAtIndex(self): - rowCount = self.explorer.model.rowCount(self.explorer.rootIndex()) - i, _ = QInputDialog.getInt( - self, - 'Jump to', - f'Enter page number: (max is {rowCount})', - value=-1, - min=1, - max=rowCount, - flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint) - if (i == -1): - return - - index = self.explorer.model.index(i-1, 0, self.explorer.rootIndex()) - self.explorer.setCurrentIndex(index) - - def zoomIn(self): - self.canvas.zoomView(True) - - def zoomOut(self): - self.canvas.zoomView(False) diff --git a/app/components/toolbar/tabs/containers/base.py b/app/components/toolbar/tabs/containers/base.py index db05f82..d666c7b 100644 --- a/app/components/toolbar/tabs/containers/base.py +++ b/app/components/toolbar/tabs/containers/base.py @@ -75,9 +75,13 @@ def initializeButton(self, name: str, config: ButtonConfig): button.setCheckable(config["toggle"]) # Connect button to main window function - if hasattr(self.mainWindow, name): + try: button.clicked.connect( getattr(self.mainWindow, name)) - else: - button.clicked.connect( - getattr(self.mainWindow, 'poricomNoop')) + except AttributeError: + try: + button.clicked.connect( + getattr(self.mainWindow.mainView, name)) + except AttributeError: + button.clicked.connect( + getattr(self.mainWindow, 'poricomNoop')) diff --git a/app/components/views/__init__.py b/app/components/views/__init__.py index c780e57..963217c 100644 --- a/app/components/views/__init__.py +++ b/app/components/views/__init__.py @@ -17,5 +17,5 @@ along with this program. If not, see . """ -from .image import BaseImageView +from .main import MainView from .ocr import FullScreenOCRView diff --git a/app/components/views/main.py b/app/components/views/main.py new file mode 100644 index 0000000..1e07012 --- /dev/null +++ b/app/components/views/main.py @@ -0,0 +1,103 @@ +""" +Poricom Main Window Component + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (Qt) +from PyQt5.QtWidgets import (QInputDialog, QMainWindow, QSplitter) + +from .ocr import BaseOCRView +from components.explorers import ImageExplorer +from utils.config import config +from utils.constants import MAIN_VIEW_RATIO + +class MainView(QSplitter): + + def __init__(self, parent: QMainWindow, tracker=None): + super().__init__(parent) + self.tracker = tracker + self.config = config + + self.canvas = BaseOCRView(self, self.tracker) + self.explorer = ImageExplorer(self, self.tracker.filepath) + + self.addWidget(self.explorer) + self.addWidget(self.canvas) + self.setChildrenCollapsible(False) + for i, s in enumerate(MAIN_VIEW_RATIO): + self.setStretchFactor(i, s) + + def viewImageFromExplorer(self, filename, filenext): + if not self.canvas.splitViewMode: + self.tracker.pixImage = filename + if self.canvas.splitViewMode: + self.tracker.pixImage = (filename, filenext) + if not self.tracker.pixImage.isValid(): + return False + self.canvas.resetTransform() + self.canvas.currentScale = 1 + self.canvas.verticalScrollBar().setSliderPosition(0) + self.canvas.viewImage() + # self.canvas.setFocus() + return True + + def loadPrevImage(self): + index = self.explorer.indexAbove(self.explorer.currentIndex()) + if self.canvas.splitViewMode: + tempIndex = self.explorer.indexAbove(index) + if tempIndex.isValid(): + index = tempIndex + if (not index.isValid()): + return + self.explorer.setCurrentIndex(index) + + def loadNextImage(self): + index = self.explorer.indexBelow(self.explorer.currentIndex()) + if self.canvas.splitViewMode: + tempIndex = self.explorer.indexBelow(index) + if tempIndex.isValid(): + index = tempIndex + if (not index.isValid()): + return + self.explorer.setCurrentIndex(index) + + def loadImageAtIndex(self): + rowCount = self.explorer.model().rowCount(self.explorer.rootIndex()) + i, _ = QInputDialog.getInt( + self, + 'Jump to', + f'Enter page number: (max is {rowCount})', + value=-1, + min=1, + max=rowCount, + flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint) + if (i == -1): + return + + index = self.explorer.model().index(i-1, 0, self.explorer.rootIndex()) + self.explorer.setCurrentIndex(index) + + def zoomIn(self): + self.canvas.zoomView(True, usingButton=True) + + def zoomOut(self): + self.canvas.zoomView(False, usingButton=True) + + def resizeEvent(self, event): + self.explorer.setMinimumWidth(0.1*self.width()) + self.canvas.setMinimumWidth(0.6*self.width()) + return super().resizeEvent(event) diff --git a/app/utils/constants.py b/app/utils/constants.py index 25a2684..e40a4d9 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -30,6 +30,9 @@ # --------------------------------------- UI ---------------------------------------- # +# Main view +MAIN_VIEW_RATIO = [1, 9] + # Toolbar TOOLBAR_ICON_SIZE = 0.05 # Fraction of primary screen height From 38a7085a049e7d83c08813fde0d2b5a5cd38f70c Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 14 Jan 2023 21:30:29 +0800 Subject: [PATCH 073/137] Update image view to use base settings Remove the need to access parent config --- .gitignore | 5 +- app/MainWindow.py | 4 +- app/components/popups/__init__.py | 20 +++++ app/components/popups/base.py | 43 +++++++++++ app/components/settings/__init__.py | 20 +++++ app/components/settings/base.py | 114 ++++++++++++++++++++++++++++ app/components/views/image/base.py | 58 +++++++------- app/components/views/main.py | 4 +- app/utils/constants.py | 16 ++++ 9 files changed, 253 insertions(+), 31 deletions(-) create mode 100644 app/components/popups/__init__.py create mode 100644 app/components/popups/base.py create mode 100644 app/components/settings/__init__.py create mode 100644 app/components/settings/base.py diff --git a/.gitignore b/.gitignore index b5a6c65..b55b78c 100644 --- a/.gitignore +++ b/.gitignore @@ -143,4 +143,7 @@ dmypy.json cython_debug/ # Documentation -*.mp4 \ No newline at end of file +*.mp4 + +# Settings +*.ini diff --git a/app/MainWindow.py b/app/MainWindow.py index e4c952f..75c168f 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -191,11 +191,11 @@ def modifyFontSettings(self): def toggleSplitView(self): self.canvas.toggleSplitView() - if self.canvas.splitViewMode(): + if self.canvas.splitViewMode: self.canvas.setViewImageMode(2) index = self.explorer.currentIndex() self.explorer.currentChanged(index, index) - elif not self.canvas.splitViewMode(): + elif not self.canvas.splitViewMode: index = self.explorer.currentIndex() self.explorer.currentChanged(index, index) diff --git a/app/components/popups/__init__.py b/app/components/popups/__init__.py new file mode 100644 index 0000000..121e868 --- /dev/null +++ b/app/components/popups/__init__.py @@ -0,0 +1,20 @@ +""" +Poricom Popups + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BasePopup diff --git a/app/components/popups/base.py b/app/components/popups/base.py new file mode 100644 index 0000000..6e9ea48 --- /dev/null +++ b/app/components/popups/base.py @@ -0,0 +1,43 @@ +""" +Cloe Popups + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMessageBox + + +class BasePopup(QMessageBox): + """Base popup object to display info + + Args: + title (str): Text to show on the title bar + message (str): Text to show on the main area + buttons (StandardButtons, optional): Buttons to show below the popup. + Defaults to Ok button + """ + + def __init__( + self, + title: str, + message: str, + buttons: QMessageBox.StandardButtons = QMessageBox.Ok, + *args, + **kwargs + ): + super().__init__(QMessageBox.NoIcon, title, message, buttons, *args, **kwargs) + self.setAttribute(Qt.WA_DeleteOnClose) diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py new file mode 100644 index 0000000..11facfd --- /dev/null +++ b/app/components/settings/__init__.py @@ -0,0 +1,20 @@ +""" +Poricom Settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import BaseSettings diff --git a/app/components/settings/base.py b/app/components/settings/base.py new file mode 100644 index 0000000..e053c15 --- /dev/null +++ b/app/components/settings/base.py @@ -0,0 +1,114 @@ +""" +Poricom Settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from typing import Any, Callable + +from PyQt5.QtCore import QSettings +from PyQt5.QtWidgets import QWidget + +from components.popups import BasePopup +from utils.constants import SETTINGS_FILE_DEFAULT + + +class BaseSettings(QWidget): + """Base settings widget to allow save/load/reset of settings + + Args: + parent (QWidget): Parent widget. Set to SettingsMenu object. + file (str): Path to configuration file. Must be in ini format. Defaults to SETTINGS_FILE_DEFAULT. + prefix (str, optional): Text added to the saved property. Defaults to "". + """ + + def __init__(self, parent: QWidget, file: str = SETTINGS_FILE_DEFAULT, prefix: str = ""): + super().__init__(parent) + self.settings = QSettings(file, QSettings.IniFormat) + + # Settings widgets may sometimes share the same configuration file. + # Set the prefix to a unique value to avoid this. + self._prefix = prefix + + self.setDefaults({}) + self.setTypes({}) + + def setDefaults(self, defaults: dict[str, Any]): + """Set the default dictionary + + `self._defaults` contains the default values for ALL properties. + Any property that is saved/loaded from settings should have a default. + Otherwise, the property will not be saved/loaded. + """ + self._defaults = defaults + + def setTypes(self, types: dict[str, Callable]): + """Set the types dictionary + + By default, if the value is a non-QVariant, it is read as a str. + Use `self._types` to set the correct property type. + """ + self._types = types + + def getProperty(self, prop: str): + return getattr(self, prop) + + def setProperty(self, prop: str, value: Any): + try: + t = self._types[prop] + if t == bool: + return setattr(self, prop, value.lower() == "true") + return setattr(self, prop, t(value)) + except KeyError: + return setattr(self, prop, value) + + def addProperty(self, prop: str, value: Any, t: Callable = str): + self._defaults[prop] = value + self._types[prop] = t + + def removeProperty(self, prop: str): + del self._defaults[prop] + del self._types[prop] + + def saveSettings(self, hasMessage=True): + for propName, _ in self._defaults.items(): + self.settings.setValue( + f"{self._prefix}{propName}", self.getProperty(propName) + ) + if hasMessage: + BasePopup("Save Settings", "Configuration has been saved.").exec() + + def loadSettings(self): + for propName, propDefault in self._defaults.items(): + prop = self.settings.value(f"{self._prefix}{propName}", propDefault) + self.setProperty(propName, prop) + + def confirmResetSettings(self): + confirm = BasePopup( + "Reset Settings", + "Are you sure? This will delete the current configuration.", + BasePopup.Ok | BasePopup.Cancel, + ) + response = confirm.exec() + if response == BasePopup.Ok: + self.resetSettings() + + def resetSettings(self): + try: + self.settings.clear() + except Exception: + pass + self.loadSettings() diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index 9c0f3c1..882941a 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -23,8 +23,10 @@ from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView, QMainWindow) from components.services import BaseWorker +from components.settings import BaseSettings +from utils.constants import IMAGE_VIEW_DEFAULT, IMAGE_VIEW_TYPES -class BaseImageView(QGraphicsView): +class BaseImageView(QGraphicsView, BaseSettings): """ Base image view to allow view/zoom/pan functions """ @@ -37,9 +39,6 @@ def __init__(self, parent: QMainWindow, tracker=None): self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self._viewImageMode = parent.config["VIEW_IMAGE_MODE"] - self._splitViewMode = parent.config["SPLIT_VIEW_MODE"] - self._zoomPanMode = False self.currentScale = 0 self._scrollAtMin = 0 @@ -48,8 +47,30 @@ def __init__(self, parent: QMainWindow, tracker=None): self._trackPadAtMax = 0 self._scrollSuppressed = False + self.setDefaults(IMAGE_VIEW_DEFAULT) + self.setTypes(IMAGE_VIEW_TYPES) + self.loadSettings() + self.initializePixmapItem() +# ------------------------------------ Settings ------------------------------------- # + + def setViewImageMode(self, mode: int): + # TODO: This should be an enum not an int + self.setProperty('viewImageMode', mode) + self.saveSettings(hasMessage=False) + self.viewImage() + + def toggleSplitView(self): + self.setProperty('splitViewMode', "false" if self.splitViewMode else "true") + self.saveSettings(hasMessage=False) + + def toggleZoomPanMode(self): + self.setProperty('zoomPanMode', "false" if self.zoomPanMode else "true") + self.saveSettings(hasMessage=False) + +# -------------------------------------- View --------------------------------------- # + def initializePixmapItem(self): self.setScene(QGraphicsScene()) self.pixmap = self.scene().addPixmap(self.tracker.pixImage.scaledToWidth( @@ -60,31 +81,17 @@ def viewImage(self): self.currentScale = 0 w = self.viewport().geometry().width() h = self.viewport().geometry().height() - if self._viewImageMode == 0: + if self.viewImageMode == 0: self.pixmap.setPixmap( self.tracker.pixImage.scaledToWidth(w, Qt.SmoothTransformation)) - elif self._viewImageMode == 1: + elif self.viewImageMode == 1: self.pixmap.setPixmap( self.tracker.pixImage.scaledToHeight(h, Qt.SmoothTransformation)) - elif self._viewImageMode == 2: + elif self.viewImageMode == 2: self.pixmap.setPixmap(self.tracker.pixImage.scaled( w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)) self.scene().setSceneRect(QRectF(self.pixmap.pixmap().rect())) - # TODO: Cloe settings component might be useful - def setViewImageMode(self, mode): - self._viewImageMode = mode - self.parent.config["VIEW_IMAGE_MODE"] = mode - self.parent.config["SELECTED_INDEX"]['imageScaling'] = mode - self.viewImage() - - def splitViewMode(self): - return self._splitViewMode - - def toggleSplitView(self): - self._splitViewMode = not self._splitViewMode - self.parent.config["SPLIT_VIEW_MODE"] = self._splitViewMode - def zoomView(self, isZoomIn): if isZoomIn and self.currentScale < 8: factor = 1.25 @@ -95,16 +102,13 @@ def zoomView(self, isZoomIn): self.currentScale -= 1 self.scale(factor, factor) - def toggleZoomPanMode(self): - self._zoomPanMode = not self._zoomPanMode - def resizeEvent(self, event): self.viewImage() super().resizeEvent(event) def wheelEvent(self, event): pressedKey = QApplication.keyboardModifiers() - zoomMode = pressedKey == Qt.ControlModifier or self._zoomPanMode + zoomMode = pressedKey == Qt.ControlModifier or self.zoomPanMode # self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) # TODO: Rewrite individual event handlers as separate functions @@ -172,7 +176,7 @@ def suppressScroll(): def mouseMoveEvent(self, event): pressedKey = QApplication.keyboardModifiers() - panMode = pressedKey == Qt.ControlModifier or self._zoomPanMode + panMode = pressedKey == Qt.ControlModifier or self.zoomPanMode if panMode: self.setDragMode(QGraphicsView.ScrollHandDrag) @@ -186,6 +190,8 @@ def mouseDoubleClickEvent(self, event): self.viewImage(self.currentScale) super().mouseDoubleClickEvent(event) +# ------------------------------------ Shortcut ------------------------------------- # + # TODO: Keyboard shortcuts should be in another class def keyPressEvent(self, event): if event.key() == Qt.Key_Left: diff --git a/app/components/views/main.py b/app/components/views/main.py index 1e07012..1d1f096 100644 --- a/app/components/views/main.py +++ b/app/components/views/main.py @@ -92,10 +92,10 @@ def loadImageAtIndex(self): self.explorer.setCurrentIndex(index) def zoomIn(self): - self.canvas.zoomView(True, usingButton=True) + self.canvas.zoomView(True) def zoomOut(self): - self.canvas.zoomView(False, usingButton=True) + self.canvas.zoomView(False) def resizeEvent(self, event): self.explorer.setMinimumWidth(0.1*self.width()) diff --git a/app/utils/constants.py b/app/utils/constants.py index e40a4d9..c6ed233 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -28,6 +28,22 @@ EXPLORER_ROOT_DEFAULT = './assets/images/' +# ------------------------------------ Settings ------------------------------------- # + +SETTINGS_FILE_DEFAULT = './utils/poricom-config.ini' + +# View +IMAGE_VIEW_DEFAULT = { + "viewImageMode": 0, + "splitViewMode": "false", + "zoomPanMode": "false" +} +IMAGE_VIEW_TYPES = { + "viewImageMode": int, + "splitViewMode": bool, + "zoomPanMode": bool +} + # --------------------------------------- UI ---------------------------------------- # # Main view From e8fc5283dde855ad3040fa4b17337f84603757c1 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 00:22:28 +0800 Subject: [PATCH 074/137] Fix issue where external capture is not showing Fix resizeEvent conflict with OCRView, inherits base ocr and image views --- app/components/views/main.py | 4 +-- app/components/views/ocr/__init__.py | 2 +- app/components/views/ocr/base.py | 16 ++++++------ app/components/views/ocr/fullscreen.py | 12 ++++++--- app/components/views/ocr/ocr.py | 34 ++++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 app/components/views/ocr/ocr.py diff --git a/app/components/views/main.py b/app/components/views/main.py index 1d1f096..fd4b950 100644 --- a/app/components/views/main.py +++ b/app/components/views/main.py @@ -20,7 +20,7 @@ from PyQt5.QtCore import (Qt) from PyQt5.QtWidgets import (QInputDialog, QMainWindow, QSplitter) -from .ocr import BaseOCRView +from .ocr import OCRView from components.explorers import ImageExplorer from utils.config import config from utils.constants import MAIN_VIEW_RATIO @@ -32,7 +32,7 @@ def __init__(self, parent: QMainWindow, tracker=None): self.tracker = tracker self.config = config - self.canvas = BaseOCRView(self, self.tracker) + self.canvas = OCRView(self, self.tracker) self.explorer = ImageExplorer(self, self.tracker.filepath) self.addWidget(self.explorer) diff --git a/app/components/views/ocr/__init__.py b/app/components/views/ocr/__init__.py index e26fef0..a39b64a 100644 --- a/app/components/views/ocr/__init__.py +++ b/app/components/views/ocr/__init__.py @@ -17,5 +17,5 @@ along with this program. If not, see . """ -from .base import BaseOCRView from .fullscreen import FullScreenOCRView +from .ocr import OCRView diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 382f38b..34a2199 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -1,5 +1,5 @@ """ -Poricom View Components +Poricom Views Copyright (C) `2021-2022` `` @@ -20,12 +20,11 @@ from PyQt5.QtCore import (pyqtSlot, Qt, QThreadPool, QTimer) from PyQt5.QtWidgets import (QGraphicsView, QLabel, QMainWindow) -from ..image import BaseImageView from components.services import BaseWorker from utils.image_io import logText, pixboxToText -class BaseOCRView(BaseImageView): +class BaseOCRView(QGraphicsView): """Base view with OCR capabilities Args: @@ -34,7 +33,8 @@ class BaseOCRView(BaseImageView): """ def __init__(self, parent: QMainWindow, tracker=None): # TODO: Remove references to tracker - super().__init__(parent, tracker) + super().__init__(parent) + self.tracker = tracker self.timer = QTimer() self.timer.setInterval(300) @@ -59,6 +59,10 @@ def handleTextFinished(self): self.canvasText.adjustSize() except RuntimeError: pass + try: + self.timer.timeout.connect(self.rubberBandStopped) + except TypeError: + pass @pyqtSlot() def rubberBandStopped(self): @@ -75,8 +79,6 @@ def rubberBandStopped(self): worker.signals.result.connect(self.handleTextResult) worker.signals.finished.connect(self.handleTextFinished) self.timer.timeout.disconnect(self.rubberBandStopped) - worker.signals.finished.connect( - lambda: self.timer.timeout.connect(self.rubberBandStopped)) QThreadPool.globalInstance().start(worker) def mouseMoveEvent(self, event): @@ -91,7 +93,7 @@ def mouseReleaseEvent(self, event): text = self.canvasText.text() logText(text, mode=logToFile, path=logPath) try: - if not self.parent.config["PERSIST_TEXT_MODE"]: + if not self.parent().config["PERSIST_TEXT_MODE"]: self.canvasText.hide() except AttributeError: pass diff --git a/app/components/views/ocr/fullscreen.py b/app/components/views/ocr/fullscreen.py index 2ea93cb..37f36f2 100644 --- a/app/components/views/ocr/fullscreen.py +++ b/app/components/views/ocr/fullscreen.py @@ -18,7 +18,7 @@ """ from PyQt5.QtCore import (Qt, QRectF) -from PyQt5.QtWidgets import (QApplication, QMainWindow) +from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QMainWindow) from PyQt5.QtGui import QCursor, QMouseEvent from .base import BaseOCRView @@ -30,15 +30,19 @@ class FullScreenOCRView(BaseOCRView): """ def __init__(self, parent: QMainWindow, tracker=None): super().__init__(parent, tracker) + self.externalWindow = parent + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setScene(QGraphicsScene()) + def takeScreenshot(self, screenIndex: int): screen = QApplication.screens()[screenIndex] s = screen.size() - self.pixmap.setPixmap(screen.grabWindow( + self.pixmap = self.scene().addPixmap(screen.grabWindow( 0).scaled(s.width(), s.height())) - self.scene.setSceneRect(QRectF(self.pixmap.pixmap().rect())) + self.scene().setSceneRect(QRectF(self.pixmap.pixmap().rect())) def getActiveScreenIndex(self): cursor = QCursor.pos() @@ -46,4 +50,4 @@ def getActiveScreenIndex(self): def mouseReleaseEvent(self, event: QMouseEvent): super().mouseReleaseEvent(event) - self.parent.close() + self.externalWindow.close() diff --git a/app/components/views/ocr/ocr.py b/app/components/views/ocr/ocr.py new file mode 100644 index 0000000..fd830a6 --- /dev/null +++ b/app/components/views/ocr/ocr.py @@ -0,0 +1,34 @@ +""" +Poricom Views + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (pyqtSlot) +from PyQt5.QtWidgets import (QMainWindow) + +from ..image import BaseImageView +from .base import BaseOCRView + + +class OCRView(BaseImageView, BaseOCRView): + def __init__(self, parent: QMainWindow, tracker=None): + # TODO: Remove references to tracker + super().__init__(parent, tracker) + + @pyqtSlot() + def rubberBandStopped(self): + super().rubberBandStopped() From 1bdc97a0daaa37292271e4772db2681353d66543 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 00:24:56 +0800 Subject: [PATCH 075/137] Rename MainView to WorkspaceView Avoid confusion on main app file and main view --- app/MainWindow.py | 4 ++-- app/components/views/__init__.py | 2 +- app/components/views/{main.py => workspace.py} | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) rename app/components/views/{main.py => workspace.py} (96%) diff --git a/app/MainWindow.py b/app/MainWindow.py index 75c168f..523efb2 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -30,7 +30,7 @@ from utils.config import config, saveOnClose from components.services import BaseWorker from components.toolbar import BaseToolbar -from components.views import MainView, FullScreenOCRView +from components.views import WorkspaceView, FullScreenOCRView from Popups import (FontPicker, LanguagePicker, ScaleImagePicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) @@ -54,7 +54,7 @@ def __init__(self, parent=None, tracker=None): self.vLayout = QVBoxLayout() - self.mainView = MainView(self, self.tracker) + self.mainView = WorkspaceView(self, self.tracker) self.ribbon = BaseToolbar(self) self.vLayout.addWidget(self.ribbon) diff --git a/app/components/views/__init__.py b/app/components/views/__init__.py index 963217c..bc908b8 100644 --- a/app/components/views/__init__.py +++ b/app/components/views/__init__.py @@ -17,5 +17,5 @@ along with this program. If not, see . """ -from .main import MainView +from .workspace import WorkspaceView from .ocr import FullScreenOCRView diff --git a/app/components/views/main.py b/app/components/views/workspace.py similarity index 96% rename from app/components/views/main.py rename to app/components/views/workspace.py index fd4b950..2806fdb 100644 --- a/app/components/views/main.py +++ b/app/components/views/workspace.py @@ -25,8 +25,10 @@ from utils.config import config from utils.constants import MAIN_VIEW_RATIO -class MainView(QSplitter): - +class WorkspaceView(QSplitter): + """ + Main view of the program. Includes the explorer and the view. + """ def __init__(self, parent: QMainWindow, tracker=None): super().__init__(parent) self.tracker = tracker From 25db697e9fe2aac43cd0cd8415fc57b43d482b2b Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 00:30:14 +0800 Subject: [PATCH 076/137] Update parenting behavior in image view Remove manual parenting since it is not needed in QSplitter --- app/components/views/image/base.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index 882941a..7ceab38 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -18,22 +18,25 @@ """ from time import sleep +from typing import TYPE_CHECKING from PyQt5.QtCore import (Qt, QRect, QRectF, QSize, QThreadPool) -from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView, QMainWindow) +from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView, QSplitter) from components.services import BaseWorker from components.settings import BaseSettings from utils.constants import IMAGE_VIEW_DEFAULT, IMAGE_VIEW_TYPES +if TYPE_CHECKING: + from ..workspace import WorkspaceView + class BaseImageView(QGraphicsView, BaseSettings): """ Base image view to allow view/zoom/pan functions """ - def __init__(self, parent: QMainWindow, tracker=None): + def __init__(self, parent: 'WorkspaceView', tracker=None): super().__init__(parent) - self.parent = parent self.tracker = tracker self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) @@ -139,7 +142,7 @@ def suppressScroll(): self.verticalScrollBar().value() == self.verticalScrollBar().maximum()): if (event.angleDelta().y() > -wheelDelta): if (self._trackPadAtMax == trackpadScrollLimit): - self.parent.loadNextImage() + self.parent().loadNextImage() self._trackPadAtMax = 0 suppressScroll() return @@ -147,7 +150,7 @@ def suppressScroll(): self._trackPadAtMax += 1 elif (event.angleDelta().y() <= -wheelDelta): if (self._scrollAtMax == mouseScrollLimit): - self.parent.loadNextImage() + self.parent().loadNextImage() self._scrollAtMax = 0 suppressScroll() return @@ -158,7 +161,7 @@ def suppressScroll(): self.verticalScrollBar().value() == self.verticalScrollBar().minimum()): if (event.angleDelta().y() < wheelDelta): if (self._trackPadAtMin == trackpadScrollLimit): - self.parent.loadPrevImage() + self.parent().loadPrevImage() self._trackPadAtMin = 0 suppressScroll() return @@ -166,7 +169,7 @@ def suppressScroll(): self._trackPadAtMin += 1 elif (event.angleDelta().y() >= wheelDelta): if (self._scrollAtMin == mouseScrollLimit): - self.parent.loadPrevImage() + self.parent().loadPrevImage() self._scrollAtMin = 0 suppressScroll() return @@ -195,10 +198,10 @@ def mouseDoubleClickEvent(self, event): # TODO: Keyboard shortcuts should be in another class def keyPressEvent(self, event): if event.key() == Qt.Key_Left: - self.parent.loadPrevImage() + self.parent().loadPrevImage() return if event.key() == Qt.Key_Right: - self.parent.loadNextImage() + self.parent().loadNextImage() return if event.key() == Qt.Key_Minus: self.zoomView(isZoomIn=False) From 1d268ac9a457b86fc5ef04ce87848f037d601805 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 00:39:43 +0800 Subject: [PATCH 077/137] Update base ocr to inherit from settings Remove dependency on parent config file --- app/components/settings/base.py | 1 + app/components/views/ocr/base.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/components/settings/base.py b/app/components/settings/base.py index e053c15..8f069b1 100644 --- a/app/components/settings/base.py +++ b/app/components/settings/base.py @@ -78,6 +78,7 @@ def setProperty(self, prop: str, value: Any): def addProperty(self, prop: str, value: Any, t: Callable = str): self._defaults[prop] = value self._types[prop] = t + self.setProperty(prop, value) def removeProperty(self, prop: str): del self._defaults[prop] diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 34a2199..0cfe090 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -21,10 +21,11 @@ from PyQt5.QtWidgets import (QGraphicsView, QLabel, QMainWindow) from components.services import BaseWorker +from components.settings import BaseSettings from utils.image_io import logText, pixboxToText -class BaseOCRView(QGraphicsView): +class BaseOCRView(QGraphicsView, BaseSettings): """Base view with OCR capabilities Args: @@ -48,6 +49,8 @@ def __init__(self, parent: QMainWindow, tracker=None): self.setDragMode(QGraphicsView.RubberBandDrag) + self.addProperty('persistTextMode', "false", bool) + def handleTextResult(self, result): try: self.canvasText.setText(result) @@ -93,7 +96,7 @@ def mouseReleaseEvent(self, event): text = self.canvasText.text() logText(text, mode=logToFile, path=logPath) try: - if not self.parent().config["PERSIST_TEXT_MODE"]: + if not self.persistTextMode: self.canvasText.hide() except AttributeError: pass From 0d0fb7591ae95fe0656cbade4e2ccfcf1e01731f Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 16:37:50 +0800 Subject: [PATCH 078/137] Refactor utils file structure --- app/MainWindow.py | 2 +- app/components/views/ocr/base.py | 13 ++-- app/utils/constants.py | 2 + app/utils/scripts/__init__.py | 22 +++++++ app/utils/scripts/logText.py | 35 ++++++++++ .../mangaFileToImageDir.py} | 59 +++-------------- app/utils/scripts/pixmapToText.py | 65 +++++++++++++++++++ 7 files changed, 139 insertions(+), 59 deletions(-) create mode 100644 app/utils/scripts/__init__.py create mode 100644 app/utils/scripts/logText.py rename app/utils/{image_io.py => scripts/mangaFileToImageDir.py} (55%) create mode 100644 app/utils/scripts/pixmapToText.py diff --git a/app/MainWindow.py b/app/MainWindow.py index 523efb2..0c07206 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -26,8 +26,8 @@ from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QDesktopWidget, QMainWindow, QApplication, QPushButton, QFileDialog) -from utils.image_io import mangaFileToImageDir from utils.config import config, saveOnClose +from utils.scripts import mangaFileToImageDir from components.services import BaseWorker from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 0cfe090..69f1a37 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -22,8 +22,7 @@ from components.services import BaseWorker from components.settings import BaseSettings -from utils.image_io import logText, pixboxToText - +from utils.scripts import logText, pixmapToText class BaseOCRView(QGraphicsView, BaseSettings): """Base view with OCR capabilities @@ -75,10 +74,10 @@ def rubberBandStopped(self): self.canvasText.adjustSize() self.canvasText.show() - lang = self.tracker.language + self.tracker.orientation + language = self.tracker.language + self.tracker.orientation pixbox = self.grab(self.rubberBandRect()) - worker = BaseWorker(pixboxToText, pixbox, lang, self.tracker.ocrModel) + worker = BaseWorker(pixmapToText, pixbox, language, self.tracker.ocrModel) worker.signals.result.connect(self.handleTextResult) worker.signals.finished.connect(self.handleTextFinished) self.timer.timeout.disconnect(self.rubberBandStopped) @@ -91,10 +90,10 @@ def mouseMoveEvent(self, event): super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): - logPath = self.tracker.filepath + "/log.txt" - logToFile = self.tracker.writeMode + logPath = self.tracker.filepath + "/text-log.txt" + isLogFile = self.tracker.writeMode text = self.canvasText.text() - logText(text, mode=logToFile, path=logPath) + logText(text, isLogFile=isLogFile, path=logPath) try: if not self.persistTextMode: self.canvasText.hide() diff --git a/app/utils/constants.py b/app/utils/constants.py index c6ed233..10ec6a3 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -23,6 +23,8 @@ IMAGE_EXTENSIONS = ["*.bmp", "*.gif", "*.jpeg", "*.jpg", "*.pbm", "*.pgm", "*.png", "*.ppm", "*.webp", "*.xbm", "*.xpm",] # Paths +TESSERACT_LANGUAGES = "./assets/languages/" + TOOLBAR_ICONS = './assets/images/icons/' TOOLBAR_ICON_DEFAULT = './assets/images/icons/default_icon.png' diff --git a/app/utils/scripts/__init__.py b/app/utils/scripts/__init__.py new file mode 100644 index 0000000..5b5328c --- /dev/null +++ b/app/utils/scripts/__init__.py @@ -0,0 +1,22 @@ +""" +Poricom Helper Functions + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .logText import logText +from .mangaFileToImageDir import mangaFileToImageDir +from .pixmapToText import pixmapToText diff --git a/app/utils/scripts/logText.py b/app/utils/scripts/logText.py new file mode 100644 index 0000000..08e5705 --- /dev/null +++ b/app/utils/scripts/logText.py @@ -0,0 +1,35 @@ +""" +Poricom Helper Functions + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtGui import QGuiApplication + +def logText(text: str, isLogFile: bool=False, path: str="."): + """Log text by copying to clipboard + + Args: + text (str): Text to log. + isLogFile (bool, optional): Set flag to save copied text to clipboard. Defaults to False. + path (str, optional): Path to log file. Defaults to ".". + """ + clipboard = QGuiApplication.clipboard() + clipboard.setText(text) + + if isLogFile: + with open(path, 'a', encoding="utf-8") as fh: + fh.write(text + "\n") diff --git a/app/utils/image_io.py b/app/utils/scripts/mangaFileToImageDir.py similarity index 55% rename from app/utils/image_io.py rename to app/utils/scripts/mangaFileToImageDir.py index 73ba8d8..4f55fcc 100644 --- a/app/utils/image_io.py +++ b/app/utils/scripts/mangaFileToImageDir.py @@ -1,5 +1,5 @@ """ -Poricom Image Processing Utility +Poricom Helper Functions Copyright (C) `2021-2022` `` @@ -17,26 +17,23 @@ along with this program. If not, see . """ -from io import BytesIO from os.path import splitext, basename from pathlib import Path -from PyQt5.QtCore import QBuffer -from PyQt5.QtGui import QGuiApplication -from PIL import Image import zipfile import rarfile import pdf2image -try: - from tesserocr import PyTessBaseAPI -except UnicodeDecodeError: - pass -from utils.config import config +def mangaFileToImageDir(filepath: str): + """Converts a manga file to a directory of images + Args: + filepath (str): Path to manga file. -def mangaFileToImageDir(filepath): + Returns: + str: Path to directory of images. + """ extractPath, extension = splitext(filepath) cachePath = f"./poricom_cache/{basename(extractPath)}" @@ -62,43 +59,3 @@ def mangaFileToImageDir(filepath): f"{cachePath}/{i+1}_{filename}.png", 'PNG') return cachePath - - -def pixboxToText(pixmap, lang="jpn_vert", model=None): - - buffer = QBuffer() - buffer.open(QBuffer.ReadWrite) - pixmap.save(buffer, "PNG") - bytes = BytesIO(buffer.data()) - - if bytes.getbuffer().nbytes == 0: - return - - pillowImage = Image.open(bytes) - text = "" - - if model is not None: - text = model(pillowImage) - - # PSM = 1 works most of the time except on smaller bounding boxes. - # By smaller, we mean textboxes with less text. Usually these - # boxes have at most one vertical line of text. - else: - try: - with PyTessBaseAPI(path=config["LANG_PATH"], lang=lang, oem=1, psm=1) as api: - api.SetImage(pillowImage) - text = api.GetUTF8Text() - except NameError: - # PyTessBaseAPI is undefined and there is no fallback model - return None - - return text.strip() - - -def logText(text, mode=False, path="."): - clipboard = QGuiApplication.clipboard() - clipboard.setText(text) - - if mode: - with open(path, 'a', encoding="utf-8") as fh: - fh.write(text + "\n") diff --git a/app/utils/scripts/pixmapToText.py b/app/utils/scripts/pixmapToText.py new file mode 100644 index 0000000..e8df06a --- /dev/null +++ b/app/utils/scripts/pixmapToText.py @@ -0,0 +1,65 @@ +""" +Poricom Helper Functions + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from io import BytesIO +from typing import Optional + +from manga_ocr import MangaOcr +from PIL import Image +from PyQt5.QtCore import QBuffer +from PyQt5.QtGui import QPixmap +try: + from tesserocr import PyTessBaseAPI +except UnicodeDecodeError: + pass + +from ..constants import TESSERACT_LANGUAGES + + +def pixmapToText(pixmap: QPixmap, language: str = "jpn_vert", model: Optional[MangaOcr] = None) -> str: + """ + Convert QPixmap object to text using the model + """ + + buffer = QBuffer() + buffer.open(QBuffer.ReadWrite) + pixmap.save(buffer, "PNG") + bytes = BytesIO(buffer.data()) + + if bytes.getbuffer().nbytes == 0: + return "" + + pillowImage = Image.open(bytes) + text = "" + + if model is not None: + text = model(pillowImage) + + # PSM = 1 works most of the time except on smaller bounding boxes. + # By smaller, we mean textboxes with less text. Usually these + # boxes have at most one vertical line of text. + else: + try: + with PyTessBaseAPI(path=TESSERACT_LANGUAGES, lang=language, oem=1, psm=1) as api: + api.SetImage(pillowImage) + text = api.GetUTF8Text() + except NameError: + return None + + return text.strip() \ No newline at end of file From 7c2774852327bb1f752032850a148763c66be630 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 16:39:05 +0800 Subject: [PATCH 079/137] Move event filters to services --- app/components/services/__init__.py | 1 + app/components/services/filters.py | 30 +++++++++++++++++++++++++++++ app/main.py | 3 ++- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 app/components/services/filters.py diff --git a/app/components/services/__init__.py b/app/components/services/__init__.py index 96e5c75..28818b1 100644 --- a/app/components/services/__init__.py +++ b/app/components/services/__init__.py @@ -16,4 +16,5 @@ along with this program. If not, see . """ +from .filters import WinEventFilter from .workers import BaseWorker, BaseWorkerSignal diff --git a/app/components/services/filters.py b/app/components/services/filters.py new file mode 100644 index 0000000..3504795 --- /dev/null +++ b/app/components/services/filters.py @@ -0,0 +1,30 @@ +""" +Poricom Services + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (QAbstractNativeEventFilter) + + +class WinEventFilter(QAbstractNativeEventFilter): + def __init__(self, keybinder): + self.keybinder = keybinder + super().__init__() + + def nativeEventFilter(self, eventType, message): + ret = self.keybinder.handler(eventType, message) + return ret, 0 \ No newline at end of file diff --git a/app/main.py b/app/main.py index 6f122c4..1936c16 100644 --- a/app/main.py +++ b/app/main.py @@ -23,7 +23,8 @@ from PyQt5.QtCore import QAbstractEventDispatcher from pyqtkeybind import keybinder -from MainWindow import MainWindow, WinEventFilter +from components.services import WinEventFilter +from MainWindow import MainWindow from Trackers import Tracker from utils.config import config From b2f3c101cb729bf6754988827d94138d5df65a56 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 16:42:50 +0800 Subject: [PATCH 080/137] Use huggingface for connection error handling --- app/MainWindow.py | 78 ++++++++++-------------------------------- app/utils/constants.py | 8 +++++ 2 files changed, 27 insertions(+), 59 deletions(-) diff --git a/app/MainWindow.py b/app/MainWindow.py index 0c07206..1ac1c39 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -1,5 +1,5 @@ """ -Poricom Main Window Component +Poricom Windows Copyright (C) `2021-2022` `` @@ -22,27 +22,18 @@ import toml from manga_ocr import MangaOcr -from PyQt5.QtCore import (Qt, QAbstractNativeEventFilter, QThreadPool) +from PyQt5.QtCore import (Qt, QThreadPool) from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QDesktopWidget, QMainWindow, QApplication, QPushButton, QFileDialog) -from utils.config import config, saveOnClose -from utils.scripts import mangaFileToImageDir from components.services import BaseWorker from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView from Popups import (FontPicker, LanguagePicker, ScaleImagePicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) - - -class WinEventFilter(QAbstractNativeEventFilter): - def __init__(self, keybinder): - self.keybinder = keybinder - super().__init__() - - def nativeEventFilter(self, eventType, message): - ret = self.keybinder.handler(eventType, message) - return ret, 0 +from utils.config import config, saveOnClose +from utils.constants import LOAD_MODEL_MESSAGE +from utils.scripts import mangaFileToImageDir class MainWindow(QMainWindow): @@ -229,11 +220,7 @@ def loadModel(self): if loadModelButton.isChecked() and self.config["LOAD_MODEL_POPUP"]: confirmation = CheckboxPopup( "Load the MangaOCR model?", - "If you are running this for the first time, this will " + - "download the MangaOcr model which is about 400 MB in size. " + - "This will improve the accuracy of Japanese text detection " + - "in Poricom. If it is already in your cache, it will take a " + - "few seconds to load the model.", + LOAD_MODEL_MESSAGE, MessagePopup.Ok | MessagePopup.Cancel ) ret = confirmation.exec() @@ -247,56 +234,29 @@ def loadModel(self): def loadModelHelper(tracker): betterOCR = tracker.switchOCRMode() if betterOCR: - import http.client as httplib - - def isConnected(url=self.config["CHECK_INTERNET_URL"]): - if not self.config["CHECK_INTERNET_POPUP"]: - return True - connection = httplib.HTTPSConnection(url, timeout=2) - try: - connection.request("HEAD", "/") - return True - except Exception: - tracker.switchOCRMode() - return False - finally: - connection.close() - - connected = isConnected() - if connected: - try: - tracker.ocrModel = MangaOcr() - except ValueError: - return (betterOCR, False) - return (betterOCR, connected) + try: + tracker.ocrModel = MangaOcr() + return "success" + except Exception as e: + tracker.switchOCRMode() + return str(e) else: tracker.ocrModel = None - return (betterOCR, True) + return "success" - def modelLoadedConfirmation(typeConnectionTuple): - usingMangaOCR, connected = typeConnectionTuple - modelName = "MangaOCR" if usingMangaOCR else "Tesseract" - if connected: + def loadModelConfirm(message: str): + modelName = "MangaOCR" if self.tracker.ocrModel else "Tesseract" + if message == "success": MessagePopup( f"{modelName} model loaded", f"You are now using the {modelName} model for Japanese text detection." ).exec() - - elif not connected: - connectionErrorMessage = CheckboxPopup( - "Connection Error", - "Please try again or make sure your Internet connection is on.", - checkboxMessage=( - "Check this box if you keep getting this error even with connection on." - ) - ) - connectionErrorMessage.exec() - self.config["CHECK_INTERNET_POPUP"] = \ - not connectionErrorMessage.checkBox().isChecked() + else: + MessagePopup("Load Model Error", message).exec() loadModelButton.setChecked(False) worker = BaseWorker(loadModelHelper, self.tracker) - worker.signals.result.connect(modelLoadedConfirmation) + worker.signals.result.connect(loadModelConfirm) worker.signals.finished.connect(lambda: loadModelButton.setEnabled(True)) diff --git a/app/utils/constants.py b/app/utils/constants.py index 10ec6a3..b60d309 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -30,6 +30,14 @@ EXPLORER_ROOT_DEFAULT = './assets/images/' +# Messages +LOAD_MODEL_MESSAGE = ( + "If you are running this for the first time, this will download the MangaOcr model" + "which is about 400 MB in size. This will improve the accuracy of Japanese text" + "detection in Poricom. If it is already in your cache, it will take a few seconds" + "to load the model." +) + # ------------------------------------ Settings ------------------------------------- # SETTINGS_FILE_DEFAULT = './utils/poricom-config.ini' From 6270d06ce822f0a77267811a596d29f58e03e827 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 22:44:35 +0800 Subject: [PATCH 081/137] Refactor options to use BaseSettings Remove widget dependency on config file --- app/MainWindow.py | 7 ++- app/Popups.py | 45 -------------- app/components/settings/__init__.py | 1 + app/components/settings/base.py | 12 ++++ app/components/settings/popups/__init__.py | 21 +++++++ app/components/settings/popups/base.py | 68 +++++++++++++++++++++ app/components/settings/popups/container.py | 44 +++++++++++++ app/components/settings/popups/tesseract.py | 54 ++++++++++++++++ app/utils/constants.py | 10 +++ 9 files changed, 215 insertions(+), 47 deletions(-) create mode 100644 app/components/settings/popups/__init__.py create mode 100644 app/components/settings/popups/base.py create mode 100644 app/components/settings/popups/container.py create mode 100644 app/components/settings/popups/tesseract.py diff --git a/app/MainWindow.py b/app/MainWindow.py index 1ac1c39..a7827bb 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -27,9 +27,10 @@ QPushButton, QFileDialog) from components.services import BaseWorker +from components.settings import OptionsContainer, TesseractOptions from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView -from Popups import (FontPicker, LanguagePicker, ScaleImagePicker, +from Popups import (FontPicker, ScaleImagePicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) from utils.config import config, saveOnClose from utils.constants import LOAD_MODEL_MESSAGE @@ -264,8 +265,10 @@ def loadModelConfirm(message: str): loadModelButton.setEnabled(False) def modifyTesseract(self): - confirmation = PickerPopup(LanguagePicker(self, self.tracker)) + confirmation = OptionsContainer(TesseractOptions(self)) confirmation.exec() + if confirmation: + self.canvas.loadSettings() def toggleLogging(self): self.tracker.switchWriteMode() diff --git a/app/Popups.py b/app/Popups.py index 6d1dfee..60e558e 100644 --- a/app/Popups.py +++ b/app/Popups.py @@ -71,51 +71,6 @@ def applySelections(self, selections): editSelectionConfig(index, selection) -class LanguagePicker(BasePicker): - def __init__(self, parent, tracker): - config = parent.config - listTop = config["LANGUAGE"] - listBot = config["ORIENTATION"] - optionLists = [listTop, listBot] - - super().__init__(parent, tracker, optionLists) - self.pickTop.currentIndexChanged.connect(self.changeLanguage) - self.pickTop.setCurrentIndex(config["SELECTED_INDEX"]["language"]) - self.nameTop.setText("Language: ") - self.pickBot.currentIndexChanged.connect(self.changeOrientation) - self.pickBot.setCurrentIndex(config["SELECTED_INDEX"]["orientation"]) - self.nameBot.setText("Orientation: ") - - self.languageIndex = self.pickTop.currentIndex() - self.orientationIndex = self.pickBot.currentIndex() - - def changeLanguage(self, i): - self.languageIndex = i - selectedLanguage = self.pickTop.currentText().strip() - if selectedLanguage == "Japanese": - self.tracker.language = "jpn" - if selectedLanguage == "Korean": - self.tracker.language = "kor" - if selectedLanguage == "Chinese SIM": - self.tracker.language = "chi_sim" - if selectedLanguage == "Chinese TRA": - self.tracker.language = "chi_tra" - if selectedLanguage == "English": - self.tracker.language = "eng" - - def changeOrientation(self, i): - self.orientationIndex = i - selectedOrientation = self.pickBot.currentText().strip() - if selectedOrientation == "Vertical": - self.tracker.orientation = "_vert" - if selectedOrientation == "Horizontal": - self.tracker.orientation = "" - - def applyChanges(self): - self.applySelections(['language', 'orientation']) - return True - - class FontPicker(BasePicker): def __init__(self, parent, tracker): config = parent.config diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py index 11facfd..74c43f9 100644 --- a/app/components/settings/__init__.py +++ b/app/components/settings/__init__.py @@ -18,3 +18,4 @@ """ from .base import BaseSettings +from .popups import OptionsContainer, TesseractOptions diff --git a/app/components/settings/base.py b/app/components/settings/base.py index 8f069b1..b0d7bf3 100644 --- a/app/components/settings/base.py +++ b/app/components/settings/base.py @@ -55,6 +55,12 @@ def setDefaults(self, defaults: dict[str, Any]): """ self._defaults = defaults + def addDefaults(self, defaults: dict[str, Any]): + """ + Extends the defaults dictionary + """ + self.setDefaults({**self._defaults, **defaults}) + def setTypes(self, types: dict[str, Callable]): """Set the types dictionary @@ -62,6 +68,12 @@ def setTypes(self, types: dict[str, Callable]): Use `self._types` to set the correct property type. """ self._types = types + + def setTypes(self, types: dict[str, Callable]): + """ + Extends the types dictionary + """ + self.setTypes({**self._types, **types}) def getProperty(self, prop: str): return getattr(self, prop) diff --git a/app/components/settings/popups/__init__.py b/app/components/settings/popups/__init__.py new file mode 100644 index 0000000..1f47a47 --- /dev/null +++ b/app/components/settings/popups/__init__.py @@ -0,0 +1,21 @@ +""" +Poricom Settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +# TODO: Most components here have no docstrings +from .container import OptionsContainer +from .tesseract import TesseractOptions diff --git a/app/components/settings/popups/base.py b/app/components/settings/popups/base.py new file mode 100644 index 0000000..b4d1ac8 --- /dev/null +++ b/app/components/settings/popups/base.py @@ -0,0 +1,68 @@ +""" +Poricom Settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from typing import Any, Callable + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import (QComboBox, QGridLayout, QLabel, QWidget) +from stringcase import titlecase, capitalcase + +from ..base import BaseSettings + + +class BaseOptions(BaseSettings): + def __init__(self, parent: QWidget, optionLists:list[list[str]]=[]): + super().__init__(parent) + self.setAttribute(Qt.WA_DeleteOnClose) + + self.setLayout(QGridLayout()) + self.layout().setContentsMargins(0, 0, 0, 0) + + self.comboBoxList: list[QComboBox] = [] + self.labelList: list[QLabel] = [] + + for i in range(len(optionLists)): + optionList = optionLists[i] + + self.comboBoxList.append(QComboBox()) + self.comboBoxList[i].addItems(optionList) + self.layout().addWidget(self.comboBoxList[i], i, 1) + self.labelList.append(QLabel("")) + self.layout().addWidget(self.labelList[i], i, 0) + + def setOptionIndex(self, option: str, index: int = 0): + optionIndex = self.settings.value(f"{option}Index", index, int) + comboBox = self.getProperty(f"{option}ComboBox") + comboBox.setCurrentIndex(optionIndex) + self.addProperty(f"{option}Index", optionIndex, int) + + def addOptionProperties(self, props: list[tuple[str, Any, Callable]]): + for i, p in enumerate(props): + # Property + prop, propDefault, propType = p + self.addProperty(prop, propDefault, propType) + + # Label + self.labelList[i].setText(f"{titlecase(prop)}: ") + + # Combo Box + comboBox = self.comboBoxList[i] + self.setProperty(f"{prop}ComboBox", comboBox) + comboBox.currentIndexChanged.connect(self.getProperty(f"change{capitalcase(prop)}")) + self.setOptionIndex(prop) \ No newline at end of file diff --git a/app/components/settings/popups/container.py b/app/components/settings/popups/container.py new file mode 100644 index 0000000..c770b59 --- /dev/null +++ b/app/components/settings/popups/container.py @@ -0,0 +1,44 @@ +""" +Poricom settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (Qt) +from PyQt5.QtWidgets import (QVBoxLayout, QDialog, QDialogButtonBox) + +from .base import BaseOptions + +class OptionsContainer(QDialog): + def __init__(self, options: BaseOptions): + super().__init__(None, Qt.WindowCloseButtonHint | Qt.WindowSystemMenuHint | Qt.WindowTitleHint) + self.options = options + self.setLayout(QVBoxLayout()) + self.layout().addWidget(options) + self.buttonBox = QDialogButtonBox( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.layout().addWidget(self.buttonBox) + + self.buttonBox.rejected.connect(self.cancelClickedEvent) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + def accept(self): + self.options.saveSettings(hasMessage=False) + return super().accept() + + def cancelClickedEvent(self): + self.close() diff --git a/app/components/settings/popups/tesseract.py b/app/components/settings/popups/tesseract.py new file mode 100644 index 0000000..23cea3f --- /dev/null +++ b/app/components/settings/popups/tesseract.py @@ -0,0 +1,54 @@ +""" +Poricom Popups + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import (Qt) +from PyQt5.QtWidgets import (QGridLayout, QVBoxLayout, QWidget, QLabel, QCheckBox, + QLineEdit, QComboBox, QDialog, QDialogButtonBox, QMessageBox) + +from utils.constants import LANGUAGE, ORIENTATION + +from .base import BaseOptions + +class TesseractOptions(BaseOptions): + def __init__(self, parent: QWidget): + super().__init__(parent, [LANGUAGE, ORIENTATION]) + # TODO: Use constants here + self.addOptionProperties([("language", "jpn", str), ("orientation", "_vert", str)]) + + def changeLanguage(self, i): + self.languageIndex = i + selectedLanguage = self.languageComboBox.currentText().strip() + if selectedLanguage == "Japanese": + self.language = "jpn" + if selectedLanguage == "Korean": + self.language = "kor" + if selectedLanguage == "Chinese SIM": + self.language = "chi_sim" + if selectedLanguage == "Chinese TRA": + self.language = "chi_tra" + if selectedLanguage == "English": + self.language = "eng" + + def changeOrientation(self, i): + self.orientationIndex = i + selectedOrientation = self.orientationComboBox.currentText().strip() + if selectedOrientation == "Vertical": + self.orientation = "_vert" + if selectedOrientation == "Horizontal": + self.orientation = "" diff --git a/app/utils/constants.py b/app/utils/constants.py index b60d309..b470827 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -22,6 +22,10 @@ IMAGE_EXTENSIONS = ["*.bmp", "*.gif", "*.jpeg", "*.jpg", "*.pbm", "*.pgm", "*.png", "*.ppm", "*.webp", "*.xbm", "*.xpm",] +# Settings +LANGUAGE = [ " Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English"] +ORIENTATION = [ " Vertical", " Horizontal"] + # Paths TESSERACT_LANGUAGES = "./assets/languages/" @@ -54,6 +58,12 @@ "zoomPanMode": bool } +# Tesseract +TESSERACT_DEFAULTS = { + "language": "jpn", + "orientation": "_vert" +} + # --------------------------------------- UI ---------------------------------------- # # Main view From 7799e37dfc5253f3832650e56de76caf0f2d55c7 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 22:46:34 +0800 Subject: [PATCH 082/137] Update view to read saved settings --- app/components/settings/base.py | 16 +++++++++++----- app/components/views/image/base.py | 4 ++-- app/components/views/ocr/base.py | 9 +++++---- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/components/settings/base.py b/app/components/settings/base.py index b0d7bf3..46c49f8 100644 --- a/app/components/settings/base.py +++ b/app/components/settings/base.py @@ -57,9 +57,12 @@ def setDefaults(self, defaults: dict[str, Any]): def addDefaults(self, defaults: dict[str, Any]): """ - Extends the defaults dictionary + Extends the defaults dictionary, if it exists """ - self.setDefaults({**self._defaults, **defaults}) + try: + self.setDefaults({**self._defaults, **defaults}) + except AttributeError: + self.setDefaults(defaults) def setTypes(self, types: dict[str, Callable]): """Set the types dictionary @@ -69,11 +72,14 @@ def setTypes(self, types: dict[str, Callable]): """ self._types = types - def setTypes(self, types: dict[str, Callable]): + def addTypes(self, types: dict[str, Callable]): """ - Extends the types dictionary + Extends the types dictionary, if it exists """ - self.setTypes({**self._types, **types}) + try: + self.setTypes({**self._types, **types}) + except AttributeError: + self.setTypes(types) def getProperty(self, prop: str): return getattr(self, prop) diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index 7ceab38..116fabf 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -50,8 +50,8 @@ def __init__(self, parent: 'WorkspaceView', tracker=None): self._trackPadAtMax = 0 self._scrollSuppressed = False - self.setDefaults(IMAGE_VIEW_DEFAULT) - self.setTypes(IMAGE_VIEW_TYPES) + self.addDefaults(IMAGE_VIEW_DEFAULT) + self.addTypes(IMAGE_VIEW_TYPES) self.loadSettings() self.initializePixmapItem() diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 69f1a37..120535f 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -22,6 +22,7 @@ from components.services import BaseWorker from components.settings import BaseSettings +from utils.constants import TESSERACT_DEFAULTS from utils.scripts import logText, pixmapToText class BaseOCRView(QGraphicsView, BaseSettings): @@ -48,6 +49,7 @@ def __init__(self, parent: QMainWindow, tracker=None): self.setDragMode(QGraphicsView.RubberBandDrag) + self.addDefaults(TESSERACT_DEFAULTS) self.addProperty('persistTextMode', "false", bool) def handleTextResult(self, result): @@ -68,16 +70,15 @@ def handleTextFinished(self): @pyqtSlot() def rubberBandStopped(self): - if (self.canvasText.isHidden()): self.canvasText.setText("") self.canvasText.adjustSize() self.canvasText.show() - language = self.tracker.language + self.tracker.orientation - pixbox = self.grab(self.rubberBandRect()) + language = self.language + self.orientation + pixmap = self.grab(self.rubberBandRect()) - worker = BaseWorker(pixmapToText, pixbox, language, self.tracker.ocrModel) + worker = BaseWorker(pixmapToText, pixmap, language, self.tracker.ocrModel) worker.signals.result.connect(self.handleTextResult) worker.signals.finished.connect(self.handleTextFinished) self.timer.timeout.disconnect(self.rubberBandStopped) From 48f89eff004ff59853e51b3f7eaf22303758c092 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 15 Jan 2023 22:48:39 +0800 Subject: [PATCH 083/137] Add docstrings to options module --- app/components/settings/popups/__init__.py | 2 +- app/components/settings/popups/base.py | 25 +++++++++++++++++++-- app/components/settings/popups/container.py | 11 +++++++++ app/components/settings/popups/tesseract.py | 6 ++--- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/app/components/settings/popups/__init__.py b/app/components/settings/popups/__init__.py index 1f47a47..d78e704 100644 --- a/app/components/settings/popups/__init__.py +++ b/app/components/settings/popups/__init__.py @@ -16,6 +16,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -# TODO: Most components here have no docstrings + from .container import OptionsContainer from .tesseract import TesseractOptions diff --git a/app/components/settings/popups/base.py b/app/components/settings/popups/base.py index b4d1ac8..9cb52e7 100644 --- a/app/components/settings/popups/base.py +++ b/app/components/settings/popups/base.py @@ -19,14 +19,17 @@ from typing import Any, Callable +from stringcase import titlecase, capitalcase from PyQt5.QtCore import Qt from PyQt5.QtWidgets import (QComboBox, QGridLayout, QLabel, QWidget) -from stringcase import titlecase, capitalcase from ..base import BaseSettings class BaseOptions(BaseSettings): + """ + Allows saving/selecting options + """ def __init__(self, parent: QWidget, optionLists:list[list[str]]=[]): super().__init__(parent) self.setAttribute(Qt.WA_DeleteOnClose) @@ -47,12 +50,28 @@ def __init__(self, parent: QWidget, optionLists:list[list[str]]=[]): self.layout().addWidget(self.labelList[i], i, 0) def setOptionIndex(self, option: str, index: int = 0): + """Set the combo box index based on the option name + + Args: + option (str): Option name in camelcase + index (int, optional): Combo box index. Defaults to 0. + """ optionIndex = self.settings.value(f"{option}Index", index, int) comboBox = self.getProperty(f"{option}ComboBox") comboBox.setCurrentIndex(optionIndex) self.addProperty(f"{option}Index", optionIndex, int) - def addOptionProperties(self, props: list[tuple[str, Any, Callable]]): + def initializeProperties(self, props: list[tuple[str, Any, Callable]]): + """Initialize property values and names + + Args: + props (list[tuple[str, Any, Callable]]): List of props. \ + Each prop must have the following format: (name, default, type). + It is recommended that the name is in camelcase. + + Note: + Child classes must implement change{PropName} method + """ for i, p in enumerate(props): # Property prop, propDefault, propType = p @@ -64,5 +83,7 @@ def addOptionProperties(self, props: list[tuple[str, Any, Callable]]): # Combo Box comboBox = self.comboBoxList[i] self.setProperty(f"{prop}ComboBox", comboBox) + + # Child classes must implement change{PropName} method comboBox.currentIndexChanged.connect(self.getProperty(f"change{capitalcase(prop)}")) self.setOptionIndex(prop) \ No newline at end of file diff --git a/app/components/settings/popups/container.py b/app/components/settings/popups/container.py index c770b59..c29a6a7 100644 --- a/app/components/settings/popups/container.py +++ b/app/components/settings/popups/container.py @@ -23,8 +23,15 @@ from .base import BaseOptions class OptionsContainer(QDialog): + """Dialog to contain option widgets + + Args: + options (BaseOptions): Child option widget + """ def __init__(self, options: BaseOptions): super().__init__(None, Qt.WindowCloseButtonHint | Qt.WindowSystemMenuHint | Qt.WindowTitleHint) + self.setAttribute(Qt.WA_DeleteOnClose) + self.options = options self.setLayout(QVBoxLayout()) self.layout().addWidget(options) @@ -42,3 +49,7 @@ def accept(self): def cancelClickedEvent(self): self.close() + + def closeEvent(self, event): + self.options.close() + return super().closeEvent(event) diff --git a/app/components/settings/popups/tesseract.py b/app/components/settings/popups/tesseract.py index 23cea3f..5ef0741 100644 --- a/app/components/settings/popups/tesseract.py +++ b/app/components/settings/popups/tesseract.py @@ -17,9 +17,7 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (Qt) -from PyQt5.QtWidgets import (QGridLayout, QVBoxLayout, QWidget, QLabel, QCheckBox, - QLineEdit, QComboBox, QDialog, QDialogButtonBox, QMessageBox) +from PyQt5.QtWidgets import (QWidget) from utils.constants import LANGUAGE, ORIENTATION @@ -29,7 +27,7 @@ class TesseractOptions(BaseOptions): def __init__(self, parent: QWidget): super().__init__(parent, [LANGUAGE, ORIENTATION]) # TODO: Use constants here - self.addOptionProperties([("language", "jpn", str), ("orientation", "_vert", str)]) + self.initializeProperties([("language", "jpn", str), ("orientation", "_vert", str)]) def changeLanguage(self, i): self.languageIndex = i From 3bb1119fff269467d2886f210d8a3698be7a1f3d Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 16:29:05 +0800 Subject: [PATCH 084/137] Refactor ImageScalingOptions to use BaseOptions --- app/MainWindow.py | 7 ++-- app/Popups.py | 22 ----------- app/components/settings/__init__.py | 2 +- app/components/settings/popups/__init__.py | 1 + app/components/settings/popups/base.py | 1 + .../settings/popups/imageScaling.py | 38 +++++++++++++++++++ app/utils/constants.py | 2 + 7 files changed, 46 insertions(+), 27 deletions(-) create mode 100644 app/components/settings/popups/imageScaling.py diff --git a/app/MainWindow.py b/app/MainWindow.py index a7827bb..a874dd2 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -27,11 +27,10 @@ QPushButton, QFileDialog) from components.services import BaseWorker -from components.settings import OptionsContainer, TesseractOptions +from components.settings import ImageScalingOptions, OptionsContainer, TesseractOptions from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView -from Popups import (FontPicker, ScaleImagePicker, - ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) +from Popups import (FontPicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) from utils.config import config, saveOnClose from utils.constants import LOAD_MODEL_MESSAGE from utils.scripts import mangaFileToImageDir @@ -192,7 +191,7 @@ def toggleSplitView(self): self.explorer.currentChanged(index, index) def scaleImage(self): - confirmation = PickerPopup(ScaleImagePicker(self, self.tracker)) + confirmation = OptionsContainer(ImageScalingOptions(self)) confirmation.exec() def hideExplorer(self): diff --git a/app/Popups.py b/app/Popups.py index 60e558e..c3ca146 100644 --- a/app/Popups.py +++ b/app/Popups.py @@ -122,28 +122,6 @@ def applyChanges(self): return True -class ScaleImagePicker(BasePicker): - def __init__(self, parent, tracker): - config = parent.config - listTop = config["IMAGE_SCALING"] - optionLists = [listTop] - - super().__init__(parent, tracker, optionLists) - self.pickTop.currentIndexChanged.connect(self.changeScaling) - self.pickTop.setCurrentIndex(config["SELECTED_INDEX"]["imageScaling"]) - self.nameTop.setText("Image Scaling: ") - - self.imageScalingIndex = self.pickTop.currentIndex() - - def changeScaling(self, i): - self.imageScalingIndex = i - - def applyChanges(self): - self.applySelections(['imageScaling']) - self.parent.canvas.setViewImageMode(self.imageScalingIndex) - return True - - class ShortcutPicker(BasePicker): def __init__(self, parent, tracker): config = parent.config diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py index 74c43f9..75ec14c 100644 --- a/app/components/settings/__init__.py +++ b/app/components/settings/__init__.py @@ -18,4 +18,4 @@ """ from .base import BaseSettings -from .popups import OptionsContainer, TesseractOptions +from .popups import ImageScalingOptions, OptionsContainer, TesseractOptions diff --git a/app/components/settings/popups/__init__.py b/app/components/settings/popups/__init__.py index d78e704..e5d716b 100644 --- a/app/components/settings/popups/__init__.py +++ b/app/components/settings/popups/__init__.py @@ -18,4 +18,5 @@ """ from .container import OptionsContainer +from .imageScaling import ImageScalingOptions from .tesseract import TesseractOptions diff --git a/app/components/settings/popups/base.py b/app/components/settings/popups/base.py index 9cb52e7..a3de2f8 100644 --- a/app/components/settings/popups/base.py +++ b/app/components/settings/popups/base.py @@ -32,6 +32,7 @@ class BaseOptions(BaseSettings): """ def __init__(self, parent: QWidget, optionLists:list[list[str]]=[]): super().__init__(parent) + self.mainWindow = parent self.setAttribute(Qt.WA_DeleteOnClose) self.setLayout(QGridLayout()) diff --git a/app/components/settings/popups/imageScaling.py b/app/components/settings/popups/imageScaling.py new file mode 100644 index 0000000..0369310 --- /dev/null +++ b/app/components/settings/popups/imageScaling.py @@ -0,0 +1,38 @@ +""" +Poricom Settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtWidgets import (QWidget) + +from .base import BaseOptions +from utils.constants import IMAGE_SCALING + + +class ImageScalingOptions(BaseOptions): + def __init__(self, parent: QWidget): + super().__init__(parent, [IMAGE_SCALING]) + # TODO: Use constants here + # TODO: Image scaling must be an enum not an int + self.initializeProperties([("imageScaling", 0, int)]) + + def changeImageScaling(self, i): + self.imageScalingIndex = i + + def saveSettings(self, hasMessage=False): + self.mainWindow.canvas.setViewImageMode(self.imageScalingIndex) + return super().saveSettings(hasMessage) diff --git a/app/utils/constants.py b/app/utils/constants.py index b470827..c16e3bc 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -26,6 +26,8 @@ LANGUAGE = [ " Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English"] ORIENTATION = [ " Vertical", " Horizontal"] +IMAGE_SCALING = [ " Fit to Width", " Fit to Height", " Fit to Screen"] + # Paths TESSERACT_LANGUAGES = "./assets/languages/" From 2fe3bcc3d02da69cbc36e1f4d7318209b9f96278 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 16:33:26 +0800 Subject: [PATCH 085/137] Refactor FontOptions to inherit BaseOptions --- app/MainWindow.py | 6 +-- app/Popups.py | 53 +------------------- app/components/settings/__init__.py | 2 +- app/components/settings/popups/__init__.py | 1 + app/components/settings/popups/font.py | 58 ++++++++++++++++++++++ app/components/views/ocr/base.py | 4 +- app/utils/config.py | 13 ----- app/utils/constants.py | 8 +++ app/utils/scripts/__init__.py | 1 + app/utils/scripts/editStylesheet.py | 33 ++++++++++++ 10 files changed, 108 insertions(+), 71 deletions(-) create mode 100644 app/components/settings/popups/font.py create mode 100644 app/utils/scripts/editStylesheet.py diff --git a/app/MainWindow.py b/app/MainWindow.py index a874dd2..1df4f35 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -27,10 +27,10 @@ QPushButton, QFileDialog) from components.services import BaseWorker -from components.settings import ImageScalingOptions, OptionsContainer, TesseractOptions +from components.settings import FontOptions, ImageScalingOptions, OptionsContainer, TesseractOptions from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView -from Popups import (FontPicker, ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) +from Popups import (ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) from utils.config import config, saveOnClose from utils.constants import LOAD_MODEL_MESSAGE from utils.scripts import mangaFileToImageDir @@ -169,7 +169,7 @@ def toggleStylesheet(self): app.setStyleSheet(fh.read()) def modifyFontSettings(self): - confirmation = PickerPopup(FontPicker(self, self.tracker)) + confirmation = OptionsContainer(FontOptions(self)) ret = confirmation.exec() if ret: diff --git a/app/Popups.py b/app/Popups.py index c3ca146..45c0d91 100644 --- a/app/Popups.py +++ b/app/Popups.py @@ -21,7 +21,7 @@ from PyQt5.QtWidgets import (QGridLayout, QVBoxLayout, QWidget, QLabel, QCheckBox, QLineEdit, QComboBox, QDialog, QDialogButtonBox, QMessageBox) -from utils.config import (editSelectionConfig, editStylesheet) +from utils.config import (editSelectionConfig) class MessagePopup(QMessageBox): @@ -71,57 +71,6 @@ def applySelections(self, selections): editSelectionConfig(index, selection) -class FontPicker(BasePicker): - def __init__(self, parent, tracker): - config = parent.config - listTop = config["FONT_STYLE"] - listBot = config["FONT_SIZE"] - optionLists = [listTop, listBot] - - super().__init__(parent, tracker, optionLists) - self.pickTop.currentIndexChanged.connect(self.changeFontStyle) - self.pickTop.setCurrentIndex(config["SELECTED_INDEX"]["fontStyle"]) - self.nameTop.setText("Font Style: ") - self.pickBot.currentIndexChanged.connect(self.changeFontSize) - self.pickBot.setCurrentIndex(config["SELECTED_INDEX"]["fontSize"]) - self.nameBot.setText("Font Size: ") - - self.persistText = QComboBox() - self.persistText.addItems(["Disabled", "Enabled"]) - self.layout.addWidget(QLabel("Persist Text: "), 2, 0) - self.layout.addWidget(self.persistText, 2, 1) - self.persistText.setCurrentIndex(config["PERSIST_TEXT_MODE"]) - self.persistText.currentIndexChanged.connect(self.changePersistText) - - self.fontStyleText = f" font-family: '{self.pickTop.currentText().strip()}';\n" - self.fontSizeText = f" font-size: {self.pickBot.currentText().strip()}pt;\n" - self.fontStyleIndex = self.pickTop.currentIndex() - self.fontSizeIndex = self.pickBot.currentIndex() - self.persistTextIndex = self.persistText.currentIndex() - - def changeFontStyle(self, i): - self.fontStyleIndex = i - selectedFontStyle = self.pickTop.currentText().strip() - replacementText = f" font-family: '{selectedFontStyle}';\n" - self.fontStyleText = replacementText - - def changeFontSize(self, i): - self.fontSizeIndex = i - selectedFontSize = int(self.pickBot.currentText().strip()) - replacementText = f" font-size: {selectedFontSize}pt;\n" - self.fontSizeText = replacementText - - def changePersistText(self, i): - self.persistTextIndex = i - - def applyChanges(self): - self.applySelections(['fontStyle', 'fontSize']) - editStylesheet(41, self.fontStyleText) - editStylesheet(42, self.fontSizeText) - self.parent.config["PERSIST_TEXT_MODE"] = self.persistTextIndex - return True - - class ShortcutPicker(BasePicker): def __init__(self, parent, tracker): config = parent.config diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py index 75ec14c..e3dad24 100644 --- a/app/components/settings/__init__.py +++ b/app/components/settings/__init__.py @@ -18,4 +18,4 @@ """ from .base import BaseSettings -from .popups import ImageScalingOptions, OptionsContainer, TesseractOptions +from .popups import FontOptions, ImageScalingOptions, OptionsContainer, TesseractOptions diff --git a/app/components/settings/popups/__init__.py b/app/components/settings/popups/__init__.py index e5d716b..e6cc9c4 100644 --- a/app/components/settings/popups/__init__.py +++ b/app/components/settings/popups/__init__.py @@ -18,5 +18,6 @@ """ from .container import OptionsContainer +from .font import FontOptions from .imageScaling import ImageScalingOptions from .tesseract import TesseractOptions diff --git a/app/components/settings/popups/font.py b/app/components/settings/popups/font.py new file mode 100644 index 0000000..cbc3e43 --- /dev/null +++ b/app/components/settings/popups/font.py @@ -0,0 +1,58 @@ +""" +Poricom Settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtWidgets import (QWidget) + +from .base import BaseOptions +from utils.constants import FONT_SIZE, FONT_STYLE, TOGGLE_CHOICES +from utils.scripts import editStylesheet + + +class FontOptions(BaseOptions): + def __init__(self, parent: QWidget): + super().__init__(parent, [FONT_STYLE, FONT_SIZE, TOGGLE_CHOICES]) + self.initializeProperties([ + ("fontStyle", " font-family: 'Helvetica';\n", str), + ("fontSize", " font-size: 16pt;\n", str), + ("persistText", "true", bool), + ]) + self.setOptionIndex("fontSize", 2) + self.setOptionIndex("persistText", 1) + + def changeFontStyle(self, i): + self.fontStyleIndex = i + selectedFontStyle = self.fontStyleComboBox.currentText().strip() + replacementText = f" font-family: '{selectedFontStyle}';\n" + self.fontStyle = replacementText + + def changeFontSize(self, i): + self.fontSizeIndex = i + selectedFontSize = int(self.fontSizeComboBox.currentText().strip()) + replacementText = f" font-size: {selectedFontSize}pt;\n" + self.fontSize = replacementText + + def changePersistText(self, i): + self.persistTextIndex = i + self.persistText = True if i else False + + def saveSettings(self, hasMessage=False): + editStylesheet(41, self.fontStyle) + editStylesheet(42, self.fontSize) + self.mainWindow.canvas.setProperty('persistText', "true" if self.persistText else "false") + return super().saveSettings(hasMessage) diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 120535f..ff7b956 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -50,7 +50,7 @@ def __init__(self, parent: QMainWindow, tracker=None): self.setDragMode(QGraphicsView.RubberBandDrag) self.addDefaults(TESSERACT_DEFAULTS) - self.addProperty('persistTextMode', "false", bool) + self.addProperty('persistText', "true", bool) def handleTextResult(self, result): try: @@ -96,7 +96,7 @@ def mouseReleaseEvent(self, event): text = self.canvasText.text() logText(text, isLogFile=isLogFile, path=logPath) try: - if not self.persistTextMode: + if not self.persistText: self.canvasText.hide() except AttributeError: pass diff --git a/app/utils/config.py b/app/utils/config.py index 6010300..0f23d9e 100644 --- a/app/utils/config.py +++ b/app/utils/config.py @@ -38,16 +38,3 @@ def editSelectionConfig(index, cBoxName, config="utils/config.toml"): data["SELECTED_INDEX"][cBoxName] = index with open(config, 'w', encoding='utf-8') as fh: toml.dump(data, fh) - - -def editStylesheet(index, replacementText): - sheetLight = './assets/styles.qss' - sheetDark = './assets/styles-dark.qss' - with open(sheetLight, 'r') as slFh, open(sheetDark, 'r') as sdFh: - lineLight = slFh.readlines() - linesDark = sdFh.readlines() - lineLight[index] = replacementText - linesDark[index] = replacementText - with open(sheetLight, 'w') as slFh, open(sheetDark, 'w') as sdFh: - slFh.writelines(lineLight) - sdFh.writelines(linesDark) diff --git a/app/utils/constants.py b/app/utils/constants.py index c16e3bc..37ef2c1 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -23,12 +23,20 @@ IMAGE_EXTENSIONS = ["*.bmp", "*.gif", "*.jpeg", "*.jpg", "*.pbm", "*.pgm", "*.png", "*.ppm", "*.webp", "*.xbm", "*.xpm",] # Settings +TOGGLE_CHOICES = [ " Disabled", " Enabled"] + LANGUAGE = [ " Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English"] ORIENTATION = [ " Vertical", " Horizontal"] IMAGE_SCALING = [ " Fit to Width", " Fit to Height", " Fit to Screen"] +FONT_SIZE = [ " 12", " 14", " 16", " 20", " 24", " 32", " 40", " 56", " 72"] +FONT_STYLE = [ " Helvetica", " Poppins", " Arial", " Verdana", " Times New Roman"] + # Paths +STYLESHEET_LIGHT = './assets/styles.qss' +STYLESHEET_DARK = './assets/styles-dark.qss' + TESSERACT_LANGUAGES = "./assets/languages/" TOOLBAR_ICONS = './assets/images/icons/' diff --git a/app/utils/scripts/__init__.py b/app/utils/scripts/__init__.py index 5b5328c..603452d 100644 --- a/app/utils/scripts/__init__.py +++ b/app/utils/scripts/__init__.py @@ -17,6 +17,7 @@ along with this program. If not, see . """ +from .editStylesheet import editStylesheet from .logText import logText from .mangaFileToImageDir import mangaFileToImageDir from .pixmapToText import pixmapToText diff --git a/app/utils/scripts/editStylesheet.py b/app/utils/scripts/editStylesheet.py new file mode 100644 index 0000000..a0c09e5 --- /dev/null +++ b/app/utils/scripts/editStylesheet.py @@ -0,0 +1,33 @@ +""" +Poricom Helper Functions + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from ..constants import STYLESHEET_LIGHT, STYLESHEET_DARK + +def editStylesheet(index: int, style: str): + """ + Replace stylesheet at line `index` with input `style` + """ + with open(STYLESHEET_LIGHT, 'r') as slFh, open(STYLESHEET_DARK, 'r') as sdFh: + lineLight = slFh.readlines() + linesDark = sdFh.readlines() + lineLight[index] = style + linesDark[index] = style + with open(STYLESHEET_LIGHT, 'w') as slFh, open(STYLESHEET_DARK, 'w') as sdFh: + slFh.writelines(lineLight) + sdFh.writelines(linesDark) From c774f22364d132223aca2ba651b179a7b53b8abf Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 16:41:12 +0800 Subject: [PATCH 086/137] Rename FontOptions to PreviewOptions --- app/MainWindow.py | 4 ++-- app/components/settings/__init__.py | 2 +- app/components/settings/popups/__init__.py | 2 +- app/components/settings/popups/{font.py => preview.py} | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename app/components/settings/popups/{font.py => preview.py} (98%) diff --git a/app/MainWindow.py b/app/MainWindow.py index 1df4f35..5229798 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -27,7 +27,7 @@ QPushButton, QFileDialog) from components.services import BaseWorker -from components.settings import FontOptions, ImageScalingOptions, OptionsContainer, TesseractOptions +from components.settings import PreviewOptions, ImageScalingOptions, OptionsContainer, TesseractOptions from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView from Popups import (ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) @@ -169,7 +169,7 @@ def toggleStylesheet(self): app.setStyleSheet(fh.read()) def modifyFontSettings(self): - confirmation = OptionsContainer(FontOptions(self)) + confirmation = OptionsContainer(PreviewOptions(self)) ret = confirmation.exec() if ret: diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py index e3dad24..96307a7 100644 --- a/app/components/settings/__init__.py +++ b/app/components/settings/__init__.py @@ -18,4 +18,4 @@ """ from .base import BaseSettings -from .popups import FontOptions, ImageScalingOptions, OptionsContainer, TesseractOptions +from .popups import ImageScalingOptions, OptionsContainer, PreviewOptions, TesseractOptions diff --git a/app/components/settings/popups/__init__.py b/app/components/settings/popups/__init__.py index e6cc9c4..1df3628 100644 --- a/app/components/settings/popups/__init__.py +++ b/app/components/settings/popups/__init__.py @@ -18,6 +18,6 @@ """ from .container import OptionsContainer -from .font import FontOptions from .imageScaling import ImageScalingOptions +from .preview import PreviewOptions from .tesseract import TesseractOptions diff --git a/app/components/settings/popups/font.py b/app/components/settings/popups/preview.py similarity index 98% rename from app/components/settings/popups/font.py rename to app/components/settings/popups/preview.py index cbc3e43..0e59f26 100644 --- a/app/components/settings/popups/font.py +++ b/app/components/settings/popups/preview.py @@ -24,7 +24,7 @@ from utils.scripts import editStylesheet -class FontOptions(BaseOptions): +class PreviewOptions(BaseOptions): def __init__(self, parent: QWidget): super().__init__(parent, [FONT_STYLE, FONT_SIZE, TOGGLE_CHOICES]) self.initializeProperties([ From 54f81c5fe4b68ef187b425a30eeeab7577cdc6ec Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 16:41:48 +0800 Subject: [PATCH 087/137] Load preview settings on app start --- app/components/views/ocr/ocr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/views/ocr/ocr.py b/app/components/views/ocr/ocr.py index fd830a6..ebb1358 100644 --- a/app/components/views/ocr/ocr.py +++ b/app/components/views/ocr/ocr.py @@ -28,6 +28,7 @@ class OCRView(BaseImageView, BaseOCRView): def __init__(self, parent: QMainWindow, tracker=None): # TODO: Remove references to tracker super().__init__(parent, tracker) + self.loadSettings() @pyqtSlot() def rubberBandStopped(self): From 646a5be24a93f69334d3f8b4ec845a59cef05230 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 21:27:12 +0800 Subject: [PATCH 088/137] Refactor ShortcutOptions to inherit BaseOptions --- app/MainWindow.py | 12 +--- app/Popups.py | 52 ----------------- app/components/settings/__init__.py | 2 +- app/components/settings/popups/__init__.py | 1 + app/components/settings/popups/shortcut.py | 66 ++++++++++++++++++++++ app/main.py | 10 ++-- app/utils/constants.py | 4 +- 7 files changed, 80 insertions(+), 67 deletions(-) create mode 100644 app/components/settings/popups/shortcut.py diff --git a/app/MainWindow.py b/app/MainWindow.py index 5229798..ba805dd 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -27,10 +27,10 @@ QPushButton, QFileDialog) from components.services import BaseWorker -from components.settings import PreviewOptions, ImageScalingOptions, OptionsContainer, TesseractOptions +from components.settings import PreviewOptions, ImageScalingOptions, OptionsContainer, ShortcutOptions, TesseractOptions from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView -from Popups import (ShortcutPicker, PickerPopup, MessagePopup, CheckboxPopup) +from Popups import (MessagePopup, CheckboxPopup) from utils.config import config, saveOnClose from utils.constants import LOAD_MODEL_MESSAGE from utils.scripts import mangaFileToImageDir @@ -203,13 +203,7 @@ def toggleMouseMode(self): self.canvas.toggleZoomPanMode() def modifyHotkeys(self): - confirmation = PickerPopup(ShortcutPicker(self, self.tracker)) - ret = confirmation.exec() - if ret: - MessagePopup( - "Shortcut Remapped", - "Close the app to apply changes." - ).exec() + OptionsContainer(ShortcutOptions(self)).exec() # ------------------------------ Misc Functions ------------------------------ # diff --git a/app/Popups.py b/app/Popups.py index 45c0d91..416a6cc 100644 --- a/app/Popups.py +++ b/app/Popups.py @@ -71,58 +71,6 @@ def applySelections(self, selections): editSelectionConfig(index, selection) -class ShortcutPicker(BasePicker): - def __init__(self, parent, tracker): - config = parent.config - listTop = config["MODIFIER"] - optionLists = [listTop] - - super().__init__(parent, tracker, optionLists) - self.pickTop.currentIndexChanged.connect(self.changeModifier) - self.pickTop.setCurrentIndex(config["SELECTED_INDEX"]["modifier"]) - self.nameTop.setText("Modifier: ") - - self.pickBot = QLineEdit(config["SHORTCUT"]["captureExternalKey"]) - self.layout.addWidget(self.pickBot, 1, 1) - self.nameBot = QLabel("Key: ") - self.layout.addWidget(self.nameBot, 1, 0) - - self.modifierIndex = self.pickTop.currentIndex() - - def keyInvalidError(self): - MessagePopup( - "Invalid Key", - "Please select an alphanumeric key." - ).exec() - - def changeModifier(self, i): - self.modifierIndex = i - - def setShortcut(self, keyName, modifierText, keyText): - - tooltip = f"{self.parent.config['SHORTCUT'][f'{keyName}Tip']}{modifierText}{keyText}." - self.parent.config["SHORTCUT"][keyName] = f"{modifierText}{keyText}" - self.parent.config["SHORTCUT"][f"{keyName}Key"] = keyText - self.parent.config["TBAR_FUNCS"]["FILE"][f"{keyName}Helper"]["helpMsg"] = tooltip - - def applyChanges(self): - selectedModifier = self.pickTop.currentText().strip() + "+" - if selectedModifier == "No Modifier+": - selectedModifier = "" - - if not self.pickBot.text().isalnum(): - self.keyInvalidError() - return False - if len(self.pickBot.text()) != 1: - self.keyInvalidError() - return False - - self.setShortcut('captureExternal', selectedModifier, - self.pickBot.text()) - self.applySelections(['modifier']) - return True - - class PickerPopup(QDialog): def __init__(self, widget): super(QDialog, self).__init__(None, diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py index 96307a7..32784d0 100644 --- a/app/components/settings/__init__.py +++ b/app/components/settings/__init__.py @@ -18,4 +18,4 @@ """ from .base import BaseSettings -from .popups import ImageScalingOptions, OptionsContainer, PreviewOptions, TesseractOptions +from .popups import ImageScalingOptions, OptionsContainer, PreviewOptions, ShortcutOptions, TesseractOptions diff --git a/app/components/settings/popups/__init__.py b/app/components/settings/popups/__init__.py index 1df3628..1b9a45c 100644 --- a/app/components/settings/popups/__init__.py +++ b/app/components/settings/popups/__init__.py @@ -20,4 +20,5 @@ from .container import OptionsContainer from .imageScaling import ImageScalingOptions from .preview import PreviewOptions +from .shortcut import ShortcutOptions from .tesseract import TesseractOptions diff --git a/app/components/settings/popups/shortcut.py b/app/components/settings/popups/shortcut.py new file mode 100644 index 0000000..8d57ee7 --- /dev/null +++ b/app/components/settings/popups/shortcut.py @@ -0,0 +1,66 @@ +""" +Poricom Settings + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtWidgets import (QLabel, QLineEdit, QWidget) + +from .base import BaseOptions +from components.popups import BasePopup +from utils.constants import MODIFIER + +class ShortcutOptions(BaseOptions): + def __init__(self, parent: QWidget): + super().__init__(parent, [MODIFIER]) + self.initializeProperties([("modifier", "Alt", str)]) + self.setOptionIndex("modifier", 2) + self.addDefaults({ + "captureExternalKey": "Q", + "captureExternalShortcut": "Alt+Q" + }) + self.loadSettings() + + self.keyLineEdit = QLineEdit(self.captureExternalKey) + self.layout().addWidget(self.keyLineEdit, 1, 1) + self.layout().addWidget(QLabel("Key: "), 1, 0) + + def raiseKeyInvalidError(self, message: str): + BasePopup("Invalid Key", message).exec() + + def changeModifier(self, i): + self.modifierIndex = i + self.modifier = self.modifierComboBox.currentText().strip() + "+" + if self.modifier == "No Modifier+": + self.modifier = "" + + def changeShortcut(self): + self.captureExternalShortcut = self.modifier + self.captureExternalKey + + def saveSettings(self, hasMessage=False): + if not self.keyLineEdit.text().isalnum(): + self.raiseKeyInvalidError("Please select an alphanumeric key.") + return + if len(self.keyLineEdit.text()) != 1: + self.raiseKeyInvalidError("Please select exactly one key.") + return + self.captureExternalKey = self.keyLineEdit.text() + + self.changeShortcut() + + super().saveSettings(hasMessage) + + BasePopup("Shortcut Remapped", "Close the app to apply changes.").exec() diff --git a/app/main.py b/app/main.py index 1936c16..f863470 100644 --- a/app/main.py +++ b/app/main.py @@ -20,13 +20,14 @@ from PyQt5.QtWidgets import QApplication from PyQt5.QtGui import QIcon -from PyQt5.QtCore import QAbstractEventDispatcher +from PyQt5.QtCore import QAbstractEventDispatcher, QSettings from pyqtkeybind import keybinder from components.services import WinEventFilter from MainWindow import MainWindow from Trackers import Tracker from utils.config import config +from utils.constants import SETTINGS_FILE_DEFAULT if __name__ == '__main__': @@ -41,10 +42,11 @@ with open(styles, 'r') as fh: app.setStyleSheet(fh.read()) + settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) + shortcut = settings.value("captureExternalShortcut", "Alt+Q") keybinder.init() - previousShortcut = config["SHORTCUT"]["captureExternal"] keybinder.register_hotkey( - widget.winId(), config["SHORTCUT"]["captureExternal"], widget.captureExternal) + widget.winId(), shortcut, widget.captureExternal) winEventFilter = WinEventFilter(keybinder) eventDispatcher = QAbstractEventDispatcher.instance() eventDispatcher.installNativeEventFilter(winEventFilter) @@ -53,5 +55,5 @@ widget.loadModel() app.exec_() - # keybinder.unregister_hotkey(widget.winId(), previousShortcut) + # keybinder.unregister_hotkey(widget.winId(), shortcut) sys.exit() diff --git a/app/utils/constants.py b/app/utils/constants.py index 37ef2c1..1826091 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -33,6 +33,8 @@ FONT_SIZE = [ " 12", " 14", " 16", " 20", " 24", " 32", " 40", " 56", " 72"] FONT_STYLE = [ " Helvetica", " Poppins", " Arial", " Verdana", " Times New Roman"] +MODIFIER = [ " Ctrl", " Shift", " Alt", " Ctrl+Alt", " Shift+Alt", " Shift+Ctrl", " Shift+Alt+Ctrl", " No Modifier"] + # Paths STYLESHEET_LIGHT = './assets/styles.qss' STYLESHEET_DARK = './assets/styles-dark.qss' @@ -151,7 +153,7 @@ }, "captureExternalHelper": { "title": "External capture", - "message": "This will minimize the app and perform OCR on the current screen. Alternatively, you may use the shortcut Alt+Q.", + "message": "This will minimize the app and perform OCR on the current screen. Alternatively, you may use the shortcut Alt+Q (default).", "path": "captureExternalHelper.png", "toggle": False, "align": "AlignLeft", From 51cfcdbe9965c0790264ae9dee386635736debf2 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 21:29:41 +0800 Subject: [PATCH 089/137] Load tesseract settings on fullscreen view --- app/components/settings/base.py | 6 ++++-- app/components/views/ocr/fullscreen.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/components/settings/base.py b/app/components/settings/base.py index 46c49f8..78aeb13 100644 --- a/app/components/settings/base.py +++ b/app/components/settings/base.py @@ -110,8 +110,10 @@ def saveSettings(self, hasMessage=True): if hasMessage: BasePopup("Save Settings", "Configuration has been saved.").exec() - def loadSettings(self): - for propName, propDefault in self._defaults.items(): + def loadSettings(self, settings: dict[str, Any] = {}): + if not settings: + settings = self._defaults + for propName, propDefault in settings.items(): prop = self.settings.value(f"{self._prefix}{propName}", propDefault) self.setProperty(propName, prop) diff --git a/app/components/views/ocr/fullscreen.py b/app/components/views/ocr/fullscreen.py index 37f36f2..90a42d3 100644 --- a/app/components/views/ocr/fullscreen.py +++ b/app/components/views/ocr/fullscreen.py @@ -22,6 +22,7 @@ from PyQt5.QtGui import QCursor, QMouseEvent from .base import BaseOCRView +from utils.constants import TESSERACT_DEFAULTS class FullScreenOCRView(BaseOCRView): @@ -36,6 +37,7 @@ def __init__(self, parent: QMainWindow, tracker=None): self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setScene(QGraphicsScene()) + self.loadSettings(TESSERACT_DEFAULTS) def takeScreenshot(self, screenIndex: int): screen = QApplication.screens()[screenIndex] From 4f50f9b75a29937b671ce2be8cf776132a05de31 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 21:30:43 +0800 Subject: [PATCH 090/137] Update CheckboxPopup to use QSettings --- app/MainWindow.py | 58 ++++++------ app/Popups.py | 94 ------------------- app/components/popups/__init__.py | 1 + app/components/popups/checkbox.py | 46 +++++++++ .../toolbar/tabs/containers/base.py | 2 +- 5 files changed, 79 insertions(+), 122 deletions(-) delete mode 100644 app/Popups.py create mode 100644 app/components/popups/checkbox.py diff --git a/app/MainWindow.py b/app/MainWindow.py index ba805dd..4932fe1 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -26,33 +26,38 @@ from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QDesktopWidget, QMainWindow, QApplication, QPushButton, QFileDialog) +from components.popups import BasePopup, CheckboxPopup from components.services import BaseWorker -from components.settings import PreviewOptions, ImageScalingOptions, OptionsContainer, ShortcutOptions, TesseractOptions +from components.settings import BaseSettings, PreviewOptions, ImageScalingOptions, OptionsContainer, ShortcutOptions, TesseractOptions from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView -from Popups import (MessagePopup, CheckboxPopup) from utils.config import config, saveOnClose from utils.constants import LOAD_MODEL_MESSAGE from utils.scripts import mangaFileToImageDir -class MainWindow(QMainWindow): - +class MainWindow(QMainWindow, BaseSettings): def __init__(self, parent=None, tracker=None): - super(QWidget, self).__init__(parent) + super().__init__(parent) self.tracker = tracker self.config = config self.vLayout = QVBoxLayout() self.mainView = WorkspaceView(self, self.tracker) - self.ribbon = BaseToolbar(self) - self.vLayout.addWidget(self.ribbon) + self.toolbar = BaseToolbar(self) + self.vLayout.addWidget(self.toolbar) self.vLayout.addWidget(self.mainView) - _mainWidget = QWidget() - _mainWidget.setLayout(self.vLayout) - self.setCentralWidget(_mainWidget) + mainWidget = QWidget() + mainWidget.setLayout(self.vLayout) + self.setCentralWidget(mainWidget) + + self.setDefaults({"hasLoadModelPopup": "true"}) + self.setTypes({"hasLoadModelPopup": bool}) + self.loadSettings() + print(self.hasLoadModelPopup) + self.threadpool = QThreadPool() @@ -73,9 +78,9 @@ def closeEvent(self, event): saveOnClose(self.config) return QMainWindow.closeEvent(self, event) - def poricomNoop(self): - MessagePopup( - "WIP", + def noop(self): + BasePopup( + "Not Implemented", "This function is not yet implemented." ).exec() @@ -93,9 +98,9 @@ def openDir(self): self.tracker.filepath = filepath self.explorer.setDirectory(filepath) except FileNotFoundError: - MessagePopup( - f"No images found in the directory", - f"Please select a directory with images." + BasePopup( + "No images found in the directory", + "Please select a directory with images." ).exec() def openManga(self): @@ -111,7 +116,7 @@ def setDirectory(filepath): self.tracker.filepath = filepath self.explorer.setDirectory(filepath) - openMangaButton = self.ribbon.findChild( + openMangaButton = self.toolbar.findChild( QPushButton, "openManga") worker = BaseWorker(mangaFileToImageDir, filename) @@ -208,18 +213,17 @@ def modifyHotkeys(self): # ------------------------------ Misc Functions ------------------------------ # def loadModel(self): - loadModelButton = self.ribbon.findChild(QPushButton, "loadModel") + loadModelButton = self.toolbar.findChild(QPushButton, "loadModel") loadModelButton.setChecked(not self.tracker.ocrModel) - if loadModelButton.isChecked() and self.config["LOAD_MODEL_POPUP"]: - confirmation = CheckboxPopup( + if loadModelButton.isChecked() and self.hasLoadModelPopup: + ret = CheckboxPopup( + "hasLoadModelPopup", "Load the MangaOCR model?", LOAD_MODEL_MESSAGE, - MessagePopup.Ok | MessagePopup.Cancel - ) - ret = confirmation.exec() - self.config["LOAD_MODEL_POPUP"] = not confirmation.checkBox().isChecked() - if (ret == MessagePopup.Ok): + CheckboxPopup.Ok | CheckboxPopup.Cancel + ).exec() + if (ret == CheckboxPopup.Ok): pass else: loadModelButton.setChecked(False) @@ -241,12 +245,12 @@ def loadModelHelper(tracker): def loadModelConfirm(message: str): modelName = "MangaOCR" if self.tracker.ocrModel else "Tesseract" if message == "success": - MessagePopup( + BasePopup( f"{modelName} model loaded", f"You are now using the {modelName} model for Japanese text detection." ).exec() else: - MessagePopup("Load Model Error", message).exec() + BasePopup("Load Model Error", message).exec() loadModelButton.setChecked(False) worker = BaseWorker(loadModelHelper, self.tracker) diff --git a/app/Popups.py b/app/Popups.py deleted file mode 100644 index 416a6cc..0000000 --- a/app/Popups.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -Poricom Popup Components - -Copyright (C) `2021-2022` `` - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -from PyQt5.QtCore import (Qt) -from PyQt5.QtWidgets import (QGridLayout, QVBoxLayout, QWidget, QLabel, QCheckBox, - QLineEdit, QComboBox, QDialog, QDialogButtonBox, QMessageBox) - -from utils.config import (editSelectionConfig) - - -class MessagePopup(QMessageBox): - def __init__(self, title, message, flags=QMessageBox.Ok): - super(QMessageBox, self).__init__( - QMessageBox.NoIcon, title, message, flags) - - -class CheckboxPopup(MessagePopup): - def __init__(self, title, message, flags=QMessageBox.Ok, - checkboxMessage="Don't show this dialog again"): - super(MessagePopup, self).__init__( - MessagePopup.NoIcon, title, message, flags) - self.checkbox = QCheckBox(checkboxMessage) - self.setCheckBox(self.checkbox) - -class BasePicker(QWidget): - def __init__(self, parent, tracker, optionLists=[]): - super(QWidget, self).__init__() - self.parent = parent - self.tracker = tracker - - self.layout = QGridLayout(self) - self.layout.setContentsMargins(0, 0, 0, 0) - - _comboBoxList = [] - _labelList = [] - - for i in range(len(optionLists)): - optionList = optionLists[i] - - _comboBoxList.append(QComboBox()) - _comboBoxList[i].addItems(optionList) - self.layout.addWidget(_comboBoxList[i], i, 1) - _labelList.append(QLabel("")) - self.layout.addWidget(_labelList[i], i, 0) - - self.pickTop = _comboBoxList[0] - self.pickBot = _comboBoxList[-1] - self.nameTop = _labelList[0] - self.nameBot = _labelList[-1] - - def applySelections(self, selections): - for selection in selections: - index = getattr(self, f"{selection}Index") - self.parent.config["SELECTED_INDEX"][selection] = index - editSelectionConfig(index, selection) - - -class PickerPopup(QDialog): - def __init__(self, widget): - super(QDialog, self).__init__(None, - Qt.WindowCloseButtonHint | Qt.WindowSystemMenuHint | Qt.WindowTitleHint) - self.widget = widget - self.setLayout(QVBoxLayout()) - self.layout().addWidget(widget) - self.buttonBox = QDialogButtonBox( - QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.layout().addWidget(self.buttonBox) - - self.buttonBox.rejected.connect(self.cancelClickedEvent) - self.buttonBox.accepted.connect(self.accept) - self.buttonBox.rejected.connect(self.reject) - - def accept(self): - if self.widget.applyChanges(): - return super().accept() - - def cancelClickedEvent(self): - self.close() diff --git a/app/components/popups/__init__.py b/app/components/popups/__init__.py index 121e868..1563758 100644 --- a/app/components/popups/__init__.py +++ b/app/components/popups/__init__.py @@ -18,3 +18,4 @@ """ from .base import BasePopup +from .checkbox import CheckboxPopup diff --git a/app/components/popups/checkbox.py b/app/components/popups/checkbox.py new file mode 100644 index 0000000..bdd4c92 --- /dev/null +++ b/app/components/popups/checkbox.py @@ -0,0 +1,46 @@ +""" +Poricom Popups + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtCore import QSettings +from PyQt5.QtWidgets import (QCheckBox) + +from components.popups import BasePopup +from utils.constants import SETTINGS_FILE_DEFAULT + +class CheckboxPopup(BasePopup): + """Popup message with a checkbox + + Args: + prop (str): Name of the boolean property to be saved. + checkboxMessage (str, optional): Checkbox label. + Defaults to "Don't show this dialog again". + """ + def __init__(self, prop: str, title: str, message: str, + buttons: BasePopup.StandardButtons = BasePopup.Ok, + checkboxMessage="Don't show this dialog again"): + super().__init__(title, message, buttons) + + self.setCheckBox(QCheckBox(checkboxMessage, self)) + + self.prop = prop + self.accepted.connect(self.saveSettings) + + def saveSettings(self): + settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) + settings.setValue(self.prop, not self.checkBox().isChecked()) diff --git a/app/components/toolbar/tabs/containers/base.py b/app/components/toolbar/tabs/containers/base.py index d666c7b..d1ab13f 100644 --- a/app/components/toolbar/tabs/containers/base.py +++ b/app/components/toolbar/tabs/containers/base.py @@ -84,4 +84,4 @@ def initializeButton(self, name: str, config: ButtonConfig): getattr(self.mainWindow.mainView, name)) except AttributeError: button.clicked.connect( - getattr(self.mainWindow, 'poricomNoop')) + getattr(self.mainWindow, 'noop')) From 1d440ddcfe994736cf5e054cc174c80259d1087a Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 23:36:46 +0800 Subject: [PATCH 091/137] Use QSettings to save/load settings Remove toml as dependency --- app/MainWindow.py | 38 +-- app/Trackers.py | 59 ++--- app/components/popups/base.py | 2 +- app/components/settings/popups/container.py | 2 +- app/components/views/image/base.py | 2 +- app/components/views/workspace.py | 5 +- app/main.py | 12 +- app/utils/config.py | 40 --- app/utils/config.toml | 256 -------------------- app/utils/constants.py | 13 + environment/base.yaml | 1 - 11 files changed, 51 insertions(+), 379 deletions(-) delete mode 100644 app/utils/config.py delete mode 100644 app/utils/config.toml diff --git a/app/MainWindow.py b/app/MainWindow.py index 4932fe1..0a7bb79 100644 --- a/app/MainWindow.py +++ b/app/MainWindow.py @@ -20,7 +20,6 @@ from shutil import rmtree from time import sleep -import toml from manga_ocr import MangaOcr from PyQt5.QtCore import (Qt, QThreadPool) from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QDesktopWidget, QMainWindow, QApplication, @@ -31,8 +30,7 @@ from components.settings import BaseSettings, PreviewOptions, ImageScalingOptions, OptionsContainer, ShortcutOptions, TesseractOptions from components.toolbar import BaseToolbar from components.views import WorkspaceView, FullScreenOCRView -from utils.config import config, saveOnClose -from utils.constants import LOAD_MODEL_MESSAGE +from utils.constants import LOAD_MODEL_MESSAGE, MAIN_WINDOW_DEFAULTS, MAIN_WINDOW_TYPES, STYLESHEET_DARK, STYLESHEET_LIGHT from utils.scripts import mangaFileToImageDir @@ -40,7 +38,6 @@ class MainWindow(QMainWindow, BaseSettings): def __init__(self, parent=None, tracker=None): super().__init__(parent) self.tracker = tracker - self.config = config self.vLayout = QVBoxLayout() @@ -53,11 +50,9 @@ def __init__(self, parent=None, tracker=None): mainWidget.setLayout(self.vLayout) self.setCentralWidget(mainWidget) - self.setDefaults({"hasLoadModelPopup": "true"}) - self.setTypes({"hasLoadModelPopup": bool}) + self.setDefaults(MAIN_WINDOW_DEFAULTS) + self.setTypes(MAIN_WINDOW_TYPES) self.loadSettings() - print(self.hasLoadModelPopup) - self.threadpool = QThreadPool() @@ -74,9 +69,8 @@ def closeEvent(self, event): rmtree("./poricom_cache") except FileNotFoundError: pass - self.config["NAV_ROOT"] = self.tracker.filepath - saveOnClose(self.config) - return QMainWindow.closeEvent(self, event) + self.saveSettings(False) + return super().closeEvent(event) def noop(self): BasePopup( @@ -97,6 +91,7 @@ def openDir(self): try: self.tracker.filepath = filepath self.explorer.setDirectory(filepath) + self.explorerPath = filepath except FileNotFoundError: BasePopup( "No images found in the directory", @@ -152,25 +147,16 @@ def captureExternal(self): # ------------------------------ View Functions ------------------------------ # def toggleStylesheet(self): - config = "./utils/config.toml" - lightMode = "./assets/styles.qss" - darkMode = "./assets/styles-dark.qss" - - data = toml.load(config) - if data["STYLES_DEFAULT"] == lightMode: - data["STYLES_DEFAULT"] = darkMode - elif data["STYLES_DEFAULT"] == darkMode: - data["STYLES_DEFAULT"] = lightMode - with open(config, 'w') as fh: - toml.dump(data, fh) + if self.stylesheetPath == STYLESHEET_LIGHT: + self.stylesheetPath = STYLESHEET_DARK + elif self.stylesheetPath == STYLESHEET_DARK: + self.stylesheetPath = STYLESHEET_LIGHT app = QApplication.instance() if app is None: raise RuntimeError("No Qt Application found.") - styles = data["STYLES_DEFAULT"] - self.config["STYLES_DEFAULT"] = data["STYLES_DEFAULT"] - with open(styles, 'r') as fh: + with open(self.stylesheetPath, 'r') as fh: app.setStyleSheet(fh.read()) def modifyFontSettings(self): @@ -182,7 +168,7 @@ def modifyFontSettings(self): if app is None: raise RuntimeError("No Qt Application found.") - with open(config["STYLES_DEFAULT"], 'r') as fh: + with open(self.stylesheetPath, 'r') as fh: app.setStyleSheet(fh.read()) def toggleSplitView(self): diff --git a/app/Trackers.py b/app/Trackers.py index f489067..9654efe 100644 --- a/app/Trackers.py +++ b/app/Trackers.py @@ -20,25 +20,34 @@ from os.path import isfile, join, splitext, normpath, abspath, exists, dirname from os import listdir +from PyQt5.QtCore import QSettings from PyQt5.QtGui import QPixmap, QPainter -from utils.config import config +from utils.constants import EXPLORER_ROOT_DEFAULT, IMAGE_EXTENSIONS, SETTINGS_FILE_DEFAULT +settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) +split = settings.value("splitViewMode").lower() == "true" +explorerPath = settings.value("explorerPath", EXPLORER_ROOT_DEFAULT) +# TODO: This needs refactoring +# 1. Rename the module to `states` +# 2. Instead of one object there should be several state objects i.e. image states, filepath states +# 3. It might also be better if images states is an object tracked by WorkspaceView (parent) +# then pass the references to its children class Tracker: def __init__(self): try: - self.filepath = abspath(config["NAV_ROOT"]) + self.filepath = abspath(explorerPath) except FileNotFoundError: - self.filepath = abspath(config["DEFAULT_NAV_ROOT"]) + self.filepath = abspath(explorerPath) try: filename, filenext, *_ = self._imageList except ValueError: filename, *_ = self._imageList filenext = None - if not config["SPLIT_VIEW_MODE"]: + if not split: self._pixImage = PImage(filename) - if config["SPLIT_VIEW_MODE"]: + if split: splitImage = self.twoFileToImage(filename, filenext) self._pixImage = PImage(splitImage, filename) self._pixMask = PImage(filename) @@ -47,31 +56,9 @@ def __init__(self): self._imageList = [] - selectedLanguage = config["LANGUAGE"][config["SELECTED_INDEX"]["language"]] - self._language = self.selectionToLangCode(selectedLanguage.strip()) - selectedOrientation = config["ORIENTATION"][config["SELECTED_INDEX"]["orientation"]] - self._orientation = self.selectionToOrientCode(selectedOrientation.strip()) - self._betterOCR = False self._ocrModel = None - def selectionToLangCode(self, selectedLanguage): - if selectedLanguage == "Japanese": - langCode = "jpn" - if selectedLanguage == "Korean": - langCode = "kor" - if selectedLanguage == "Chinese SIM": - langCode = "chi_sim" - if selectedLanguage == "Chinese TRA": - langCode = "chi_tra" - if selectedLanguage == "English": - langCode = "eng" - return langCode - - def selectionToOrientCode(self, selectedOrientation): - isVert = selectedOrientation == "Vertical" - return "_vert" if isVert else "" - def twoFileToImage(self, fileLeft, fileRight): imageLeft, imageRight = PImage(fileRight), PImage(fileLeft) if not (imageLeft.isValid()): @@ -132,29 +119,13 @@ def filepath(self): def filepath(self, filepath): fileList = filter(lambda f: isfile(join(filepath, f)), listdir(filepath)) imageList = list(map(lambda p: normpath(join(filepath, p)), filter( - (lambda f: ('*'+splitext(f)[1]) in config["IMAGE_EXTENSIONS"]), fileList))) + (lambda f: ('*'+splitext(f)[1]) in IMAGE_EXTENSIONS), fileList))) if len(imageList) <= 0: raise FileNotFoundError("Empty directory") self._filepath = filepath self._imageList = imageList - @property - def language(self): - return self._language - - @language.setter - def language(self, language): - self._language = language - - @property - def orientation(self): - return self._orientation - - @orientation.setter - def orientation(self, orientation): - self._orientation = orientation - @property def ocrModel(self): return self._ocrModel diff --git a/app/components/popups/base.py b/app/components/popups/base.py index 6e9ea48..cda9e26 100644 --- a/app/components/popups/base.py +++ b/app/components/popups/base.py @@ -1,5 +1,5 @@ """ -Cloe Popups +Poricom Popups Copyright (C) `2021-2022` `` diff --git a/app/components/settings/popups/container.py b/app/components/settings/popups/container.py index c29a6a7..5c037b4 100644 --- a/app/components/settings/popups/container.py +++ b/app/components/settings/popups/container.py @@ -18,7 +18,7 @@ """ from PyQt5.QtCore import (Qt) -from PyQt5.QtWidgets import (QVBoxLayout, QDialog, QDialogButtonBox) +from PyQt5.QtWidgets import (QDialog, QDialogButtonBox, QVBoxLayout) from .base import BaseOptions diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index 116fabf..0b69dbc 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -21,7 +21,7 @@ from typing import TYPE_CHECKING from PyQt5.QtCore import (Qt, QRect, QRectF, QSize, QThreadPool) -from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView, QSplitter) +from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView) from components.services import BaseWorker from components.settings import BaseSettings diff --git a/app/components/views/workspace.py b/app/components/views/workspace.py index 2806fdb..65ee096 100644 --- a/app/components/views/workspace.py +++ b/app/components/views/workspace.py @@ -1,5 +1,5 @@ """ -Poricom Main Window Component +Poricom Workspace View Component Copyright (C) `2021-2022` `` @@ -22,9 +22,9 @@ from .ocr import OCRView from components.explorers import ImageExplorer -from utils.config import config from utils.constants import MAIN_VIEW_RATIO +# TODO: Move view settings here class WorkspaceView(QSplitter): """ Main view of the program. Includes the explorer and the view. @@ -32,7 +32,6 @@ class WorkspaceView(QSplitter): def __init__(self, parent: QMainWindow, tracker=None): super().__init__(parent) self.tracker = tracker - self.config = config self.canvas = OCRView(self, self.tracker) self.explorer = ImageExplorer(self, self.tracker.filepath) diff --git a/app/main.py b/app/main.py index f863470..2dcdc5c 100644 --- a/app/main.py +++ b/app/main.py @@ -26,23 +26,23 @@ from components.services import WinEventFilter from MainWindow import MainWindow from Trackers import Tracker -from utils.config import config -from utils.constants import SETTINGS_FILE_DEFAULT +from utils.constants import APP_NAME, APP_LOGO, SETTINGS_FILE_DEFAULT, STYLESHEET_LIGHT if __name__ == '__main__': app = QApplication(sys.argv) - app.setApplicationName("Poricom") - app.setWindowIcon(QIcon(config["LOGO"])) + app.setApplicationName(APP_NAME) + app.setWindowIcon(QIcon(APP_LOGO)) tracker = Tracker() widget = MainWindow(parent=None, tracker=tracker) - styles = config["STYLES_DEFAULT"] + settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) + + styles = settings.value("stylesheetPath", STYLESHEET_LIGHT) with open(styles, 'r') as fh: app.setStyleSheet(fh.read()) - settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) shortcut = settings.value("captureExternalShortcut", "Alt+Q") keybinder.init() keybinder.register_hotkey( diff --git a/app/utils/config.py b/app/utils/config.py deleted file mode 100644 index 0f23d9e..0000000 --- a/app/utils/config.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Poricom Configuration Utilities - -Copyright (C) `2021-2022` `` - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -import toml -config = toml.load("./utils/config.toml") - - -def saveOnClose(data, config="utils/config.toml"): - with open(config, 'w', encoding='utf-8') as fh: - toml.dump(data, fh) - - -def editConfig(index, replacementText, config="utils/config.toml"): - data = toml.load(config) - data[index] = replacementText - with open(config, 'w', encoding='utf-8') as fh: - toml.dump(data, fh) - - -def editSelectionConfig(index, cBoxName, config="utils/config.toml"): - data = toml.load(config) - data["SELECTED_INDEX"][cBoxName] = index - with open(config, 'w', encoding='utf-8') as fh: - toml.dump(data, fh) diff --git a/app/utils/config.toml b/app/utils/config.toml deleted file mode 100644 index d03227e..0000000 --- a/app/utils/config.toml +++ /dev/null @@ -1,256 +0,0 @@ -# Poricom Default Configuration File -# -# Copyright (C) `2021-2022` `` -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# Lists -IMAGE_EXTENSIONS = [ "*.bmp", "*.gif", "*.jpg", "*.jpeg", "*.png", "*.pbm", "*.pgm", "*.ppm", "*.webp", "*.xbm", "*.xpm",] -LANGUAGE = [ " Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English",] -ORIENTATION = [ " Vertical", " Horizontal",] -FONT_STYLE = [ " Helvetica", " Poppins", " Arial", " Verdana", " Times New Roman",] -FONT_SIZE = [ " 12", " 14", " 16", " 20", " 24", " 32", " 40", " 56", " 72",] -IMAGE_SCALING = [ " Fit to Width", " Fit to Height", " Fit to Screen",] -MODIFIER = [ " Ctrl", " Shift", " Alt", " Ctrl+Alt", " Shift+Alt", " Shift+Ctrl", " Shift+Alt+Ctrl", " No Modifier",] - -# Filepath -STYLES_PATH = "./assets/" -DEFAULT_NAV_ROOT = "./assets/images/" -NAV_ROOT = "./assets/images/" -TBAR_ICONS = "./assets/images/icons/" -TBAR_ICONS_LIGHT = "./assets/images/icons/" -LANG_PATH = "./assets/languages/" - -STYLES_DEFAULT = "./assets/styles.qss" -LOGO = "./assets/images/icons/logo.ico" -HOME_IMAGE = "./assets/images/home.png" -ABOUT_IMAGE = "./assets/images/about.png" -TBAR_ICON_DEFAULT = "./assets/images/icons/default_icon.png" - -# Sizes -RBN_HEIGHT = 2.4 -TBAR_ISIZE_REL = 0.1 -TBAR_ISIZE_MARGIN = 1.3 -NAV_VIEW_RATIO = [ 1, 9,] - -# Mode -VIEW_IMAGE_MODE = 0 -SPLIT_VIEW_MODE = false -PERSIST_TEXT_MODE = 1 - -# Popups -LOAD_MODEL_POPUP = true -CHECK_INTERNET_POPUP = true -CHECK_INTERNET_URL = "8.8.8.8" - -[SELECTED_INDEX] -language = 0 -orientation = 0 -fontStyle = 0 -fontSize = 2 -imageScaling = 0 -modifier = 2 - -[PICKER_INDEX] -language = 49 -orientation = 50 -fontStyle = 51 -fontSize = 52 -imageScaling = 53 -modifier = 54 - -[SHORTCUT] -captureExternal = "Alt+Q" -captureExternalKey = "Q" -captureExternalTip = "This will minimize the app and perform OCR on the current screen. Alternatively, you may use the shortcut: " - -[NAV_FUNCS] -pathChanged = "viewImageFromFDialog" -navClicked = "viewImageFromExplorer" - -# Ribbon buttons (always on) -[MODE_FUNCS] - - [MODE_FUNCS.zoomIn] - helpTitle = "Zoom in" - helpMsg = "Hint: Double click the image to reset zoom." - path = "zoomIn.png" - toggle = false - align = "AlignRight" - iconH = 0.45 - iconW = 0.45 - - [MODE_FUNCS.zoomOut] - helpTitle = "Zoom out" - helpMsg = "Hint: Double click the image to reset zoom." - path = "zoomOut.png" - toggle = false - align = "AlignRight" - iconH = 0.45 - iconW = 0.45 - - [MODE_FUNCS.loadImageAtIndex] - helpTitle = "" - helpMsg = "Jump to page" - path = "loadImageAtIndex.png" - toggle = false - align = "AlignRight" - iconH = 0.45 - iconW = 1.3 - - [MODE_FUNCS.loadPrevImage] - helpTitle = "" - helpMsg = "Show previous image" - path = "loadPrevImage.png" - toggle = false - align = "AlignRight" - iconH = 0.45 - iconW = 0.6 - - [MODE_FUNCS.loadNextImage] - helpTitle = "" - helpMsg = "Show next image" - path = "loadNextImage.png" - toggle = false - align = "AlignRight" - iconH = 0.45 - iconW = 0.6 - -# Ribbon buttons -[TBAR_FUNCS] - - [TBAR_FUNCS.FILE] - - [TBAR_FUNCS.FILE.openDir] - helpTitle = "Open manga directory" - helpMsg = "Open a directory containing images." - path = "openDir.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.FILE.openManga] - helpTitle = "Open manga file" - helpMsg = "Supports the following formats: cbr, cbz, pdf." - path = "openManga.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.FILE.captureExternalHelper] - helpTitle = "External capture" - helpMsg = "This will minimize the app and perform OCR on the current screen. Alternatively, you may use the shortcut Alt+Q." - path = "captureExternalHelper.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.VIEW] - - [TBAR_FUNCS.VIEW.toggleStylesheet] - helpTitle = "Change theme" - helpMsg = "Switch between light and dark mode." - path = "toggleStylesheet.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.VIEW.hideExplorer] - helpTitle = "Hide explorer" - helpMsg = "Hide the file explorer from view" - path = "hideExplorer.png" - toggle = true - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.VIEW.modifyFontSettings] - helpTitle = "Modify preview text" - helpMsg = "Change font style and font size of preview text." - path = "modifyFontSettings.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.VIEW.toggleSplitView] - helpTitle = "Turn on split view" - helpMsg = "View two images at once." - path = "toggleSplitView.png" - toggle = true - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.VIEW.scaleImage] - helpTitle = "Adjust image scaling" - helpMsg = "Fit an image according to the available options: fit to width, fit to height, fit to screen" - path = "scaleImage.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.CONTROLS] - - [TBAR_FUNCS.CONTROLS.toggleMouseMode] - helpTitle = "Change mouse behavior" - helpMsg = "This will disable text detection. Turn this on only if do not want to hold CTRL key to zoom and pan on an image." - path = "toggleMouseMode.png" - toggle = true - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.CONTROLS.modifyHotkeys] - helpTitle = "Remap hotkeys" - helpMsg = "Change shortcut for external captures." - path = "modifyHotkeys.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.MISC] - - [TBAR_FUNCS.MISC.loadModel] - helpTitle = "Switch detection model" - helpMsg = "Switch between MangaOCR and Tesseract models." - path = "loadModel.png" - toggle = true - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.MISC.modifyTesseract] - helpTitle = "Tesseract settings" - helpMsg = "Set the language and orientation for the Tesseract model." - path = "modifyTesseract.png" - toggle = false - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 - - [TBAR_FUNCS.MISC.toggleLogging] - helpTitle = "Enable text logging" - helpMsg = "Save detected text to a text file located in the current project directory." - path = "toggleLogging.png" - toggle = true - align = "AlignLeft" - iconH = 1.0 - iconW = 1.0 \ No newline at end of file diff --git a/app/utils/constants.py b/app/utils/constants.py index 1826091..582b83e 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -20,6 +20,9 @@ # ------------------------------------- General ------------------------------------- # +APP_NAME = "Poricom" +APP_LOGO = "./assets/images/icons/logo.ico" + IMAGE_EXTENSIONS = ["*.bmp", "*.gif", "*.jpeg", "*.jpg", "*.pbm", "*.pgm", "*.png", "*.ppm", "*.webp", "*.xbm", "*.xpm",] # Settings @@ -58,6 +61,16 @@ SETTINGS_FILE_DEFAULT = './utils/poricom-config.ini' +# Window +MAIN_WINDOW_DEFAULTS = { + "hasLoadModelPopup": "true", + "explorerPath": "./assets/images/", + "stylesheetPath": "./assets/styles.qss" +} +MAIN_WINDOW_TYPES = { + "hasLoadModelPopup": bool +} + # View IMAGE_VIEW_DEFAULT = { "viewImageMode": 0, diff --git a/environment/base.yaml b/environment/base.yaml index b97fb86..59cb598 100644 --- a/environment/base.yaml +++ b/environment/base.yaml @@ -15,5 +15,4 @@ dependencies: - pyqtkeybind - rarfile - pdf2image - - toml - huggingface-hub==0.7.0 From 32be0b723e8d551e32ff0da00be1023a85217555 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 23:39:13 +0800 Subject: [PATCH 092/137] Refactor windows component file structure --- app/components/windows/__init__.py | 20 ++++++ .../windows/base.py} | 22 ++---- app/components/windows/external.py | 70 +++++++++++++++++++ app/main.py | 2 +- 4 files changed, 96 insertions(+), 18 deletions(-) create mode 100644 app/components/windows/__init__.py rename app/{MainWindow.py => components/windows/base.py} (90%) create mode 100644 app/components/windows/external.py diff --git a/app/components/windows/__init__.py b/app/components/windows/__init__.py new file mode 100644 index 0000000..639b764 --- /dev/null +++ b/app/components/windows/__init__.py @@ -0,0 +1,20 @@ +""" +Poricom Windows + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from .base import MainWindow diff --git a/app/MainWindow.py b/app/components/windows/base.py similarity index 90% rename from app/MainWindow.py rename to app/components/windows/base.py index 0a7bb79..075e76d 100644 --- a/app/MainWindow.py +++ b/app/components/windows/base.py @@ -21,15 +21,16 @@ from time import sleep from manga_ocr import MangaOcr -from PyQt5.QtCore import (Qt, QThreadPool) -from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QDesktopWidget, QMainWindow, QApplication, +from PyQt5.QtCore import (QThreadPool) +from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QMainWindow, QApplication, QPushButton, QFileDialog) +from .external import ExternalWindow from components.popups import BasePopup, CheckboxPopup from components.services import BaseWorker from components.settings import BaseSettings, PreviewOptions, ImageScalingOptions, OptionsContainer, ShortcutOptions, TesseractOptions from components.toolbar import BaseToolbar -from components.views import WorkspaceView, FullScreenOCRView +from components.views import WorkspaceView from utils.constants import LOAD_MODEL_MESSAGE, MAIN_WINDOW_DEFAULTS, MAIN_WINDOW_TYPES, STYLESHEET_DARK, STYLESHEET_LIGHT from utils.scripts import mangaFileToImageDir @@ -129,20 +130,7 @@ def captureExternalHelper(self): self.captureExternal() def captureExternal(self): - externalWindow = QMainWindow() - externalWindow.layout().setContentsMargins(0, 0, 0, 0) - externalWindow.setStyleSheet("border:0px; margin:0px") - externalWindow.setAttribute(Qt.WA_DeleteOnClose) - - externalWindow.setCentralWidget( - FullScreenOCRView(externalWindow, self.tracker)) - fullScreen = externalWindow.centralWidget() - - screenIndex = fullScreen.getActiveScreenIndex() - screen = QDesktopWidget().screenGeometry(screenIndex) - fullScreen.takeScreenshot(screenIndex) - externalWindow.move(screen.left(), screen.top()) - externalWindow.showFullScreen() + ExternalWindow(self).showFullScreen() # ------------------------------ View Functions ------------------------------ # diff --git a/app/components/windows/external.py b/app/components/windows/external.py new file mode 100644 index 0000000..272bace --- /dev/null +++ b/app/components/windows/external.py @@ -0,0 +1,70 @@ +""" +Poricom Windows + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from typing import TYPE_CHECKING + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QCloseEvent, QCursor +from PyQt5.QtWidgets import QApplication, QDesktopWidget, QMainWindow + +from components.views import FullScreenOCRView + +if TYPE_CHECKING: + from .base import MainWindow + + +class ExternalWindow(QMainWindow): + """ + External window widget to enclose FullScreenOCRView + """ + def __init__(self, parent: "MainWindow"): + super().__init__() + self.mainWindow = parent + + # By setting the border thickness and margin to zero, + # we ensure that the whole screen is captured. + self.layout().setContentsMargins(0, 0, 0, 0) + self.setStyleSheet("border:0px; margin:0px") + + # Delete external window on close + self.setAttribute(Qt.WA_DeleteOnClose) + + # WindowStaysOnTopHint & Popup flags ensures that the widget is the top window. + self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.Popup) + + self.setCentralWidget(FullScreenOCRView(self, parent.tracker)) + # self.ocrModel = parent.ocrModel + + def showFullScreen(self): + # Overridden to show on the active screen + fullscreen: FullScreenOCRView = self.centralWidget() + screenIndex = fullscreen.getActiveScreenIndex() + + # TODO: Find an alternative way to show the active screen, + # since QDesktopWidget is obsolete according to Qt docs + screen = QDesktopWidget().screenGeometry(screenIndex) + fullscreen.takeScreenshot(screenIndex) + self.move(screen.left(), screen.top()) + + return super().showFullScreen() + + def closeEvent(self, event: QCloseEvent): + # Ensure that object is deleted before closing + self.deleteLater() + return super().closeEvent(event) \ No newline at end of file diff --git a/app/main.py b/app/main.py index 2dcdc5c..d3f35c6 100644 --- a/app/main.py +++ b/app/main.py @@ -24,7 +24,7 @@ from pyqtkeybind import keybinder from components.services import WinEventFilter -from MainWindow import MainWindow +from components.windows import MainWindow from Trackers import Tracker from utils.constants import APP_NAME, APP_LOGO, SETTINGS_FILE_DEFAULT, STYLESHEET_LIGHT From 289454b92022a64092b3d02bec2bad2b2ddccdac Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 21 Jan 2023 23:42:04 +0800 Subject: [PATCH 093/137] Move binaries and settings to bin --- app/Trackers.py | 2 +- app/{utils => bin}/unrar.exe | Bin app/utils/constants.py | 2 +- app/utils/scripts/mangaFileToImageDir.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename app/{utils => bin}/unrar.exe (100%) diff --git a/app/Trackers.py b/app/Trackers.py index 9654efe..d68bb09 100644 --- a/app/Trackers.py +++ b/app/Trackers.py @@ -26,7 +26,7 @@ from utils.constants import EXPLORER_ROOT_DEFAULT, IMAGE_EXTENSIONS, SETTINGS_FILE_DEFAULT settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) -split = settings.value("splitViewMode").lower() == "true" +split = settings.value("splitViewMode", "false").lower() == "true" explorerPath = settings.value("explorerPath", EXPLORER_ROOT_DEFAULT) # TODO: This needs refactoring diff --git a/app/utils/unrar.exe b/app/bin/unrar.exe similarity index 100% rename from app/utils/unrar.exe rename to app/bin/unrar.exe diff --git a/app/utils/constants.py b/app/utils/constants.py index 582b83e..95ad056 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -59,7 +59,7 @@ # ------------------------------------ Settings ------------------------------------- # -SETTINGS_FILE_DEFAULT = './utils/poricom-config.ini' +SETTINGS_FILE_DEFAULT = './bin/poricom-config.ini' # Window MAIN_WINDOW_DEFAULTS = { diff --git a/app/utils/scripts/mangaFileToImageDir.py b/app/utils/scripts/mangaFileToImageDir.py index 4f55fcc..30b5f81 100644 --- a/app/utils/scripts/mangaFileToImageDir.py +++ b/app/utils/scripts/mangaFileToImageDir.py @@ -41,7 +41,7 @@ def mangaFileToImageDir(filepath: str): with zipfile.ZipFile(filepath, 'r') as zipRef: zipRef.extractall(cachePath) - rarfile.UNRAR_TOOL = "utils/unrar.exe" + rarfile.UNRAR_TOOL = "bin/unrar.exe" if extension in [".cbr", ".rar"]: with rarfile.RarFile(filepath) as zipRef: zipRef.extractall(cachePath) From 8147e725904d88dc5bdd8bfb357488b73af908ea Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 22 Jan 2023 00:25:30 +0800 Subject: [PATCH 094/137] Format with black --- app/Trackers.py | 29 +++-- app/components/explorers/image.py | 6 +- app/components/explorers/models/image.py | 6 +- app/components/misc/screenAware.py | 3 +- app/components/popups/checkbox.py | 15 ++- app/components/services/filters.py | 4 +- app/components/services/workers/base.py | 3 +- app/components/services/workers/signal.py | 2 +- app/components/settings/__init__.py | 8 +- app/components/settings/base.py | 6 +- app/components/settings/popups/base.py | 13 +- app/components/settings/popups/container.py | 16 ++- .../settings/popups/imageScaling.py | 2 +- app/components/settings/popups/preview.py | 20 +-- app/components/settings/popups/shortcut.py | 10 +- app/components/settings/popups/tesseract.py | 7 +- app/components/toolbar/base.py | 7 +- app/components/toolbar/tabs/base.py | 6 +- .../toolbar/tabs/containers/base.py | 23 ++-- .../toolbar/tabs/containers/navigate.py | 3 +- app/components/views/image/base.py | 74 ++++++----- app/components/views/ocr/base.py | 12 +- app/components/views/ocr/fullscreen.py | 10 +- app/components/views/ocr/ocr.py | 4 +- app/components/views/workspace.py | 24 ++-- app/components/windows/base.py | 70 ++++++----- app/components/windows/external.py | 5 +- app/main.py | 7 +- app/utils/constants.py | 116 ++++++++++-------- app/utils/scripts/editStylesheet.py | 5 +- app/utils/scripts/logText.py | 5 +- app/utils/scripts/mangaFileToImageDir.py | 8 +- app/utils/scripts/pixmapToText.py | 13 +- app/utils/types.py | 2 + 34 files changed, 324 insertions(+), 220 deletions(-) diff --git a/app/Trackers.py b/app/Trackers.py index d68bb09..58fe95c 100644 --- a/app/Trackers.py +++ b/app/Trackers.py @@ -23,7 +23,11 @@ from PyQt5.QtCore import QSettings from PyQt5.QtGui import QPixmap, QPainter -from utils.constants import EXPLORER_ROOT_DEFAULT, IMAGE_EXTENSIONS, SETTINGS_FILE_DEFAULT +from utils.constants import ( + EXPLORER_ROOT_DEFAULT, + IMAGE_EXTENSIONS, + SETTINGS_FILE_DEFAULT, +) settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) split = settings.value("splitViewMode", "false").lower() == "true" @@ -71,10 +75,10 @@ def twoFileToImage(self, fileLeft, fileRight): h = imageLeft.height() splitImage = QPixmap(w, h) painter = QPainter(splitImage) - painter.drawPixmap(0, 0, imageLeft.width(), imageLeft.height(), - imageLeft) - painter.drawPixmap(imageLeft.width(), 0, imageRight.width(), - imageRight.height(), imageRight) + painter.drawPixmap(0, 0, imageLeft.width(), imageLeft.height(), imageLeft) + painter.drawPixmap( + imageLeft.width(), 0, imageRight.width(), imageRight.height(), imageRight + ) painter.end() return splitImage @@ -85,11 +89,11 @@ def pixImage(self): @pixImage.setter def pixImage(self, image): - if (type(image) is str and PImage(image).isValid()): + if type(image) is str and PImage(image).isValid(): self._pixImage = PImage(image) self._pixImage.filename = abspath(image) self._filepath = abspath(dirname(image)) - if (type(image) is tuple): + if type(image) is tuple: fileLeft, fileRight = image if not fileRight: if fileLeft: @@ -118,8 +122,14 @@ def filepath(self): @filepath.setter def filepath(self, filepath): fileList = filter(lambda f: isfile(join(filepath, f)), listdir(filepath)) - imageList = list(map(lambda p: normpath(join(filepath, p)), filter( - (lambda f: ('*'+splitext(f)[1]) in IMAGE_EXTENSIONS), fileList))) + imageList = list( + map( + lambda p: normpath(join(filepath, p)), + filter( + (lambda f: ("*" + splitext(f)[1]) in IMAGE_EXTENSIONS), fileList + ), + ) + ) if len(imageList) <= 0: raise FileNotFoundError("Empty directory") @@ -152,7 +162,6 @@ def switchOCRMode(self): class PImage(QPixmap): - def __init__(self, *args): super(QPixmap, self).__init__(args[0]) diff --git a/app/components/explorers/image.py b/app/components/explorers/image.py index 2ac7ba8..6ed016b 100644 --- a/app/components/explorers/image.py +++ b/app/components/explorers/image.py @@ -16,12 +16,13 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (Qt) -from PyQt5.QtWidgets import (QMainWindow, QTreeView) +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMainWindow, QTreeView from .models import ImageModel from utils.constants import EXPLORER_ROOT_DEFAULT + class ImageExplorer(QTreeView): """View to allow exploring images @@ -29,6 +30,7 @@ class ImageExplorer(QTreeView): parent (QMainWindow): Image explorer parent. Set to main window. initialDir (str, optional): Initial directory. Defaults to EXPLORER_ROOT_DEFAULT. """ + def __init__(self, parent: QMainWindow, initialDir: str = EXPLORER_ROOT_DEFAULT): super().__init__(parent) # TODO: It might be better if the parent is set to the QSplitter diff --git a/app/components/explorers/models/image.py b/app/components/explorers/models/image.py index d676676..ccda546 100644 --- a/app/components/explorers/models/image.py +++ b/app/components/explorers/models/image.py @@ -16,15 +16,17 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (QModelIndex) -from PyQt5.QtWidgets import (QFileSystemModel) +from PyQt5.QtCore import QModelIndex +from PyQt5.QtWidgets import QFileSystemModel from utils.constants import IMAGE_EXTENSIONS + class ImageModel(QFileSystemModel): """ Image model based on the native file system """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setNameFilterDisables(False) diff --git a/app/components/misc/screenAware.py b/app/components/misc/screenAware.py index 2b4372a..fd1da21 100644 --- a/app/components/misc/screenAware.py +++ b/app/components/misc/screenAware.py @@ -17,13 +17,14 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QApplication, QWidget) +from PyQt5.QtWidgets import QApplication, QWidget class ScreenAwareWidget(QWidget): """ Screen-aware widget. Allows retrieving desktop screen dimensions """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/app/components/popups/checkbox.py b/app/components/popups/checkbox.py index bdd4c92..67971f2 100644 --- a/app/components/popups/checkbox.py +++ b/app/components/popups/checkbox.py @@ -18,11 +18,12 @@ """ from PyQt5.QtCore import QSettings -from PyQt5.QtWidgets import (QCheckBox) +from PyQt5.QtWidgets import QCheckBox from components.popups import BasePopup from utils.constants import SETTINGS_FILE_DEFAULT + class CheckboxPopup(BasePopup): """Popup message with a checkbox @@ -31,9 +32,15 @@ class CheckboxPopup(BasePopup): checkboxMessage (str, optional): Checkbox label. Defaults to "Don't show this dialog again". """ - def __init__(self, prop: str, title: str, message: str, - buttons: BasePopup.StandardButtons = BasePopup.Ok, - checkboxMessage="Don't show this dialog again"): + + def __init__( + self, + prop: str, + title: str, + message: str, + buttons: BasePopup.StandardButtons = BasePopup.Ok, + checkboxMessage="Don't show this dialog again", + ): super().__init__(title, message, buttons) self.setCheckBox(QCheckBox(checkboxMessage, self)) diff --git a/app/components/services/filters.py b/app/components/services/filters.py index 3504795..2764923 100644 --- a/app/components/services/filters.py +++ b/app/components/services/filters.py @@ -17,7 +17,7 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (QAbstractNativeEventFilter) +from PyQt5.QtCore import QAbstractNativeEventFilter class WinEventFilter(QAbstractNativeEventFilter): @@ -27,4 +27,4 @@ def __init__(self, keybinder): def nativeEventFilter(self, eventType, message): ret = self.keybinder.handler(eventType, message) - return ret, 0 \ No newline at end of file + return ret, 0 diff --git a/app/components/services/workers/base.py b/app/components/services/workers/base.py index 486a17b..d122b85 100644 --- a/app/components/services/workers/base.py +++ b/app/components/services/workers/base.py @@ -19,10 +19,11 @@ from typing import Callable -from PyQt5.QtCore import (pyqtSlot, QRunnable) +from PyQt5.QtCore import pyqtSlot, QRunnable from .signal import BaseWorkerSignal + class BaseWorker(QRunnable): """Runnable object to support multithreading diff --git a/app/components/services/workers/signal.py b/app/components/services/workers/signal.py index a13446a..26e7f89 100644 --- a/app/components/services/workers/signal.py +++ b/app/components/services/workers/signal.py @@ -17,7 +17,7 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (pyqtSignal, QObject) +from PyQt5.QtCore import pyqtSignal, QObject class BaseWorkerSignal(QObject): diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py index 32784d0..f6f8e58 100644 --- a/app/components/settings/__init__.py +++ b/app/components/settings/__init__.py @@ -18,4 +18,10 @@ """ from .base import BaseSettings -from .popups import ImageScalingOptions, OptionsContainer, PreviewOptions, ShortcutOptions, TesseractOptions +from .popups import ( + ImageScalingOptions, + OptionsContainer, + PreviewOptions, + ShortcutOptions, + TesseractOptions, +) diff --git a/app/components/settings/base.py b/app/components/settings/base.py index 78aeb13..527b18c 100644 --- a/app/components/settings/base.py +++ b/app/components/settings/base.py @@ -35,7 +35,9 @@ class BaseSettings(QWidget): prefix (str, optional): Text added to the saved property. Defaults to "". """ - def __init__(self, parent: QWidget, file: str = SETTINGS_FILE_DEFAULT, prefix: str = ""): + def __init__( + self, parent: QWidget, file: str = SETTINGS_FILE_DEFAULT, prefix: str = "" + ): super().__init__(parent) self.settings = QSettings(file, QSettings.IniFormat) @@ -71,7 +73,7 @@ def setTypes(self, types: dict[str, Callable]): Use `self._types` to set the correct property type. """ self._types = types - + def addTypes(self, types: dict[str, Callable]): """ Extends the types dictionary, if it exists diff --git a/app/components/settings/popups/base.py b/app/components/settings/popups/base.py index a3de2f8..6e06c88 100644 --- a/app/components/settings/popups/base.py +++ b/app/components/settings/popups/base.py @@ -21,7 +21,7 @@ from stringcase import titlecase, capitalcase from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import (QComboBox, QGridLayout, QLabel, QWidget) +from PyQt5.QtWidgets import QComboBox, QGridLayout, QLabel, QWidget from ..base import BaseSettings @@ -30,7 +30,8 @@ class BaseOptions(BaseSettings): """ Allows saving/selecting options """ - def __init__(self, parent: QWidget, optionLists:list[list[str]]=[]): + + def __init__(self, parent: QWidget, optionLists: list[list[str]] = []): super().__init__(parent) self.mainWindow = parent self.setAttribute(Qt.WA_DeleteOnClose) @@ -80,11 +81,13 @@ def initializeProperties(self, props: list[tuple[str, Any, Callable]]): # Label self.labelList[i].setText(f"{titlecase(prop)}: ") - + # Combo Box comboBox = self.comboBoxList[i] self.setProperty(f"{prop}ComboBox", comboBox) # Child classes must implement change{PropName} method - comboBox.currentIndexChanged.connect(self.getProperty(f"change{capitalcase(prop)}")) - self.setOptionIndex(prop) \ No newline at end of file + comboBox.currentIndexChanged.connect( + self.getProperty(f"change{capitalcase(prop)}") + ) + self.setOptionIndex(prop) diff --git a/app/components/settings/popups/container.py b/app/components/settings/popups/container.py index 5c037b4..369ca4c 100644 --- a/app/components/settings/popups/container.py +++ b/app/components/settings/popups/container.py @@ -17,26 +17,30 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (Qt) -from PyQt5.QtWidgets import (QDialog, QDialogButtonBox, QVBoxLayout) +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout from .base import BaseOptions + class OptionsContainer(QDialog): """Dialog to contain option widgets Args: options (BaseOptions): Child option widget """ + def __init__(self, options: BaseOptions): - super().__init__(None, Qt.WindowCloseButtonHint | Qt.WindowSystemMenuHint | Qt.WindowTitleHint) + super().__init__( + None, + Qt.WindowCloseButtonHint | Qt.WindowSystemMenuHint | Qt.WindowTitleHint, + ) self.setAttribute(Qt.WA_DeleteOnClose) self.options = options self.setLayout(QVBoxLayout()) self.layout().addWidget(options) - self.buttonBox = QDialogButtonBox( - QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.layout().addWidget(self.buttonBox) self.buttonBox.rejected.connect(self.cancelClickedEvent) @@ -49,7 +53,7 @@ def accept(self): def cancelClickedEvent(self): self.close() - + def closeEvent(self, event): self.options.close() return super().closeEvent(event) diff --git a/app/components/settings/popups/imageScaling.py b/app/components/settings/popups/imageScaling.py index 0369310..c348763 100644 --- a/app/components/settings/popups/imageScaling.py +++ b/app/components/settings/popups/imageScaling.py @@ -17,7 +17,7 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QWidget) +from PyQt5.QtWidgets import QWidget from .base import BaseOptions from utils.constants import IMAGE_SCALING diff --git a/app/components/settings/popups/preview.py b/app/components/settings/popups/preview.py index 0e59f26..bf8b9be 100644 --- a/app/components/settings/popups/preview.py +++ b/app/components/settings/popups/preview.py @@ -17,7 +17,7 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QWidget) +from PyQt5.QtWidgets import QWidget from .base import BaseOptions from utils.constants import FONT_SIZE, FONT_STYLE, TOGGLE_CHOICES @@ -27,11 +27,13 @@ class PreviewOptions(BaseOptions): def __init__(self, parent: QWidget): super().__init__(parent, [FONT_STYLE, FONT_SIZE, TOGGLE_CHOICES]) - self.initializeProperties([ - ("fontStyle", " font-family: 'Helvetica';\n", str), - ("fontSize", " font-size: 16pt;\n", str), - ("persistText", "true", bool), - ]) + self.initializeProperties( + [ + ("fontStyle", " font-family: 'Helvetica';\n", str), + ("fontSize", " font-size: 16pt;\n", str), + ("persistText", "true", bool), + ] + ) self.setOptionIndex("fontSize", 2) self.setOptionIndex("persistText", 1) @@ -46,7 +48,7 @@ def changeFontSize(self, i): selectedFontSize = int(self.fontSizeComboBox.currentText().strip()) replacementText = f" font-size: {selectedFontSize}pt;\n" self.fontSize = replacementText - + def changePersistText(self, i): self.persistTextIndex = i self.persistText = True if i else False @@ -54,5 +56,7 @@ def changePersistText(self, i): def saveSettings(self, hasMessage=False): editStylesheet(41, self.fontStyle) editStylesheet(42, self.fontSize) - self.mainWindow.canvas.setProperty('persistText', "true" if self.persistText else "false") + self.mainWindow.canvas.setProperty( + "persistText", "true" if self.persistText else "false" + ) return super().saveSettings(hasMessage) diff --git a/app/components/settings/popups/shortcut.py b/app/components/settings/popups/shortcut.py index 8d57ee7..eaad453 100644 --- a/app/components/settings/popups/shortcut.py +++ b/app/components/settings/popups/shortcut.py @@ -17,21 +17,21 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QLabel, QLineEdit, QWidget) +from PyQt5.QtWidgets import QLabel, QLineEdit, QWidget from .base import BaseOptions from components.popups import BasePopup from utils.constants import MODIFIER + class ShortcutOptions(BaseOptions): def __init__(self, parent: QWidget): super().__init__(parent, [MODIFIER]) self.initializeProperties([("modifier", "Alt", str)]) self.setOptionIndex("modifier", 2) - self.addDefaults({ - "captureExternalKey": "Q", - "captureExternalShortcut": "Alt+Q" - }) + self.addDefaults( + {"captureExternalKey": "Q", "captureExternalShortcut": "Alt+Q"} + ) self.loadSettings() self.keyLineEdit = QLineEdit(self.captureExternalKey) diff --git a/app/components/settings/popups/tesseract.py b/app/components/settings/popups/tesseract.py index 5ef0741..ff2ff94 100644 --- a/app/components/settings/popups/tesseract.py +++ b/app/components/settings/popups/tesseract.py @@ -17,17 +17,20 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QWidget) +from PyQt5.QtWidgets import QWidget from utils.constants import LANGUAGE, ORIENTATION from .base import BaseOptions + class TesseractOptions(BaseOptions): def __init__(self, parent: QWidget): super().__init__(parent, [LANGUAGE, ORIENTATION]) # TODO: Use constants here - self.initializeProperties([("language", "jpn", str), ("orientation", "_vert", str)]) + self.initializeProperties( + [("language", "jpn", str), ("orientation", "_vert", str)] + ) def changeLanguage(self, i): self.languageIndex = i diff --git a/app/components/toolbar/base.py b/app/components/toolbar/base.py index 73362ca..2da74e9 100644 --- a/app/components/toolbar/base.py +++ b/app/components/toolbar/base.py @@ -17,11 +17,12 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QMainWindow, QSizePolicy, QTabWidget) +from PyQt5.QtWidgets import QMainWindow, QSizePolicy, QTabWidget from .tabs import BaseToolbarTab, NavigateToolbarContainer from utils.constants import TOOLBAR_FUNCTIONS + class BaseToolbar(QTabWidget): """ Toolbar widget @@ -31,6 +32,7 @@ class BaseToolbar(QTabWidget): Notes: Parent must be passed to children to call main window functions. """ + def __init__(self, parent: QMainWindow): super(QTabWidget, self).__init__(parent) self.parent = parent @@ -40,6 +42,5 @@ def __init__(self, parent: QMainWindow): for tabName, funcs in TOOLBAR_FUNCTIONS.items(): tab = BaseToolbarTab(parent=self.parent, funcs=funcs) tab.layout().addStretch() - tab.layout().addWidget( - NavigateToolbarContainer(self.parent)) + tab.layout().addWidget(NavigateToolbarContainer(self.parent)) self.addTab(tab, tabName.upper()) diff --git a/app/components/toolbar/tabs/base.py b/app/components/toolbar/tabs/base.py index 63df709..9285868 100644 --- a/app/components/toolbar/tabs/base.py +++ b/app/components/toolbar/tabs/base.py @@ -17,11 +17,12 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QHBoxLayout, QMainWindow) +from PyQt5.QtWidgets import QHBoxLayout, QMainWindow from .containers import BaseToolbarContainer from utils.types import ButtonConfigDict + class BaseToolbarTab(BaseToolbarContainer): """Tab widget to arrange toolbar tab containers @@ -29,7 +30,8 @@ class BaseToolbarTab(BaseToolbarContainer): parent (QMainWindow): Toolbar tab parent. Set to main window. funcs (ButtonConfigDict, optional): Toolbar function configuration. Defaults to {}. """ - def __init__(self, parent: QMainWindow, funcs: ButtonConfigDict={}): + + def __init__(self, parent: QMainWindow, funcs: ButtonConfigDict = {}): super().__init__(parent) self.initializeButtons(funcs) diff --git a/app/components/toolbar/tabs/containers/base.py b/app/components/toolbar/tabs/containers/base.py index d1ab13f..f889f04 100644 --- a/app/components/toolbar/tabs/containers/base.py +++ b/app/components/toolbar/tabs/containers/base.py @@ -19,20 +19,22 @@ from os.path import exists -from PyQt5.QtCore import (QSize) -from PyQt5.QtGui import (QIcon) -from PyQt5.QtWidgets import (QMainWindow, QPushButton) +from PyQt5.QtCore import QSize +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QMainWindow, QPushButton from components.misc import ScreenAwareWidget from utils.constants import TOOLBAR_ICON_DEFAULT, TOOLBAR_ICON_SIZE, TOOLBAR_ICONS from utils.types import ButtonConfig + class BaseToolbarContainer(ScreenAwareWidget): """Widget that contains the toolbar functions Args: parent (QMainWindow): Container parent. Set to main window. """ + def __init__(self, parent: QMainWindow): super().__init__(parent) @@ -57,13 +59,13 @@ def initializeButton(self, name: str, config: ButtonConfig): # Set button icon and size path = TOOLBAR_ICONS + config["path"] - if (exists(path)): + if exists(path): icon = QIcon(path) else: icon = QIcon(TOOLBAR_ICON_DEFAULT) button.setIcon(icon) - w = self.primaryScreenHeight()*TOOLBAR_ICON_SIZE*config["iconWidth"] - h = self.primaryScreenHeight()*TOOLBAR_ICON_SIZE*config["iconHeight"] + w = self.primaryScreenHeight() * TOOLBAR_ICON_SIZE * config["iconWidth"] + h = self.primaryScreenHeight() * TOOLBAR_ICON_SIZE * config["iconHeight"] button.setIconSize(QSize(w, h)) tooltip = f"\ @@ -76,12 +78,9 @@ def initializeButton(self, name: str, config: ButtonConfig): # Connect button to main window function try: - button.clicked.connect( - getattr(self.mainWindow, name)) + button.clicked.connect(getattr(self.mainWindow, name)) except AttributeError: try: - button.clicked.connect( - getattr(self.mainWindow.mainView, name)) + button.clicked.connect(getattr(self.mainWindow.mainView, name)) except AttributeError: - button.clicked.connect( - getattr(self.mainWindow, 'noop')) + button.clicked.connect(getattr(self.mainWindow, "noop")) diff --git a/app/components/toolbar/tabs/containers/navigate.py b/app/components/toolbar/tabs/containers/navigate.py index df04d29..f9f441c 100644 --- a/app/components/toolbar/tabs/containers/navigate.py +++ b/app/components/toolbar/tabs/containers/navigate.py @@ -17,11 +17,12 @@ along with this program. If not, see . """ -from PyQt5.QtWidgets import (QGridLayout, QMainWindow) +from PyQt5.QtWidgets import QGridLayout, QMainWindow from .base import BaseToolbarContainer from utils.constants import NAVIGATION_FUNCTIONS + class NavigateToolbarContainer(BaseToolbarContainer): """Widget that contains the toolbar navigation functions diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index 0b69dbc..865fe08 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -20,8 +20,8 @@ from time import sleep from typing import TYPE_CHECKING -from PyQt5.QtCore import (Qt, QRect, QRectF, QSize, QThreadPool) -from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QGraphicsView) +from PyQt5.QtCore import Qt, QRect, QRectF, QSize, QThreadPool +from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsView from components.services import BaseWorker from components.settings import BaseSettings @@ -30,12 +30,13 @@ if TYPE_CHECKING: from ..workspace import WorkspaceView + class BaseImageView(QGraphicsView, BaseSettings): """ - Base image view to allow view/zoom/pan functions + Base image view to allow view/zoom/pan functions """ - def __init__(self, parent: 'WorkspaceView', tracker=None): + def __init__(self, parent: "WorkspaceView", tracker=None): super().__init__(parent) self.tracker = tracker @@ -56,28 +57,31 @@ def __init__(self, parent: 'WorkspaceView', tracker=None): self.initializePixmapItem() -# ------------------------------------ Settings ------------------------------------- # + # ------------------------------------ Settings ------------------------------------- # def setViewImageMode(self, mode: int): # TODO: This should be an enum not an int - self.setProperty('viewImageMode', mode) + self.setProperty("viewImageMode", mode) self.saveSettings(hasMessage=False) self.viewImage() def toggleSplitView(self): - self.setProperty('splitViewMode', "false" if self.splitViewMode else "true") + self.setProperty("splitViewMode", "false" if self.splitViewMode else "true") self.saveSettings(hasMessage=False) def toggleZoomPanMode(self): - self.setProperty('zoomPanMode', "false" if self.zoomPanMode else "true") + self.setProperty("zoomPanMode", "false" if self.zoomPanMode else "true") self.saveSettings(hasMessage=False) -# -------------------------------------- View --------------------------------------- # + # -------------------------------------- View --------------------------------------- # def initializePixmapItem(self): self.setScene(QGraphicsScene()) - self.pixmap = self.scene().addPixmap(self.tracker.pixImage.scaledToWidth( - self.viewport().geometry().width(), Qt.SmoothTransformation)) + self.pixmap = self.scene().addPixmap( + self.tracker.pixImage.scaledToWidth( + self.viewport().geometry().width(), Qt.SmoothTransformation + ) + ) def viewImage(self): # self.verticalScrollBar().setSliderPosition(0) @@ -86,13 +90,18 @@ def viewImage(self): h = self.viewport().geometry().height() if self.viewImageMode == 0: self.pixmap.setPixmap( - self.tracker.pixImage.scaledToWidth(w, Qt.SmoothTransformation)) + self.tracker.pixImage.scaledToWidth(w, Qt.SmoothTransformation) + ) elif self.viewImageMode == 1: self.pixmap.setPixmap( - self.tracker.pixImage.scaledToHeight(h, Qt.SmoothTransformation)) + self.tracker.pixImage.scaledToHeight(h, Qt.SmoothTransformation) + ) elif self.viewImageMode == 2: - self.pixmap.setPixmap(self.tracker.pixImage.scaled( - w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)) + self.pixmap.setPixmap( + self.tracker.pixImage.scaled( + w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation + ) + ) self.scene().setSceneRect(QRectF(self.pixmap.pixmap().rect())) def zoomView(self, isZoomIn): @@ -135,21 +144,25 @@ def suppressScroll(): self._scrollSuppressed = True worker = BaseWorker(sleep, 0.3) worker.signals.finished.connect( - lambda: setattr(self, "_scrollSuppressed", False)) + lambda: setattr(self, "_scrollSuppressed", False) + ) QThreadPool.globalInstance().start(worker) - if (event.angleDelta().y() < 0 and - self.verticalScrollBar().value() == self.verticalScrollBar().maximum()): - if (event.angleDelta().y() > -wheelDelta): - if (self._trackPadAtMax == trackpadScrollLimit): + if ( + event.angleDelta().y() < 0 + and self.verticalScrollBar().value() + == self.verticalScrollBar().maximum() + ): + if event.angleDelta().y() > -wheelDelta: + if self._trackPadAtMax == trackpadScrollLimit: self.parent().loadNextImage() self._trackPadAtMax = 0 suppressScroll() return else: self._trackPadAtMax += 1 - elif (event.angleDelta().y() <= -wheelDelta): - if (self._scrollAtMax == mouseScrollLimit): + elif event.angleDelta().y() <= -wheelDelta: + if self._scrollAtMax == mouseScrollLimit: self.parent().loadNextImage() self._scrollAtMax = 0 suppressScroll() @@ -157,18 +170,21 @@ def suppressScroll(): else: self._scrollAtMax += 1 - if (event.angleDelta().y() > 0 and - self.verticalScrollBar().value() == self.verticalScrollBar().minimum()): - if (event.angleDelta().y() < wheelDelta): - if (self._trackPadAtMin == trackpadScrollLimit): + if ( + event.angleDelta().y() > 0 + and self.verticalScrollBar().value() + == self.verticalScrollBar().minimum() + ): + if event.angleDelta().y() < wheelDelta: + if self._trackPadAtMin == trackpadScrollLimit: self.parent().loadPrevImage() self._trackPadAtMin = 0 suppressScroll() return else: self._trackPadAtMin += 1 - elif (event.angleDelta().y() >= wheelDelta): - if (self._scrollAtMin == mouseScrollLimit): + elif event.angleDelta().y() >= wheelDelta: + if self._scrollAtMin == mouseScrollLimit: self.parent().loadPrevImage() self._scrollAtMin = 0 suppressScroll() @@ -193,7 +209,7 @@ def mouseDoubleClickEvent(self, event): self.viewImage(self.currentScale) super().mouseDoubleClickEvent(event) -# ------------------------------------ Shortcut ------------------------------------- # + # ------------------------------------ Shortcut ------------------------------------- # # TODO: Keyboard shortcuts should be in another class def keyPressEvent(self, event): diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index ff7b956..02ee5b1 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -17,14 +17,15 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (pyqtSlot, Qt, QThreadPool, QTimer) -from PyQt5.QtWidgets import (QGraphicsView, QLabel, QMainWindow) +from PyQt5.QtCore import pyqtSlot, Qt, QThreadPool, QTimer +from PyQt5.QtWidgets import QGraphicsView, QLabel, QMainWindow from components.services import BaseWorker from components.settings import BaseSettings from utils.constants import TESSERACT_DEFAULTS from utils.scripts import logText, pixmapToText + class BaseOCRView(QGraphicsView, BaseSettings): """Base view with OCR capabilities @@ -32,6 +33,7 @@ class BaseOCRView(QGraphicsView, BaseSettings): parent (QMainWindow): View parent. Set to main window tracker (Any, optional): State tracker. Defaults to None. """ + def __init__(self, parent: QMainWindow, tracker=None): # TODO: Remove references to tracker super().__init__(parent) @@ -50,14 +52,14 @@ def __init__(self, parent: QMainWindow, tracker=None): self.setDragMode(QGraphicsView.RubberBandDrag) self.addDefaults(TESSERACT_DEFAULTS) - self.addProperty('persistText', "true", bool) + self.addProperty("persistText", "true", bool) def handleTextResult(self, result): try: self.canvasText.setText(result) except RuntimeError: pass - + def handleTextFinished(self): try: self.canvasText.adjustSize() @@ -70,7 +72,7 @@ def handleTextFinished(self): @pyqtSlot() def rubberBandStopped(self): - if (self.canvasText.isHidden()): + if self.canvasText.isHidden(): self.canvasText.setText("") self.canvasText.adjustSize() self.canvasText.show() diff --git a/app/components/views/ocr/fullscreen.py b/app/components/views/ocr/fullscreen.py index 90a42d3..53c3dd9 100644 --- a/app/components/views/ocr/fullscreen.py +++ b/app/components/views/ocr/fullscreen.py @@ -17,8 +17,8 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (Qt, QRectF) -from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QMainWindow) +from PyQt5.QtCore import Qt, QRectF +from PyQt5.QtWidgets import QApplication, QGraphicsScene, QMainWindow from PyQt5.QtGui import QCursor, QMouseEvent from .base import BaseOCRView @@ -29,6 +29,7 @@ class FullScreenOCRView(BaseOCRView): """ Fullscreen view with OCR capabilities """ + def __init__(self, parent: QMainWindow, tracker=None): super().__init__(parent, tracker) self.externalWindow = parent @@ -42,8 +43,9 @@ def __init__(self, parent: QMainWindow, tracker=None): def takeScreenshot(self, screenIndex: int): screen = QApplication.screens()[screenIndex] s = screen.size() - self.pixmap = self.scene().addPixmap(screen.grabWindow( - 0).scaled(s.width(), s.height())) + self.pixmap = self.scene().addPixmap( + screen.grabWindow(0).scaled(s.width(), s.height()) + ) self.scene().setSceneRect(QRectF(self.pixmap.pixmap().rect())) def getActiveScreenIndex(self): diff --git a/app/components/views/ocr/ocr.py b/app/components/views/ocr/ocr.py index ebb1358..ac2931a 100644 --- a/app/components/views/ocr/ocr.py +++ b/app/components/views/ocr/ocr.py @@ -17,8 +17,8 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (pyqtSlot) -from PyQt5.QtWidgets import (QMainWindow) +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtWidgets import QMainWindow from ..image import BaseImageView from .base import BaseOCRView diff --git a/app/components/views/workspace.py b/app/components/views/workspace.py index 65ee096..24cd644 100644 --- a/app/components/views/workspace.py +++ b/app/components/views/workspace.py @@ -17,8 +17,8 @@ along with this program. If not, see . """ -from PyQt5.QtCore import (Qt) -from PyQt5.QtWidgets import (QInputDialog, QMainWindow, QSplitter) +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QInputDialog, QMainWindow, QSplitter from .ocr import OCRView from components.explorers import ImageExplorer @@ -29,6 +29,7 @@ class WorkspaceView(QSplitter): """ Main view of the program. Includes the explorer and the view. """ + def __init__(self, parent: QMainWindow, tracker=None): super().__init__(parent) self.tracker = tracker @@ -62,7 +63,7 @@ def loadPrevImage(self): tempIndex = self.explorer.indexAbove(index) if tempIndex.isValid(): index = tempIndex - if (not index.isValid()): + if not index.isValid(): return self.explorer.setCurrentIndex(index) @@ -72,7 +73,7 @@ def loadNextImage(self): tempIndex = self.explorer.indexBelow(index) if tempIndex.isValid(): index = tempIndex - if (not index.isValid()): + if not index.isValid(): return self.explorer.setCurrentIndex(index) @@ -80,16 +81,17 @@ def loadImageAtIndex(self): rowCount = self.explorer.model().rowCount(self.explorer.rootIndex()) i, _ = QInputDialog.getInt( self, - 'Jump to', - f'Enter page number: (max is {rowCount})', + "Jump to", + f"Enter page number: (max is {rowCount})", value=-1, min=1, max=rowCount, - flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint) - if (i == -1): + flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint, + ) + if i == -1: return - index = self.explorer.model().index(i-1, 0, self.explorer.rootIndex()) + index = self.explorer.model().index(i - 1, 0, self.explorer.rootIndex()) self.explorer.setCurrentIndex(index) def zoomIn(self): @@ -99,6 +101,6 @@ def zoomOut(self): self.canvas.zoomView(False) def resizeEvent(self, event): - self.explorer.setMinimumWidth(0.1*self.width()) - self.canvas.setMinimumWidth(0.6*self.width()) + self.explorer.setMinimumWidth(0.1 * self.width()) + self.canvas.setMinimumWidth(0.6 * self.width()) return super().resizeEvent(event) diff --git a/app/components/windows/base.py b/app/components/windows/base.py index 075e76d..b8ed22c 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -21,17 +21,36 @@ from time import sleep from manga_ocr import MangaOcr -from PyQt5.QtCore import (QThreadPool) -from PyQt5.QtWidgets import (QVBoxLayout, QWidget, QMainWindow, QApplication, - QPushButton, QFileDialog) +from PyQt5.QtCore import QThreadPool +from PyQt5.QtWidgets import ( + QVBoxLayout, + QWidget, + QMainWindow, + QApplication, + QPushButton, + QFileDialog, +) from .external import ExternalWindow from components.popups import BasePopup, CheckboxPopup from components.services import BaseWorker -from components.settings import BaseSettings, PreviewOptions, ImageScalingOptions, OptionsContainer, ShortcutOptions, TesseractOptions +from components.settings import ( + BaseSettings, + PreviewOptions, + ImageScalingOptions, + OptionsContainer, + ShortcutOptions, + TesseractOptions, +) from components.toolbar import BaseToolbar from components.views import WorkspaceView -from utils.constants import LOAD_MODEL_MESSAGE, MAIN_WINDOW_DEFAULTS, MAIN_WINDOW_TYPES, STYLESHEET_DARK, STYLESHEET_LIGHT +from utils.constants import ( + LOAD_MODEL_MESSAGE, + MAIN_WINDOW_DEFAULTS, + MAIN_WINDOW_TYPES, + STYLESHEET_DARK, + STYLESHEET_LIGHT, +) from utils.scripts import mangaFileToImageDir @@ -60,7 +79,7 @@ def __init__(self, parent=None, tracker=None): @property def canvas(self): return self.mainView.canvas - + @property def explorer(self): return self.mainView.explorer @@ -74,18 +93,15 @@ def closeEvent(self, event): return super().closeEvent(event) def noop(self): - BasePopup( - "Not Implemented", - "This function is not yet implemented." - ).exec() + BasePopup("Not Implemented", "This function is not yet implemented.").exec() -# ------------------------------ File Functions ------------------------------ # + # ------------------------------ File Functions ------------------------------ # def openDir(self): filepath = QFileDialog.getExistingDirectory( self, "Open Directory", - self.tracker.filepath # , QFileDialog.DontUseNativeDialog + self.tracker.filepath, # , QFileDialog.DontUseNativeDialog ) if filepath: @@ -96,7 +112,7 @@ def openDir(self): except FileNotFoundError: BasePopup( "No images found in the directory", - "Please select a directory with images." + "Please select a directory with images.", ).exec() def openManga(self): @@ -104,21 +120,20 @@ def openManga(self): self, "Open Manga File", self.tracker.filepath, - "Manga (*.cbz *.cbr *.zip *.rar *.pdf)" + "Manga (*.cbz *.cbr *.zip *.rar *.pdf)", ) if filename: + def setDirectory(filepath): self.tracker.filepath = filepath self.explorer.setDirectory(filepath) - openMangaButton = self.toolbar.findChild( - QPushButton, "openManga") + openMangaButton = self.toolbar.findChild(QPushButton, "openManga") worker = BaseWorker(mangaFileToImageDir, filename) worker.signals.result.connect(setDirectory) - worker.signals.finished.connect( - lambda: openMangaButton.setEnabled(True)) + worker.signals.finished.connect(lambda: openMangaButton.setEnabled(True)) self.threadpool.start(worker) openMangaButton.setEnabled(False) @@ -132,7 +147,7 @@ def captureExternalHelper(self): def captureExternal(self): ExternalWindow(self).showFullScreen() -# ------------------------------ View Functions ------------------------------ # + # ------------------------------ View Functions ------------------------------ # def toggleStylesheet(self): if self.stylesheetPath == STYLESHEET_LIGHT: @@ -144,7 +159,7 @@ def toggleStylesheet(self): if app is None: raise RuntimeError("No Qt Application found.") - with open(self.stylesheetPath, 'r') as fh: + with open(self.stylesheetPath, "r") as fh: app.setStyleSheet(fh.read()) def modifyFontSettings(self): @@ -156,7 +171,7 @@ def modifyFontSettings(self): if app is None: raise RuntimeError("No Qt Application found.") - with open(self.stylesheetPath, 'r') as fh: + with open(self.stylesheetPath, "r") as fh: app.setStyleSheet(fh.read()) def toggleSplitView(self): @@ -176,7 +191,7 @@ def scaleImage(self): def hideExplorer(self): self.explorer.setVisible(not self.explorer.isVisible()) -# ----------------------------- Control Functions ---------------------------- # + # ----------------------------- Control Functions ---------------------------- # def toggleMouseMode(self): self.canvas.toggleZoomPanMode() @@ -184,7 +199,7 @@ def toggleMouseMode(self): def modifyHotkeys(self): OptionsContainer(ShortcutOptions(self)).exec() -# ------------------------------ Misc Functions ------------------------------ # + # ------------------------------ Misc Functions ------------------------------ # def loadModel(self): loadModelButton = self.toolbar.findChild(QPushButton, "loadModel") @@ -195,9 +210,9 @@ def loadModel(self): "hasLoadModelPopup", "Load the MangaOCR model?", LOAD_MODEL_MESSAGE, - CheckboxPopup.Ok | CheckboxPopup.Cancel + CheckboxPopup.Ok | CheckboxPopup.Cancel, ).exec() - if (ret == CheckboxPopup.Ok): + if ret == CheckboxPopup.Ok: pass else: loadModelButton.setChecked(False) @@ -221,7 +236,7 @@ def loadModelConfirm(message: str): if message == "success": BasePopup( f"{modelName} model loaded", - f"You are now using the {modelName} model for Japanese text detection." + f"You are now using the {modelName} model for Japanese text detection.", ).exec() else: BasePopup("Load Model Error", message).exec() @@ -229,8 +244,7 @@ def loadModelConfirm(message: str): worker = BaseWorker(loadModelHelper, self.tracker) worker.signals.result.connect(loadModelConfirm) - worker.signals.finished.connect(lambda: - loadModelButton.setEnabled(True)) + worker.signals.finished.connect(lambda: loadModelButton.setEnabled(True)) self.threadpool.start(worker) loadModelButton.setEnabled(False) diff --git a/app/components/windows/external.py b/app/components/windows/external.py index 272bace..c2c53b0 100644 --- a/app/components/windows/external.py +++ b/app/components/windows/external.py @@ -33,6 +33,7 @@ class ExternalWindow(QMainWindow): """ External window widget to enclose FullScreenOCRView """ + def __init__(self, parent: "MainWindow"): super().__init__() self.mainWindow = parent @@ -41,7 +42,7 @@ def __init__(self, parent: "MainWindow"): # we ensure that the whole screen is captured. self.layout().setContentsMargins(0, 0, 0, 0) self.setStyleSheet("border:0px; margin:0px") - + # Delete external window on close self.setAttribute(Qt.WA_DeleteOnClose) @@ -67,4 +68,4 @@ def showFullScreen(self): def closeEvent(self, event: QCloseEvent): # Ensure that object is deleted before closing self.deleteLater() - return super().closeEvent(event) \ No newline at end of file + return super().closeEvent(event) diff --git a/app/main.py b/app/main.py index d3f35c6..d791db3 100644 --- a/app/main.py +++ b/app/main.py @@ -28,7 +28,7 @@ from Trackers import Tracker from utils.constants import APP_NAME, APP_LOGO, SETTINGS_FILE_DEFAULT, STYLESHEET_LIGHT -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) app.setApplicationName(APP_NAME) @@ -40,13 +40,12 @@ settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) styles = settings.value("stylesheetPath", STYLESHEET_LIGHT) - with open(styles, 'r') as fh: + with open(styles, "r") as fh: app.setStyleSheet(fh.read()) shortcut = settings.value("captureExternalShortcut", "Alt+Q") keybinder.init() - keybinder.register_hotkey( - widget.winId(), shortcut, widget.captureExternal) + keybinder.register_hotkey(widget.winId(), shortcut, widget.captureExternal) winEventFilter = WinEventFilter(keybinder) eventDispatcher = QAbstractEventDispatcher.instance() eventDispatcher.installNativeEventFilter(winEventFilter) diff --git a/app/utils/constants.py b/app/utils/constants.py index 95ad056..5cf72fd 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -23,31 +23,52 @@ APP_NAME = "Poricom" APP_LOGO = "./assets/images/icons/logo.ico" -IMAGE_EXTENSIONS = ["*.bmp", "*.gif", "*.jpeg", "*.jpg", "*.pbm", "*.pgm", "*.png", "*.ppm", "*.webp", "*.xbm", "*.xpm",] +IMAGE_EXTENSIONS = [ + "*.bmp", + "*.gif", + "*.jpeg", + "*.jpg", + "*.pbm", + "*.pgm", + "*.png", + "*.ppm", + "*.webp", + "*.xbm", + "*.xpm", +] # Settings -TOGGLE_CHOICES = [ " Disabled", " Enabled"] +TOGGLE_CHOICES = [" Disabled", " Enabled"] -LANGUAGE = [ " Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English"] -ORIENTATION = [ " Vertical", " Horizontal"] +LANGUAGE = [" Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English"] +ORIENTATION = [" Vertical", " Horizontal"] -IMAGE_SCALING = [ " Fit to Width", " Fit to Height", " Fit to Screen"] +IMAGE_SCALING = [" Fit to Width", " Fit to Height", " Fit to Screen"] -FONT_SIZE = [ " 12", " 14", " 16", " 20", " 24", " 32", " 40", " 56", " 72"] -FONT_STYLE = [ " Helvetica", " Poppins", " Arial", " Verdana", " Times New Roman"] +FONT_SIZE = [" 12", " 14", " 16", " 20", " 24", " 32", " 40", " 56", " 72"] +FONT_STYLE = [" Helvetica", " Poppins", " Arial", " Verdana", " Times New Roman"] -MODIFIER = [ " Ctrl", " Shift", " Alt", " Ctrl+Alt", " Shift+Alt", " Shift+Ctrl", " Shift+Alt+Ctrl", " No Modifier"] +MODIFIER = [ + " Ctrl", + " Shift", + " Alt", + " Ctrl+Alt", + " Shift+Alt", + " Shift+Ctrl", + " Shift+Alt+Ctrl", + " No Modifier", +] # Paths -STYLESHEET_LIGHT = './assets/styles.qss' -STYLESHEET_DARK = './assets/styles-dark.qss' +STYLESHEET_LIGHT = "./assets/styles.qss" +STYLESHEET_DARK = "./assets/styles-dark.qss" TESSERACT_LANGUAGES = "./assets/languages/" -TOOLBAR_ICONS = './assets/images/icons/' -TOOLBAR_ICON_DEFAULT = './assets/images/icons/default_icon.png' +TOOLBAR_ICONS = "./assets/images/icons/" +TOOLBAR_ICON_DEFAULT = "./assets/images/icons/default_icon.png" -EXPLORER_ROOT_DEFAULT = './assets/images/' +EXPLORER_ROOT_DEFAULT = "./assets/images/" # Messages LOAD_MODEL_MESSAGE = ( @@ -59,35 +80,26 @@ # ------------------------------------ Settings ------------------------------------- # -SETTINGS_FILE_DEFAULT = './bin/poricom-config.ini' +SETTINGS_FILE_DEFAULT = "./bin/poricom-config.ini" # Window MAIN_WINDOW_DEFAULTS = { "hasLoadModelPopup": "true", "explorerPath": "./assets/images/", - "stylesheetPath": "./assets/styles.qss" -} -MAIN_WINDOW_TYPES = { - "hasLoadModelPopup": bool + "stylesheetPath": "./assets/styles.qss", } +MAIN_WINDOW_TYPES = {"hasLoadModelPopup": bool} # View IMAGE_VIEW_DEFAULT = { "viewImageMode": 0, "splitViewMode": "false", - "zoomPanMode": "false" -} -IMAGE_VIEW_TYPES = { - "viewImageMode": int, - "splitViewMode": bool, - "zoomPanMode": bool + "zoomPanMode": "false", } +IMAGE_VIEW_TYPES = {"viewImageMode": int, "splitViewMode": bool, "zoomPanMode": bool} # Tesseract -TESSERACT_DEFAULTS = { - "language": "jpn", - "orientation": "_vert" -} +TESSERACT_DEFAULTS = {"language": "jpn", "orientation": "_vert"} # --------------------------------------- UI ---------------------------------------- # @@ -105,7 +117,7 @@ "toggle": False, "align": "AlignRight", "iconHeight": 0.45, - "iconWidth": 0.45 + "iconWidth": 0.45, }, "zoomOut": { "title": "Zoom out", @@ -114,7 +126,7 @@ "toggle": False, "align": "AlignRight", "iconHeight": 0.45, - "iconWidth": 0.45 + "iconWidth": 0.45, }, "loadImageAtIndex": { "title": "", @@ -123,7 +135,7 @@ "toggle": False, "align": "AlignRight", "iconHeight": 0.45, - "iconWidth": 1.3 + "iconWidth": 1.3, }, "loadPrevImage": { "title": "", @@ -132,7 +144,7 @@ "toggle": False, "align": "AlignRight", "iconHeight": 0.45, - "iconWidth": 0.6 + "iconWidth": 0.6, }, "loadNextImage": { "title": "", @@ -141,8 +153,8 @@ "toggle": False, "align": "AlignRight", "iconHeight": 0.45, - "iconWidth": 0.6 - } + "iconWidth": 0.6, + }, } TOOLBAR_FUNCTIONS: dict[str, ButtonConfigDict] = { "file": { @@ -153,7 +165,7 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "openManga": { "title": "Open manga file", @@ -162,7 +174,7 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "captureExternalHelper": { "title": "External capture", @@ -171,8 +183,8 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 - } + "iconWidth": 1.0, + }, }, "view": { "toggleStylesheet": { @@ -182,7 +194,7 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "hideExplorer": { "title": "Hide explorer", @@ -191,7 +203,7 @@ "toggle": True, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "modifyFontSettings": { "title": "Modify preview text", @@ -200,7 +212,7 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "toggleSplitView": { "title": "Turn on split view", @@ -209,7 +221,7 @@ "toggle": True, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "scaleImage": { "title": "Adjust image scaling", @@ -218,8 +230,8 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 - } + "iconWidth": 1.0, + }, }, "controls": { "toggleMouseMode": { @@ -229,7 +241,7 @@ "toggle": True, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "modifyHotkeys": { "title": "Remap hotkeys", @@ -238,8 +250,8 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 - } + "iconWidth": 1.0, + }, }, "misc": { "loadModel": { @@ -249,7 +261,7 @@ "toggle": True, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "modifyTesseract": { "title": "Tesseract settings", @@ -258,7 +270,7 @@ "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 + "iconWidth": 1.0, }, "toggleLogging": { "title": "Enable text logging", @@ -267,7 +279,7 @@ "toggle": True, "align": "AlignLeft", "iconHeight": 1.0, - "iconWidth": 1.0 - } - } + "iconWidth": 1.0, + }, + }, } diff --git a/app/utils/scripts/editStylesheet.py b/app/utils/scripts/editStylesheet.py index a0c09e5..8b9adbd 100644 --- a/app/utils/scripts/editStylesheet.py +++ b/app/utils/scripts/editStylesheet.py @@ -19,15 +19,16 @@ from ..constants import STYLESHEET_LIGHT, STYLESHEET_DARK + def editStylesheet(index: int, style: str): """ Replace stylesheet at line `index` with input `style` """ - with open(STYLESHEET_LIGHT, 'r') as slFh, open(STYLESHEET_DARK, 'r') as sdFh: + with open(STYLESHEET_LIGHT, "r") as slFh, open(STYLESHEET_DARK, "r") as sdFh: lineLight = slFh.readlines() linesDark = sdFh.readlines() lineLight[index] = style linesDark[index] = style - with open(STYLESHEET_LIGHT, 'w') as slFh, open(STYLESHEET_DARK, 'w') as sdFh: + with open(STYLESHEET_LIGHT, "w") as slFh, open(STYLESHEET_DARK, "w") as sdFh: slFh.writelines(lineLight) sdFh.writelines(linesDark) diff --git a/app/utils/scripts/logText.py b/app/utils/scripts/logText.py index 08e5705..592d78d 100644 --- a/app/utils/scripts/logText.py +++ b/app/utils/scripts/logText.py @@ -19,7 +19,8 @@ from PyQt5.QtGui import QGuiApplication -def logText(text: str, isLogFile: bool=False, path: str="."): + +def logText(text: str, isLogFile: bool = False, path: str = "."): """Log text by copying to clipboard Args: @@ -31,5 +32,5 @@ def logText(text: str, isLogFile: bool=False, path: str="."): clipboard.setText(text) if isLogFile: - with open(path, 'a', encoding="utf-8") as fh: + with open(path, "a", encoding="utf-8") as fh: fh.write(text + "\n") diff --git a/app/utils/scripts/mangaFileToImageDir.py b/app/utils/scripts/mangaFileToImageDir.py index 30b5f81..ef018c7 100644 --- a/app/utils/scripts/mangaFileToImageDir.py +++ b/app/utils/scripts/mangaFileToImageDir.py @@ -38,7 +38,7 @@ def mangaFileToImageDir(filepath: str): cachePath = f"./poricom_cache/{basename(extractPath)}" if extension in [".cbz", ".zip"]: - with zipfile.ZipFile(filepath, 'r') as zipRef: + with zipfile.ZipFile(filepath, "r") as zipRef: zipRef.extractall(cachePath) rarfile.UNRAR_TOOL = "bin/unrar.exe" @@ -51,11 +51,11 @@ def mangaFileToImageDir(filepath: str): images = pdf2image.convert_from_path(filepath) except pdf2image.exceptions.PDFInfoNotInstalledError: images = pdf2image.convert_from_path( - filepath, poppler_path="poppler/Library/bin") + filepath, poppler_path="poppler/Library/bin" + ) for i in range(len(images)): filename = basename(extractPath) Path(cachePath).mkdir(parents=True, exist_ok=True) - images[i].save( - f"{cachePath}/{i+1}_{filename}.png", 'PNG') + images[i].save(f"{cachePath}/{i+1}_{filename}.png", "PNG") return cachePath diff --git a/app/utils/scripts/pixmapToText.py b/app/utils/scripts/pixmapToText.py index e8df06a..1cec4f7 100644 --- a/app/utils/scripts/pixmapToText.py +++ b/app/utils/scripts/pixmapToText.py @@ -24,6 +24,7 @@ from PIL import Image from PyQt5.QtCore import QBuffer from PyQt5.QtGui import QPixmap + try: from tesserocr import PyTessBaseAPI except UnicodeDecodeError: @@ -32,7 +33,9 @@ from ..constants import TESSERACT_LANGUAGES -def pixmapToText(pixmap: QPixmap, language: str = "jpn_vert", model: Optional[MangaOcr] = None) -> str: +def pixmapToText( + pixmap: QPixmap, language: str = "jpn_vert", model: Optional[MangaOcr] = None +) -> str: """ Convert QPixmap object to text using the model """ @@ -50,16 +53,18 @@ def pixmapToText(pixmap: QPixmap, language: str = "jpn_vert", model: Optional[Ma if model is not None: text = model(pillowImage) - + # PSM = 1 works most of the time except on smaller bounding boxes. # By smaller, we mean textboxes with less text. Usually these # boxes have at most one vertical line of text. else: try: - with PyTessBaseAPI(path=TESSERACT_LANGUAGES, lang=language, oem=1, psm=1) as api: + with PyTessBaseAPI( + path=TESSERACT_LANGUAGES, lang=language, oem=1, psm=1 + ) as api: api.SetImage(pillowImage) text = api.GetUTF8Text() except NameError: return None - return text.strip() \ No newline at end of file + return text.strip() diff --git a/app/utils/types.py b/app/utils/types.py index 8db2d63..72de520 100644 --- a/app/utils/types.py +++ b/app/utils/types.py @@ -18,6 +18,7 @@ from typing import TypedDict + class ButtonConfig(TypedDict): title: str message: str @@ -27,4 +28,5 @@ class ButtonConfig(TypedDict): iconHeight: float iconWidth: float + ButtonConfigDict = dict[str, ButtonConfig] From 6f7b6b6073cfd02996c679311ed0f2c9de4191cf Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 22 Jan 2023 00:27:19 +0800 Subject: [PATCH 095/137] Move services outside components --- app/components/views/image/base.py | 2 +- app/components/views/ocr/base.py | 2 +- app/components/windows/base.py | 2 +- app/main.py | 2 +- app/{components => }/services/__init__.py | 0 app/{components => }/services/filters.py | 0 app/{components => }/services/workers/__init__.py | 0 app/{components => }/services/workers/base.py | 0 app/{components => }/services/workers/signal.py | 0 9 files changed, 4 insertions(+), 4 deletions(-) rename app/{components => }/services/__init__.py (100%) rename app/{components => }/services/filters.py (100%) rename app/{components => }/services/workers/__init__.py (100%) rename app/{components => }/services/workers/base.py (100%) rename app/{components => }/services/workers/signal.py (100%) diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index 865fe08..e78d820 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -23,8 +23,8 @@ from PyQt5.QtCore import Qt, QRect, QRectF, QSize, QThreadPool from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsView -from components.services import BaseWorker from components.settings import BaseSettings +from services import BaseWorker from utils.constants import IMAGE_VIEW_DEFAULT, IMAGE_VIEW_TYPES if TYPE_CHECKING: diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 02ee5b1..3317a3b 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -20,8 +20,8 @@ from PyQt5.QtCore import pyqtSlot, Qt, QThreadPool, QTimer from PyQt5.QtWidgets import QGraphicsView, QLabel, QMainWindow -from components.services import BaseWorker from components.settings import BaseSettings +from services import BaseWorker from utils.constants import TESSERACT_DEFAULTS from utils.scripts import logText, pixmapToText diff --git a/app/components/windows/base.py b/app/components/windows/base.py index b8ed22c..cefd39c 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -33,7 +33,6 @@ from .external import ExternalWindow from components.popups import BasePopup, CheckboxPopup -from components.services import BaseWorker from components.settings import ( BaseSettings, PreviewOptions, @@ -44,6 +43,7 @@ ) from components.toolbar import BaseToolbar from components.views import WorkspaceView +from services import BaseWorker from utils.constants import ( LOAD_MODEL_MESSAGE, MAIN_WINDOW_DEFAULTS, diff --git a/app/main.py b/app/main.py index d791db3..cbc250e 100644 --- a/app/main.py +++ b/app/main.py @@ -23,8 +23,8 @@ from PyQt5.QtCore import QAbstractEventDispatcher, QSettings from pyqtkeybind import keybinder -from components.services import WinEventFilter from components.windows import MainWindow +from services import WinEventFilter from Trackers import Tracker from utils.constants import APP_NAME, APP_LOGO, SETTINGS_FILE_DEFAULT, STYLESHEET_LIGHT diff --git a/app/components/services/__init__.py b/app/services/__init__.py similarity index 100% rename from app/components/services/__init__.py rename to app/services/__init__.py diff --git a/app/components/services/filters.py b/app/services/filters.py similarity index 100% rename from app/components/services/filters.py rename to app/services/filters.py diff --git a/app/components/services/workers/__init__.py b/app/services/workers/__init__.py similarity index 100% rename from app/components/services/workers/__init__.py rename to app/services/workers/__init__.py diff --git a/app/components/services/workers/base.py b/app/services/workers/base.py similarity index 100% rename from app/components/services/workers/base.py rename to app/services/workers/base.py diff --git a/app/components/services/workers/signal.py b/app/services/workers/signal.py similarity index 100% rename from app/components/services/workers/signal.py rename to app/services/workers/signal.py From 11a4b39b86105db098eafb29af6c76b42ff6278b Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 29 Jan 2023 00:52:22 +0800 Subject: [PATCH 096/137] Add method to reset zoom level --- app/components/views/image/base.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index e78d820..e8bd335 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -20,7 +20,8 @@ from time import sleep from typing import TYPE_CHECKING -from PyQt5.QtCore import Qt, QRect, QRectF, QSize, QThreadPool +from PyQt5.QtCore import Qt, QRectF, QThreadPool +from PyQt5.QtGui import QTransform from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsView from components.settings import BaseSettings @@ -122,7 +123,7 @@ def wheelEvent(self, event): pressedKey = QApplication.keyboardModifiers() zoomMode = pressedKey == Qt.ControlModifier or self.zoomPanMode - # self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) + self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) # TODO: Rewrite individual event handlers as separate functions if zoomMode: if event.angleDelta().y() > 0: @@ -135,11 +136,9 @@ def wheelEvent(self, event): return if not zoomMode: - mouseScrollLimit = 3 trackpadScrollLimit = 36 wheelDelta = 120 - def suppressScroll(): self._scrollSuppressed = True worker = BaseWorker(sleep, 0.3) @@ -205,8 +204,9 @@ def mouseMoveEvent(self, event): super().mouseMoveEvent(event) def mouseDoubleClickEvent(self, event): - self.currentScale = 1 - self.viewImage(self.currentScale) + self.setTransform(QTransform()) + self.viewImage() + self.verticalScrollBar().setSliderPosition(0) super().mouseDoubleClickEvent(event) # ------------------------------------ Shortcut ------------------------------------- # From 18ef4aa123fc7dc845c333a807cae067d5836e77 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 29 Jan 2023 14:37:04 +0800 Subject: [PATCH 097/137] Format with black --- app/components/views/image/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index 4ff17ff..067fa69 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -136,6 +136,7 @@ def wheelEvent(self, event): mouseScrollLimit = 3 trackpadScrollLimit = 36 wheelDelta = 120 + def suppressScroll(): self._scrollSuppressed = True worker = BaseWorker(sleep, 0.3) From 51f38aa534c65a3ec909088bfeb2edc2a5af36ec Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 29 Jan 2023 14:37:14 +0800 Subject: [PATCH 098/137] Remove duplicate icon --- app/assets/images/icons/scaleImage.png | Bin 1509 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/assets/images/icons/scaleImage.png diff --git a/app/assets/images/icons/scaleImage.png b/app/assets/images/icons/scaleImage.png deleted file mode 100644 index c555350731f704248b65245adb5e6f96dc00a4dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1509 zcmV`^3k*Iy=Qw9=*98Es>sHnS+6&G1N7+s(wa&&|suqjOMAWlcld`!p0N`BiI6$u< zdh-Eb2Z7$VT~nyX{6y5|1AsARmVxap&wI*tP1XYd9zDv_sw(@Lqt zK5r&9^#GW#g-P#EdtP7bx-n)Z?Rf`HJpcg2gvD0JeKV~Io1V85%+?PnHzErG04au$ zrRD?K2>?hTgr?{1grA+}1KA1yNHB!k&DXZ~98YQi5O8hMp-fn8cDt|Jn%i~V9Z{N} z=Fw4F0W`@3dx`PZgyV@oA71Hpx5u8i*(~_XdnRpuo@iP5{!*MX<|q;mfPvNUIKNsh zEk+1oMgxc9&hIO=%@9rWcjk$fi^byka=F~qz^ODsNFsm>;SUU~Y7;ER z#pJZ!G|9NI`Qh(FLkMUsZastSe;o}z3}A_Yua#EM4Lie~I0S{C;?|Q_YHZU6n;$Bd z%UyDuB^dK3jJbS6^S5#98D#x}Ocbq-dz1;sMm*h~A(f^)R$rE-eCl1b1HpXfvyq<2D}v=I7^c_B75YR_pzbD`vyM*=aT(nDQ$ibfw#U zOaq6&^e`08&j6p>Xnwy&qmg&&^+#b$Q3b*Epi;T`0MVp}#zF`ogb+dqA%qY@2qA&{bvJ<$KvqFAlZYMdz)3Ri8+(d=~g5vdaZ%6$*_82rYZ<9upkj*Xh?HQ}b`?Ihso z{^kR>ZUwp?1kW}ajeOiWJkP5QX+8kV5a^RMS7i^Av%SMho>$#u%;U`3Xzrb>R4VSU zapP7gE23GaUVjvbw=|^}@T;J^njZ?+qM}+Z9UnHJwYIj419J?3X7Ianj`Mw-dInj4 z1>}7^!$fr`v(*qSbUK~8ZrYo8iMTO^xb-lwW6Y3|3xeD2_HGlHZvZ?G;2?p%h+EGf z`)@&g_u;Zq`2aZ3A3~J%`6>whj$IG&<%vcOgwSrccLz$JB%szeZa#KB1FXLV4PvG8 zfiVX-xmaXEs3)T#P%vA^K7=-UBf1cBI>HN5sMDAmDBVxKgSY z<({lr;JZ?(bYc9+@m5{;2pAttIGn+J9YkA(Jy~-~R(qD(QBaiT`@X+otN8#>E0sQu zvNV2F^OKOZJ7nPd{>s*B)%wla5tqiNCQ|_<)%;KvL&!`3D&gjbvKB%n0#LrcQca=x zp)7?k^#GJoJ*CtiY0pa>oCM>qwC5c(^#F`9Glp^tzqxKDtvSP!ED%yV+8Jf)0RXqY ze7ld;{xm`uafUlJnFs*56~bm?_O;vXQc`oY*4B1qsrf(x0Sw*>6a_1l4~U+$Sj6}Y#bpA1Q7ZZ?K@+XrP-ew; zO(BMP2S8&3n|2xe8G5~=wrdhX2qA Date: Sun, 29 Jan 2023 14:45:18 +0800 Subject: [PATCH 099/137] Fix AttributeError in BaseImageView --- app/components/views/image/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index 067fa69..bb1d7b1 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -88,15 +88,15 @@ def viewImage(self): h = self.viewport().geometry().height() if self.viewImageMode == 0: self.pixmap.setPixmap( - self.tracker.pixImage.scaledToWidth(w, Qt.SmoothTransformation) + self.state.baseImage.scaledToWidth(w, Qt.SmoothTransformation) ) elif self.viewImageMode == 1: self.pixmap.setPixmap( - self.tracker.pixImage.scaledToHeight(h, Qt.SmoothTransformation) + self.state.baseImage.scaledToHeight(h, Qt.SmoothTransformation) ) elif self.viewImageMode == 2: self.pixmap.setPixmap( - self.tracker.pixImage.scaled( + self.state.baseImage.scaled( w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation ) ) From 2ee186a4bbdc9cc12b701f55f1b71ea40a746304 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 25 Mar 2023 22:24:17 +0800 Subject: [PATCH 100/137] Refactor States to use string for model names --- app/components/windows/base.py | 6 +++--- app/services/states.py | 26 ++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/app/components/windows/base.py b/app/components/windows/base.py index 1705f69..281bb49 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -148,13 +148,13 @@ def loadModel(self): return def loadModelHelper(state: State): - betterOCR = state.switchOCRMode() - if betterOCR: + ocrModelName = state.setOCRModelName() + if ocrModelName == "MangaOCR": try: state.ocrModel = MangaOcr() return "success" except Exception as e: - state.switchOCRMode() + state.toggleOCRModelName() return str(e) else: state.ocrModel = None diff --git a/app/services/states.py b/app/services/states.py index 29b45be..c626a65 100644 --- a/app/services/states.py +++ b/app/services/states.py @@ -18,6 +18,8 @@ """ from os.path import isfile, exists +from typing import Literal + from PyQt5.QtGui import QPixmap @@ -48,12 +50,15 @@ def isValid(self): return exists(self._filename) and isfile(self._filename) +OCRModelNames = Literal["Tesseract", "MangaOCR"] + + class State: def __init__(self): self._baseImage = Pixmap("") - self._betterOCR = False self._ocrModel = None + self._ocrModelName: OCRModelNames = "Tesseract" @property def baseImage(self): @@ -81,6 +86,19 @@ def ocrModel(self): def ocrModel(self, ocrModel): self._ocrModel = ocrModel - def switchOCRMode(self): - self._betterOCR = not self._betterOCR - return self._betterOCR + @property + def ocrModelName(self): + return self._ocrModelName + + def setOCRModelName(self, ocrModelName: OCRModelNames = None): + if ocrModelName: + self._ocrModelName = ocrModelName + else: + self.toggleOCRModelName() + return self._ocrModelName + + def toggleOCRModelName(self): + if self._ocrModelName == "Tesseract": + self._ocrModelName = "MangaOCR" + elif self._ocrModelName == "MangaOCR": + self._ocrModelName = "Tesseract" From ebda469dc910eaadac859c187e43f47706d23377 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 25 Mar 2023 22:26:55 +0800 Subject: [PATCH 101/137] Add check for model name when processing result --- app/components/views/ocr/base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index ff3507a..5eca507 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -22,6 +22,7 @@ from PyQt5.QtCore import pyqtSlot, Qt, QThreadPool, QTimer from PyQt5.QtWidgets import QGraphicsView, QLabel, QMainWindow +from components.popups import BasePopup from components.settings import BaseSettings from services import BaseWorker, State from utils.constants import ( @@ -58,6 +59,12 @@ def __init__(self, parent: QMainWindow, state: State = None): self.addProperty("persistText", "true", bool) def handleTextResult(self, result): + if result == None and self.state.ocrModelName == "Tesseract": + BasePopup( + "Tesseract not loaded", + "Tesseract model cannot be loaded in your machine, please use the MangaOcr instead.", + ).exec() + return try: self.canvasText.setText(result) except RuntimeError: From f2791afe026c29e0f1fb486e96a916e67f6a60f5 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 25 Mar 2023 23:09:26 +0800 Subject: [PATCH 102/137] Update OCR behavior to handle instant selections --- app/components/views/ocr/base.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index ff3507a..802efcc 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -46,6 +46,8 @@ def __init__(self, parent: QMainWindow, state: State = None): self.timer.setSingleShot(True) self.timer.timeout.connect(self.rubberBandStopped) + self.previousSelection = self.rubberBandRect() + self.canvasText = QLabel("", self, Qt.WindowStaysOnTopHint) self.canvasText.setWordWrap(True) self.canvasText.hide() @@ -60,6 +62,9 @@ def __init__(self, parent: QMainWindow, state: State = None): def handleTextResult(self, result): try: self.canvasText.setText(result) + self.canvasText.adjustSize() + if self.canvasText.isHidden(): + self.canvasText.show() except RuntimeError: pass @@ -75,13 +80,13 @@ def handleTextFinished(self): @pyqtSlot() def rubberBandStopped(self): - if self.canvasText.isHidden(): - self.canvasText.setText("") - self.canvasText.adjustSize() - self.canvasText.show() - language = self.language + self.orientation - pixmap = self.grab(self.rubberBandRect()) + selection = ( + self.previousSelection + if self.rubberBandRect().isNull() + else self.rubberBandRect() + ) + pixmap = self.grab(selection) worker = BaseWorker(pixmapToText, pixmap, language, self.state.ocrModel) worker.signals.result.connect(self.handleTextResult) @@ -92,6 +97,7 @@ def rubberBandStopped(self): def mouseMoveEvent(self, event): rubberBandVisible = not self.rubberBandRect().isNull() if (event.buttons() & Qt.LeftButton) and rubberBandVisible: + self.previousSelection = self.rubberBandRect() self.timer.start() super().mouseMoveEvent(event) From 4ab1a5976605f7bf08ec14bfc5f4c0c8a4a4c614 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 25 Mar 2023 23:29:50 +0800 Subject: [PATCH 103/137] Update build file --- build/main.spec | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/build/main.spec b/build/main.spec index 98e4b89..8268053 100644 --- a/build/main.spec +++ b/build/main.spec @@ -15,8 +15,8 @@ datas += copy_metadata('numpy') datas += copy_metadata('tokenizers') added_files = [ - ('./assets', './assets'), - ('./utils', './utils'), + ('../app/assets', './assets'), + ('../app/bin', './bin'), ('path\\to\\user\\.conda\\pkgs\\poppler-22.01.0-h24fffdf_2', './poppler') ] @@ -24,11 +24,11 @@ added_files = [ block_cipher = None -a = Analysis(['main.py'], - pathex=[], +a = Analysis(['../app/main.py'], + pathex=['../app'], binaries=[], datas=datas+added_files, - hiddenimports=['toml'], + hiddenimports=['stringcase'], hookspath=[], hooksconfig={}, runtime_hooks=[], @@ -41,7 +41,7 @@ pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) -PATH_TO_TORCH_LIB = "path\\to\\env\\lib\\site-packages\\torch\\lib\\" +PATH_TO_TORCH_LIB = "torch\\lib\\" excluded_files = [ 'asmjit.lib', 'c10.lib', @@ -72,9 +72,7 @@ excluded_files = [ '_C.lib' ] excluded_files = [PATH_TO_TORCH_LIB + x for x in excluded_files] -a.datas = [x for x in a.datas if not - os.path.abspath(x[1]) in excluded_files] - +a.datas = [x for x in a.datas if not x[0] in excluded_files] exe = EXE(pyz, a.scripts, @@ -90,7 +88,7 @@ exe = EXE(pyz, target_arch=None, codesign_identity=None, entitlements_file=None, - icon="./assets/images/icons/logo.ico") + icon="../app/assets/images/icons/logo.ico") coll = COLLECT(exe, a.binaries, a.zipfiles, From 452e27c2b7999d1e588de17140b0c9bd81ce4de4 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 25 Mar 2023 23:48:53 +0800 Subject: [PATCH 104/137] Remove error when a folder is selected in explorer --- app/utils/scripts/combineTwoImages.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/utils/scripts/combineTwoImages.py b/app/utils/scripts/combineTwoImages.py index 6ab12dd..caa4021 100644 --- a/app/utils/scripts/combineTwoImages.py +++ b/app/utils/scripts/combineTwoImages.py @@ -28,7 +28,8 @@ def combineTwoImages(fileLeft: Union[str, QPixmap], fileRight: Union[str, QPixma """ imageLeft, imageRight = QPixmap(fileRight), QPixmap(fileLeft) if imageRight.isNull(): - raise FileNotFoundError("The first file is null.") + # raise FileNotFoundError("The first file is null.") + pass w = imageLeft.width() + imageRight.width() h = max(imageLeft.height(), imageRight.height()) From 433f25b0fac2087d495ebb5471ec04ab324a32c6 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 26 Mar 2023 12:36:07 +0800 Subject: [PATCH 105/137] Add method to load model locally --- app/components/windows/base.py | 7 ++++++- app/main.py | 1 - app/utils/constants.py | 7 ++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/components/windows/base.py b/app/components/windows/base.py index 281bb49..7c5ddeb 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -151,7 +151,12 @@ def loadModelHelper(state: State): ocrModelName = state.setOCRModelName() if ocrModelName == "MangaOCR": try: - state.ocrModel = MangaOcr() + if self.mangaOCRPath: + state.ocrModel = MangaOcr( + pretrained_model_name_or_path=self.mangaOCRPath + ) + else: + state.ocrModel = MangaOcr() return "success" except Exception as e: state.toggleOCRModelName() diff --git a/app/main.py b/app/main.py index 91d7af9..f65aad9 100644 --- a/app/main.py +++ b/app/main.py @@ -28,7 +28,6 @@ from utils.constants import APP_NAME, APP_LOGO, SETTINGS_FILE_DEFAULT, STYLESHEET_LIGHT if __name__ == "__main__": - app = QApplication(sys.argv) app.setApplicationName(APP_NAME) app.setWindowIcon(QIcon(APP_LOGO)) diff --git a/app/utils/constants.py b/app/utils/constants.py index 2464baf..8d48109 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -72,9 +72,9 @@ # Messages LOAD_MODEL_MESSAGE = ( - "If you are running this for the first time, this will download the MangaOcr model" - "which is about 400 MB in size. This will improve the accuracy of Japanese text" - "detection in Poricom. If it is already in your cache, it will take a few seconds" + "If you are running this for the first time, this will download the MangaOcr model " + "which is about 400 MB in size. This will improve the accuracy of Japanese text " + "detection in Poricom. If it is already in your cache, it will take a few seconds " "to load the model." ) @@ -84,6 +84,7 @@ # Window MAIN_WINDOW_DEFAULTS = { + "mangaOCRPath": "", "hasLoadModelPopup": "true", "logToFile": "false", "stylesheetPath": "./assets/styles.qss", From f30f77924d2fb1ee58f9f6388bdd9649aa92b0f5 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 30 Apr 2023 11:34:26 +0800 Subject: [PATCH 106/137] fix: remove usingButton argument in zoom functions --- app/components/views/workspace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/views/workspace.py b/app/components/views/workspace.py index 78f9e5e..6e3d6b0 100644 --- a/app/components/views/workspace.py +++ b/app/components/views/workspace.py @@ -116,10 +116,10 @@ def toggleMouseMode(self): self.canvas.toggleZoomPanMode() def zoomIn(self): - self.canvas.zoomView(True, usingButton=True) + self.canvas.zoomView(True) def zoomOut(self): - self.canvas.zoomView(False, usingButton=True) + self.canvas.zoomView(False) # ----------------------------------- Navigation ------------------------------------ # From bba2792ebf1eb6d3543754c0892e9ec719125011 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 30 Apr 2023 14:08:36 +0800 Subject: [PATCH 107/137] chore: set comment line length to 88 --- app/components/views/image/base.py | 6 +++--- app/components/views/workspace.py | 8 ++++---- app/components/windows/base.py | 9 +++++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/components/views/image/base.py b/app/components/views/image/base.py index bb1d7b1..8d71831 100644 --- a/app/components/views/image/base.py +++ b/app/components/views/image/base.py @@ -58,7 +58,7 @@ def __init__(self, parent: "WorkspaceView", state: State = None): self.initializePixmapItem() - # ------------------------------------ Settings ------------------------------------- # + # ---------------------------------- Settings ----------------------------------- # def modifyViewImageMode(self, mode: int): # TODO: This should be an enum not an int @@ -75,7 +75,7 @@ def toggleZoomPanMode(self): self.setProperty("zoomPanMode", "false" if self.zoomPanMode else "true") self.saveSettings(hasMessage=False) - # -------------------------------------- View --------------------------------------- # + # ------------------------------------ View ------------------------------------- # def initializePixmapItem(self): self.setScene(QGraphicsScene()) @@ -207,7 +207,7 @@ def mouseDoubleClickEvent(self, event): self.verticalScrollBar().setSliderPosition(0) super().mouseDoubleClickEvent(event) - # ------------------------------------ Shortcut ------------------------------------- # + # ---------------------------------- Shortcut ----------------------------------- # # TODO: Keyboard shortcuts should be in another class def keyPressEvent(self, event): diff --git a/app/components/views/workspace.py b/app/components/views/workspace.py index 78f9e5e..7cc2e87 100644 --- a/app/components/views/workspace.py +++ b/app/components/views/workspace.py @@ -55,7 +55,7 @@ def resizeEvent(self, event): self.canvas.setMinimumWidth(0.7 * self.width()) return super().resizeEvent(event) - # ------------------------------------ Explorer ------------------------------------- # + # ---------------------------------- Explorer ----------------------------------- # def openDir(self): filepath = self.explorer.getDirectory(self.explorerPath) @@ -82,7 +82,7 @@ def openManga(self): def hideExplorer(self): self.explorer.setVisible(not self.explorer.isVisible()) - # -------------------------------------- View --------------------------------------- # + # ------------------------------------ View ------------------------------------- # def viewImageFromExplorer(self, filename, filenext): if not self.canvas.splitViewMode: @@ -110,7 +110,7 @@ def toggleSplitView(self): def modifyImageScaling(self): OptionsContainer(ImageScalingOptions(self)).exec() - # -------------------------------------- Zoom --------------------------------------- # + # ------------------------------------ Zoom ------------------------------------- # def toggleMouseMode(self): self.canvas.toggleZoomPanMode() @@ -121,7 +121,7 @@ def zoomIn(self): def zoomOut(self): self.canvas.zoomView(False, usingButton=True) - # ----------------------------------- Navigation ------------------------------------ # + # --------------------------------- Navigation ---------------------------------- # def loadPrevImage(self): index = self.explorer.indexAbove(self.explorer.currentIndex()) diff --git a/app/components/windows/base.py b/app/components/windows/base.py index 7c5ddeb..688236e 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -87,7 +87,7 @@ def closeEvent(self, event): def noop(self): BasePopup("Not Implemented", "This function is not yet implemented.").exec() - # ------------------------------ File Functions ------------------------------ # + # ------------------------------- File Functions -------------------------------- # def captureExternalHelper(self): self.showMinimized() @@ -98,7 +98,7 @@ def captureExternalHelper(self): def captureExternal(self): ExternalWindow(self).showFullScreen() - # ------------------------------ View Functions ------------------------------ # + # ------------------------------- View Functions -------------------------------- # def toggleStylesheet(self): if self.stylesheetPath == STYLESHEET_LIGHT: @@ -125,16 +125,17 @@ def modifyFontSettings(self): with open(self.stylesheetPath, "r") as fh: app.setStyleSheet(fh.read()) - # ----------------------------- Control Functions ---------------------------- # + # ------------------------------ Control Functions ------------------------------ # def modifyHotkeys(self): OptionsContainer(ShortcutOptions(self)).exec() - # ------------------------------ Misc Functions ------------------------------ # + # ------------------------------- Misc Functions -------------------------------- # def loadModel(self): loadModelButton = self.toolbar.findChild(QPushButton, "loadModel") loadModelButton.setChecked(not self.state.ocrModel) + self.state.ocrModelName if loadModelButton.isChecked() and self.hasLoadModelPopup: ret = CheckboxPopup( From 64537f87879a4c6e8e9d56d1c3966d2a7a765bef Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 30 Apr 2023 14:09:36 +0800 Subject: [PATCH 108/137] feat: add ModelOptions component --- app/components/settings/__init__.py | 1 + app/components/settings/popups/__init__.py | 1 + app/components/settings/popups/model.py | 48 ++++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 app/components/settings/popups/model.py diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py index f6f8e58..d72b439 100644 --- a/app/components/settings/__init__.py +++ b/app/components/settings/__init__.py @@ -20,6 +20,7 @@ from .base import BaseSettings from .popups import ( ImageScalingOptions, + ModelOptions, OptionsContainer, PreviewOptions, ShortcutOptions, diff --git a/app/components/settings/popups/__init__.py b/app/components/settings/popups/__init__.py index 1b9a45c..8b20d63 100644 --- a/app/components/settings/popups/__init__.py +++ b/app/components/settings/popups/__init__.py @@ -19,6 +19,7 @@ from .container import OptionsContainer from .imageScaling import ImageScalingOptions +from .model import ModelOptions from .preview import PreviewOptions from .shortcut import ShortcutOptions from .tesseract import TesseractOptions diff --git a/app/components/settings/popups/model.py b/app/components/settings/popups/model.py new file mode 100644 index 0000000..7d43227 --- /dev/null +++ b/app/components/settings/popups/model.py @@ -0,0 +1,48 @@ +""" +Poricom Popups + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtWidgets import QWidget + +from utils.constants import OCR_MODEL, TOGGLE_CHOICES + +from .base import BaseOptions + + +class ModelOptions(BaseOptions): + def __init__(self, parent: QWidget): + super().__init__(parent, [OCR_MODEL, TOGGLE_CHOICES]) + # TODO: Use constants here + self.initializeProperties( + [("ocrModel", "MangaOCR", str), ("useOcrOffline", "false", bool)] + ) + + def changeOcrModel(self, i): + self.ocrModelIndex = i + + def changeUseOcrOffline(self, i): + self.useOcrOfflineIndex = i + self.useOcrOffline = True if i else False + + def saveSettings(self, hasMessage=True): + ocrModelName = self.ocrModelComboBox.currentText() + self.mainWindow.state.setOCRModelName(ocrModelName) + self.mainWindow.setProperty( + "useOcrOffline", "true" if self.useOcrOffline else "useOcrOffline" + ) + return super().saveSettings(hasMessage) From ca8f4530511bd8dfccf8ed1bacf6a1b73fab4b4a Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 30 Apr 2023 14:11:02 +0800 Subject: [PATCH 109/137] feat: add load model function in State --- app/services/states.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/services/states.py b/app/services/states.py index c626a65..fcdd76c 100644 --- a/app/services/states.py +++ b/app/services/states.py @@ -20,7 +20,7 @@ from os.path import isfile, exists from typing import Literal - +from manga_ocr import MangaOcr from PyQt5.QtGui import QPixmap from utils.scripts import combineTwoImages @@ -60,6 +60,8 @@ def __init__(self): self._ocrModel = None self._ocrModelName: OCRModelNames = "Tesseract" + # ------------------------------------ Image ------------------------------------ # + @property def baseImage(self): return self._baseImage @@ -78,6 +80,8 @@ def baseImage(self, image): self._baseImage = Pixmap(splitImage, fileLeft) + # ------------------------------------- OCR ------------------------------------- # + @property def ocrModel(self): return self._ocrModel @@ -102,3 +106,18 @@ def toggleOCRModelName(self): self._ocrModelName = "MangaOCR" elif self._ocrModelName == "MangaOCR": self._ocrModelName = "Tesseract" + return self._ocrModelName + + def loadOCRModel(self, path: str = None): + if self._ocrModelName == "Tesseract": + return "success" + elif self._ocrModelName == "MangaOCR": + try: + if path: + self.ocrModel = MangaOcr(pretrained_model_name_or_path=path) + else: + self.ocrModel = MangaOcr() + return "success" + except Exception as e: + self.setOCRModelName("Tesseract") + return str(e) From 6ba74f556ce688b22a03bc64d92c09901c998069 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 30 Apr 2023 14:47:12 +0800 Subject: [PATCH 110/137] feat: implement UI to set offline model path --- app/components/windows/base.py | 57 +++++++++++++++++----------------- app/main.py | 2 +- app/utils/constants.py | 14 ++++++--- 3 files changed, 40 insertions(+), 33 deletions(-) diff --git a/app/components/windows/base.py b/app/components/windows/base.py index 688236e..9325be8 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -20,16 +20,23 @@ from shutil import rmtree from time import sleep -from manga_ocr import MangaOcr from PyQt5.QtCore import QThreadPool -from PyQt5.QtWidgets import QVBoxLayout, QWidget, QMainWindow, QApplication, QPushButton +from PyQt5.QtWidgets import ( + QApplication, + QFileDialog, + QMainWindow, + QPushButton, + QVBoxLayout, + QWidget, +) from .external import ExternalWindow from components.popups import BasePopup, CheckboxPopup from components.settings import ( BaseSettings, - PreviewOptions, + ModelOptions, OptionsContainer, + PreviewOptions, ShortcutOptions, TesseractOptions, ) @@ -133,11 +140,25 @@ def modifyHotkeys(self): # ------------------------------- Misc Functions -------------------------------- # def loadModel(self): + confirmation = OptionsContainer(ModelOptions(self)) + confirmation.exec() + + if confirmation: + self.loadSettings({"useOcrOffline": "false"}) + if self.useOcrOffline: + startPath = self.mainView.explorerPath or "." + ocrPath = QFileDialog.getExistingDirectory( + self, "Set MangaOCR Directory", startPath + ) + if ocrPath: + self.mangaOCRPath = ocrPath + self.loadModelAfterPopup() + + def loadModelAfterPopup(self): loadModelButton = self.toolbar.findChild(QPushButton, "loadModel") - loadModelButton.setChecked(not self.state.ocrModel) - self.state.ocrModelName + isMangaOCR = self.state.ocrModelName == "MangaOCR" - if loadModelButton.isChecked() and self.hasLoadModelPopup: + if isMangaOCR and self.hasLoadModelPopup: ret = CheckboxPopup( "hasLoadModelPopup", "Load the MangaOCR model?", @@ -145,29 +166,10 @@ def loadModel(self): CheckboxPopup.Ok | CheckboxPopup.Cancel, ).exec() if ret == CheckboxPopup.Cancel: - loadModelButton.setChecked(False) return - def loadModelHelper(state: State): - ocrModelName = state.setOCRModelName() - if ocrModelName == "MangaOCR": - try: - if self.mangaOCRPath: - state.ocrModel = MangaOcr( - pretrained_model_name_or_path=self.mangaOCRPath - ) - else: - state.ocrModel = MangaOcr() - return "success" - except Exception as e: - state.toggleOCRModelName() - return str(e) - else: - state.ocrModel = None - return "success" - def loadModelConfirm(message: str): - modelName = "MangaOCR" if self.state.ocrModel else "Tesseract" + modelName = self.state.ocrModelName if message == "success": BasePopup( f"{modelName} model loaded", @@ -175,9 +177,8 @@ def loadModelConfirm(message: str): ).exec() else: BasePopup("Load Model Error", message).exec() - loadModelButton.setChecked(False) - worker = BaseWorker(loadModelHelper, self.state) + worker = BaseWorker(self.state.loadOCRModel, self.mangaOCRPath) worker.signals.result.connect(loadModelConfirm) worker.signals.finished.connect(lambda: loadModelButton.setEnabled(True)) diff --git a/app/main.py b/app/main.py index f65aad9..f62fb78 100644 --- a/app/main.py +++ b/app/main.py @@ -48,7 +48,7 @@ eventDispatcher.installNativeEventFilter(winEventFilter) widget.showMaximized() - widget.loadModel() + widget.loadModelAfterPopup() app.exec_() # keybinder.unregister_hotkey(widget.winId(), shortcut) diff --git a/app/utils/constants.py b/app/utils/constants.py index 8d48109..95b7053 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -37,9 +37,10 @@ "*.xpm", ] -# Settings +# Settings Popup Choices TOGGLE_CHOICES = [" Disabled", " Enabled"] +OCR_MODEL = ["MangaOCR", "Tesseract"] LANGUAGE = [" Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English"] ORIENTATION = [" Vertical", " Horizontal"] @@ -84,12 +85,17 @@ # Window MAIN_WINDOW_DEFAULTS = { - "mangaOCRPath": "", + "useOcrOffline": "false", "hasLoadModelPopup": "true", "logToFile": "false", + "mangaOCRPath": "", "stylesheetPath": "./assets/styles.qss", } -MAIN_WINDOW_TYPES = {"hasLoadModelPopup": bool, "logToFile": bool} +MAIN_WINDOW_TYPES = { + "useOcrOffline": bool, + "hasLoadModelPopup": bool, + "logToFile": bool, +} # View MAIN_VIEW_DEFAULTS = {"explorerPath": EXPLORER_ROOT_DEFAULT} @@ -266,7 +272,7 @@ "title": "Switch detection model", "message": "Switch between MangaOCR and Tesseract models.", "path": "loadModel.png", - "toggle": True, + "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, "iconWidth": 1.0, From 0f4913985eca1d9bdcf7f82356c3a13b00a565d7 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 30 Apr 2023 14:48:16 +0800 Subject: [PATCH 111/137] refactor: update default ocrModelName in State --- app/services/states.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/services/states.py b/app/services/states.py index fcdd76c..66da92c 100644 --- a/app/services/states.py +++ b/app/services/states.py @@ -21,8 +21,10 @@ from typing import Literal from manga_ocr import MangaOcr +from PyQt5.QtCore import QSettings from PyQt5.QtGui import QPixmap +from utils.constants import SETTINGS_FILE_DEFAULT from utils.scripts import combineTwoImages @@ -57,8 +59,10 @@ class State: def __init__(self): self._baseImage = Pixmap("") + settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) + ocrModelName = settings.value("ocrModel", "MangaOCR") self._ocrModel = None - self._ocrModelName: OCRModelNames = "Tesseract" + self._ocrModelName: OCRModelNames = ocrModelName # ------------------------------------ Image ------------------------------------ # @@ -109,7 +113,8 @@ def toggleOCRModelName(self): return self._ocrModelName def loadOCRModel(self, path: str = None): - if self._ocrModelName == "Tesseract": + if self._ocrModelName == "Tesseract" or self._ocrModel != None: + self.ocrModel = None return "success" elif self._ocrModelName == "MangaOCR": try: From e565c7cfd596ee461ad6aa5e9911e5de98e0f6bc Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 30 Apr 2023 14:49:38 +0800 Subject: [PATCH 112/137] feat: remove redundant user flow --- app/components/windows/base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/components/windows/base.py b/app/components/windows/base.py index 9325be8..59baf30 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -17,6 +17,7 @@ along with this program. If not, see . """ +import re from shutil import rmtree from time import sleep @@ -145,7 +146,7 @@ def loadModel(self): if confirmation: self.loadSettings({"useOcrOffline": "false"}) - if self.useOcrOffline: + if self.useOcrOffline and not self.mangaOCRPath: startPath = self.mainView.explorerPath or "." ocrPath = QFileDialog.getExistingDirectory( self, "Set MangaOCR Directory", startPath @@ -167,6 +168,7 @@ def loadModelAfterPopup(self): ).exec() if ret == CheckboxPopup.Cancel: return + self.loadSettings({"hasLoadModelPopup": "true"}) def loadModelConfirm(message: str): modelName = self.state.ocrModelName @@ -177,6 +179,8 @@ def loadModelConfirm(message: str): ).exec() else: BasePopup("Load Model Error", message).exec() + if re.search("^unable to parse .* as a URL or as a local path$", message): + self.mangaOCRPath = "" worker = BaseWorker(self.state.loadOCRModel, self.mangaOCRPath) worker.signals.result.connect(loadModelConfirm) From 34be14917369639e0d0cc65647aa453d9b569a86 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 30 Apr 2023 14:51:03 +0800 Subject: [PATCH 113/137] fix: handle boolean prop in BaseSettings --- app/components/settings/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/components/settings/base.py b/app/components/settings/base.py index 527b18c..d023284 100644 --- a/app/components/settings/base.py +++ b/app/components/settings/base.py @@ -90,7 +90,8 @@ def setProperty(self, prop: str, value: Any): try: t = self._types[prop] if t == bool: - return setattr(self, prop, value.lower() == "true") + v = value if type(value) == bool else value.lower() == "true" + return setattr(self, prop, v) return setattr(self, prop, t(value)) except KeyError: return setattr(self, prop, value) From ede289087ceb53be4faccb1d5c58564aba844422 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 30 Apr 2023 15:05:02 +0800 Subject: [PATCH 114/137] fix: set correct OCR model for tesseract --- app/services/states.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/states.py b/app/services/states.py index 66da92c..1d28b46 100644 --- a/app/services/states.py +++ b/app/services/states.py @@ -113,8 +113,8 @@ def toggleOCRModelName(self): return self._ocrModelName def loadOCRModel(self, path: str = None): - if self._ocrModelName == "Tesseract" or self._ocrModel != None: - self.ocrModel = None + if self._ocrModelName == "Tesseract": + self._ocrModel = None return "success" elif self._ocrModelName == "MangaOCR": try: From 57f8c23bb7b791ddfddf48b3db30bfa06ab8d1ff Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 30 Apr 2023 15:09:13 +0800 Subject: [PATCH 115/137] chore: ignore files for offline model loading --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b55b78c..48d50df 100644 --- a/.gitignore +++ b/.gitignore @@ -146,4 +146,7 @@ cython_debug/ *.mp4 # Settings -*.ini +**/bin/*.ini + +# Models +**/bin/manga-ocr From fff4ae262d5a1553fe0f105c22e107df94b8872f Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 30 Apr 2023 20:27:20 +0800 Subject: [PATCH 116/137] fix: prevent loading model on popup cancel --- app/components/windows/base.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/components/windows/base.py b/app/components/windows/base.py index 59baf30..a3b4d65 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -153,7 +153,11 @@ def loadModel(self): ) if ocrPath: self.mangaOCRPath = ocrPath - self.loadModelAfterPopup() + elif not self.useOcrOffline: + self.mangaOCRPath = "" + + if confirmation: + self.loadModelAfterPopup() def loadModelAfterPopup(self): loadModelButton = self.toolbar.findChild(QPushButton, "loadModel") @@ -168,7 +172,7 @@ def loadModelAfterPopup(self): ).exec() if ret == CheckboxPopup.Cancel: return - self.loadSettings({"hasLoadModelPopup": "true"}) + self.loadSettings({"hasLoadModelPopup": "true"}) def loadModelConfirm(message: str): modelName = self.state.ocrModelName @@ -179,7 +183,9 @@ def loadModelConfirm(message: str): ).exec() else: BasePopup("Load Model Error", message).exec() - if re.search("^unable to parse .* as a URL or as a local path$", message): + if re.search( + "^unable to parse .* as a URL or as a local path$", message + ): self.mangaOCRPath = "" worker = BaseWorker(self.state.loadOCRModel, self.mangaOCRPath) From 085f83883181bc61eb06db1153680347afb0bd42 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 6 May 2023 21:11:17 +0800 Subject: [PATCH 117/137] fix: add missing package and downgrade huggingface * include sacremoses package by downgrading * fix transformers logging error on build --- environment/base.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/environment/base.yaml b/environment/base.yaml index 59cb598..a0245c5 100644 --- a/environment/base.yaml +++ b/environment/base.yaml @@ -15,4 +15,5 @@ dependencies: - pyqtkeybind - rarfile - pdf2image - - huggingface-hub==0.7.0 + - stringcase + - huggingface-hub==0.4.0 From 25a577fde2f9a6c4ab0dd6f73392ee4ff8012048 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 6 May 2023 23:30:35 +0800 Subject: [PATCH 118/137] fix: update pyinstaller and huggingface versions * downgrade versions since pyinstaller broke at some point after 4.10 --- environment/base.yaml | 11 ++++++----- environment/build.yaml | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/environment/base.yaml b/environment/base.yaml index a0245c5..dd42820 100644 --- a/environment/base.yaml +++ b/environment/base.yaml @@ -7,13 +7,14 @@ channels: dependencies: - python=3.9 - pip - - conda-forge:tesserocr + - poppler + - tesserocr - pip: - - pyqt5 - - pillow + - huggingface-hub==0.7.0 - manga-ocr + - pdf2image + - pillow + - pyqt5 - pyqtkeybind - rarfile - - pdf2image - stringcase - - huggingface-hub==0.4.0 diff --git a/environment/build.yaml b/environment/build.yaml index 2ab9f1b..dddabf4 100644 --- a/environment/build.yaml +++ b/environment/build.yaml @@ -6,4 +6,4 @@ channels: dependencies: - pip: - - pyinstaller + - pyinstaller==4.10 From b7bc009b60d543a00daa481bd4687e97434fb831 Mon Sep 17 00:00:00 2001 From: yupm <35495603+yupm@users.noreply.github.com> Date: Sat, 20 May 2023 11:21:38 +0900 Subject: [PATCH 119/137] build: add Linux (debian) support * add initial implementation for linux build * chore: rename and move build files * chore: format imports * fix: add missing package and downgrade huggingface * include sacremoses package by downgrading * fix transformers logging error on build * fix: update pyinstaller and huggingface versions * downgrade versions since pyinstaller broke at some point after 4.10 * chore: remove redundant dependency * fix: remove unused package * rename and move svg logo * remove app/components from build/linux/main.spec * fix rar and pdf format on Linux --- .gitignore | 7 +- app/assets/images/icons/logo.svg | 41 ++++++++++ app/components/windows/base.py | 3 +- app/utils/constants.py | 5 ++ app/utils/scripts/mangaFileToImageDir.py | 8 +- build/linux/build.sh | 23 ++++++ build/linux/main.spec | 99 ++++++++++++++++++++++++ build/linux/poricom.desktop | 20 +++++ build/{ => windows}/main.spec | 13 ++-- 9 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 app/assets/images/icons/logo.svg create mode 100755 build/linux/build.sh create mode 100644 build/linux/main.spec create mode 100644 build/linux/poricom.desktop rename build/{ => windows}/main.spec (88%) diff --git a/.gitignore b/.gitignore index 48d50df..65a6efa 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,11 @@ code/__pycache__/ # Distribution / packaging .Python -build/ +build/* +!/build/linux/ +build/linux/*/ +!/build/windows/ +build/windows/*/ develop-eggs/ dist/ downloads/ @@ -32,6 +36,7 @@ MANIFEST # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec +!build/**/main.spec # Installer logs pip-log.txt diff --git a/app/assets/images/icons/logo.svg b/app/assets/images/icons/logo.svg new file mode 100644 index 0000000..027275c --- /dev/null +++ b/app/assets/images/icons/logo.svg @@ -0,0 +1,41 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + diff --git a/app/components/windows/base.py b/app/components/windows/base.py index a3b4d65..f2c9769 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -48,6 +48,7 @@ LOAD_MODEL_MESSAGE, MAIN_WINDOW_DEFAULTS, MAIN_WINDOW_TYPES, + PORICOM_CACHE, STYLESHEET_DARK, STYLESHEET_LIGHT, ) @@ -85,7 +86,7 @@ def explorer(self): def closeEvent(self, event): try: - rmtree("./poricom_cache") + rmtree(PORICOM_CACHE) except FileNotFoundError: pass self.saveSettings(False) diff --git a/app/utils/constants.py b/app/utils/constants.py index 95b7053..b6160de 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -17,6 +17,7 @@ """ from .types import ButtonConfigDict +from sys import platform # ------------------------------------- General ------------------------------------- # @@ -60,6 +61,8 @@ " No Modifier", ] +PLATFORM = platform + # Paths STYLESHEET_LIGHT = "./assets/styles.qss" STYLESHEET_DARK = "./assets/styles-dark.qss" @@ -71,6 +74,8 @@ EXPLORER_ROOT_DEFAULT = "./assets/images/" +PORICOM_CACHE = "/tmp/poricom_cache" if PLATFORM == "linux" else "./poricom_cache" + # Messages LOAD_MODEL_MESSAGE = ( "If you are running this for the first time, this will download the MangaOcr model " diff --git a/app/utils/scripts/mangaFileToImageDir.py b/app/utils/scripts/mangaFileToImageDir.py index ef018c7..a71fcac 100644 --- a/app/utils/scripts/mangaFileToImageDir.py +++ b/app/utils/scripts/mangaFileToImageDir.py @@ -24,6 +24,8 @@ import rarfile import pdf2image +from utils.constants import PORICOM_CACHE, PLATFORM + def mangaFileToImageDir(filepath: str): """Converts a manga file to a directory of images @@ -35,13 +37,15 @@ def mangaFileToImageDir(filepath: str): str: Path to directory of images. """ extractPath, extension = splitext(filepath) - cachePath = f"./poricom_cache/{basename(extractPath)}" + cachePath = f"{PORICOM_CACHE}/{basename(extractPath)}" if extension in [".cbz", ".zip"]: with zipfile.ZipFile(filepath, "r") as zipRef: zipRef.extractall(cachePath) - rarfile.UNRAR_TOOL = "bin/unrar.exe" + if "win" in PLATFORM.lower(): + rarfile.UNRAR_TOOL = "bin/unrar.exe" + if extension in [".cbr", ".rar"]: with rarfile.RarFile(filepath) as zipRef: zipRef.extractall(cachePath) diff --git a/build/linux/build.sh b/build/linux/build.sh new file mode 100755 index 0000000..0840a05 --- /dev/null +++ b/build/linux/build.sh @@ -0,0 +1,23 @@ +echo "Creating Poricom pyinstaller package" +pyinstaller main.spec + +echo "Bundle Poricom for distribution" +mkdir -p package/opt +mkdir -p package/usr/share/applications +mkdir -p package/usr/share/icons/hicolor/scalable/apps + +cp -r dist/app package/opt/poricom +cp poricom.desktop package/usr/share/applications +cp package/opt/poricom/assets/images/icons/logo.svg package/usr/share/icons/hicolor/scalable/apps/logo.svg + +find package/opt/poricom -type f -exec chmod 644 -- {} + +find package/opt/poricom -type d -exec chmod 755 -- {} + +find package/usr/share -type f -exec chmod 644 -- {} + +chmod +x package/opt/poricom/Poricom + +echo "Create deb package" +#sudo apt install ruby -y +#gem install fpm +fpm -C package -s dir -t deb -n "poricom" -v 1.0.0 -p poricom.deb + +echo "Linux build (deb) finished." diff --git a/build/linux/main.spec b/build/linux/main.spec new file mode 100644 index 0000000..f10f623 --- /dev/null +++ b/build/linux/main.spec @@ -0,0 +1,99 @@ +# -*- mode: python ; coding: utf-8 -*- +from PyInstaller.utils.hooks import collect_data_files +from PyInstaller.utils.hooks import copy_metadata +import os, torch, glob + +datas = [] +datas += collect_data_files('unidic_lite') +datas += collect_data_files('manga_ocr') +datas += copy_metadata('tqdm') +datas += copy_metadata('regex') +datas += copy_metadata('requests') +datas += copy_metadata('packaging') +datas += copy_metadata('filelock') +datas += copy_metadata('numpy') +datas += copy_metadata('tokenizers') + +added_files = [ + ('../../app/assets', './assets'), + ('../../app/bin', './bin') +] + +block_cipher = None + +a = Analysis(['../../app/main.py'], + pathex=['../../app'], + binaries=[], + datas=datas+added_files, + hiddenimports= ['stringcase'], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) + +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) + +excluded_binaries = [ + 'libtorch_cpu.so', + 'libtorch_cuda_cpp.so', + 'libtorch_cuda_cu.so', + 'libtorch_cuda_linalg.so' +] +a.binaries = [x for x in a.binaries if not x[0] in excluded_binaries] + +libcudnn_path = os.path.split(torch.__path__[0])[0] + '/nvidia/cudnn/lib' +libcudnn_ops_infer_path = libcudnn_path + '/libcudnn_ops_infer.so.8' +libcudnn_cnn_infer_path = libcudnn_path + '/libcudnn_cnn_infer.so.8' + +included_binaries = [ + ('libcudnn_ops_infer.so.8', libcudnn_ops_infer_path,'BINARY'), + ('libcudnn_cnn_infer.so.8', libcudnn_cnn_infer_path, 'BINARY' ) +] + +a.binaries = a.binaries + included_binaries + +exe = EXE(pyz, + a.scripts, + [], + exclude_binaries=True, + name='Poricom', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon="logo.svg") + +coll = COLLECT(exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='app') + +excluded_files = [ + 'libtorch_cuda_linalg.so', + 'libnccl.so.2', + 'libcufft.so.10', + 'libcusparse.so.11', + 'unrar.exe' +] + +for binary in excluded_files: + for filePath in glob.glob('**/'+ binary, recursive=True): + try: + print("Removing: {}".format(filePath)) + os.remove(filePath) + except OSError: + print("Error while deleting: {}".format(filePath)) diff --git a/build/linux/poricom.desktop b/build/linux/poricom.desktop new file mode 100644 index 0000000..e04b930 --- /dev/null +++ b/build/linux/poricom.desktop @@ -0,0 +1,20 @@ +[Desktop Entry] + +# The type of the thing this desktop file refers to (e.g. can be Link) +Type=Application + +# The application name. +Name=Poricom + +# Tooltip comment to show in menus. +Comment=Manga OCR desktop application + +# The path (folder) in which the executable is run +Path=/opt/poricom + +# The executable (can include arguments) +Exec=/opt/poricom/Poricom + +# The icon for the entry, using the name from `hicolor/scalable` without the extension. +# You can also use a full path to a file in /opt. +Icon=logo diff --git a/build/main.spec b/build/windows/main.spec similarity index 88% rename from build/main.spec rename to build/windows/main.spec index 8268053..034e30c 100644 --- a/build/main.spec +++ b/build/windows/main.spec @@ -7,7 +7,6 @@ datas += collect_data_files('unidic_lite') datas += collect_data_files('manga_ocr') datas += copy_metadata('tqdm') datas += copy_metadata('regex') -datas += copy_metadata('sacremoses') datas += copy_metadata('requests') datas += copy_metadata('packaging') datas += copy_metadata('filelock') @@ -15,17 +14,17 @@ datas += copy_metadata('numpy') datas += copy_metadata('tokenizers') added_files = [ - ('../app/assets', './assets'), - ('../app/bin', './bin'), - ('path\\to\\user\\.conda\\pkgs\\poppler-22.01.0-h24fffdf_2', './poppler') + ('../../app/assets', './assets'), + ('../../app/bin', './bin'), + ('path\\to\\user\\.conda\\pkgs\\poppler-version', './poppler') ] block_cipher = None -a = Analysis(['../app/main.py'], - pathex=['../app'], +a = Analysis(['../../app/main.py'], + pathex=['../../app'], binaries=[], datas=datas+added_files, hiddenimports=['stringcase'], @@ -88,7 +87,7 @@ exe = EXE(pyz, target_arch=None, codesign_identity=None, entitlements_file=None, - icon="../app/assets/images/icons/logo.ico") + icon="../../app/assets/images/icons/logo.ico") coll = COLLECT(exe, a.binaries, a.zipfiles, From fc7bb8152de6dacba2cf76a1762eb05c7552d77e Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 20 May 2023 10:35:32 +0800 Subject: [PATCH 120/137] fix: copy last instead of previous selection --- app/components/views/ocr/base.py | 7 +++++-- app/utils/scripts/__init__.py | 1 + app/utils/scripts/copyToClipboard.py | 28 ++++++++++++++++++++++++++++ app/utils/scripts/logText.py | 13 ++++--------- 4 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 app/utils/scripts/copyToClipboard.py diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 7e15b15..c2dc6e6 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -30,7 +30,7 @@ TEXT_LOGGING_DEFAULTS, TEXT_LOGGING_TYPES, ) -from utils.scripts import logText, pixmapToText +from utils.scripts import copyToClipboard, logText, pixmapToText class BaseOCRView(QGraphicsView, BaseSettings): @@ -78,6 +78,7 @@ def handleTextResult(self, result): def handleTextFinished(self): try: self.canvasText.adjustSize() + copyToClipboard(self.canvasText.text()) except RuntimeError: pass try: @@ -111,7 +112,9 @@ def mouseMoveEvent(self, event): def mouseReleaseEvent(self, event): logPath = join(self.explorerPath, "text-log.txt") text = self.canvasText.text() - logText(text, isLogFile=self.logToFile, path=logPath) + if self.logToFile: + logText(text, path=logPath) + try: if not self.persistText: self.canvasText.hide() diff --git a/app/utils/scripts/__init__.py b/app/utils/scripts/__init__.py index 7124755..68b81e0 100644 --- a/app/utils/scripts/__init__.py +++ b/app/utils/scripts/__init__.py @@ -18,6 +18,7 @@ """ from .combineTwoImages import combineTwoImages +from .copyToClipboard import copyToClipboard from .editStylesheet import editStylesheet from .logText import logText from .mangaFileToImageDir import mangaFileToImageDir diff --git a/app/utils/scripts/copyToClipboard.py b/app/utils/scripts/copyToClipboard.py new file mode 100644 index 0000000..1d1d40f --- /dev/null +++ b/app/utils/scripts/copyToClipboard.py @@ -0,0 +1,28 @@ +""" +Poricom Helper Functions + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtGui import QGuiApplication + + +def copyToClipboard(text: str): + """ + Copy input text to clipboard + """ + clipboard = QGuiApplication.clipboard() + clipboard.setText(text) diff --git a/app/utils/scripts/logText.py b/app/utils/scripts/logText.py index 592d78d..e3378b5 100644 --- a/app/utils/scripts/logText.py +++ b/app/utils/scripts/logText.py @@ -20,17 +20,12 @@ from PyQt5.QtGui import QGuiApplication -def logText(text: str, isLogFile: bool = False, path: str = "."): - """Log text by copying to clipboard +def logText(text: str, path: str = "."): + """Log by appending text to log file Args: text (str): Text to log. - isLogFile (bool, optional): Set flag to save copied text to clipboard. Defaults to False. path (str, optional): Path to log file. Defaults to ".". """ - clipboard = QGuiApplication.clipboard() - clipboard.setText(text) - - if isLogFile: - with open(path, "a", encoding="utf-8") as fh: - fh.write(text + "\n") + with open(path, "a", encoding="utf-8") as fh: + fh.write(text + "\n") From 38ad64fea779ff976b2c5a9233f717c5099ad57b Mon Sep 17 00:00:00 2001 From: yupm <35495603+yupm@users.noreply.github.com> Date: Sun, 26 Mar 2023 21:10:30 +0900 Subject: [PATCH 121/137] Add initial implementation of translate component * Show text and translations in an adjustable popup window * Add threading to load `argostranslate` model * Update imports to be consistent --- app/assets/styles-dark.qss | 6 ++ app/assets/styles.qss | 6 ++ app/components/popups/__init__.py | 1 + app/components/popups/translationDialog.py | 86 ++++++++++++++++++++++ app/components/views/ocr/ocr.py | 27 +++++++ app/components/windows/base.py | 17 ++++- app/services/states.py | 10 +++ environment/base.yaml | 4 +- 8 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 app/components/popups/translationDialog.py diff --git a/app/assets/styles-dark.qss b/app/assets/styles-dark.qss index 998b4bd..f3fd6c0 100644 --- a/app/assets/styles-dark.qss +++ b/app/assets/styles-dark.qss @@ -184,6 +184,12 @@ QAbstractSpinBox:hover { border: 0.1em solid #6A88CB; } +QTextEdit { + background-color: #31363b; + color: #eff0f1; + font-size: 16pt; +} + QAbstractSpinBox:up-button{ width: 0em; height: 0em; diff --git a/app/assets/styles.qss b/app/assets/styles.qss index 90613ce..38c0777 100644 --- a/app/assets/styles.qss +++ b/app/assets/styles.qss @@ -184,6 +184,12 @@ QAbstractSpinBox:hover { border: 0.1em solid #3b3b42; } +QTextEdit { + background-color: #9394a5; + color: #eff0f1; + font-size: 16pt; +} + QAbstractSpinBox:up-button{ width: 0em; height: 0em; diff --git a/app/components/popups/__init__.py b/app/components/popups/__init__.py index 1563758..b87b2fa 100644 --- a/app/components/popups/__init__.py +++ b/app/components/popups/__init__.py @@ -19,3 +19,4 @@ from .base import BasePopup from .checkbox import CheckboxPopup +from .translationDialog import TranslationDialog diff --git a/app/components/popups/translationDialog.py b/app/components/popups/translationDialog.py new file mode 100644 index 0000000..5b76580 --- /dev/null +++ b/app/components/popups/translationDialog.py @@ -0,0 +1,86 @@ +import argostranslate.package as package +import argostranslate.translate +import cutlet +from PyQt5.QtWidgets import QLabel, QTextEdit, QVBoxLayout, QDialog +from PyQt5.QtCore import QThreadPool, Qt + +from services import BaseWorker, State + + +class TranslationDialog(QDialog): + def __init__(self): + super().__init__() + layout = QVBoxLayout() + + self.ocrLabel = QLabel("OCR") + layout.addWidget(self.ocrLabel) + + self.ocrLineEdit = QTextEdit("") + layout.addWidget(self.ocrLineEdit) + + self.romanjiLabel = QLabel("Romanji") + layout.addWidget(self.romanjiLabel) + + self.romanjiLineEdit = QTextEdit("") + layout.addWidget(self.romanjiLineEdit) + + self.translationLabel = QLabel("Translation") + layout.addWidget(self.translationLabel) + + self.translationLineEdit = QTextEdit("") + layout.addWidget(self.translationLineEdit) + + self.setLayout(layout) + self.setWindowFlags(Qt.WindowStaysOnTopHint) + self.setAttribute(Qt.WA_X11NetWmWindowTypeUtility) + self.resize(500, 200) + + self.text = "" + self.katakanaToRomaji = cutlet.Cutlet() + self.state = State() + self.threadpool = QThreadPool.globalInstance() + self.loadTranslationModel() + + def loadTranslationModel(self): + def loadArgosTranslate(state: State): + package.update_package_index() + from_code = "ja" + to_code = "en" + + # Download and install Argos Translate package. + availablePackages = argostranslate.package.get_available_packages() + availablePackage = list( + filter( + lambda x: x.from_code == from_code and x.to_code == to_code, + availablePackages, + ) + )[0] + downloadPath = availablePackage.download() + argostranslate.package.install_from_path(downloadPath) + + # Set translation + installedLanguages = argostranslate.translate.get_installed_languages() + from_lang = list(filter(lambda x: x.code == from_code, installedLanguages))[ + 0 + ] + to_lang = list(filter(lambda x: x.code == to_code, installedLanguages))[0] + state.translationModel = from_lang.get_translation(to_lang) + + worker = BaseWorker(loadArgosTranslate, self.state) + self.threadpool.start(worker) + + def setText(self, text): + self.text = text + self.ocrLineEdit.setText(text) + romajiText = "" + argosText = "" + try: + romajiText = self.katakanaToRomaji.romaji(text) + except: + romajiText = "" + try: + argosText = self.state.translationModel.translate(text) + except: + argosText = "" + self.romanjiLineEdit.setText(romajiText) + self.translationLineEdit.setText(argosText) diff --git a/app/components/views/ocr/ocr.py b/app/components/views/ocr/ocr.py index e1f8e44..8b2bc50 100644 --- a/app/components/views/ocr/ocr.py +++ b/app/components/views/ocr/ocr.py @@ -17,19 +17,46 @@ along with this program. If not, see . """ +from os.path import join + from PyQt5.QtCore import pyqtSlot from PyQt5.QtWidgets import QMainWindow from ..image import BaseImageView from .base import BaseOCRView from services import State +from components.popups import TranslationDialog +from utils.scripts import logText class OCRView(BaseImageView, BaseOCRView): def __init__(self, parent: QMainWindow, state: State = None): super().__init__(parent, state) self.loadSettings() + self.translationDialog = TranslationDialog() @pyqtSlot() def rubberBandStopped(self): super().rubberBandStopped() + + def handleTextResult(self, result): + try: + self.canvasText.hide() + self.translationDialog.setText(result) + self.translationDialog.show() + except RuntimeError: + pass + + def mouseReleaseEvent(self, event): + logPath = join(self.explorerPath, "text-log.txt") + text = self.translationDialog.text + logText(text, isLogFile=self.logToFile, path=logPath) + try: + if not self.persistText: + self.canvasText.hide() + except AttributeError: + pass + super(BaseOCRView, self).mouseReleaseEvent(event) + + def closeEvent(self, event): + self.translationDialog.close() diff --git a/app/components/windows/base.py b/app/components/windows/base.py index a3b4d65..9ebf54e 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -21,7 +21,7 @@ from shutil import rmtree from time import sleep -from PyQt5.QtCore import QThreadPool +from PyQt5.QtCore import Qt, QThreadPool from PyQt5.QtWidgets import ( QApplication, QFileDialog, @@ -90,8 +90,23 @@ def closeEvent(self, event): pass self.saveSettings(False) self.mainView.saveSettings(False) + self.canvas.close() return super().closeEvent(event) + def changeEvent(self, event): + if ( + not self.isActiveWindow() + and not self.canvas.translationDialog.isActiveWindow() + ): + self.canvas.translationDialog.setWindowFlags( + self.windowFlags() & ~Qt.WindowStaysOnTopHint + ) + else: + self.canvas.translationDialog.setWindowFlags( + self.windowFlags() | Qt.WindowStaysOnTopHint + ) + return super().changeEvent(event) + def noop(self): BasePopup("Not Implemented", "This function is not yet implemented.").exec() diff --git a/app/services/states.py b/app/services/states.py index 1d28b46..70438ad 100644 --- a/app/services/states.py +++ b/app/services/states.py @@ -64,6 +64,8 @@ def __init__(self): self._ocrModel = None self._ocrModelName: OCRModelNames = ocrModelName + self._translationModel = None + # ------------------------------------ Image ------------------------------------ # @property @@ -126,3 +128,11 @@ def loadOCRModel(self, path: str = None): except Exception as e: self.setOCRModelName("Tesseract") return str(e) + + @property + def translationModel(self): + return self._translationModel + + @translationModel.setter + def translationModel(self, translationModel): + self._translationModel = translationModel diff --git a/environment/base.yaml b/environment/base.yaml index dd42820..a663e92 100644 --- a/environment/base.yaml +++ b/environment/base.yaml @@ -10,6 +10,8 @@ dependencies: - poppler - tesserocr - pip: + - argostranslate + - cutlet - huggingface-hub==0.7.0 - manga-ocr - pdf2image @@ -17,4 +19,4 @@ dependencies: - pyqt5 - pyqtkeybind - rarfile - - stringcase + - stringcase From 334d4938128c1b613ea9c131bbcac369c3f9d26d Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 21 May 2023 13:41:06 +0800 Subject: [PATCH 122/137] refactor: move translate methods to states --- app/services/states.py | 79 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/app/services/states.py b/app/services/states.py index 70438ad..5610d2a 100644 --- a/app/services/states.py +++ b/app/services/states.py @@ -20,6 +20,12 @@ from os.path import isfile, exists from typing import Literal +from argostranslate.package import ( + get_available_packages, + install_from_path, + update_package_index, +) +from argostranslate.translate import get_installed_languages from manga_ocr import MangaOcr from PyQt5.QtCore import QSettings from PyQt5.QtGui import QPixmap @@ -53,6 +59,7 @@ def isValid(self): OCRModelNames = Literal["Tesseract", "MangaOCR"] +TranslateModelNames = Literal["ArgosTranslate", "ChatGPT", "DeepL"] class State: @@ -60,11 +67,14 @@ def __init__(self): self._baseImage = Pixmap("") settings = QSettings(SETTINGS_FILE_DEFAULT, QSettings.IniFormat) + ocrModelName = settings.value("ocrModel", "MangaOCR") self._ocrModel = None self._ocrModelName: OCRModelNames = ocrModelName - self._translationModel = None + translateModelName = settings.value("translateModel", "ArgosTranslate") + self._translateModel = None + self._translateModelName: TranslateModelNames = translateModelName # ------------------------------------ Image ------------------------------------ # @@ -129,10 +139,67 @@ def loadOCRModel(self, path: str = None): self.setOCRModelName("Tesseract") return str(e) + # ---------------------------------- Translate ---------------------------------- # + + @property + def translateModel(self): + return self._translateModel + + @translateModel.setter + def translateModel(self, translateModel): + self._translateModel = translateModel + @property - def translationModel(self): - return self._translationModel + def translateModelName(self): + return self._translateModelName + + def setTranslateModelName(self, translateModelName: TranslateModelNames = None): + self._translateModelName = translateModelName + return self._translateModelName + + def downloadArgosTranslateModel(self, fromCode="ja", toCode="en"): + update_package_index() + availablePackages = get_available_packages() + availablePackage = list( + filter( + lambda x: x.from_code == fromCode and x.to_code == toCode, + availablePackages, + ) + )[0] + downloadPath = availablePackage.download() + install_from_path(downloadPath) + + def getArgosTranslateModel(self, fromCode="ja", toCode="en"): + installedLanguages = get_installed_languages() + fromLang = list(filter(lambda x: x.code == fromCode, installedLanguages)) + toLang = list(filter(lambda x: x.code == toCode, installedLanguages)) + + if not fromLang or not toLang: + return None + return fromLang[0], toLang[0] + + def loadTranslateModel(self): + if self._translateModelName == "ArgosTranslate": + languages = self.getArgosTranslateModel() + if languages: + fromLang, toLang = languages + self.translateModel = fromLang.get_translation(toLang) + else: + self.downloadArgosTranslateModel() + languages = self.getArgosTranslateModel() + if not languages: + return "Error while loading offline model." + fromLang, toLang = languages + self.translateModel = fromLang.get_translation(toLang) + return "success" + else: + self.translateModel = None + return "success" - @translationModel.setter - def translationModel(self, translationModel): - self._translationModel = translationModel + def predictTranslate(self, text): + if self.translateModelName == "ArgosTranslate": + return self.translateModel.translate(text) + elif self.translateModelName == "ChatGPT": + pass + elif self.translateModelName == "DeepL": + pass From 93852e48f0748a0d6677e39d09bcbe32215f143f Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 21 May 2023 14:02:20 +0800 Subject: [PATCH 123/137] feat: add TranslateOptions component --- app/components/settings/__init__.py | 1 + app/components/settings/popups/__init__.py | 1 + app/components/settings/popups/translate.py | 76 +++++++++++++++++++++ app/components/windows/base.py | 28 ++++++++ app/utils/constants.py | 14 +++- 5 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 app/components/settings/popups/translate.py diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py index d72b439..48130a9 100644 --- a/app/components/settings/__init__.py +++ b/app/components/settings/__init__.py @@ -25,4 +25,5 @@ PreviewOptions, ShortcutOptions, TesseractOptions, + TranslateOptions, ) diff --git a/app/components/settings/popups/__init__.py b/app/components/settings/popups/__init__.py index 8b20d63..74249be 100644 --- a/app/components/settings/popups/__init__.py +++ b/app/components/settings/popups/__init__.py @@ -23,3 +23,4 @@ from .preview import PreviewOptions from .shortcut import ShortcutOptions from .tesseract import TesseractOptions +from .translate import TranslateOptions diff --git a/app/components/settings/popups/translate.py b/app/components/settings/popups/translate.py new file mode 100644 index 0000000..cbbd55f --- /dev/null +++ b/app/components/settings/popups/translate.py @@ -0,0 +1,76 @@ +""" +Poricom Popups + +Copyright (C) `2021-2022` `` + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from PyQt5.QtWidgets import QLabel, QLineEdit, QWidget + +from utils.constants import TRANSLATE_MODEL, TOGGLE_CHOICES + +from .base import BaseOptions + + +class TranslateOptions(BaseOptions): + def __init__(self, parent: QWidget): + super().__init__(parent, [TOGGLE_CHOICES, TRANSLATE_MODEL]) + # TODO: Use constants here + self.initializeProperties( + [ + ("enableTranslate", "false", bool), + ("translateModel", "ArgosTranslate", str), + ] + ) + + i = len(self.comboBoxList) + self.apiLabel = QLabel("API Key") + self.apiLineEdit = QLineEdit("", self) + self.layout().addWidget(self.apiLabel, i, 0) + self.layout().addWidget(self.apiLineEdit, i, 1) + self.updateDisplay() + + def updateDisplay(self): + translateModelName = self.translateModelComboBox.currentText() + if translateModelName == "ArgosTranslate": + self.apiLabel.hide() + self.apiLineEdit.hide() + elif translateModelName == "ChatGPT" or translateModelName == "DeepL": + self.apiLabel.show() + self.apiLineEdit.show() + else: + self.apiLabel.hide() + self.apiLineEdit.hide() + + def changeEnableTranslate(self, i): + self.enableTranslateIndex = i + self.enableTranslate = True if i else False + + def changeTranslateModel(self, i): + self.translateModelIndex = i + self.translateModel = i + try: + self.updateDisplay() + # Handle case where extra widgets are still undefined + except AttributeError as e: + print(e) + + def saveSettings(self, hasMessage=True): + translateModelName = self.translateModelComboBox.currentText() + self.mainWindow.state.setTranslateModelName(translateModelName) + self.mainWindow.setProperty( + "enableTranslate", "true" if self.enableTranslate else "enableTranslate" + ) + return super().saveSettings(hasMessage) diff --git a/app/components/windows/base.py b/app/components/windows/base.py index 9ebf54e..0d0ce72 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -40,6 +40,7 @@ PreviewOptions, ShortcutOptions, TesseractOptions, + TranslateOptions, ) from components.toolbar import BaseToolbar from components.views import WorkspaceView @@ -210,6 +211,33 @@ def loadModelConfirm(message: str): self.threadpool.start(worker) loadModelButton.setEnabled(False) + def loadTranslateModel(self): + confirmation = OptionsContainer(TranslateOptions(self)) + confirmation.exec() + if confirmation: + self.loadSettings({"enableTranslate": "false"}) + self.loadTranslateAfterPopup() + + def loadTranslateAfterPopup(self): + loadModelButton = self.toolbar.findChild(QPushButton, "loadTranslateModel") + + def loadModelConfirm(message: str): + modelName = self.state.translateModelName + if message == "success": + BasePopup( + f"{modelName} model loaded", + f"You are now using the {modelName} model for Japanese text translation.", + ).exec() + else: + BasePopup("Load Model Error", message).exec() + + worker = BaseWorker(self.state.loadTranslateModel) + worker.signals.result.connect(loadModelConfirm) + worker.signals.finished.connect(lambda: loadModelButton.setEnabled(True)) + + self.threadpool.start(worker) + loadModelButton.setEnabled(False) + def modifyTesseract(self): confirmation = OptionsContainer(TesseractOptions(self)) confirmation.exec() diff --git a/app/utils/constants.py b/app/utils/constants.py index 95b7053..b7abe1d 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -41,6 +41,7 @@ TOGGLE_CHOICES = [" Disabled", " Enabled"] OCR_MODEL = ["MangaOCR", "Tesseract"] +TRANSLATE_MODEL = ["ArgosTranslate", "ChatGPT", "DeepL"] LANGUAGE = [" Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English"] ORIENTATION = [" Vertical", " Horizontal"] @@ -269,14 +270,23 @@ }, "misc": { "loadModel": { - "title": "Switch detection model", - "message": "Switch between MangaOCR and Tesseract models.", + "title": "Load detection model", + "message": "Manage translation model settings.", "path": "loadModel.png", "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, "iconWidth": 1.0, }, + "loadTranslateModel": { + "title": "Load translation model", + "message": "Manage translation model settings and API keys.", + "path": "modifyTesseract.png", + "toggle": False, + "align": "AlignLeft", + "iconHeight": 1.0, + "iconWidth": 1.0, + }, "modifyTesseract": { "title": "Tesseract settings", "message": "Set the language and orientation for the Tesseract model.", From a22732c52858ea0d1454e19d0812490d32335dca Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 21 May 2023 14:05:40 +0800 Subject: [PATCH 124/137] refactor: use state methods in TranslationDialog --- app/components/popups/translationDialog.py | 87 ++++++---------------- app/components/views/ocr/ocr.py | 2 +- 2 files changed, 23 insertions(+), 66 deletions(-) diff --git a/app/components/popups/translationDialog.py b/app/components/popups/translationDialog.py index 5b76580..6d00d6b 100644 --- a/app/components/popups/translationDialog.py +++ b/app/components/popups/translationDialog.py @@ -1,86 +1,43 @@ -import argostranslate.package as package -import argostranslate.translate import cutlet from PyQt5.QtWidgets import QLabel, QTextEdit, QVBoxLayout, QDialog -from PyQt5.QtCore import QThreadPool, Qt +from PyQt5.QtCore import Qt -from services import BaseWorker, State +from services import State class TranslationDialog(QDialog): - def __init__(self): - super().__init__() - layout = QVBoxLayout() - - self.ocrLabel = QLabel("OCR") - layout.addWidget(self.ocrLabel) + def __init__(self, parent=None, state: State = None): + super().__init__(parent, flags=Qt.WindowStaysOnTopHint) + self.setAttribute(Qt.WA_DeleteOnClose | Qt.WA_X11NetWmWindowTypeUtility) + self.setLayout(QVBoxLayout()) self.ocrLineEdit = QTextEdit("") - layout.addWidget(self.ocrLineEdit) - - self.romanjiLabel = QLabel("Romanji") - layout.addWidget(self.romanjiLabel) - - self.romanjiLineEdit = QTextEdit("") - layout.addWidget(self.romanjiLineEdit) - - self.translationLabel = QLabel("Translation") - layout.addWidget(self.translationLabel) - - self.translationLineEdit = QTextEdit("") - layout.addWidget(self.translationLineEdit) - - self.setLayout(layout) - self.setWindowFlags(Qt.WindowStaysOnTopHint) - self.setAttribute(Qt.WA_X11NetWmWindowTypeUtility) + self.romajiLineEdit = QTextEdit("") + self.translateLineEdit = QTextEdit("") + self.layout().addWidget(QLabel("Detected Text")) + self.layout().addWidget(self.ocrLineEdit) + self.layout().addWidget(QLabel("Romaji")) + self.layout().addWidget(self.romajiLineEdit) + self.layout().addWidget(QLabel("Translation")) + self.layout().addWidget(self.translateLineEdit) self.resize(500, 200) self.text = "" self.katakanaToRomaji = cutlet.Cutlet() - self.state = State() - self.threadpool = QThreadPool.globalInstance() - self.loadTranslationModel() - - def loadTranslationModel(self): - def loadArgosTranslate(state: State): - package.update_package_index() - from_code = "ja" - to_code = "en" - - # Download and install Argos Translate package. - availablePackages = argostranslate.package.get_available_packages() - availablePackage = list( - filter( - lambda x: x.from_code == from_code and x.to_code == to_code, - availablePackages, - ) - )[0] - downloadPath = availablePackage.download() - argostranslate.package.install_from_path(downloadPath) - - # Set translation - installedLanguages = argostranslate.translate.get_installed_languages() - from_lang = list(filter(lambda x: x.code == from_code, installedLanguages))[ - 0 - ] - to_lang = list(filter(lambda x: x.code == to_code, installedLanguages))[0] - state.translationModel = from_lang.get_translation(to_lang) - - worker = BaseWorker(loadArgosTranslate, self.state) - self.threadpool.start(worker) + self.state = state def setText(self, text): self.text = text self.ocrLineEdit.setText(text) - romajiText = "" - argosText = "" try: romajiText = self.katakanaToRomaji.romaji(text) - except: + except Exception as e: + print(e) romajiText = "" try: - argosText = self.state.translationModel.translate(text) - except: + argosText = self.state.predictTranslate(text) + except Exception as e: + print(e) argosText = "" - self.romanjiLineEdit.setText(romajiText) - self.translationLineEdit.setText(argosText) + self.romajiLineEdit.setText(romajiText) + self.translateLineEdit.setText(argosText) diff --git a/app/components/views/ocr/ocr.py b/app/components/views/ocr/ocr.py index 8b2bc50..1981c79 100644 --- a/app/components/views/ocr/ocr.py +++ b/app/components/views/ocr/ocr.py @@ -33,7 +33,7 @@ class OCRView(BaseImageView, BaseOCRView): def __init__(self, parent: QMainWindow, state: State = None): super().__init__(parent, state) self.loadSettings() - self.translationDialog = TranslationDialog() + self.translationDialog = TranslationDialog(state=state) @pyqtSlot() def rubberBandStopped(self): From c501dc99ffeaf805a857869ea428af1b0c139378 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 21 May 2023 16:34:29 +0800 Subject: [PATCH 125/137] fix: cancel action when popup is not confirmed --- app/components/windows/base.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/components/windows/base.py b/app/components/windows/base.py index 0d0ce72..39b8102 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -158,9 +158,9 @@ def modifyHotkeys(self): def loadModel(self): confirmation = OptionsContainer(ModelOptions(self)) - confirmation.exec() + confirmed = confirmation.exec() - if confirmation: + if confirmed: self.loadSettings({"useOcrOffline": "false"}) if self.useOcrOffline and not self.mangaOCRPath: startPath = self.mainView.explorerPath or "." @@ -172,7 +172,7 @@ def loadModel(self): elif not self.useOcrOffline: self.mangaOCRPath = "" - if confirmation: + if confirmed: self.loadModelAfterPopup() def loadModelAfterPopup(self): @@ -213,8 +213,8 @@ def loadModelConfirm(message: str): def loadTranslateModel(self): confirmation = OptionsContainer(TranslateOptions(self)) - confirmation.exec() - if confirmation: + confirmed = confirmation.exec() + if confirmed: self.loadSettings({"enableTranslate": "false"}) self.loadTranslateAfterPopup() @@ -240,8 +240,8 @@ def loadModelConfirm(message: str): def modifyTesseract(self): confirmation = OptionsContainer(TesseractOptions(self)) - confirmation.exec() - if confirmation: + confirmed = confirmation.exec() + if confirmed: self.canvas.loadSettings() def toggleLogging(self): From 740273001da486ff302aa8b82696cfb6c923674d Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 21 May 2023 16:37:34 +0800 Subject: [PATCH 126/137] refactor: move tesseract settings to ModelOptions --- app/components/settings/__init__.py | 1 - app/components/settings/popups/__init__.py | 1 - app/components/settings/popups/base.py | 4 +- app/components/settings/popups/model.py | 59 +++++++++++++++++++-- app/components/settings/popups/tesseract.py | 55 ------------------- app/components/windows/base.py | 7 --- app/utils/constants.py | 13 +---- 7 files changed, 60 insertions(+), 80 deletions(-) delete mode 100644 app/components/settings/popups/tesseract.py diff --git a/app/components/settings/__init__.py b/app/components/settings/__init__.py index 48130a9..d48b808 100644 --- a/app/components/settings/__init__.py +++ b/app/components/settings/__init__.py @@ -24,6 +24,5 @@ OptionsContainer, PreviewOptions, ShortcutOptions, - TesseractOptions, TranslateOptions, ) diff --git a/app/components/settings/popups/__init__.py b/app/components/settings/popups/__init__.py index 74249be..a095c27 100644 --- a/app/components/settings/popups/__init__.py +++ b/app/components/settings/popups/__init__.py @@ -22,5 +22,4 @@ from .model import ModelOptions from .preview import PreviewOptions from .shortcut import ShortcutOptions -from .tesseract import TesseractOptions from .translate import TranslateOptions diff --git a/app/components/settings/popups/base.py b/app/components/settings/popups/base.py index 6e06c88..beb99b2 100644 --- a/app/components/settings/popups/base.py +++ b/app/components/settings/popups/base.py @@ -80,7 +80,9 @@ def initializeProperties(self, props: list[tuple[str, Any, Callable]]): self.addProperty(prop, propDefault, propType) # Label - self.labelList[i].setText(f"{titlecase(prop)}: ") + label = self.labelList[i] + label.setText(f"{titlecase(prop)}: ") + self.setProperty(f"{prop}Label", label) # Combo Box comboBox = self.comboBoxList[i] diff --git a/app/components/settings/popups/model.py b/app/components/settings/popups/model.py index 7d43227..534a99a 100644 --- a/app/components/settings/popups/model.py +++ b/app/components/settings/popups/model.py @@ -19,28 +19,79 @@ from PyQt5.QtWidgets import QWidget -from utils.constants import OCR_MODEL, TOGGLE_CHOICES +from utils.constants import LANGUAGE, OCR_MODEL, ORIENTATION, TOGGLE_CHOICES from .base import BaseOptions class ModelOptions(BaseOptions): def __init__(self, parent: QWidget): - super().__init__(parent, [OCR_MODEL, TOGGLE_CHOICES]) + super().__init__(parent, [OCR_MODEL, TOGGLE_CHOICES, LANGUAGE, ORIENTATION]) # TODO: Use constants here self.initializeProperties( - [("ocrModel", "MangaOCR", str), ("useOcrOffline", "false", bool)] + [ + ("ocrModel", "MangaOCR", str), + ("useOcrOffline", "false", bool), + ("language", "jpn", str), + ("orientation", "_vert", str), + ] ) + self.updateDisplay() + + def updateDisplay(self): + ocrModelName = self.ocrModelComboBox.currentText().strip() + if ocrModelName == "MangaOCR": + self.languageLabel.hide() + self.languageComboBox.hide() + self.orientationLabel.hide() + self.orientationComboBox.hide() + elif ocrModelName == "Tesseract": + self.languageLabel.show() + self.languageComboBox.show() + self.orientationLabel.show() + self.orientationComboBox.show() + else: + self.languageLabel.show() + self.languageComboBox.show() + self.orientationLabel.hide() + self.orientationComboBox.hide() def changeOcrModel(self, i): self.ocrModelIndex = i + try: + self.updateDisplay() + # Handle case where extra widgets are still undefined + except AttributeError as e: + print(e) def changeUseOcrOffline(self, i): self.useOcrOfflineIndex = i self.useOcrOffline = True if i else False + def changeLanguage(self, i): + self.languageIndex = i + selectedLanguage = self.languageComboBox.currentText().strip() + if selectedLanguage == "Japanese": + self.language = "jpn" + if selectedLanguage == "Korean": + self.language = "kor" + if selectedLanguage == "Chinese SIM": + self.language = "chi_sim" + if selectedLanguage == "Chinese TRA": + self.language = "chi_tra" + if selectedLanguage == "English": + self.language = "eng" + + def changeOrientation(self, i): + self.orientationIndex = i + selectedOrientation = self.orientationComboBox.currentText().strip() + if selectedOrientation == "Vertical": + self.orientation = "_vert" + if selectedOrientation == "Horizontal": + self.orientation = "" + def saveSettings(self, hasMessage=True): - ocrModelName = self.ocrModelComboBox.currentText() + ocrModelName = self.ocrModelComboBox.currentText().strip() self.mainWindow.state.setOCRModelName(ocrModelName) self.mainWindow.setProperty( "useOcrOffline", "true" if self.useOcrOffline else "useOcrOffline" diff --git a/app/components/settings/popups/tesseract.py b/app/components/settings/popups/tesseract.py deleted file mode 100644 index ff2ff94..0000000 --- a/app/components/settings/popups/tesseract.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -Poricom Popups - -Copyright (C) `2021-2022` `` - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -from PyQt5.QtWidgets import QWidget - -from utils.constants import LANGUAGE, ORIENTATION - -from .base import BaseOptions - - -class TesseractOptions(BaseOptions): - def __init__(self, parent: QWidget): - super().__init__(parent, [LANGUAGE, ORIENTATION]) - # TODO: Use constants here - self.initializeProperties( - [("language", "jpn", str), ("orientation", "_vert", str)] - ) - - def changeLanguage(self, i): - self.languageIndex = i - selectedLanguage = self.languageComboBox.currentText().strip() - if selectedLanguage == "Japanese": - self.language = "jpn" - if selectedLanguage == "Korean": - self.language = "kor" - if selectedLanguage == "Chinese SIM": - self.language = "chi_sim" - if selectedLanguage == "Chinese TRA": - self.language = "chi_tra" - if selectedLanguage == "English": - self.language = "eng" - - def changeOrientation(self, i): - self.orientationIndex = i - selectedOrientation = self.orientationComboBox.currentText().strip() - if selectedOrientation == "Vertical": - self.orientation = "_vert" - if selectedOrientation == "Horizontal": - self.orientation = "" diff --git a/app/components/windows/base.py b/app/components/windows/base.py index 39b8102..6187dbe 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -39,7 +39,6 @@ OptionsContainer, PreviewOptions, ShortcutOptions, - TesseractOptions, TranslateOptions, ) from components.toolbar import BaseToolbar @@ -238,12 +237,6 @@ def loadModelConfirm(message: str): self.threadpool.start(worker) loadModelButton.setEnabled(False) - def modifyTesseract(self): - confirmation = OptionsContainer(TesseractOptions(self)) - confirmed = confirmation.exec() - if confirmed: - self.canvas.loadSettings() - def toggleLogging(self): self.logToFile = not self.logToFile self.canvas.loadSettings() diff --git a/app/utils/constants.py b/app/utils/constants.py index b7abe1d..b548fca 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -40,8 +40,8 @@ # Settings Popup Choices TOGGLE_CHOICES = [" Disabled", " Enabled"] -OCR_MODEL = ["MangaOCR", "Tesseract"] -TRANSLATE_MODEL = ["ArgosTranslate", "ChatGPT", "DeepL"] +OCR_MODEL = [" MangaOCR", " Tesseract"] +TRANSLATE_MODEL = [" ArgosTranslate", " ChatGPT", " DeepL"] LANGUAGE = [" Japanese", " Korean", " Chinese SIM", " Chinese TRA ", " English"] ORIENTATION = [" Vertical", " Horizontal"] @@ -287,15 +287,6 @@ "iconHeight": 1.0, "iconWidth": 1.0, }, - "modifyTesseract": { - "title": "Tesseract settings", - "message": "Set the language and orientation for the Tesseract model.", - "path": "modifyTesseract.png", - "toggle": False, - "align": "AlignLeft", - "iconHeight": 1.0, - "iconWidth": 1.0, - }, "toggleLogging": { "title": "Enable text logging", "message": "Save detected text to a text file located in the current project directory.", From 6ccddc1c6511f3f14025da78b0d4c20fe62eb14b Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 21 May 2023 16:41:47 +0800 Subject: [PATCH 127/137] chore: rename files and variables --- .../{modifyTesseract.png => loadTranslateModel.png} | Bin app/components/settings/popups/translate.py | 3 +-- app/utils/constants.py | 2 +- environment/base.yaml | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) rename app/assets/images/icons/{modifyTesseract.png => loadTranslateModel.png} (100%) diff --git a/app/assets/images/icons/modifyTesseract.png b/app/assets/images/icons/loadTranslateModel.png similarity index 100% rename from app/assets/images/icons/modifyTesseract.png rename to app/assets/images/icons/loadTranslateModel.png diff --git a/app/components/settings/popups/translate.py b/app/components/settings/popups/translate.py index cbbd55f..8a22a28 100644 --- a/app/components/settings/popups/translate.py +++ b/app/components/settings/popups/translate.py @@ -60,7 +60,6 @@ def changeEnableTranslate(self, i): def changeTranslateModel(self, i): self.translateModelIndex = i - self.translateModel = i try: self.updateDisplay() # Handle case where extra widgets are still undefined @@ -68,7 +67,7 @@ def changeTranslateModel(self, i): print(e) def saveSettings(self, hasMessage=True): - translateModelName = self.translateModelComboBox.currentText() + translateModelName = self.translateModelComboBox.currentText().strip() self.mainWindow.state.setTranslateModelName(translateModelName) self.mainWindow.setProperty( "enableTranslate", "true" if self.enableTranslate else "enableTranslate" diff --git a/app/utils/constants.py b/app/utils/constants.py index b548fca..c03fb0e 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -281,7 +281,7 @@ "loadTranslateModel": { "title": "Load translation model", "message": "Manage translation model settings and API keys.", - "path": "modifyTesseract.png", + "path": "loadTranslateModel.png", "toggle": False, "align": "AlignLeft", "iconHeight": 1.0, diff --git a/environment/base.yaml b/environment/base.yaml index a663e92..22e63ef 100644 --- a/environment/base.yaml +++ b/environment/base.yaml @@ -19,4 +19,4 @@ dependencies: - pyqt5 - pyqtkeybind - rarfile - - stringcase + - stringcase From c1c7653d53b701c4c54feadd766ad8a5bab919d3 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 27 May 2023 23:10:37 +0800 Subject: [PATCH 128/137] fix: prevent popup when model is disabled --- app/components/windows/base.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/app/components/windows/base.py b/app/components/windows/base.py index 6187dbe..0db5f31 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -69,7 +69,7 @@ def __init__(self, parent: QWidget = None): mainWidget.setLayout(self.vLayout) self.setCentralWidget(mainWidget) - self.setDefaults(MAIN_WINDOW_DEFAULTS) + self.setDefaults({**MAIN_WINDOW_DEFAULTS, "enableTranslate": "false"}) self.setTypes(MAIN_WINDOW_TYPES) self.loadSettings() @@ -93,20 +93,6 @@ def closeEvent(self, event): self.canvas.close() return super().closeEvent(event) - def changeEvent(self, event): - if ( - not self.isActiveWindow() - and not self.canvas.translationDialog.isActiveWindow() - ): - self.canvas.translationDialog.setWindowFlags( - self.windowFlags() & ~Qt.WindowStaysOnTopHint - ) - else: - self.canvas.translationDialog.setWindowFlags( - self.windowFlags() | Qt.WindowStaysOnTopHint - ) - return super().changeEvent(event) - def noop(self): BasePopup("Not Implemented", "This function is not yet implemented.").exec() @@ -178,6 +164,9 @@ def loadModelAfterPopup(self): loadModelButton = self.toolbar.findChild(QPushButton, "loadModel") isMangaOCR = self.state.ocrModelName == "MangaOCR" + if not isMangaOCR: + return + if isMangaOCR and self.hasLoadModelPopup: ret = CheckboxPopup( "hasLoadModelPopup", @@ -215,10 +204,13 @@ def loadTranslateModel(self): confirmed = confirmation.exec() if confirmed: self.loadSettings({"enableTranslate": "false"}) + self.canvas.loadSettings({"enableTranslate": "false"}) self.loadTranslateAfterPopup() def loadTranslateAfterPopup(self): loadModelButton = self.toolbar.findChild(QPushButton, "loadTranslateModel") + if not self.enableTranslate: + return def loadModelConfirm(message: str): modelName = self.state.translateModelName From c345ed6e7922ea832c97a6fa7b5e0f9f1ee0f313 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 27 May 2023 23:11:48 +0800 Subject: [PATCH 129/137] feat: load translate model on app start --- app/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/main.py b/app/main.py index f62fb78..60fbc97 100644 --- a/app/main.py +++ b/app/main.py @@ -49,6 +49,7 @@ widget.showMaximized() widget.loadModelAfterPopup() + widget.loadTranslateAfterPopup() app.exec_() # keybinder.unregister_hotkey(widget.winId(), shortcut) From feaf29fd647987f2627ad1cb8b84e7fe3f09b8ee Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 27 May 2023 23:13:50 +0800 Subject: [PATCH 130/137] feat: add methods for ChatGPT and DeepL --- app/services/states.py | 50 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/app/services/states.py b/app/services/states.py index 5610d2a..a8bf3ac 100644 --- a/app/services/states.py +++ b/app/services/states.py @@ -18,6 +18,7 @@ """ from os.path import isfile, exists +from requests import post from typing import Literal from argostranslate.package import ( @@ -30,7 +31,7 @@ from PyQt5.QtCore import QSettings from PyQt5.QtGui import QPixmap -from utils.constants import SETTINGS_FILE_DEFAULT +from utils.constants import SETTINGS_FILE_DEFAULT, TRANSLATE_MODEL from utils.scripts import combineTwoImages @@ -72,9 +73,12 @@ def __init__(self): self._ocrModel = None self._ocrModelName: OCRModelNames = ocrModelName - translateModelName = settings.value("translateModel", "ArgosTranslate") + translateModelIndex = settings.value("translateModelIndex", 0) + translateModelName = TRANSLATE_MODEL[int(translateModelIndex)].strip() + translateApiKey = settings.value("translateApiKey", 0) self._translateModel = None self._translateModelName: TranslateModelNames = translateModelName + self._translateApiKey = translateApiKey # ------------------------------------ Image ------------------------------------ # @@ -157,6 +161,14 @@ def setTranslateModelName(self, translateModelName: TranslateModelNames = None): self._translateModelName = translateModelName return self._translateModelName + @property + def translateApiKey(self): + return self._translateApiKey + + def setTranslateApiKey(self, translateApiKey): + self._translateApiKey = translateApiKey + return self._translateApiKey + def downloadArgosTranslateModel(self, fromCode="ja", toCode="en"): update_package_index() availablePackages = get_available_packages() @@ -200,6 +212,36 @@ def predictTranslate(self, text): if self.translateModelName == "ArgosTranslate": return self.translateModel.translate(text) elif self.translateModelName == "ChatGPT": - pass + headers = { + "content-type": "application/json", + "authorization": f"Bearer {self.translateApiKey}", + } + body = { + "model": "text-davinci-003", + "prompt": f"Translate this to English:\n{text}", + "temperature": 0.3, + "max_tokens": 128, + } + response = post( + "https://api.openai.com/v1/completions", json=body, headers=headers + ).json() + try: + return response["choices"][0]["text"].strip() + except Exception as e: + return text elif self.translateModelName == "DeepL": - pass + headers = { + "content-type": "application/json", + "authorization": f"DeepL-Auth-Key {self.translateApiKey}", + } + body = { + "text": text, + "target_lang": "EN", + } + response = post( + "https://api-free.deepl.com/v2/translate", json=body, headers=headers + ).json() + try: + return response["translations"]["text"].strip() + except Exception as e: + return text From 8d33d128dbda3f3cd38dc1d3c64f1b91d5c43ac0 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 27 May 2023 23:16:11 +0800 Subject: [PATCH 131/137] refactor: move translation logic to BaseOCRView --- app/components/popups/translationDialog.py | 20 +++++---------- app/components/settings/popups/translate.py | 6 +++-- app/components/views/ocr/base.py | 12 ++++++++- app/components/views/ocr/ocr.py | 27 --------------------- 4 files changed, 21 insertions(+), 44 deletions(-) diff --git a/app/components/popups/translationDialog.py b/app/components/popups/translationDialog.py index 6d00d6b..04c3acc 100644 --- a/app/components/popups/translationDialog.py +++ b/app/components/popups/translationDialog.py @@ -2,12 +2,10 @@ from PyQt5.QtWidgets import QLabel, QTextEdit, QVBoxLayout, QDialog from PyQt5.QtCore import Qt -from services import State - class TranslationDialog(QDialog): - def __init__(self, parent=None, state: State = None): - super().__init__(parent, flags=Qt.WindowStaysOnTopHint) + def __init__(self, parent=None): + super().__init__(parent) self.setAttribute(Qt.WA_DeleteOnClose | Qt.WA_X11NetWmWindowTypeUtility) self.setLayout(QVBoxLayout()) @@ -22,22 +20,16 @@ def __init__(self, parent=None, state: State = None): self.layout().addWidget(self.translateLineEdit) self.resize(500, 200) - self.text = "" self.katakanaToRomaji = cutlet.Cutlet() - self.state = state - def setText(self, text): - self.text = text + def setSourceText(self, text: str): self.ocrLineEdit.setText(text) try: romajiText = self.katakanaToRomaji.romaji(text) except Exception as e: print(e) romajiText = "" - try: - argosText = self.state.predictTranslate(text) - except Exception as e: - print(e) - argosText = "" self.romajiLineEdit.setText(romajiText) - self.translateLineEdit.setText(argosText) + + def setTranslateText(self, text: str): + self.translateLineEdit.setText(text) diff --git a/app/components/settings/popups/translate.py b/app/components/settings/popups/translate.py index 8a22a28..c903a99 100644 --- a/app/components/settings/popups/translate.py +++ b/app/components/settings/popups/translate.py @@ -37,13 +37,13 @@ def __init__(self, parent: QWidget): i = len(self.comboBoxList) self.apiLabel = QLabel("API Key") - self.apiLineEdit = QLineEdit("", self) + self.apiLineEdit = QLineEdit(self.mainWindow.state.translateModelName, self) self.layout().addWidget(self.apiLabel, i, 0) self.layout().addWidget(self.apiLineEdit, i, 1) self.updateDisplay() def updateDisplay(self): - translateModelName = self.translateModelComboBox.currentText() + translateModelName = self.translateModelComboBox.currentText().strip() if translateModelName == "ArgosTranslate": self.apiLabel.hide() self.apiLineEdit.hide() @@ -68,7 +68,9 @@ def changeTranslateModel(self, i): def saveSettings(self, hasMessage=True): translateModelName = self.translateModelComboBox.currentText().strip() + translateApiKey = self.apiLineEdit.text().strip() self.mainWindow.state.setTranslateModelName(translateModelName) + self.mainWindow.state.setTranslateApiKey(translateApiKey) self.mainWindow.setProperty( "enableTranslate", "true" if self.enableTranslate else "enableTranslate" ) diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index c2dc6e6..2845616 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -22,7 +22,7 @@ from PyQt5.QtCore import pyqtSlot, Qt, QThreadPool, QTimer from PyQt5.QtWidgets import QGraphicsView, QLabel, QMainWindow -from components.popups import BasePopup +from components.popups import BasePopup, TranslationDialog from components.settings import BaseSettings from services import BaseWorker, State from utils.constants import ( @@ -56,9 +56,12 @@ def __init__(self, parent: QMainWindow, state: State = None): self.setDragMode(QGraphicsView.RubberBandDrag) + self.translationDialog = TranslationDialog() + self.addDefaults({**TESSERACT_DEFAULTS, **TEXT_LOGGING_DEFAULTS}) self.addTypes(TEXT_LOGGING_TYPES) self.addProperty("persistText", "true", bool) + self.addProperty("enableTranslate", "false", bool) def handleTextResult(self, result): if result == None and self.state.ocrModelName == "Tesseract": @@ -115,6 +118,13 @@ def mouseReleaseEvent(self, event): if self.logToFile: logText(text, path=logPath) + if self.enableTranslate: + self.translationDialog.setSourceText(text) + worker = BaseWorker(self.state.predictTranslate, text) + worker.signals.result.connect(self.translationDialog.setTranslateText) + worker.signals.finished.connect(self.translationDialog.show) + QThreadPool.globalInstance().start(worker) + try: if not self.persistText: self.canvasText.hide() diff --git a/app/components/views/ocr/ocr.py b/app/components/views/ocr/ocr.py index 1981c79..e1f8e44 100644 --- a/app/components/views/ocr/ocr.py +++ b/app/components/views/ocr/ocr.py @@ -17,46 +17,19 @@ along with this program. If not, see . """ -from os.path import join - from PyQt5.QtCore import pyqtSlot from PyQt5.QtWidgets import QMainWindow from ..image import BaseImageView from .base import BaseOCRView from services import State -from components.popups import TranslationDialog -from utils.scripts import logText class OCRView(BaseImageView, BaseOCRView): def __init__(self, parent: QMainWindow, state: State = None): super().__init__(parent, state) self.loadSettings() - self.translationDialog = TranslationDialog(state=state) @pyqtSlot() def rubberBandStopped(self): super().rubberBandStopped() - - def handleTextResult(self, result): - try: - self.canvasText.hide() - self.translationDialog.setText(result) - self.translationDialog.show() - except RuntimeError: - pass - - def mouseReleaseEvent(self, event): - logPath = join(self.explorerPath, "text-log.txt") - text = self.translationDialog.text - logText(text, isLogFile=self.logToFile, path=logPath) - try: - if not self.persistText: - self.canvasText.hide() - except AttributeError: - pass - super(BaseOCRView, self).mouseReleaseEvent(event) - - def closeEvent(self, event): - self.translationDialog.close() From 485f9e98cac5b3d003ef0ff40eaa7823a3869080 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 28 May 2023 19:51:17 +0800 Subject: [PATCH 132/137] refactor: move translate widget to workspace --- app/components/popups/__init__.py | 1 - app/components/views/ocr/base.py | 23 +++++++++++-------- app/components/views/ocr/ocr.py | 3 +++ .../translate.py} | 10 ++++---- app/components/views/workspace.py | 8 +++++-- app/components/windows/base.py | 10 ++++---- app/utils/constants.py | 7 ++++-- 7 files changed, 38 insertions(+), 24 deletions(-) rename app/components/{popups/translationDialog.py => views/translate.py} (80%) diff --git a/app/components/popups/__init__.py b/app/components/popups/__init__.py index b87b2fa..1563758 100644 --- a/app/components/popups/__init__.py +++ b/app/components/popups/__init__.py @@ -19,4 +19,3 @@ from .base import BasePopup from .checkbox import CheckboxPopup -from .translationDialog import TranslationDialog diff --git a/app/components/views/ocr/base.py b/app/components/views/ocr/base.py index 2845616..e13441d 100644 --- a/app/components/views/ocr/base.py +++ b/app/components/views/ocr/base.py @@ -22,13 +22,15 @@ from PyQt5.QtCore import pyqtSlot, Qt, QThreadPool, QTimer from PyQt5.QtWidgets import QGraphicsView, QLabel, QMainWindow -from components.popups import BasePopup, TranslationDialog +from components.popups import BasePopup from components.settings import BaseSettings from services import BaseWorker, State from utils.constants import ( TESSERACT_DEFAULTS, TEXT_LOGGING_DEFAULTS, TEXT_LOGGING_TYPES, + TRANSLATE_DEFAULTS, + TRANSLATE_TYPES, ) from utils.scripts import copyToClipboard, logText, pixmapToText @@ -56,12 +58,15 @@ def __init__(self, parent: QMainWindow, state: State = None): self.setDragMode(QGraphicsView.RubberBandDrag) - self.translationDialog = TranslationDialog() - - self.addDefaults({**TESSERACT_DEFAULTS, **TEXT_LOGGING_DEFAULTS}) - self.addTypes(TEXT_LOGGING_TYPES) + self.addDefaults( + { + **TESSERACT_DEFAULTS, + **TEXT_LOGGING_DEFAULTS, + **TRANSLATE_DEFAULTS, + } + ) + self.addTypes({**TEXT_LOGGING_TYPES, **TRANSLATE_TYPES}) self.addProperty("persistText", "true", bool) - self.addProperty("enableTranslate", "false", bool) def handleTextResult(self, result): if result == None and self.state.ocrModelName == "Tesseract": @@ -119,10 +124,10 @@ def mouseReleaseEvent(self, event): logText(text, path=logPath) if self.enableTranslate: - self.translationDialog.setSourceText(text) + self.translateWidget.setSourceText(text) worker = BaseWorker(self.state.predictTranslate, text) - worker.signals.result.connect(self.translationDialog.setTranslateText) - worker.signals.finished.connect(self.translationDialog.show) + worker.signals.result.connect(self.translateWidget.setTranslateText) + worker.signals.finished.connect(self.translateWidget.show) QThreadPool.globalInstance().start(worker) try: diff --git a/app/components/views/ocr/ocr.py b/app/components/views/ocr/ocr.py index e1f8e44..98fb47f 100644 --- a/app/components/views/ocr/ocr.py +++ b/app/components/views/ocr/ocr.py @@ -28,6 +28,9 @@ class OCRView(BaseImageView, BaseOCRView): def __init__(self, parent: QMainWindow, state: State = None): super().__init__(parent, state) + + self.translateWidget = parent.translateView + self.loadSettings() @pyqtSlot() diff --git a/app/components/popups/translationDialog.py b/app/components/views/translate.py similarity index 80% rename from app/components/popups/translationDialog.py rename to app/components/views/translate.py index 04c3acc..c5cab49 100644 --- a/app/components/popups/translationDialog.py +++ b/app/components/views/translate.py @@ -1,24 +1,22 @@ import cutlet -from PyQt5.QtWidgets import QLabel, QTextEdit, QVBoxLayout, QDialog -from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QLabel, QTextEdit, QVBoxLayout, QWidget -class TranslationDialog(QDialog): +class TranslateView(QWidget): def __init__(self, parent=None): super().__init__(parent) - self.setAttribute(Qt.WA_DeleteOnClose | Qt.WA_X11NetWmWindowTypeUtility) - self.setLayout(QVBoxLayout()) self.ocrLineEdit = QTextEdit("") self.romajiLineEdit = QTextEdit("") self.translateLineEdit = QTextEdit("") + + self.setLayout(QVBoxLayout()) self.layout().addWidget(QLabel("Detected Text")) self.layout().addWidget(self.ocrLineEdit) self.layout().addWidget(QLabel("Romaji")) self.layout().addWidget(self.romajiLineEdit) self.layout().addWidget(QLabel("Translation")) self.layout().addWidget(self.translateLineEdit) - self.resize(500, 200) self.katakanaToRomaji = cutlet.Cutlet() diff --git a/app/components/views/workspace.py b/app/components/views/workspace.py index 9cf3aa5..e886faa 100644 --- a/app/components/views/workspace.py +++ b/app/components/views/workspace.py @@ -21,6 +21,7 @@ from PyQt5.QtWidgets import QInputDialog, QMainWindow, QSplitter from .ocr import OCRView +from .translate import TranslateView from components.explorers import ImageExplorer from components.popups import BasePopup from components.settings import BaseSettings, ImageScalingOptions, OptionsContainer @@ -42,17 +43,20 @@ def __init__(self, parent: QMainWindow, state: State): self.setDefaults(MAIN_VIEW_DEFAULTS) self.loadSettings() + self.translateView = TranslateView(self) self.canvas = OCRView(self, self.state) self.explorer = ImageExplorer(self, self.explorerPath) self.addWidget(self.explorer) self.addWidget(self.canvas) + self.addWidget(self.translateView) self.setChildrenCollapsible(False) for i, s in enumerate(MAIN_VIEW_RATIO): self.setStretchFactor(i, s) def resizeEvent(self, event): - self.explorer.setMinimumWidth(0.1 * self.width()) - self.canvas.setMinimumWidth(0.7 * self.width()) + self.explorer.setMinimumWidth(0.15 * self.width()) + self.canvas.setMinimumWidth(0.6 * self.width()) + self.translateView.setMinimumWidth(0.2 * self.width()) return super().resizeEvent(event) # ---------------------------------- Explorer ----------------------------------- # diff --git a/app/components/windows/base.py b/app/components/windows/base.py index 0db5f31..ea75ae6 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -50,6 +50,8 @@ MAIN_WINDOW_TYPES, STYLESHEET_DARK, STYLESHEET_LIGHT, + TRANSLATE_DEFAULTS, + TRANSLATE_TYPES, ) @@ -69,8 +71,8 @@ def __init__(self, parent: QWidget = None): mainWidget.setLayout(self.vLayout) self.setCentralWidget(mainWidget) - self.setDefaults({**MAIN_WINDOW_DEFAULTS, "enableTranslate": "false"}) - self.setTypes(MAIN_WINDOW_TYPES) + self.setDefaults({**MAIN_WINDOW_DEFAULTS, **TRANSLATE_DEFAULTS}) + self.setTypes({**MAIN_WINDOW_TYPES, **TRANSLATE_TYPES}) self.loadSettings() self.threadpool = QThreadPool() @@ -203,8 +205,8 @@ def loadTranslateModel(self): confirmation = OptionsContainer(TranslateOptions(self)) confirmed = confirmation.exec() if confirmed: - self.loadSettings({"enableTranslate": "false"}) - self.canvas.loadSettings({"enableTranslate": "false"}) + self.loadSettings(TRANSLATE_DEFAULTS) + self.canvas.loadSettings(TRANSLATE_DEFAULTS) self.loadTranslateAfterPopup() def loadTranslateAfterPopup(self): diff --git a/app/utils/constants.py b/app/utils/constants.py index c03fb0e..49370ae 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -115,11 +115,14 @@ TEXT_LOGGING_DEFAULTS = {"explorerPath": EXPLORER_ROOT_DEFAULT, "logToFile": "false"} TEXT_LOGGING_TYPES = {"logToFile": bool} +# Translate +TRANSLATE_DEFAULTS = {"enableTranslate": "false"} +TRANSLATE_TYPES = {"enableTranslate": bool} # --------------------------------------- UI ---------------------------------------- # # Main view -MAIN_VIEW_RATIO = [1, 9] +MAIN_VIEW_RATIO = [3, 19, 4] # Toolbar TOOLBAR_ICON_SIZE = 0.05 # Fraction of primary screen height @@ -271,7 +274,7 @@ "misc": { "loadModel": { "title": "Load detection model", - "message": "Manage translation model settings.", + "message": "Manage OCR model settings.", "path": "loadModel.png", "toggle": False, "align": "AlignLeft", From a46b0cf88bf53c26543f4a09f8fd38bbcf569034 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 28 May 2023 19:52:27 +0800 Subject: [PATCH 133/137] feat: allow translation in external capture --- app/components/views/ocr/fullscreen.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/components/views/ocr/fullscreen.py b/app/components/views/ocr/fullscreen.py index 89e39a5..70bcac9 100644 --- a/app/components/views/ocr/fullscreen.py +++ b/app/components/views/ocr/fullscreen.py @@ -23,7 +23,11 @@ from .base import BaseOCRView from services import State -from utils.constants import TESSERACT_DEFAULTS, TEXT_LOGGING_DEFAULTS +from utils.constants import ( + TESSERACT_DEFAULTS, + TEXT_LOGGING_DEFAULTS, + TRANSLATE_DEFAULTS, +) class FullScreenOCRView(BaseOCRView): @@ -38,8 +42,16 @@ def __init__(self, parent: QMainWindow, state: State = None): self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.translateWidget = parent.mainWindow.mainView.translateView + self.setScene(QGraphicsScene()) - self.loadSettings({**TESSERACT_DEFAULTS, **TEXT_LOGGING_DEFAULTS}) + self.loadSettings( + { + **TESSERACT_DEFAULTS, + **TEXT_LOGGING_DEFAULTS, + **TRANSLATE_DEFAULTS, + } + ) def takeScreenshot(self, screenIndex: int): screen = QApplication.screens()[screenIndex] From 3cb7dd8be1ebf1c95879317d9bd378ae0adb456b Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 28 May 2023 19:54:09 +0800 Subject: [PATCH 134/137] feat: hide translate widget when disabled --- app/components/windows/base.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/app/components/windows/base.py b/app/components/windows/base.py index ea75ae6..a5ae8e3 100644 --- a/app/components/windows/base.py +++ b/app/components/windows/base.py @@ -212,20 +212,10 @@ def loadTranslateModel(self): def loadTranslateAfterPopup(self): loadModelButton = self.toolbar.findChild(QPushButton, "loadTranslateModel") if not self.enableTranslate: + self.mainView.translateView.hide() return - def loadModelConfirm(message: str): - modelName = self.state.translateModelName - if message == "success": - BasePopup( - f"{modelName} model loaded", - f"You are now using the {modelName} model for Japanese text translation.", - ).exec() - else: - BasePopup("Load Model Error", message).exec() - worker = BaseWorker(self.state.loadTranslateModel) - worker.signals.result.connect(loadModelConfirm) worker.signals.finished.connect(lambda: loadModelButton.setEnabled(True)) self.threadpool.start(worker) From 8b34a064849f8422655025003394ef023f189e05 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 28 May 2023 20:19:17 +0800 Subject: [PATCH 135/137] refactor: prioritize loading translate model --- app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 60fbc97..0c865fd 100644 --- a/app/main.py +++ b/app/main.py @@ -48,8 +48,8 @@ eventDispatcher.installNativeEventFilter(winEventFilter) widget.showMaximized() - widget.loadModelAfterPopup() widget.loadTranslateAfterPopup() + widget.loadModelAfterPopup() app.exec_() # keybinder.unregister_hotkey(widget.winId(), shortcut) From 359d0406564ab07c66bbfd023d82415b81648528 Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sun, 28 May 2023 20:44:26 +0800 Subject: [PATCH 136/137] fix: set API key to correct parameter --- app/components/settings/popups/translate.py | 2 +- app/services/states.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/settings/popups/translate.py b/app/components/settings/popups/translate.py index c903a99..ebc39b0 100644 --- a/app/components/settings/popups/translate.py +++ b/app/components/settings/popups/translate.py @@ -37,7 +37,7 @@ def __init__(self, parent: QWidget): i = len(self.comboBoxList) self.apiLabel = QLabel("API Key") - self.apiLineEdit = QLineEdit(self.mainWindow.state.translateModelName, self) + self.apiLineEdit = QLineEdit(self.mainWindow.state.translateApiKey, self) self.layout().addWidget(self.apiLabel, i, 0) self.layout().addWidget(self.apiLineEdit, i, 1) self.updateDisplay() diff --git a/app/services/states.py b/app/services/states.py index a8bf3ac..51d6ea8 100644 --- a/app/services/states.py +++ b/app/services/states.py @@ -75,7 +75,7 @@ def __init__(self): translateModelIndex = settings.value("translateModelIndex", 0) translateModelName = TRANSLATE_MODEL[int(translateModelIndex)].strip() - translateApiKey = settings.value("translateApiKey", 0) + translateApiKey = settings.value("translateApiKey", "") self._translateModel = None self._translateModelName: TranslateModelNames = translateModelName self._translateApiKey = translateApiKey From c66051c053cd1fc2f74ec0473d8e9cb8df9c40ed Mon Sep 17 00:00:00 2001 From: Alarcon Ace Belen Date: Sat, 3 Jun 2023 19:27:15 +0800 Subject: [PATCH 137/137] fix: handle invalid/empty responses --- app/services/states.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/services/states.py b/app/services/states.py index 51d6ea8..9b81138 100644 --- a/app/services/states.py +++ b/app/services/states.py @@ -222,12 +222,13 @@ def predictTranslate(self, text): "temperature": 0.3, "max_tokens": 128, } - response = post( - "https://api.openai.com/v1/completions", json=body, headers=headers - ).json() try: + response = post( + "https://api.openai.com/v1/completions", json=body, headers=headers + ).json() return response["choices"][0]["text"].strip() except Exception as e: + print(e) return text elif self.translateModelName == "DeepL": headers = { @@ -238,10 +239,11 @@ def predictTranslate(self, text): "text": text, "target_lang": "EN", } - response = post( - "https://api-free.deepl.com/v2/translate", json=body, headers=headers - ).json() try: + response = post( + "https://api-free.deepl.com/v2/translate", json=body, headers=headers + ).json() return response["translations"]["text"].strip() except Exception as e: + print(e) return text