Compare commits

..

2 Commits

Author SHA1 Message Date
9864159407 Start removal of QMessageBox static methods
Some checks failed
CI / lint (ubuntu-latest, 3.9) (push) Has been cancelled
CI / build-and-test (macos-13, 3.13) (push) Has been cancelled
CI / build-and-test (macos-13, 3.9) (push) Has been cancelled
CI / build-and-test (macos-14, 3.13) (push) Has been cancelled
CI / build-and-test (macos-14, 3.9) (push) Has been cancelled
CI / build-and-test (ubuntu-22.04, 3.13) (push) Has been cancelled
CI / build-and-test (ubuntu-22.04, 3.9) (push) Has been cancelled
CI / build-and-test (ubuntu-22.04-arm, 3.13) (push) Has been cancelled
CI / build-and-test (ubuntu-22.04-arm, 3.9) (push) Has been cancelled
CI / build-and-test (windows-latest, 3.13) (push) Has been cancelled
CI / build-and-test (windows-latest, 3.9) (push) Has been cancelled
2025-07-20 17:42:10 -07:00
194c381c7f Cleanup more dialogs 2025-07-20 17:29:16 -07:00
19 changed files with 649 additions and 517 deletions

View File

@ -26,7 +26,7 @@ from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.ctsettings import ct_ns
from comictaggerlib.md import prepare_metadata
from comictaggerlib.resulttypes import IssueResult, Result
from comictaggerlib.ui import ui_path
from comictaggerlib.ui import qtutils, ui_path
from comictalker.comictalker import ComicTalker, TalkerError
logger = logging.getLogger(__name__)
@ -209,17 +209,15 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
self.update_data()
def reject(self) -> None:
reply = QtWidgets.QMessageBox.question(
self,
"Cancel Matching",
"Are you sure you wish to cancel the matching process?",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No,
)
if reply == QtWidgets.QMessageBox.StandardButton.No:
return
qmsg = QtWidgets.QMessageBox(self)
qmsg.setIcon(qmsg.Icon.Question)
qmsg.setText("Cancel Matching")
qmsg.setInformativeText("Are you sure you wish to cancel the matching process?")
qmsg.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No)
qmsg.rejected.connect(self._cancel)
qmsg.show()
def _cancel(self) -> None:
QtWidgets.QDialog.reject(self)
def save_match(self) -> None:
@ -230,13 +228,13 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
md, error = self.parent().read_selected_tags(self._tags, ca)
if error is not None:
logger.error("Failed to load tags for %s: %s", ca.path, error)
QtWidgets.QApplication.restoreOverrideCursor()
QtWidgets.QMessageBox.critical(
return qtutils.critical(
self,
"Read Failed!",
f"One or more of the read tags failed to load for {ca.path}, check log for details",
)
return
if md.is_empty:
md = ca.metadata_from_filename(
@ -252,12 +250,11 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
self.current_match_set.md = ct_md = self.talker.fetch_comic_data(issue_id=match.issue_id)
except TalkerError as e:
QtWidgets.QApplication.restoreOverrideCursor()
QtWidgets.QMessageBox.critical(self, f"{e.source} {e.code_name} Error", f"{e}")
qtutils.critical(self, f"{e.source} {e.code_name} Error", str(e))
return
if ct_md is None or ct_md.is_empty:
QtWidgets.QMessageBox.critical(self, "Network Issue", "Could not retrieve issue details!")
return
return qtutils.critical(self, "Network Issue", "Could not retrieve issue details!")
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
md = prepare_metadata(md, ct_md, self.config)
@ -265,7 +262,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
success = ca.write_tags(md, tag_id)
QtWidgets.QApplication.restoreOverrideCursor()
if not success:
QtWidgets.QMessageBox.warning(
qtutils.warning(
self,
"Write Error",
f"Saving {tags[tag_id].name()} the tags to the archive seemed to fail!",

View File

@ -31,13 +31,13 @@ from comictaggerlib.issueidentifier import IssueIdentifierCancelled
from comictaggerlib.md import read_selected_tags
from comictaggerlib.resulttypes import Action, OnlineMatchResults, Result, Status
from comictaggerlib.tag import identify_comic
from comictaggerlib.ui import ui_path
from comictaggerlib.ui import qtutils, ui_path
from comictalker.comictalker import ComicTalker, RLCallBack
logger = logging.getLogger(__name__)
class AutoTagThread(QtCore.QThread):
class AutoTagThread(QtCore.QThread): # TODO: re-check thread semantics. Specifically with signals
autoTagComplete = QtCore.pyqtSignal(OnlineMatchResults, list)
autoTagLogMsg = QtCore.pyqtSignal(str)
autoTagProgress = QtCore.pyqtSignal(object, object, object, bytes, bytes) # see progress_callback
@ -108,7 +108,7 @@ class AutoTagThread(QtCore.QThread):
# read in tags, and parse file name if not there
md, tags_used, error = read_selected_tags(self.config.internal__read_tags, ca)
if error is not None:
QtWidgets.QMessageBox.warning(
qtutils.critical(
None,
"Aborting...",
f"One or more of the read tags failed to load for {ca.path}. Aborting to prevent any possible further damage. Check log for details.",

View File

@ -17,16 +17,28 @@
from __future__ import annotations
import logging
from typing import NamedTuple
from PyQt6 import QtCore, QtWidgets, uic
from comictaggerlib.ctsettings import ct_ns
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib.ctsettings import ct_ns, settngs_namespace
from comictaggerlib.ui import ui_path
logger = logging.getLogger(__name__)
class AutoTagSettings(NamedTuple):
settings: settngs_namespace.Auto_Tag
remove_after_success: bool
series_match_identify_thresh: bool
split_words: bool
search_string: str
class AutoTagStartWindow(QtWidgets.QDialog):
startAutoTag = QtCore.pyqtSignal(AutoTagSettings)
def __init__(self, parent: QtWidgets.QWidget, config: ct_ns, msg: str) -> None:
super().__init__(parent)
@ -77,6 +89,8 @@ class AutoTagStartWindow(QtWidgets.QDialog):
self.search_string = ""
self.name_length_match_tolerance = self.config.Issue_Identifier__series_match_search_thresh
self.split_words = self.cbxSplitWords.isChecked()
self.adjustSize()
self.setModal(True)
def search_string_toggle(self) -> None:
enable = self.cbxSpecifySearchString.isChecked()
@ -85,20 +99,26 @@ class AutoTagStartWindow(QtWidgets.QDialog):
def accept(self) -> None:
QtWidgets.QDialog.accept(self)
self.auto_save_on_low = self.cbxSaveOnLowConfidence.isChecked()
self.dont_use_year = self.cbxDontUseYear.isChecked()
self.assume_issue_one = self.cbxAssumeIssueOne.isChecked()
self.ignore_leading_digits_in_filename = self.cbxIgnoreLeadingDigitsInFilename.isChecked()
self.remove_after_success = self.cbxRemoveAfterSuccess.isChecked()
self.name_length_match_tolerance = self.sbNameMatchSearchThresh.value()
self.split_words = self.cbxSplitWords.isChecked()
# persist some settings
self.config.Auto_Tag__save_on_low_confidence = self.auto_save_on_low
self.config.Auto_Tag__use_year_when_identifying = not self.dont_use_year
self.config.Auto_Tag__assume_issue_one = self.assume_issue_one
self.config.Auto_Tag__ignore_leading_numbers_in_filename = self.ignore_leading_digits_in_filename
self.config.internal__remove_archive_after_successful_match = self.remove_after_success
if self.cbxSpecifySearchString.isChecked():
self.search_string = self.leSearchString.text()
self.startAutoTag.emit(
AutoTagSettings(
settings=settngs_namespace.Auto_Tag(
online=self.config.Auto_Tag__online,
save_on_low_confidence=self.cbxSaveOnLowConfidence.isChecked(),
use_year_when_identifying=not self.cbxDontUseYear.isChecked(),
assume_issue_one=self.cbxAssumeIssueOne.isChecked(),
ignore_leading_numbers_in_filename=self.cbxIgnoreLeadingDigitsInFilename.isChecked(),
parse_filename=self.config.Auto_Tag__parse_filename,
prefer_filename=self.config.Auto_Tag__prefer_filename,
issue_id=None,
metadata=GenericMetadata(),
clear_tags=self.config.Auto_Tag__clear_tags,
publisher_filter=self.config.Auto_Tag__publisher_filter,
use_publisher_filter=self.config.Auto_Tag__use_publisher_filter,
auto_imprint=self.cbxAutoImprint.isChecked(),
),
remove_after_success=self.cbxRemoveAfterSuccess.isChecked(),
series_match_identify_thresh=self.sbNameMatchSearchThresh.value(),
split_words=self.cbxSplitWords.isChecked(),
search_string=self.leSearchString.text(),
)
)

View File

@ -249,12 +249,13 @@ class CoverImageWidget(QtWidgets.QWidget):
self.set_display_pixmap()
def load_page(self) -> None:
if self.comic_archive is not None:
if self.page_loader is not None:
self.page_loader.abandoned = True
self.page_loader = PageLoader(self.comic_archive, self.imageIndex)
self.page_loader.loadComplete.connect(self.page_load_complete)
self.page_loader.start()
if self.comic_archive is None:
return
if self.page_loader is not None:
self.page_loader.abandoned = True
self.page_loader = PageLoader(self.comic_archive, self.imageIndex)
self.page_loader.loadComplete.connect(self.page_load_complete)
self.page_loader.start()
def page_load_complete(self, image_data: bytes) -> None:
img = get_qimage_from_data(image_data)

View File

@ -18,33 +18,51 @@ from __future__ import annotations
import logging
import operator
from enum import Enum, auto
import natsort
from PyQt6 import QtCore, QtWidgets, uic
from comicapi import utils
from comicapi.comicarchive import tags
from comicapi.genericmetadata import Credit
from comictaggerlib.ui import ui_path
from comictaggerlib.ui import qtutils, ui_path
from comictaggerlib.ui.qtutils import enable_widget
logger = logging.getLogger(__name__)
class CreditEditorWindow(QtWidgets.QDialog):
ModeEdit = 0
ModeNew = 1
class EditMode(Enum):
EDIT = auto()
NEW = auto()
def __init__(self, parent: QtWidgets.QWidget, mode: int, credit: Credit) -> None:
class CreditEditorWindow(QtWidgets.QDialog):
creditChanged = QtCore.pyqtSignal(Credit, int, EditMode)
def __init__(self, parent: QtWidgets.QWidget, tags: list[str], row: int, mode: EditMode, credit: Credit) -> None:
super().__init__(parent)
with (ui_path / "crediteditorwindow.ui").open(encoding="utf-8") as uifile:
uic.loadUi(uifile, self)
self.mode = mode
self.md_attributes = {
"credits.person": self.leName,
"credits.language": self.cbLanguage,
"credits.role": self.cbRole,
"credits.primary": self.cbPrimary,
}
if self.mode == self.ModeEdit:
self.mode = mode
self.credit = credit
self.row = row
self.tags = tags
if self.mode == EditMode.EDIT:
self.setWindowTitle("Edit Credit")
else:
self.setWindowTitle("New Credit")
self.setModal(True)
# Add the entries to the role combobox
self.cbRole.addItem("")
@ -86,13 +104,29 @@ class CreditEditorWindow(QtWidgets.QDialog):
self.cbLanguage.setCurrentIndex(i)
self.cbPrimary.setChecked(credit.primary)
self.update_tag_tweaks()
def get_credit(self) -> Credit:
lang = self.cbLanguage.currentData() or self.cbLanguage.currentText()
return Credit(self.leName.text(), self.cbRole.currentText(), self.cbPrimary.isChecked(), lang)
def update_tag_tweaks(self) -> None:
# depending on the current data tag, certain fields are disabled
enabled_widgets = set()
for tag_id in self.tags:
if not tags[tag_id].enabled:
continue
enabled_widgets.update(tags[tag_id].supported_attributes)
for md_field, widget in self.md_attributes.items():
if widget is not None and not isinstance(widget, (int)):
enable_widget(widget, md_field in enabled_widgets)
def accept(self) -> None:
if self.leName.text() == "":
QtWidgets.QMessageBox.warning(self, "Whoops", "You need to enter a name for a credit.")
else:
QtWidgets.QDialog.accept(self)
return qtutils.warning(self, "Whoops", "You need to enter a name for a credit.")
QtWidgets.QDialog.accept(self)
new = self.get_credit()
if self.credit != new:
self.creditChanged.emit(new, self.row, self.mode)

View File

@ -17,6 +17,8 @@
from __future__ import annotations
import logging
from enum import Enum, auto
from typing import NamedTuple
from PyQt6 import QtCore, QtWidgets, uic
@ -25,19 +27,35 @@ from comictaggerlib.ui import ui_path
logger = logging.getLogger(__name__)
class ExportConflictOpts:
dontCreate = 1
overwrite = 2
createUnique = 3
class ExportConflictOpts(Enum):
DONT_CREATE = auto()
OVERWRITE = auto()
CREATE_UNIQUE = auto()
class ExportConfig(NamedTuple):
conflict: ExportConflictOpts
add_to_list: bool
delete_original: bool
class ExportWindow(QtWidgets.QDialog):
def __init__(self, parent: QtWidgets.QWidget, msg: str) -> None:
export = QtCore.pyqtSignal(ExportConfig)
def __init__(self, parent: QtWidgets.QWidget) -> None:
super().__init__(parent)
with (ui_path / "exportwindow.ui").open(encoding="utf-8") as uifile:
uic.loadUi(uifile, self)
self.label.setText(msg)
self.label: QtWidgets.QLabel
self.cbxDeleteOriginal: QtWidgets.QCheckBox
self.cbxAddToList: QtWidgets.QCheckBox
self.radioDontCreate: QtWidgets.QRadioButton
self.radioCreateNew: QtWidgets.QRadioButton
self.msg = """You have selected {count} archive(s) to export to Zip format. New archives will be created in the same folder as the original.
Please choose config below, and select OK.
"""
self.setWindowFlags(
QtCore.Qt.WindowType(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
@ -46,17 +64,20 @@ class ExportWindow(QtWidgets.QDialog):
self.cbxDeleteOriginal.setChecked(False)
self.cbxAddToList.setChecked(True)
self.radioDontCreate.setChecked(True)
self.setModal(True)
self.deleteOriginal = False
self.addToList = True
self.fileConflictBehavior = ExportConflictOpts.dontCreate
def show(self, count: int) -> None:
self.label.setText(self.msg.format(count=count))
self.adjustSize()
QtWidgets.QDialog.show(self)
def accept(self) -> None:
QtWidgets.QDialog.accept(self)
self.deleteOriginal = self.cbxDeleteOriginal.isChecked()
self.addToList = self.cbxAddToList.isChecked()
conflict = ExportConflictOpts.DONT_CREATE
if self.radioDontCreate.isChecked():
self.fileConflictBehavior = ExportConflictOpts.dontCreate
conflict = ExportConflictOpts.DONT_CREATE
elif self.radioCreateNew.isChecked():
self.fileConflictBehavior = ExportConflictOpts.createUnique
conflict = ExportConflictOpts.CREATE_UNIQUE
QtWidgets.QDialog.accept(self)
self.export.emit(ExportConfig(conflict, self.cbxAddToList.isChecked(), self.cbxDeleteOriginal.isChecked()))

View File

@ -30,7 +30,7 @@ from comictaggerlib.ctsettings import ct_ns
from comictaggerlib.graphics import graphics_path
from comictaggerlib.optionalmsgdialog import OptionalMessageDialog
from comictaggerlib.settingswindow import linuxRarHelp, macRarHelp, windowsRarHelp
from comictaggerlib.ui import ui_path
from comictaggerlib.ui import qtutils, ui_path
from comictaggerlib.ui.qtutils import center_window_on_parent
logger = logging.getLogger(__name__)
@ -56,7 +56,7 @@ class FileSelectionList(QtWidgets.QWidget):
uic.loadUi(uifile, self)
self.config = config
self.twList: QtWidgets.QTableWidget
self.twList.horizontalHeader().setMinimumSectionSize(50)
self.twList.currentItemChanged.connect(self.current_item_changed_cb)
@ -108,6 +108,17 @@ class FileSelectionList(QtWidgets.QWidget):
QtWidgets.QTableWidgetSelectionRange(0, 0, self.twList.rowCount() - 1, self.twList.columnCount() - 1), False
)
def remove_deleted(self) -> None:
deleted = []
for row in range(self.twList.rowCount()):
row_ca = self.get_archive_by_row(row)
if not row_ca:
continue
if not row_ca.path.exists():
deleted.append(row_ca)
self.remove_archive_list(deleted)
def remove_archive_list(self, ca_list: list[ComicArchive]) -> None:
self.twList.setSortingEnabled(False)
current_removed = False
@ -219,11 +230,8 @@ class FileSelectionList(QtWidgets.QWidget):
self.twList.selectRow(first_added)
else:
if len(pathlist) == 1 and os.path.isfile(pathlist[0]):
QtWidgets.QMessageBox.information(
self, "File Open", "Selected file doesn't seem to be a comic archive."
)
else:
QtWidgets.QMessageBox.information(self, "File/Folder Open", "No readable comic archives were found.")
return qtutils.information(self, "File Open", "Selected file doesn't seem to be a comic archive.")
return qtutils.information(self, "File/Folder Open", "No readable comic archives were found.")
if rar_added_ro:
self.rar_ro_message()
@ -339,10 +347,15 @@ class FileSelectionList(QtWidgets.QWidget):
ca: ComicArchive = self.twList.item(row, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole)
filename_item = self.twList.item(row, FileSelectionList.fileColNum)
assert filename_item
folder_item = self.twList.item(row, FileSelectionList.folderColNum)
assert folder_item
md_item = self.twList.item(row, FileSelectionList.MDFlagColNum)
assert md_item
type_item = self.twList.item(row, FileSelectionList.typeColNum)
assert type_item
readonly_item = self.twList.item(row, FileSelectionList.readonlyColNum)
assert readonly_item
item_text = os.path.split(ca.path)[1]
filename_item.setText(item_text)
@ -398,6 +411,10 @@ class FileSelectionList(QtWidgets.QWidget):
if old_idx == new_idx:
return
ca = self.get_archive_by_row(new_idx)
if not ca or not ca.path.exists():
self.remove_deleted()
return
# don't allow change if modified
if prev is not None and new_idx != old_idx:

View File

@ -23,15 +23,14 @@ try:
If unavailable (non-console application), log an additional notice.
"""
if QtWidgets.QApplication.instance() is not None:
errorbox = QtWidgets.QMessageBox()
errorbox = QtWidgets.QMessageBox(QtWidgets.QApplication.activeWindow())
errorbox.setStandardButtons(
QtWidgets.QMessageBox.StandardButton.Abort | QtWidgets.QMessageBox.StandardButton.Ignore
)
errorbox.setText(log_msg)
if errorbox.exec() == QtWidgets.QMessageBox.StandardButton.Abort:
QtWidgets.QApplication.exit(1)
else:
logger.warning("Exception ignored")
errorbox.rejected.connect(lambda: QtWidgets.QApplication.exit(1))
errorbox.accepted.connect(lambda: logger.warning("Exception ignored"))
errorbox.show()
else:
logger.debug("No QApplication instance available.")

View File

@ -39,7 +39,7 @@ class IssueNumberTableWidgetItem(QtWidgets.QTableWidgetItem):
return (IssueString(self_str).as_float() or 0) < (IssueString(other_str).as_float() or 0)
class QueryThread(QtCore.QThread):
class QueryThread(QtCore.QThread): # TODO: Evaluate thread semantics. Specifically with signals
def __init__(
self,
talker: ComicTalker,
@ -54,7 +54,6 @@ class QueryThread(QtCore.QThread):
self.on_ratelimit = on_ratelimit
def run(self) -> None:
# QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
try:
issue_list = [
@ -66,11 +65,8 @@ class QueryThread(QtCore.QThread):
]
except TalkerError as e:
logger.exception("Failed to retrieve issue list: %s", e)
# QtWidgets.QApplication.restoreOverrideCursor()
# QtWidgets.QMessageBox.critical(None, f"{e.source} {e.code_name} Error", f"{e}")
return
# QtWidgets.QApplication.restoreOverrideCursor()
self.finish.emit(issue_list)

View File

@ -32,6 +32,8 @@ logger = logging.getLogger(__name__)
class MatchSelectionWindow(QtWidgets.QDialog):
match_selected = QtCore.pyqtSignal(IssueResult)
def __init__(
self,
parent: QtWidgets.QWidget,
@ -70,6 +72,7 @@ class MatchSelectionWindow(QtWidgets.QDialog):
self.twList.currentItemChanged.connect(self.current_item_changed)
self.twList.cellDoubleClicked.connect(self.cell_double_clicked)
self.accepted.connect(self.select)
self.update_data()
@ -160,3 +163,6 @@ class MatchSelectionWindow(QtWidgets.QDialog):
row = self.twList.currentRow()
match: IssueResult = self.twList.item(row, 0).data(QtCore.Qt.ItemDataRole.UserRole)[0]
return match
def selected(self) -> None:
self.match_selected.emit(self.current_match())

View File

@ -1,15 +1,4 @@
"""A PyQt6 dialog to show a message and let the user check a box
Example usage:
checked = OptionalMessageDialog.msg(self, "Disclaimer",
"This is beta software, and you are using it at your own risk!",
)
said_yes, checked = OptionalMessageDialog.question(self, "QtWidgets.Question",
"Are you sure you wish to do this?",
)
"""
"""A PyQt6 dialog to show a message and let the user check a box"""
#
# Copyright 2012-2014 ComicTagger Authors
@ -28,6 +17,7 @@ said_yes, checked = OptionalMessageDialog.question(self, "QtWidgets.Question",
from __future__ import annotations
import logging
from typing import Callable
from PyQt6 import QtCore, QtWidgets
@ -39,7 +29,14 @@ StyleQuestion = 1
class OptionalMessageDialog(QtWidgets.QDialog):
def __init__(
self, parent: QtWidgets.QWidget, style: int, title: str, msg: str, checked: bool = False, check_text: str = ""
self,
parent: QtWidgets.QWidget,
style: int,
title: str,
msg: str,
*,
checked: bool = False,
check_text: str = "",
) -> None:
super().__init__(parent)
@ -85,36 +82,54 @@ class OptionalMessageDialog(QtWidgets.QDialog):
layout.addWidget(self.theButtonBox)
def accept(self) -> None:
self.was_accepted = True
QtWidgets.QDialog.accept(self)
def reject(self) -> None:
self.was_accepted = False
QtWidgets.QDialog.reject(self)
@staticmethod
def msg(parent: QtWidgets.QWidget, title: str, msg: str, checked: bool = False, check_text: str = "") -> bool:
def msg(
parent: QtWidgets.QWidget,
title: str,
msg: str,
*,
callback: Callable[[bool], None],
checked: bool = False,
check_text: str = "",
) -> None:
d = OptionalMessageDialog(parent, StyleMessage, title, msg, checked=checked, check_text=check_text)
d.exec()
return d.theCheckBox.isChecked()
def finished(i: int) -> None:
callback(d.theCheckBox.isChecked())
d.finished.connect(finished)
d.show()
@staticmethod
def question(
parent: QtWidgets.QWidget, title: str, msg: str, checked: bool = False, check_text: str = ""
) -> tuple[bool, bool]:
parent: QtWidgets.QWidget,
title: str,
msg: str,
*,
callback: Callable[[bool, bool], None],
checked: bool = False,
check_text: str = "",
) -> None:
d = OptionalMessageDialog(parent, StyleQuestion, title, msg, checked=checked, check_text=check_text)
d.exec()
def finished(i: int) -> None:
callback(i == QtWidgets.QDialog.DialogCode.Accepted, d.theCheckBox.isChecked())
return d.was_accepted, d.theCheckBox.isChecked()
d.finished.connect(finished)
d.show()
@staticmethod
def msg_no_checkbox(
parent: QtWidgets.QWidget, title: str, msg: str, checked: bool = False, check_text: str = ""
) -> bool:
parent: QtWidgets.QWidget, title: str, msg: str, *, checked: bool = False, check_text: str = ""
) -> None:
d = OptionalMessageDialog(parent, StyleMessage, title, msg, checked=checked, check_text=check_text)
d.theCheckBox.hide()
d.exec()
return d.theCheckBox.isChecked()
d.show()

View File

@ -351,7 +351,7 @@ class PageListEditor(QtWidgets.QWidget):
self.comic_archive = comic_archive
self.pages_list = pages_list
if pages_list:
self.select_read_tags(self.tag_ids)
self.select_write_tags(self.tag_ids)
else:
self.cbPageType.setEnabled(False)
self.chkDoublePage.setEnabled(False)
@ -396,7 +396,7 @@ class PageListEditor(QtWidgets.QWidget):
self.first_front_page = self.get_first_front_cover()
self.firstFrontCoverChanged.emit(self.first_front_page)
def select_read_tags(self, tag_ids: list[str]) -> None:
def select_write_tags(self, tag_ids: list[str]) -> None:
# depending on the current tags, certain fields are disabled
if not tag_ids:
return

View File

@ -25,7 +25,7 @@ from comicapi.comicarchive import ComicArchive
logger = logging.getLogger(__name__)
class PageLoader(QtCore.QThread):
class PageLoader(QtCore.QThread): # TODO: Evaluate thread semantics. Specifically with signals
"""
This class holds onto a reference of each instance in a list since
problems occur if the ref count goes to zero and the GC tries to reap

View File

@ -20,6 +20,7 @@ import logging
import settngs
from PyQt6 import QtCore, QtWidgets, uic
from PyQt6.QtGui import QColorConstants
from comicapi import utils
from comicapi.comicarchive import ComicArchive, tags
@ -27,7 +28,7 @@ from comicapi.genericmetadata import GenericMetadata
from comictaggerlib.ctsettings import ct_ns
from comictaggerlib.filerenamer import FileRenamer, get_rename_dir
from comictaggerlib.settingswindow import SettingsWindow
from comictaggerlib.ui import ui_path
from comictaggerlib.ui import qtutils, ui_path
from comictaggerlib.ui.qtutils import center_window_on_parent
from comictalker.comictalker import ComicTalker
@ -70,12 +71,13 @@ class RenameWindow(QtWidgets.QDialog):
self.do_preview()
def config_renamer(self, ca: ComicArchive, md: GenericMetadata = GenericMetadata()) -> str:
def config_renamer(self, ca: ComicArchive, md: GenericMetadata = GenericMetadata()) -> tuple[str, Exception | None]:
self.renamer.set_template(self.config[0].File_Rename__template)
self.renamer.set_issue_zero_padding(self.config[0].File_Rename__issue_number_padding)
self.renamer.set_smart_cleanup(self.config[0].File_Rename__use_smart_string_cleanup)
self.renamer.replacements = self.config[0].File_Rename__replacements
self.renamer.move_only = self.config[0].File_Rename__only_move
error = None
new_ext = ca.path.suffix # default
if self.config[0].File_Rename__auto_extension:
@ -85,11 +87,6 @@ class RenameWindow(QtWidgets.QDialog):
md, _, error = self.parent().read_selected_tags(self.read_tag_ids, ca)
if error is not None:
logger.error("Failed to load tags from %s: %s", ca.path, error)
QtWidgets.QMessageBox.warning(
self,
"Read Failed!",
f"One or more of the read tags failed to load for {ca.path}, check log for details",
)
if md.is_empty:
md = ca.metadata_from_filename(
@ -100,20 +97,22 @@ class RenameWindow(QtWidgets.QDialog):
)
self.renamer.set_metadata(md, ca.path.name)
self.renamer.move = self.config[0].File_Rename__move
return new_ext
return new_ext, error
def do_preview(self) -> None:
self.twList.setRowCount(0)
self.twList.setSortingEnabled(False)
errors = False
for ca in self.comic_archive_list:
new_ext = self.config_renamer(ca)
new_ext, error = self.config_renamer(ca)
errors = errors or error is not None
try:
new_name = self.renamer.determine_name(new_ext)
except ValueError as e:
logger.exception("Invalid format string: %s", self.config[0].File_Rename__template)
QtWidgets.QMessageBox.critical(
qtutils.critical(
self,
"Invalid format string!",
"Your rename template is invalid!"
@ -128,7 +127,7 @@ class RenameWindow(QtWidgets.QDialog):
logger.exception(
"Formatter failure: %s metadata: %s", self.config[0].File_Rename__template, self.renamer.metadata
)
QtWidgets.QMessageBox.critical(
qtutils.critical(
self,
"The formatter had an issue!",
"The formatter has experienced an unexpected error!"
@ -164,7 +163,11 @@ class RenameWindow(QtWidgets.QDialog):
new_name_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
self.twList.setItem(row, 2, new_name_item)
new_name_item.setText(new_name)
if error is not None:
new_name_item.setText(f"Error reading tags: {error}")
new_name_item.setBackground(QColorConstants.Red)
else:
new_name_item.setText(new_name)
new_name_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, new_name)
self.rename_list.append(new_name)
@ -177,14 +180,18 @@ class RenameWindow(QtWidgets.QDialog):
self.twList.setColumnWidth(0, 200)
self.twList.setSortingEnabled(True)
if errors:
qtutils.warning(self, "Read Failed!", "One or more of the read tags failed to load, check log for details")
def modify_settings(self) -> None:
settingswin = SettingsWindow(self, self.config, self.talkers)
settingswin.setModal(True)
settingswin.show_rename_tab()
settingswin.exec()
if settingswin.result():
self.do_preview()
settingswin.accepted.connect(self.settings_closed)
settingswin.show()
def settings_closed(self) -> None:
self.do_preview()
def accept(self) -> None:
prog_dialog = QtWidgets.QProgressDialog("", "Cancel", 0, len(self.rename_list), self)
@ -234,22 +241,23 @@ class RenameWindow(QtWidgets.QDialog):
except Exception as e:
assert comic
logger.exception("Failed to rename comic archive: %s", comic[0].path)
QtWidgets.QMessageBox.critical(
qtutils.critical(
self,
"There was an issue when renaming!",
f"Renaming failed!<br/><br/>{type(e).__name__}: {e}<br/><br/>",
)
if failed_renames:
QtWidgets.QMessageBox.critical(
qtutils.critical(
self,
f"Failed to rename {len(failed_renames)} files!",
f"Renaming failed for {len(failed_renames)} files!<br/><br/>"
+ "<br/>".join([f"{x[0]!r} -> {x[1]!r}: {x[2]}" for x in failed_renames])
+ "<br/><br/>",
"Renaming failed for {} files!<br/><br/>{}<br/><br/>".format(
len(failed_renames),
"<br/>".join([f"{x[0]!r} -> {x[1]!r}: {x[2]}" for x in failed_renames]),
),
)
prog_dialog.hide()
prog_dialog.close()
QtCore.QCoreApplication.processEvents()
QtWidgets.QDialog.accept(self)

View File

@ -42,7 +42,7 @@ from comictalker.comictalker import ComicTalker, RLCallBack, TalkerError
logger = logging.getLogger(__name__)
class SearchThread(QtCore.QThread):
class SearchThread(QtCore.QThread): # TODO: Evaluate thread semantics. Specifically with signals
searchComplete = pyqtSignal()
progressUpdate = pyqtSignal(int, int)
ratelimit = pyqtSignal(float, float)
@ -86,7 +86,7 @@ class SearchThread(QtCore.QThread):
self.ratelimit.emit(full_time, sleep_time)
class IdentifyThread(QtCore.QThread):
class IdentifyThread(QtCore.QThread): # TODO: Evaluate thread semantics. Specifically with signals
ratelimit = pyqtSignal(float, float)
identifyComplete = pyqtSignal(IIResult, list)
identifyLogMsg = pyqtSignal(str)
@ -336,7 +336,7 @@ class SeriesSelectionWindow(SelectionWindow):
self.progdialog.setMinimumDuration(300)
if refresh or self.search_thread.isRunning():
self.progdialog.exec()
self.progdialog.open()
else:
self.progdialog = None
@ -428,15 +428,13 @@ class SeriesSelectionWindow(SelectionWindow):
def auto_select(self) -> None:
if self.comic_archive is None:
QtWidgets.QMessageBox.information(self, "Auto-Select", "You need to load a comic first!")
qtutils.information(self, "Auto-Select", "You need to load a comic first!")
return
if self.issue_number is None or self.issue_number == "":
QtWidgets.QMessageBox.information(self, "Auto-Select", "Can't auto-select without an issue number (yet!)")
qtutils.information(self, "Auto-Select", "Can't auto-select without an issue number (yet!)")
return
self.iddialog = IDProgressWindow(self)
self.iddialog.setModal(True)
self.iddialog.show()
md = GenericMetadata()
md.series = self.series_name
@ -453,7 +451,7 @@ class SeriesSelectionWindow(SelectionWindow):
self.id_thread.start()
self.iddialog.exec()
self.iddialog.open()
def log_output(self, text: str) -> None:
if self.iddialog is None:
@ -469,47 +467,46 @@ class SeriesSelectionWindow(SelectionWindow):
self.iddialog.progressBar.setValue(cur)
def identify_complete(self, result: IIResult, issues: list[IssueResult]) -> None:
if not (self.iddialog is not None and self.comic_archive is not None):
if self.iddialog is None or self.comic_archive is None:
return
found_match = None
choices = False
if result == IIResult.single_good_match:
return self.update_match(issues[0])
qmsg = QtWidgets.QMessageBox(parent=self)
qmsg.setIcon(qmsg.Icon.Information)
qmsg.setText("Auto-Select Result")
qmsg.setInformativeText(" Manual interaction needed :-(")
qmsg.finished.connect(self.iddialog.close)
if result == IIResult.no_matches:
QtWidgets.QMessageBox.information(self, "Auto-Select Result", " No issues found :-(")
elif result == IIResult.single_bad_cover_score:
QtWidgets.QMessageBox.information(
self,
"Auto-Select Result",
" Found a match, but cover doesn't seem the same. Verify before committing!",
)
found_match = issues[0]
elif result == IIResult.multiple_bad_cover_scores:
QtWidgets.QMessageBox.information(
self, "Auto-Select Result", " Found some possibilities, but no confidence. Proceed manually."
)
choices = True
elif result == IIResult.single_good_match:
found_match = issues[0]
qmsg.setInformativeText(" No matches found :-(")
return qmsg.show()
if result == IIResult.single_bad_cover_score:
qmsg.setInformativeText(" Found a match, but cover doesn't seem the same. Verify before committing!")
qmsg.finished.connect(lambda: self.update_match(issues[0]))
return qmsg.show()
selector = MatchSelectionWindow(self, issues, self.comic_archive, talker=self.talker, config=self.config)
selector.match_selected.connect(self.update_match)
qmsg.finished.connect(selector.open)
if result == IIResult.multiple_bad_cover_scores:
qmsg.setInformativeText(" Found some possibilities, but no confidence. Proceed manually.")
elif result == IIResult.multiple_good_matches:
QtWidgets.QMessageBox.information(
self, "Auto-Select Result", " Found multiple likely matches. Please select."
)
choices = True
qmsg.setInformativeText(" Found multiple likely matches. Please select.")
if choices:
selector = MatchSelectionWindow(self, issues, self.comic_archive, talker=self.talker, config=self.config)
selector.exec()
if selector.result():
# we should now have a list index
found_match = selector.current_match()
qmsg.show()
if found_match is not None:
self.iddialog.accept()
def update_match(self, match: IssueResult) -> None:
if self.iddialog is not None:
self.iddialog.close()
self.series_id = utils.xlate(found_match.series_id) or ""
self.issue_number = found_match.issue_number
self.select_by_id()
self.show_issues()
self.series_id = utils.xlate(match.series_id) or ""
self.issue_number = match.issue_number
self.select_by_id()
self.show_issues()
def show_issues(self) -> None:
title = ""
@ -567,13 +564,14 @@ class SeriesSelectionWindow(SelectionWindow):
self.progdialog.accept()
self.progdialog = None
if self.search_thread is not None and self.search_thread.ct_error:
# TODO Currently still opens the window
QtWidgets.QMessageBox.critical(
self,
parent = self.parent()
if not isinstance(parent, QtWidgets.QWidget):
parent = None
return qtutils.critical(
parent,
f"{self.search_thread.error_e.source} {self.search_thread.error_e.code_name} Error",
f"{self.search_thread.error_e}",
)
return
tmp_list = self.search_thread.ct_search_results if self.search_thread is not None else []
self.series_list = {x.id: x for x in tmp_list}
@ -674,8 +672,7 @@ class SeriesSelectionWindow(SelectionWindow):
self.twList.resizeRowsToContents()
if not self.series_list:
QtWidgets.QMessageBox.information(self, "Search Result", "No matches found!\nSeriesSelectionWindow")
QtCore.QTimer.singleShot(200, self.close_me)
return qtutils.information(self, "Search Result", "No matches found!\nSeriesSelectionWindow")
elif self.immediate_autoselect:
# defer the immediate autoselect so this dialog has time to pop up

View File

@ -37,7 +37,7 @@ from comictaggerlib.ctsettings import ct_ns
from comictaggerlib.ctsettings.plugin import group_for_plugin
from comictaggerlib.filerenamer import FileRenamer, Replacement, Replacements
from comictaggerlib.imagefetcher import ImageFetcher
from comictaggerlib.ui import ui_path
from comictaggerlib.ui import qtutils, ui_path
from comictalker.comiccacher import ComicCacher
from comictalker.comictalker import ComicTalker
@ -146,6 +146,8 @@ class SettingsWindow(QtWidgets.QDialog):
with (ui_path / "settingswindow.ui").open(encoding="utf-8") as uifile:
uic.loadUi(uifile, self)
self.leRarExePath: QtWidgets.QLineEdit
self.setWindowFlags(
QtCore.Qt.WindowType(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
)
@ -154,6 +156,8 @@ class SettingsWindow(QtWidgets.QDialog):
self.talkers = talkers
self.name = "Settings"
self.setModal(True)
if platform.system() == "Windows":
self.lblRarHelp.setText(windowsRarHelp)
@ -526,7 +530,7 @@ class SettingsWindow(QtWidgets.QDialog):
logger.error(
"Invalid format string: %s", self.config[0].File_Rename__template, exc_info=self.rename_error
)
QtWidgets.QMessageBox.critical(
return qtutils.critical(
self,
"Invalid format string!",
"Your rename template is invalid!"
@ -536,23 +540,21 @@ class SettingsWindow(QtWidgets.QDialog):
+ "<a href='https://docs.python.org/3/library/string.html#format-string-syntax'>"
+ "https://docs.python.org/3/library/string.html#format-string-syntax</a>",
)
return
else:
logger.error(
"Formatter failure: %s metadata: %s",
self.config[0].File_Rename__template,
self.renamer.metadata,
exc_info=self.rename_error,
)
QtWidgets.QMessageBox.critical(
self,
"The formatter had an issue!",
"The formatter has experienced an unexpected error!"
+ f"<br/><br/>{type(self.rename_error).__name__}: {self.rename_error}<br/><br/>"
+ "Please open an issue at "
+ "<a href='https://github.com/comictagger/comictagger'>"
+ "https://github.com/comictagger/comictagger</a>",
)
logger.error(
"Formatter failure: %s metadata: %s",
self.config[0].File_Rename__template,
self.renamer.metadata,
exc_info=self.rename_error,
)
return qtutils.critical(
self,
"The formatter had an issue!",
"The formatter has experienced an unexpected error!"
+ f"<br/><br/>{type(self.rename_error).__name__}: {self.rename_error}<br/><br/>"
+ "Please open an issue at "
+ "<a href='https://github.com/comictagger/comictagger'>"
+ "https://github.com/comictagger/comictagger</a>",
)
# Copy values from form to settings and save
archive_group = group_for_plugin(Archiver)
@ -645,12 +647,12 @@ class SettingsWindow(QtWidgets.QDialog):
cache_folder.mkdir(parents=True, exist_ok=True)
ComicCacher(cache_folder, "0")
ImageFetcher(cache_folder)
QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.")
qtutils.information(self, self.name, "Cache has been cleared.")
def reset_settings(self) -> None:
self.config = cast(settngs.Config[ct_ns], settngs.get_namespace(settngs.defaults(self.config[1])))
self.settings_to_form()
QtWidgets.QMessageBox.information(self, self.name, self.name + " have been returned to default values.")
qtutils.information(self, self.name, self.name + " have been returned to default values.")
def select_file(self, control: QtWidgets.QLineEdit, name: str) -> None:
dialog = QtWidgets.QFileDialog(self)
@ -671,9 +673,11 @@ class SettingsWindow(QtWidgets.QDialog):
else:
dialog.setWindowTitle(f"Find {name} library")
if dialog.exec():
file_list = dialog.selectedFiles()
control.setText(str(file_list[0]))
dialog.fileSelected.connect(self.set_rar_path)
dialog.open()
def set_rar_path(self, path: str) -> None:
self.leRarExePath.setText(str(path))
def show_rename_tab(self) -> None:
self.tabWidget.setCurrentIndex(5)

View File

@ -44,12 +44,12 @@ from comictaggerlib import ctsettings, ctversion
from comictaggerlib.applicationlogwindow import ApplicationLogWindow, QTextEditLogger
from comictaggerlib.autotagmatchwindow import AutoTagMatchWindow
from comictaggerlib.autotagprogresswindow import AutoTagProgressWindow, AutoTagThread
from comictaggerlib.autotagstartwindow import AutoTagStartWindow
from comictaggerlib.autotagstartwindow import AutoTagSettings, AutoTagStartWindow
from comictaggerlib.cbltransformer import CBLTransformer
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.crediteditorwindow import CreditEditorWindow
from comictaggerlib.crediteditorwindow import CreditEditorWindow, EditMode
from comictaggerlib.ctsettings import ct_ns
from comictaggerlib.exportwindow import ExportConflictOpts, ExportWindow
from comictaggerlib.exportwindow import ExportConfig, ExportConflictOpts, ExportWindow
from comictaggerlib.fileselectionlist import FileSelectionList
from comictaggerlib.graphics import graphics_path
from comictaggerlib.gtinvalidator import is_valid_gtin
@ -75,7 +75,7 @@ def execute(f: Callable[[], Any]) -> None:
f()
class QueryThread(QtCore.QThread):
class QueryThread(QtCore.QThread): # TODO: Evaluate thread semantics. Specifically with signals
def __init__(
self,
talker: ComicTalker,
@ -102,8 +102,7 @@ class QueryThread(QtCore.QThread):
on_rate_limit=RLCallBack(lambda x, y: self.on_rate_limit.emit(x, y), 60),
)
except TalkerError as e:
QtWidgets.QMessageBox.critical(None, f"{e.source} {e.code_name} Error", f"{e}")
return
return qtutils.critical(None, f"{e.source} {e.code_name} Error", f"{e}")
self.finish.emit(new_metadata, self.issue_number)
@ -359,7 +358,11 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.fileSelectionList.add_path_list(file_list)
if self.config[0].Dialog_Flags__show_disclaimer:
checked = OptionalMessageDialog.msg(
def set_checked(checked: bool) -> None:
self.config[0].Dialog_Flags__show_disclaimer = not checked
OptionalMessageDialog.msg(
self,
"Welcome!",
"""
@ -378,10 +381,14 @@ class TaggerWindow(QtWidgets.QMainWindow):
for more information.<br><br>
Have fun!
""",
callback=set_checked,
)
self.config[0].Dialog_Flags__show_disclaimer = not checked
if self.config[0].Dialog_Flags__notify_plugin_changes and getattr(sys, "frozen", False):
checked = OptionalMessageDialog.msg(
if self.config[0].Dialog_Flags__notify_plugin_changes and True:
def set_checked(checked: bool) -> None:
self.config[0].Dialog_Flags__notify_plugin_changes = not checked
OptionalMessageDialog.msg(
self,
"Plugins Have moved!",
f"""
@ -391,23 +398,19 @@ class TaggerWindow(QtWidgets.QMainWindow):
Metron: <a href="https://github.com/comictagger/metron_talker/releases">https://github.com/comictagger/metron_talker/releases</a><br/><br/>
For more information on installing plugins see the wiki page:<br/><a href="https://github.com/comictagger/comictagger/wiki/Installing-plugins">https://github.com/comictagger/comictagger/wiki/Installing-plugins</a>
""",
callback=set_checked,
)
self.config[0].Dialog_Flags__notify_plugin_changes = not checked
if self.enabled_tags():
# This should never be false
self.selected_write_tags = [self.enabled_tags()[0]]
self.selected_read_tags = [self.enabled_tags()[0]]
else:
checked = OptionalMessageDialog.msg_no_checkbox(
self,
"No tags enabled",
"""
There are no tags enabled!<br/><br/>
Go to the "Metadata Options" tab in settings to enable the builtin "Comic Rack" tags
""",
)
if self.config[0].General__check_for_new_version:
self.check_latest_version_online()
self.export_window = ExportWindow(self)
self.export_window.export.connect(self._repackage_archive)
def enabled_tags(self) -> Sequence[str]:
return [tag.id for tag in tags.values() if tag.enabled]
@ -568,20 +571,11 @@ class TaggerWindow(QtWidgets.QMainWindow):
def repackage_archive(self) -> None:
ca_list = self.fileSelectionList.get_selected_archive_list()
non_zip_count = 0
to_zip = []
largest_page_size = 0
for ca in ca_list:
largest_page_size = max(largest_page_size, len(ca.get_page_name_list()))
if not ca.is_zip():
to_zip.append(ca)
to_zip = [ca for ca in ca_list if not ca.is_zip()]
if not to_zip:
QtWidgets.QMessageBox.information(
self, self.tr("Export as Zip Archive"), self.tr("Only ZIP archives are selected!")
)
logger.warning("Export as Zip Archive. Only ZIP archives are selected")
return
return qtutils.information(self, "Export as Zip Archive", "Only ZIP archives are selected!")
if not self.dirty_flag_verification(
"Export as Zip Archive",
@ -589,98 +583,90 @@ class TaggerWindow(QtWidgets.QMainWindow):
):
return
if to_zip:
EW = ExportWindow(
self,
(
f"You have selected {len(to_zip)} archive(s) to export to Zip format. "
""" New archives will be created in the same folder as the original.
self.export_window.show(len(to_zip))
Please choose config below, and select OK.
"""
),
)
EW.adjustSize()
EW.setModal(True)
if not EW.exec():
return
def _repackage_archive(self, export_config: ExportConfig) -> None:
largest_page_size = 0
ca_list = self.fileSelectionList.get_selected_archive_list()
to_zip = []
for ca in ca_list:
if not ca.is_zip():
to_zip.append(ca)
if ca.get_number_of_pages() > largest_page_size:
largest_page_size = ca.get_number_of_pages()
prog_dialog = None
if len(to_zip) > 3 or largest_page_size > 24:
prog_dialog = QtWidgets.QProgressDialog("", "Cancel", 0, non_zip_count, self)
prog_dialog.setWindowTitle("Exporting as ZIP")
prog_dialog.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
prog_dialog.setMinimumDuration(300)
center_window_on_parent(prog_dialog)
QtCore.QCoreApplication.processEvents()
prog_dialog = None
if len(to_zip) > 3 or largest_page_size > 24:
prog_dialog = QtWidgets.QProgressDialog("", "Cancel", 0, len(to_zip), self)
prog_dialog.setWindowTitle("Exporting as ZIP")
prog_dialog.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
prog_dialog.setMinimumDuration(300)
center_window_on_parent(prog_dialog)
QtCore.QCoreApplication.processEvents()
new_archives_to_add = []
archives_to_remove = []
skipped_list = []
failed_list = []
success_count = 0
logger.debug("Exporting %d comics to zip", len(to_zip))
new_archives_to_add = []
archives_to_remove = []
skipped_list = []
failed_list = []
success_count = 0
logger.debug("Exporting %d comics to zip", len(to_zip))
for prog_idx, ca in enumerate(to_zip, 1):
logger.debug("Exporting comic %d: %s", prog_idx, ca.path)
if prog_idx % 10 == 0:
QtCore.QCoreApplication.processEvents()
if prog_dialog is not None:
if prog_dialog.wasCanceled():
break
for prog_idx, ca in enumerate(to_zip, 1):
logger.debug("Exporting comic %d: %s", prog_idx, ca.path)
if prog_dialog is not None:
if prog_dialog.wasCanceled():
break
if prog_idx % 10 == 0 or len(ca_list) < 50:
prog_dialog.setValue(prog_idx)
prog_dialog.setLabelText(str(ca.path))
QtCore.QCoreApplication.processEvents()
export_name = ca.path.with_suffix(".cbz")
export = True
export_name = ca.path.with_suffix(".cbz")
export = True
if export_name.exists():
if EW.fileConflictBehavior == ExportConflictOpts.dontCreate:
export = False
skipped_list.append(ca.path)
elif EW.fileConflictBehavior == ExportConflictOpts.createUnique:
export_name = utils.unique_file(export_name)
if export_name.exists():
if export_config.conflict == ExportConflictOpts.DONT_CREATE:
export = False
skipped_list.append(ca.path)
elif export_config.conflict == ExportConflictOpts.CREATE_UNIQUE:
export_name = utils.unique_file(export_name)
if export:
logger.debug("Exporting %s to %s", ca.path, export_name)
if ca.export_as_zip(export_name):
success_count += 1
if EW.addToList:
new_archives_to_add.append(str(export_name))
if EW.deleteOriginal:
archives_to_remove.append(ca)
ca.path.unlink(missing_ok=True)
if export:
logger.debug("Exporting %s to %s", ca.path, export_name)
if ca.export_as_zip(export_name):
success_count += 1
if export_config.add_to_list:
new_archives_to_add.append(str(export_name))
if export_config.delete_original:
archives_to_remove.append(ca)
ca.path.unlink(missing_ok=True)
else:
# last export failed, so remove the zip, if it exists
failed_list.append(ca.path)
if export_name.exists():
export_name.unlink(missing_ok=True)
else:
# last export failed, so remove the zip, if it exists
failed_list.append(ca.path)
if export_name.exists():
export_name.unlink(missing_ok=True)
if prog_dialog is not None:
prog_dialog.hide()
self.fileSelectionList.remove_archive_list(archives_to_remove)
if prog_dialog is not None:
prog_dialog.hide()
self.fileSelectionList.remove_archive_list(archives_to_remove)
summary = f"Successfully created {success_count} Zip archive(s)."
if skipped_list:
summary += (
f"\n\nThe following {len(skipped_list)} archive(s) were skipped due to file name conflicts:\n"
)
for f in skipped_list:
summary += f"\t{f}\n"
if failed_list:
summary += (
f"\n\nThe following {len(failed_list)} archive(s) failed to export due to read/write errors:\n"
)
for f in failed_list:
summary += f"\t{f}\n"
summary = f"Successfully created {success_count} Zip archive(s)."
if skipped_list:
summary += f"\n\nThe following {len(skipped_list)} archive(s) were skipped due to file name conflicts:\n"
for f in skipped_list:
summary += f"\t{f}\n"
if failed_list:
summary += f"\n\nThe following {len(failed_list)} archive(s) failed to export due to read/write errors:\n"
for f in failed_list:
summary += f"\t{f}\n"
logger.info(summary)
dlg = LogWindow(self)
dlg.set_text(summary)
dlg.setWindowTitle("Archive Export to Zip Summary")
dlg.exec()
self.fileSelectionList.add_path_list(new_archives_to_add)
logger.info(summary)
dlg = LogWindow(self)
dlg.set_text(summary)
dlg.setWindowTitle("Archive Export to Zip Summary")
dlg.show()
self.fileSelectionList.add_path_list(new_archives_to_add)
def about_app(self) -> None:
website = "https://github.com/comictagger/comictagger"
@ -688,7 +674,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
license_link = "http://www.apache.org/licenses/LICENSE-2.0"
license_name = "Apache License 2.0"
msg_box = QtWidgets.QMessageBox()
msg_box = QtWidgets.QMessageBox(self)
msg_box.setWindowTitle("About " + self.appName)
msg_box.setTextFormat(QtCore.Qt.TextFormat.RichText)
msg_box.setIconPixmap(QtGui.QPixmap(":/graphics/about.png"))
@ -704,7 +690,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
)
msg_box.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok)
msg_box.exec()
msg_box.show()
def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None:
self.droppedFiles = []
@ -1107,17 +1093,23 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.select_file(folder_mode=True)
def select_folder_archive(self) -> None:
dialog = self.file_dialog(folder_mode=True)
if dialog.exec():
file_list = dialog.selectedFiles()
if file_list:
self.fileSelectionList.twList.selectRow(self.fileSelectionList.add_path_item(file_list[0])[0])
self.select_file(folder_mode=True, recursive=True)
def select_file(self, folder_mode: bool = False) -> None:
def select_file(self, folder_mode: bool = False, recursive: bool = True) -> None:
dialog = self.file_dialog(folder_mode=folder_mode)
if dialog.exec():
file_list = dialog.selectedFiles()
self.fileSelectionList.add_path_list(file_list)
if recursive:
dialog.filesSelected.connect(self._load_files)
else:
dialog.fileSelected.connect(self._load_single_file)
dialog.open()
def _load_single_file(self, file: str) -> None:
if file:
self.fileSelectionList.twList.selectRow(self.fileSelectionList.add_path_item(file)[0])
def _load_files(self, files: list[str]) -> None:
if files:
self.fileSelectionList.add_path_list(files)
def file_dialog(self, folder_mode: bool = False) -> QtWidgets.QFileDialog:
dialog = QtWidgets.QFileDialog(self)
@ -1138,8 +1130,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
def auto_identify_search(self) -> None:
if self.comic_archive is None:
QtWidgets.QMessageBox.warning(self, "Automatic Identify Search", "You need to load a comic first!")
return
return qtutils.warning(self, "Automatic Identify Search", "You need to load a comic first!")
self.query_online(autoselect=True)
@ -1151,18 +1142,16 @@ class TaggerWindow(QtWidgets.QMainWindow):
# Only need this check is the source has issue level data.
if autoselect and issue_number == "":
QtWidgets.QMessageBox.information(
return qtutils.information(
self,
"Automatic Identify Search",
"Can't auto-identify without an issue number. The auto-tag function has the 'If no issue number, assume \"1\"' option if desired.",
)
return
if str(self.leSeries.text()).strip() != "":
series_name = str(self.leSeries.text()).strip()
else:
QtWidgets.QMessageBox.information(self, "Online Search", "Need to enter a series name to search.")
return
return qtutils.information(self, "Online Search", "Need to enter a series name to search.")
year = utils.xlate_int(self.lePubYear.text())
@ -1210,8 +1199,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.form_to_metadata()
if new_metadata is None or new_metadata.is_empty:
QtWidgets.QMessageBox.critical(None, "Search", f"Could not find an issue {new_metadata} for that series")
return
return qtutils.critical(self, "Search", f"Could not find an issue {new_metadata} for that series")
self.metadata = prepare_metadata(self.metadata, new_metadata, self.config[0])
# Now push the new combined data into the edit controls
@ -1236,59 +1224,59 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.toast.show()
def write_tags(self) -> None:
if self.metadata is not None and self.comic_archive is not None:
if self.config[0].General__prompt_on_save:
reply = QtWidgets.QMessageBox.question(
self,
"Save Tags",
f"Are you sure you wish to save {', '.join([tags[tag_id].name() for tag_id in self.selected_write_tags])} tags to this archive?",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No,
)
else:
reply = QtWidgets.QMessageBox.StandardButton.Yes
if self.metadata is None or self.comic_archive is None:
return qtutils.information(self, "Whoops!", "No data to write!")
if reply != QtWidgets.QMessageBox.StandardButton.Yes:
return
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
self.form_to_metadata()
failed_tag: str = ""
# Save each tag
for tag_id in self.selected_write_tags:
success = self.comic_archive.write_tags(self.metadata, tag_id)
if not success:
failed_tag = tags[tag_id].name()
break
self.comic_archive.load_cache(set(tags))
QtWidgets.QApplication.restoreOverrideCursor()
if failed_tag:
QtWidgets.QMessageBox.warning(
self,
"Save failed",
f"The tag save operation seemed to fail for: {failed_tag}",
)
else:
self.clear_dirty_flag()
self.update_info_box()
self.update_menus()
# Only try to read if write was successful
self.metadata, _, error = self.read_selected_tags(self.selected_read_tags, self.comic_archive)
if error is not None:
QtWidgets.QMessageBox.warning(
self,
"Read Failed!",
f"One or more of the selected read tags failed to load for {self.comic_archive.path}, check log for details",
)
logger.error("Failed to load metadata for %s: %s", self.ca.path, error)
self.fileSelectionList.update_current_row()
self.update_ui_for_archive()
if self.config[0].General__prompt_on_save:
reply = QtWidgets.QMessageBox.question(
self,
"Save Tags",
f"Are you sure you wish to save {', '.join([tags[tag_id].name() for tag_id in self.selected_write_tags])} tags to this archive?",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No,
)
else:
QtWidgets.QMessageBox.information(self, "Whoops!", "No data to write!")
reply = QtWidgets.QMessageBox.StandardButton.Yes
if reply != QtWidgets.QMessageBox.StandardButton.Yes:
return
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
self.form_to_metadata()
failed_tag: str = ""
# Save each tag
for tag_id in self.selected_write_tags:
success = self.comic_archive.write_tags(self.metadata, tag_id)
if not success:
failed_tag = tags[tag_id].name()
break
self.comic_archive.load_cache(set(tags))
QtWidgets.QApplication.restoreOverrideCursor()
if failed_tag:
qtutils.warning(
self,
"Save failed",
f"The tag save operation seemed to fail for: {failed_tag}",
)
else:
self.clear_dirty_flag()
self.update_info_box()
self.update_menus()
# Only try to read if write was successful
self.metadata, _, error = self.read_selected_tags(self.selected_read_tags, self.comic_archive)
if error is not None:
logger.error("Failed to load metadata for %s: %s", self.ca.path, error)
qtutils.warning(
self,
"Read Failed!",
f"One or more of the selected read tags failed to load for {self.comic_archive.path}, check log for details",
)
self.fileSelectionList.update_current_row()
self.update_ui_for_archive()
def select_read_tags(self, tag_ids: list[str]) -> None:
"""Should only be called from the combobox signal"""
@ -1346,7 +1334,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
enable_widget(widget, md_field in enabled_widgets)
self.update_credit_colors()
self.page_list_editor.select_read_tags(self.selected_write_tags)
self.page_list_editor.select_write_tags(self.selected_write_tags)
self.toggle_enable_embedding_hashes()
def cell_double_clicked(self, r: int, c: int) -> None:
@ -1381,7 +1369,9 @@ class TaggerWindow(QtWidgets.QMainWindow):
def modify_credits(self, edit: bool) -> None:
row = self.twCredits.rowCount()
old = Credit()
mode = EditMode.NEW
if edit:
mode = EditMode.EDIT
row = self.twCredits.currentRow()
lang = str(
self.twCredits.item(row, self.md_attributes["credits.language"]).data(QtCore.Qt.ItemDataRole.UserRole)
@ -1394,55 +1384,51 @@ class TaggerWindow(QtWidgets.QMainWindow):
lang,
)
editor = CreditEditorWindow(self, CreditEditorWindow.ModeEdit, old)
editor.setModal(True)
editor.exec()
if editor.result():
new = editor.get_credit()
editor = CreditEditorWindow(self, self.selected_write_tags, row, mode, old)
editor.creditChanged.connect(self._credit_changed)
editor.show()
if new == old:
# nothing has changed, just quit
return
def _edit_credit(self, credit: Credit, row: int) -> None:
lang = utils.get_language_from_iso(credit.language) or credit.language
self.twCredits.item(row, self.md_attributes["credits.role"]).setText(credit.role)
self.twCredits.item(row, self.md_attributes["credits.person"]).setText(credit.person)
self.twCredits.item(row, self.md_attributes["credits.language"]).setText(lang)
self.twCredits.item(row, self.md_attributes["credits.language"]).setData(
QtCore.Qt.ItemDataRole.UserRole, credit.language
)
self.update_credit_primary_flag(row, credit.primary)
# check for dupes
ok_to_mod = True
if self.is_dupe_credit(row, new.role, new.person):
# delete the dupe credit from list
qmsg = QtWidgets.QMessageBox()
qmsg.setText("Duplicate Credit!")
qmsg.setInformativeText(
"This will create a duplicate credit entry. Would you like to merge the entries, or create a duplicate?"
)
qmsg.addButton("Merge", QtWidgets.QMessageBox.ButtonRole.AcceptRole)
qmsg.addButton("Duplicate", QtWidgets.QMessageBox.ButtonRole.NoRole)
self.update_credit_colors()
self.set_dirty_flag()
if qmsg.exec() == 0:
# merge
if edit:
# just remove the row that would be same
self.twCredits.removeRow(row)
# TODO -- need to find the row of the dupe, and possible change the primary flag
def _add_credit(self, credit: Credit) -> None:
# add new entry
row = self.twCredits.rowCount()
self.add_new_credit_entry(row, credit)
ok_to_mod = False
self.update_credit_colors()
self.set_dirty_flag()
if ok_to_mod:
# modify it
if edit:
lang = utils.get_language_from_iso(new.language) or new.language
self.twCredits.item(row, self.md_attributes["credits.role"]).setText(new.role)
self.twCredits.item(row, self.md_attributes["credits.person"]).setText(new.person)
self.twCredits.item(row, self.md_attributes["credits.language"]).setText(lang)
self.twCredits.item(row, self.md_attributes["credits.language"]).setData(
QtCore.Qt.ItemDataRole.UserRole, new.language
)
self.update_credit_primary_flag(row, new.primary)
else:
# add new entry
row = self.twCredits.rowCount()
self.add_new_credit_entry(row, new)
def _credit_changed(self, credit: Credit, row: int, mode: EditMode) -> None:
self.update_credit_colors()
self.set_dirty_flag()
if self.is_dupe_credit(row, credit.role, credit.person):
# delete the dupe credit from list
qmsg = QtWidgets.QMessageBox()
qmsg.setText("Duplicate Credit!")
qmsg.setInformativeText(
"This will create a duplicate credit entry. Would you like to merge the entries, or create a duplicate?"
)
qmsg.addButton("Merge", QtWidgets.QMessageBox.ButtonRole.AcceptRole)
qmsg.addButton("Duplicate", QtWidgets.QMessageBox.ButtonRole.NoRole)
if qmsg.exec() == 0 and mode == EditMode.EDIT:
# just remove the row that would be same
self.twCredits.removeRow(row)
mode = EditMode.NEW
if mode == EditMode.EDIT:
return self._edit_credit(credit, row)
self._add_credit(credit)
def remove_credit(self) -> None:
row = self.twCredits.currentRow()
@ -1461,13 +1447,12 @@ class TaggerWindow(QtWidgets.QMainWindow):
utils.parse_url(web_link)
webbrowser.open_new_tab(web_link)
except utils.LocationParseError:
QtWidgets.QMessageBox.warning(self, "Web Link", "Web Link is invalid.")
qtutils.warning(self, "Web Link", "Web Link is invalid.")
def show_settings(self) -> None:
settingswin = SettingsWindow(self, self.config, self.talkers)
settingswin.exec()
settingswin.result()
self.adjust_source_combo()
settingswin.finished.connect(self.adjust_source_combo)
settingswin.show()
def set_app_position(self) -> None:
if self.config[0].internal__window_width != 0:
@ -1640,12 +1625,11 @@ class TaggerWindow(QtWidgets.QMainWindow):
file_md_count[tag_id] += 1
if has_md_count == 0:
QtWidgets.QMessageBox.information(
qtutils.information(
self,
"Remove Tags",
f"No archives with {', '.join([tags[tag_id].name() for tag_id in tag_ids])} tags selected!",
)
return
if has_md_count != 0 and not self.dirty_flag_verification(
"Remove Tags", "If you remove tags now, unsaved data in the form will be lost. Are you sure?"
@ -1690,7 +1674,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
progdialog.hide()
QtCore.QCoreApplication.processEvents()
self.fileSelectionList.update_selected_rows()
self._reload_page()
self.update_info_box()
self.update_menus()
@ -1703,7 +1687,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
dlg = LogWindow(self)
dlg.set_text(summary)
dlg.setWindowTitle("Tag Remove Summary")
dlg.exec()
dlg.show()
def copy_tags(self) -> None:
# copy the indicated tags in the archive
@ -1718,10 +1702,9 @@ class TaggerWindow(QtWidgets.QMainWindow):
dest_tag_ids.remove(src_tag_ids[0])
if not dest_tag_ids:
QtWidgets.QMessageBox.information(
return qtutils.information(
self, "Copy Tags", "Can't copy tag tag onto itself. Read tag and modify tag must be different."
)
return
for ca in ca_list:
for tag_id in src_tag_ids:
@ -1730,12 +1713,11 @@ class TaggerWindow(QtWidgets.QMainWindow):
continue
if has_src_count == 0:
QtWidgets.QMessageBox.information(
return qtutils.information(
self,
"Copy Tags",
f"No archives with {', '.join([tags[tag_id].name() for tag_id in src_tag_ids])} tags selected!",
)
return
if has_src_count != 0 and not self.dirty_flag_verification(
"Copy Tags", "If you copy tags now, unsaved data in the form may be lost. Are you sure?"
@ -1799,7 +1781,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
prog_dialog.hide()
QtCore.QCoreApplication.processEvents()
self.fileSelectionList.update_selected_rows()
self._reload_page()
self.update_info_box()
self.update_menus()
@ -1812,7 +1794,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
dlg = LogWindow(self)
dlg.set_text(summary)
dlg.setWindowTitle("Tag Copy Summary")
dlg.exec()
dlg.show()
def auto_tag_log(self, text: str) -> None:
if self.atprogdialog is not None:
@ -1825,8 +1807,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
tag_names = ", ".join([tags[tag_id].name() for tag_id in self.selected_write_tags])
if not ca_list:
QtWidgets.QMessageBox.information(self, "Auto-Tag", "No archives selected!")
return
return qtutils.information(self, "Auto-Tag", "No archives selected!")
if not self.dirty_flag_verification(
"Auto-Tag", "If you auto-tag now, unsaved data in the form will be lost. Are you sure?"
@ -1842,28 +1823,35 @@ class TaggerWindow(QtWidgets.QMainWindow):
),
)
atstartdlg.adjustSize()
atstartdlg.setModal(True)
if not atstartdlg.exec():
return
atstartdlg.startAutoTag.connect(self._start_auto_tag)
atstartdlg.open()
def _start_auto_tag(self, auto_tag: AutoTagSettings) -> None:
# persist some settings because it's probably going to be used next time
self.config[0].Auto_Tag__save_on_low_confidence = auto_tag.settings["save_on_low_confidence"]
self.config[0].Auto_Tag__use_year_when_identifying = auto_tag.settings["use_year_when_identifying"]
self.config[0].Auto_Tag__assume_issue_one = auto_tag.settings["assume_issue_one"]
self.config[0].Auto_Tag__ignore_leading_numbers_in_filename = auto_tag.settings[
"ignore_leading_numbers_in_filename"
]
self.config[0].internal__remove_archive_after_successful_match = auto_tag.remove_after_success
ca_list = self.fileSelectionList.get_selected_archive_list()
self.atprogdialog = AutoTagProgressWindow(self, self.current_talker())
self.atprogdialog.open()
self.atprogdialog.progressBar.setMaximum(len(ca_list))
self.atprogdialog.setWindowTitle("Auto-Tagging")
center_window_on_parent(self.atprogdialog)
temp_opts = cast(ct_ns, settngs.get_namespace(self.config, True, True, True, False)[0])
temp_opts.Auto_Tag__clear_tags = atstartdlg.cbxClearMetadata.isChecked()
temp_opts.Issue_Identifier__series_match_identify_thresh = atstartdlg.name_length_match_tolerance
temp_opts.Auto_Tag__ignore_leading_numbers_in_filename = atstartdlg.ignore_leading_digits_in_filename
temp_opts.Auto_Tag__use_year_when_identifying = not atstartdlg.dont_use_year
temp_opts.Auto_Tag__assume_issue_one = atstartdlg.assume_issue_one
temp_opts.internal__remove_archive_after_successful_match = atstartdlg.remove_after_success
temp_opts.Auto_Tag__clear_tags = auto_tag.settings["clear_tags"]
temp_opts.Issue_Identifier__series_match_identify_thresh = auto_tag.series_match_identify_thresh
temp_opts.Runtime_Options__tags_read = self.selected_read_tags
temp_opts.Runtime_Options__tags_write = self.selected_write_tags
self.autotagthread = AutoTagThread(atstartdlg.search_string, ca_list, self.config[0], self.current_talker())
self.autotagthread = AutoTagThread(auto_tag.search_string, ca_list, self.config[0], self.current_talker())
self.autotagthread.autoTagComplete.connect(self.auto_tag_finished)
self.autotagthread.autoTagLogMsg.connect(self.auto_tag_log)
@ -1875,6 +1863,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.auto_tag_log("==========================================================================\n")
self.auto_tag_log(f"Auto-Tagging Started for {len(ca_list)} items\n")
self.autotagthread.start()
self.atprogdialog.open()
def auto_tag_finished(self, match_results: OnlineMatchResults, archives_to_remove: list[ComicArchive]) -> None:
tag_names = ", ".join([tags[tag_id].name() for tag_id in self.selected_write_tags])
@ -1882,11 +1871,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.atprogdialog.close()
self.fileSelectionList.remove_archive_list(archives_to_remove)
self.fileSelectionList.update_selected_rows()
new_ca = self.fileSelectionList.get_current_archive()
if new_ca is not None:
self.load_archive(new_ca)
self._reload_page()
self.atprogdialog = None
summary = f"<p>{self.current_talker().attribution}</p>"
@ -1908,42 +1893,51 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.auto_tag_log(summary)
selectable = match_results.multiple_matches or match_results.low_confidence_matches
if selectable:
summary += (
"\n\nDo you want to manually select the ones with multiple matches and/or low-confidence matches now?"
)
if not selectable:
dlg = LogWindow(self)
dlg.set_text(summary)
dlg.setWindowTitle("Tag Remove Summary")
dlg.show()
logger.info(summary)
return
reply = QtWidgets.QMessageBox.question(
self,
"Auto-Tag Summary",
summary,
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No,
)
match_results.multiple_matches.extend(match_results.low_confidence_matches)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
matchdlg = AutoTagMatchWindow(
self,
match_results.multiple_matches,
self.selected_write_tags,
self.config[0],
self.current_talker(),
)
matchdlg.exec()
self.fileSelectionList.update_selected_rows()
new_ca = self.fileSelectionList.get_current_archive()
if new_ca is not None:
self.load_archive(new_ca)
else:
QtWidgets.QMessageBox.information(self, self.tr("Auto-Tag Summary"), self.tr(summary))
summary += (
"\n\nDo you want to manually select the ones with multiple matches and/or low-confidence matches now?"
)
logger.info(summary)
reply = QtWidgets.QMessageBox.question(
self,
"Auto-Tag Summary",
summary,
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No,
)
match_results.multiple_matches.extend(match_results.low_confidence_matches)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
matchdlg = AutoTagMatchWindow(
self,
match_results.multiple_matches,
self.selected_write_tags,
self.config[0],
self.current_talker(),
)
matchdlg.open()
matchdlg.finished.connect(self._reload_page)
return
def _reload_page(self) -> None:
self.fileSelectionList.update_selected_rows()
new_ca = self.fileSelectionList.get_current_archive()
if new_ca is not None:
self.load_archive(new_ca)
def exception(self, message: str) -> None:
errorbox = QtWidgets.QMessageBox()
errorbox.setText(message)
errorbox.exec()
errorbox.open()
def dirty_flag_verification(self, title: str, desc: str) -> bool:
if self.dirty_flag:
@ -2004,7 +1998,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
dlg = LogWindow(self)
dlg.set_text(self.comic_archive.read_raw_tags(tag.id))
dlg.setWindowTitle(f"Raw {tag.name()} Tag View")
dlg.exec()
dlg.open()
def show_wiki(self) -> None:
webbrowser.open("https://github.com/comictagger/comictagger/wiki")
@ -2057,9 +2051,8 @@ class TaggerWindow(QtWidgets.QMainWindow):
"File Rename", "If you rename files now, unsaved data in the form will be lost. Are you sure?"
):
dlg = RenameWindow(self, ca_list, self.selected_read_tags, self.config, self.talkers)
if dlg.exec() and self.comic_archive is not None:
self.fileSelectionList.update_selected_rows()
self.load_archive(self.comic_archive)
dlg.finished.connect(self._reload_page)
dlg.open()
def load_archive(self, comic_archive: ComicArchive) -> None:
self.comic_archive = None
@ -2068,8 +2061,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
if not os.path.exists(comic_archive.path):
self.fileSelectionList.dirty_flag = False
self.fileSelectionList.remove_archive_list([comic_archive])
QtCore.QTimer.singleShot(1, self.fileSelectionList.revert_selection)
self.fileSelectionList.remove_deleted()
return
self.config[0].internal__last_opened_folder = os.path.abspath(os.path.split(comic_archive.path)[0])
@ -2123,16 +2115,20 @@ class TaggerWindow(QtWidgets.QMainWindow):
def version_check_complete(self, new_version: tuple[str, str]) -> None:
if new_version[0] not in (self.version, self.config[0].Dialog_Flags__dont_notify_about_this_version):
website = "https://github.com/comictagger/comictagger"
checked = OptionalMessageDialog.msg(
def set_checked(checked: bool) -> None:
if checked:
self.config[0].Dialog_Flags__dont_notify_about_this_version = new_version[0]
OptionalMessageDialog.msg(
self,
"New version available!",
f"New version ({new_version[1]}) available!<br>(You are currently running {self.version})<br><br>"
f"Visit <a href='{website}/releases/latest'>{website}/releases/latest</a> for more info.<br><br>",
False,
"Don't tell me about this version again",
callback=set_checked,
checked=False,
check_text="Don't tell me about this version again",
)
if checked:
self.config[0].Dialog_Flags__dont_notify_about_this_version = new_version[0]
def on_incoming_socket_connection(self) -> None:
# Accept connection from other instance.

View File

@ -8,21 +8,16 @@ import traceback
import webbrowser
from collections.abc import Collection, Sequence
from PyQt6.QtCore import QUrl
from PyQt6.QtGui import QGuiApplication, QPalette
from PyQt6.QtWidgets import QWidget
logger = logging.getLogger(__name__)
try:
from PyQt6 import QtGui, QtWidgets
from PyQt6.QtCore import Qt
from PyQt6.QtCore import Qt, QUrl
from PyQt6.QtGui import QGuiApplication, QPalette
from PyQt6.QtWidgets import QWidget
qt_available = True
except ImportError:
qt_available = False
if qt_available:
try:
from PIL import Image
@ -30,6 +25,7 @@ if qt_available:
except ImportError:
pil_available = False
active_palette: QPalette | None = None
try:
from PyQt6.QtWebEngineCore import QWebEnginePage
from PyQt6.QtWebEngineWidgets import QWebEngineView
@ -156,7 +152,7 @@ if qt_available:
if e:
trace = "\n".join(traceback.format_exception(type(e), e, e.__traceback__))
QtWidgets.QMessageBox.critical(QtWidgets.QMainWindow(), "Error", msg + trace)
return critical(QtWidgets.QMainWindow(), "Error", msg + trace)
def enable_widget(widget: QtWidgets.QWidget | Collection[QtWidgets.QWidget], enable: bool) -> None:
if isinstance(widget, Sequence):
@ -240,3 +236,27 @@ if qt_available:
# QSplitter has issues with replacing a widget before it's been first shown. Assume it should be visible
new_widget.show()
return new_widget
def critical(parent: QWidget | None, title: str, text: str) -> None:
qmsg = QtWidgets.QMessageBox(parent)
qmsg.setIcon(qmsg.Icon.Critical)
qmsg.setText(title)
qmsg.setInformativeText(text)
return qmsg.show()
def warning(parent: QWidget | None, title: str, text: str) -> None:
qmsg = QtWidgets.QMessageBox(parent)
qmsg.setIcon(qmsg.Icon.Warning)
qmsg.setText(title)
qmsg.setInformativeText(text)
return qmsg.show()
def information(parent: QWidget | None, title: str, text: str) -> None:
qmsg = QtWidgets.QMessageBox(parent)
qmsg.setIcon(qmsg.Icon.Information)
qmsg.setText(title)
qmsg.setInformativeText(text)
return qmsg.show()
except ImportError:
qt_available = False

View File

@ -10,6 +10,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.ctsettings import ct_ns, group_for_plugin
from comictaggerlib.ui import qtutils
from comictalker.comictalker import ComicTalker
logger = logging.getLogger(__name__)
@ -76,9 +77,9 @@ def generate_api_widgets(
def call_check_api(*args: Any, tab: TalkerTab, talker: ComicTalker, definitions: settngs.Definitions) -> None:
check_text, check_bool = talker.check_status(get_config_from_tab(tab, definitions[group_for_plugin(talker)]))
if check_bool:
QtWidgets.QMessageBox.information(None, "API Test Success", check_text)
else:
QtWidgets.QMessageBox.warning(None, "API Test Failed", check_text)
return qtutils.information(TalkerTab.tab, "API Test Success", check_text)
qtutils.warning(TalkerTab.tab, "API Test Failed", check_text)
# get the actual config objects in case they have overwritten the default
btn_test_row = None