diff --git a/comictaggerlib/autotagmatchwindow.py b/comictaggerlib/autotagmatchwindow.py index a9494d7..3d3ba78 100644 --- a/comictaggerlib/autotagmatchwindow.py +++ b/comictaggerlib/autotagmatchwindow.py @@ -39,7 +39,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog): self, parent: QtWidgets.QWidget, match_set_list: list[Result], - style: str, + styles: list[str], fetch_func: Callable[[IssueResult], GenericMetadata], config: ct_ns, talker: ComicTalker, @@ -81,7 +81,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog): self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setText("Accept and Write Tags") self.match_set_list = match_set_list - self._style = style + self._styles = styles self.fetch_func = fetch_func self.current_match_set_idx = 0 @@ -230,7 +230,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog): match = self.current_match() ca = ComicArchive(self.current_match_set.original_path) - md = ca.read_metadata(self._style) + md = ca.read_metadata(self.config.internal__load_data_style) if md.is_empty: md = ca.metadata_from_filename( self.config.Filename_Parsing__complicated_parser, @@ -247,10 +247,15 @@ class AutoTagMatchWindow(QtWidgets.QDialog): QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor)) md.overlay(ct_md) - success = ca.write_metadata(md, self._style) + for style in self._styles: + success = ca.write_metadata(md, style) + QtWidgets.QApplication.restoreOverrideCursor() + if not success: + QtWidgets.QMessageBox.warning( + self, + "Write Error", + f"Saving {metadata_styles[style].name()} the tags to the archive seemed to fail!", + ) + break + ca.load_cache(list(metadata_styles)) - - QtWidgets.QApplication.restoreOverrideCursor() - - if not success: - QtWidgets.QMessageBox.warning(self, "Write Error", "Saving the tags to the archive seemed to fail!") diff --git a/comictaggerlib/ctsettings/file.py b/comictaggerlib/ctsettings/file.py index 21488e5..fb555c5 100644 --- a/comictaggerlib/ctsettings/file.py +++ b/comictaggerlib/ctsettings/file.py @@ -18,12 +18,13 @@ def general(parser: settngs.Manager) -> None: action=argparse.BooleanOptionalAction, help="Disable the ComicRack metadata type", ) + parser.add_setting("use_short_metadata_names", default=False, action=argparse.BooleanOptionalAction, cmdline=False) def internal(parser: settngs.Manager) -> None: # automatic settings parser.add_setting("install_id", default=uuid.uuid4().hex, cmdline=False) - parser.add_setting("save_data_style", default="cbi", cmdline=False) + parser.add_setting("save_data_style", default=["cbi"], cmdline=False) parser.add_setting("load_data_style", default="cbi", cmdline=False) parser.add_setting("last_opened_folder", default="", cmdline=False) parser.add_setting("window_width", default=0, cmdline=False) @@ -255,6 +256,11 @@ def parse_filter(config: settngs.Config[ct_ns]) -> settngs.Config[ct_ns]: def validate_file_settings(config: settngs.Config[ct_ns]) -> settngs.Config[ct_ns]: config = parse_filter(config) + + # TODO Remove this conversion check at a later date + if isinstance(config[0].internal__save_data_style, str): + config[0].internal__save_data_style = [config[0].internal__save_data_style] + if config[0].Filename_Parsing__protofolius_issue_number_scheme: config[0].Filename_Parsing__allow_issue_start_with_letter = True diff --git a/comictaggerlib/ctsettings/settngs_namespace.py b/comictaggerlib/ctsettings/settngs_namespace.py index b807eaf..896c334 100644 --- a/comictaggerlib/ctsettings/settngs_namespace.py +++ b/comictaggerlib/ctsettings/settngs_namespace.py @@ -37,7 +37,7 @@ class settngs_namespace(settngs.TypedNS): Runtime_Options__files: list[str] internal__install_id: str - internal__save_data_style: str + internal__save_data_style: list[str] internal__load_data_style: str internal__last_opened_folder: str internal__window_width: int @@ -97,6 +97,7 @@ class settngs_namespace(settngs.TypedNS): General__check_for_new_version: bool General__disable_cr: bool + General__use_short_metadata_names: bool Dialog_Flags__show_disclaimer: bool Dialog_Flags__dont_notify_about_this_version: str diff --git a/comictaggerlib/pagelisteditor.py b/comictaggerlib/pagelisteditor.py index 1c5fd20..9511203 100644 --- a/comictaggerlib/pagelisteditor.py +++ b/comictaggerlib/pagelisteditor.py @@ -115,7 +115,7 @@ class PageListEditor(QtWidgets.QWidget): self.comic_archive: ComicArchive | None = None self.pages_list: list[ImageMetadata] = [] - self.data_style = "" + self.data_styles: list[str] = [] def reset_page(self) -> None: self.pageWidget.clear() @@ -326,7 +326,7 @@ class PageListEditor(QtWidgets.QWidget): self.comic_archive = comic_archive self.pages_list = pages_list if pages_list: - self.set_metadata_style(self.data_style) + self.set_metadata_style(self.data_styles) else: self.cbPageType.setEnabled(False) self.chkDoublePage.setEnabled(False) @@ -370,11 +370,15 @@ class PageListEditor(QtWidgets.QWidget): self.first_front_page = self.get_first_front_cover() self.firstFrontCoverChanged.emit(self.first_front_page) - def set_metadata_style(self, data_style: str) -> None: + def set_metadata_style(self, data_styles: list[str]) -> None: # depending on the current data style, certain fields are disabled - if data_style: - self.metadata_style = data_style + if data_styles: + styles = [metadata_styles[style] for style in data_styles] + enabled_widgets = set() + for style in styles: + enabled_widgets.update(style.supported_attributes) + + self.data_styles = data_styles - enabled_widgets = metadata_styles[data_style].supported_attributes for metadata, widget in self.md_attributes.items(): enable_widget(widget, metadata in enabled_widgets) diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index bc5f5c2..23c3358 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -376,6 +376,7 @@ class SettingsWindow(QtWidgets.QDialog): self.tePublisherFilter.setPlainText("\n".join(self.config[0].Issue_Identifier__publisher_filter)) self.cbxCheckForNewVersion.setChecked(self.config[0].General__check_for_new_version) + self.cbxShortMetadataNames.setChecked(self.config[0].General__use_short_metadata_names) self.cbxComplicatedParser.setChecked(self.config[0].Filename_Parsing__complicated_parser) self.cbxRemoveC2C.setChecked(self.config[0].Filename_Parsing__remove_c2c) @@ -493,6 +494,12 @@ class SettingsWindow(QtWidgets.QDialog): self.config[0].General__check_for_new_version = self.cbxCheckForNewVersion.isChecked() + # Update metadata style names if required + if self.cbxShortMetadataNames.isChecked() != self.config[0].General__use_short_metadata_names: + self.config[0].General__use_short_metadata_names = self.cbxShortMetadataNames.isChecked() + self.parent().populate_style_names() + self.parent().adjust_save_style_combo() + self.config[0].Issue_Identifier__series_match_identify_thresh = self.sbNameMatchIdentifyThresh.value() self.config[0].Issue_Identifier__series_match_search_thresh = self.sbNameMatchSearchThresh.value() self.config[0].Issue_Identifier__publisher_filter = utils.split(self.tePublisherFilter.toPlainText(), "\n") diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index a695f4d..9e31465 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -215,15 +215,16 @@ class TaggerWindow(QtWidgets.QMainWindow): if config[0].Runtime_Options__type and isinstance(config[0].Runtime_Options__type[0], str): # respect the command line option tag type - config[0].internal__save_data_style = config[0].Runtime_Options__type[0] + config[0].internal__save_data_style = config[0].Runtime_Options__type config[0].internal__load_data_style = config[0].Runtime_Options__type[0] - if config[0].internal__save_data_style not in metadata_styles: - config[0].internal__save_data_style = list(metadata_styles.keys())[0] + for style in config[0].internal__save_data_style: + if style not in metadata_styles: + config[0].internal__save_data_style.remove(style) if config[0].internal__load_data_style not in metadata_styles: config[0].internal__load_data_style = list(metadata_styles.keys())[0] - self.save_data_style = config[0].internal__save_data_style - self.load_data_style = config[0].internal__load_data_style + self.save_data_styles: list[str] = config[0].internal__save_data_style + self.load_data_style: str = config[0].internal__load_data_style self.setAcceptDrops(True) self.view_tag_actions, self.remove_tag_actions = self.tag_actions() @@ -273,7 +274,7 @@ class TaggerWindow(QtWidgets.QMainWindow): # hook up the callbacks self.cbLoadDataStyle.currentIndexChanged.connect(self.set_load_data_style) - self.cbSaveDataStyle.currentIndexChanged.connect(self.set_save_data_style) + self.cbSaveDataStyle.itemChecked.connect(self.set_save_data_style) self.cbx_sources.currentIndexChanged.connect(self.set_source) self.btnEditCredit.clicked.connect(self.edit_credit) self.btnAddCredit.clicked.connect(self.add_credit) @@ -438,7 +439,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.actionCopyTags.triggered.connect(self.copy_tags) self.actionRemoveAuto.setShortcut("Ctrl+D") - self.actionRemoveAuto.setStatusTip("Remove currently selected modify tag style from the archive") + self.actionRemoveAuto.setStatusTip("Remove currently selected modify tag style(s) from the archive") self.actionRemoveAuto.triggered.connect(self.remove_auto) self.actionRepackage.setShortcut("Ctrl+E") @@ -1146,7 +1147,7 @@ class TaggerWindow(QtWidgets.QMainWindow): reply = QtWidgets.QMessageBox.question( self, "Save Tags", - f"Are you sure you wish to save {metadata_styles[self.save_data_style].name()} tags to this archive?", + f"Are you sure you wish to save {', '.join([metadata_styles[style].name() for style in self.save_data_styles])} tags to this archive?", QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No, ) @@ -1155,12 +1156,23 @@ class TaggerWindow(QtWidgets.QMainWindow): QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor)) self.form_to_metadata() - success = self.comic_archive.write_metadata(self.metadata, self.save_data_style) + failed_style: str = "" + # Save each style + for style in self.save_data_styles: + success = self.comic_archive.write_metadata(self.metadata, style) + if not success: + failed_style = metadata_styles[style].name() + break + self.comic_archive.load_cache(list(metadata_styles)) QtWidgets.QApplication.restoreOverrideCursor() - if not success: - QtWidgets.QMessageBox.warning(self, "Save failed", "The tag save operation seemed to fail!") + if failed_style: + QtWidgets.QMessageBox.warning( + self, + "Save failed", + f"The tag save operation seemed to fail for: {failed_style}", + ) else: self.clear_dirty_flag() self.update_info_box() @@ -1186,9 +1198,9 @@ class TaggerWindow(QtWidgets.QMainWindow): self.adjust_load_style_combo() self.cbLoadDataStyle.currentIndexChanged.connect(self.set_load_data_style) - def set_save_data_style(self, s: str) -> None: - self.save_data_style = self.cbSaveDataStyle.itemData(s) - self.config[0].internal__save_data_style = self.save_data_style + def set_save_data_style(self) -> None: + self.save_data_styles = self.cbSaveDataStyle.currentData() + self.config[0].internal__save_data_style = self.save_data_styles self.update_metadata_style_tweaks() self.update_menus() @@ -1196,13 +1208,16 @@ class TaggerWindow(QtWidgets.QMainWindow): self.config[0].Sources__source = self.cbx_sources.itemData(s) def update_metadata_credit_colors(self) -> None: - style = metadata_styles[self.save_data_style] - enabled = style.supported_attributes + styles = [metadata_styles[style] for style in self.save_data_styles] + enabled = set() + for style in styles: + enabled.update(style.supported_attributes) + credit_attributes = [x for x in self.md_attributes.items() if "credits." in x[0]] for r in range(self.twCredits.rowCount()): w = self.twCredits.item(r, 1) - supports_role = style.supports_credit_role(str(w.text())) + supports_role = any(style.supports_credit_role(str(w.text())) for style in styles) for credit in credit_attributes: widget_enabled = credit[0] in enabled widget = self.twCredits.item(r, credit[1]) @@ -1212,14 +1227,16 @@ class TaggerWindow(QtWidgets.QMainWindow): def update_metadata_style_tweaks(self) -> None: # depending on the current data style, certain fields are disabled + enabled_widgets = set() + for style in self.save_data_styles: + enabled_widgets.update(metadata_styles[style].supported_attributes) - enabled_widgets = metadata_styles[self.save_data_style].supported_attributes for metadata, widget in self.md_attributes.items(): if widget is not None and not isinstance(widget, (int)): enable_widget(widget, metadata in enabled_widgets) self.update_metadata_credit_colors() - self.page_list_editor.set_metadata_style(self.save_data_style) + self.page_list_editor.set_metadata_style(self.save_data_styles) def cell_double_clicked(self, r: int, c: int) -> None: self.edit_credit() @@ -1353,14 +1370,26 @@ class TaggerWindow(QtWidgets.QMainWindow): def adjust_save_style_combo(self) -> None: # select the current style - self.cbSaveDataStyle.setCurrentIndex(self.cbSaveDataStyle.findData(self.save_data_style)) + unchecked = set(metadata_styles.keys()) - set(self.save_data_styles) + for style in self.save_data_styles: + self.cbSaveDataStyle.setItemChecked(self.cbLoadDataStyle.findData(style), True) + for style in unchecked: + self.cbSaveDataStyle.setItemChecked(self.cbLoadDataStyle.findData(style), False) self.update_metadata_style_tweaks() - def populate_combo_boxes(self) -> None: + def populate_style_names(self) -> None: + # First clear all entries (called from settingswindow.py) + self.cbSaveDataStyle.clear() # Add the entries to the tag style combobox for style in metadata_styles.values(): self.cbLoadDataStyle.addItem(style.name(), style.short_name) - self.cbSaveDataStyle.addItem(style.name(), style.short_name) + if self.config[0].General__use_short_metadata_names: + self.cbSaveDataStyle.addItem(style.short_name.upper(), style.short_name) + else: + self.cbSaveDataStyle.addItem(style.name(), style.short_name) + + def populate_combo_boxes(self) -> None: + self.populate_style_names() self.adjust_load_style_combo() self.adjust_save_style_combo() @@ -1461,19 +1490,26 @@ class TaggerWindow(QtWidgets.QMainWindow): self.cbFormat.addItem("Year One") def remove_auto(self) -> None: - self.remove_tags(self.save_data_style) + self.remove_tags(self.save_data_styles) - def remove_tags(self, style: str) -> None: + def remove_tags(self, styles: list[str]) -> None: # remove the indicated tags from the archive ca_list = self.fileSelectionList.get_selected_archive_list() has_md_count = 0 + file_md_count = {} + for style in styles: + file_md_count[style] = 0 for ca in ca_list: - if ca.has_metadata(style): - has_md_count += 1 + for style in styles: + if ca.has_metadata(style): + has_md_count += 1 + file_md_count[style] += 1 if has_md_count == 0: QtWidgets.QMessageBox.information( - self, "Remove Tags", f"No archives with {metadata_styles[style].name()} tags selected!" + self, + "Remove Tags", + f"No archives with {', '.join([metadata_styles[style].name() for style in styles])} tags selected!", ) return @@ -1486,7 +1522,7 @@ class TaggerWindow(QtWidgets.QMainWindow): reply = QtWidgets.QMessageBox.question( self, "Remove Tags", - f"Are you sure you wish to remove the {metadata_styles[style].name()} tags from {has_md_count} archive(s)?", + f"Are you sure you wish to remove {', '.join([f'{metadata_styles[style].name()} tags from {count} files' for style, count in file_md_count.items()])} removing a total of {has_md_count} tag(s)?", QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No, ) @@ -1508,12 +1544,16 @@ class TaggerWindow(QtWidgets.QMainWindow): progdialog.setValue(prog_idx) progdialog.setLabelText(str(ca.path)) QtCore.QCoreApplication.processEvents() - if ca.has_metadata(style) and ca.is_writable(): - if not ca.remove_metadata(style): - failed_list.append(ca.path) - else: - success_count += 1 - ca.load_cache(list(metadata_styles)) + for style in styles: + if ca.has_metadata(style) and ca.is_writable(): + if ca.remove_metadata(style): + success_count += 1 + else: + failed_list.append(ca.path) + # Abandon any further tag removals to prevent any greater damage to archive + break + ca.reset_cache() + ca.load_cache(list(metadata_styles)) progdialog.hide() QtCore.QCoreApplication.processEvents() @@ -1521,7 +1561,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.update_info_box() self.update_menus() - summary = f"Successfully removed tags in {success_count} archive(s)." + summary = f"Successfully removed {success_count} tags in archive(s)." if len(failed_list) > 0: summary += f"\n\nThe remove operation failed in the following {len(failed_list)} archive(s):\n" for f in failed_list: @@ -1538,9 +1578,13 @@ class TaggerWindow(QtWidgets.QMainWindow): has_src_count = 0 src_style = self.load_data_style - dest_style = self.save_data_style + dest_styles = self.save_data_styles - if src_style == dest_style: + # Remove the read style from the write style + if src_style in dest_styles: + dest_styles.remove(src_style) + + if not dest_styles: QtWidgets.QMessageBox.information( self, "Copy Tags", "Can't copy tag style onto itself. Read style and modify style must be different." ) @@ -1551,7 +1595,9 @@ class TaggerWindow(QtWidgets.QMainWindow): has_src_count += 1 if has_src_count == 0: - QtWidgets.QMessageBox.information(self, "Copy Tags", f"No archives with {src_style} tags selected!") + QtWidgets.QMessageBox.information( + self, "Copy Tags", f"No archives with {metadata_styles[src_style].name()} tags selected!" + ) return if has_src_count != 0 and not self.dirty_flag_verification( @@ -1563,8 +1609,9 @@ class TaggerWindow(QtWidgets.QMainWindow): reply = QtWidgets.QMessageBox.question( self, "Copy Tags", - f"Are you sure you wish to copy the {metadata_styles[src_style].name()}" - + f" tags to {metadata_styles[dest_style].name()} tags in {has_src_count} archive(s)?", + f"Are you sure you wish to copy the {metadata_styles[src_style].name()} " + f"tags to {', '.join([metadata_styles[style].name() for style in dest_styles])} tags in " + f"{has_src_count} archive(s)?", QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No, ) @@ -1580,26 +1627,36 @@ class TaggerWindow(QtWidgets.QMainWindow): failed_list = [] success_count = 0 for prog_idx, ca in enumerate(ca_list, 1): - QtCore.QCoreApplication.processEvents() - if prog_dialog.wasCanceled(): - break - - prog_dialog.setValue(prog_idx) - prog_dialog.setLabelText(str(ca.path)) - QtCore.QCoreApplication.processEvents() + ca_saved = False if ca.has_metadata(src_style) and ca.is_writable(): md = ca.read_metadata(src_style) + else: + continue - if dest_style == "cbi" and self.config[0].Comic_Book_Lover__apply_transform_on_bulk_operation: + for style in dest_styles: + if ca.has_metadata(style): + QtCore.QCoreApplication.processEvents() + if prog_dialog.wasCanceled(): + break + + prog_dialog.setValue(prog_idx) + prog_dialog.setLabelText(str(ca.path)) + center_window_on_parent(prog_dialog) + QtCore.QCoreApplication.processEvents() + + if style == "cbi" and self.config[0].Comic_Book_Lover__apply_transform_on_bulk_operation: md = CBLTransformer(md, self.config[0]).apply() - if not ca.write_metadata(md, dest_style): - failed_list.append(ca.path) + if ca.write_metadata(md, style): + if not ca_saved: + success_count += 1 + ca_saved = True else: - success_count += 1 + failed_list.append(ca.path) - ca.load_cache([self.load_data_style, self.save_data_style, src_style, dest_style]) + ca.reset_cache() + ca.load_cache([self.load_data_style, *self.save_data_styles]) prog_dialog.hide() QtCore.QCoreApplication.processEvents() @@ -1647,12 +1704,22 @@ class TaggerWindow(QtWidgets.QMainWindow): def identify_and_tag_single_archive( self, ca: ComicArchive, match_results: OnlineMatchResults, dlg: AutoTagStartWindow ) -> tuple[bool, OnlineMatchResults]: + def metadata_save() -> bool: + for style in self.save_data_styles: + # write out the new data + if not ca.write_metadata(md, style): + self.auto_tag_log( + f"{metadata_styles[style].name()} save failed! Aborting any additional style saves.\n" + ) + return False + return True + success = False ii = IssueIdentifier(ca, self.config[0], self.current_talker()) # read in metadata, and parse file name if not there try: - md = ca.read_metadata(self.save_data_style) + md = ca.read_metadata(self.load_data_style) except Exception as e: md = GenericMetadata() logger.error("Failed to load metadata for %s: %s", ca.path, e) @@ -1792,36 +1859,33 @@ class TaggerWindow(QtWidgets.QMainWindow): if self.config[0].Issue_Identifier__auto_imprint: md.fix_publisher() - if not ca.write_metadata(md, self.save_data_style): - match_results.write_failures.append( - Result( - Action.save, - Status.write_failure, - ca.path, - online_results=matches, - match_status=MatchStatus.good_match, - ) - ) - self.auto_tag_log("Save failed ;-(\n") - else: - match_results.good_matches.append( - Result( - Action.save, - Status.success, - ca.path, - online_results=matches, - match_status=MatchStatus.good_match, - ) - ) + res = Result( + Action.save, + status=Status.success, + original_path=ca.path, + online_results=matches, + match_status=MatchStatus.good_match, + md=md, + tags_written=self.save_data_styles, + ) + + # Save styles + if metadata_save(): + match_results.good_matches.append(res) success = True self.auto_tag_log("Save complete!\n") - ca.load_cache([self.load_data_style, self.save_data_style]) + else: + res.status = Status.write_failure + match_results.write_failures.append(res) + + ca.reset_cache() + ca.load_cache([self.load_data_style] + self.save_data_styles) return success, match_results def auto_tag(self) -> None: ca_list = self.fileSelectionList.get_selected_archive_list() - style = self.save_data_style + styles = self.save_data_styles if len(ca_list) == 0: QtWidgets.QMessageBox.information(self, "Auto-Tag", "No archives selected!") @@ -1837,7 +1901,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.config[0], ( f"You have selected {len(ca_list)} archive(s) to automatically identify and write " - + metadata_styles[style].name() + + ", ".join([metadata_styles[style].name() for style in styles]) + " tags to.\n\nPlease choose config below, and select OK to Auto-Tag." ), ) @@ -1864,7 +1928,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.auto_tag_log(f"Auto-Tagging {prog_idx} of {len(ca_list)}\n") self.auto_tag_log(f"{ca.path}\n") try: - cover_idx = ca.read_metadata(style).get_cover_page_index_list()[0] + cover_idx = ca.read_metadata(self.load_data_style).get_cover_page_index_list()[0] except Exception as e: cover_idx = 0 logger.error("Failed to load metadata for %s: %s", ca.path, e) @@ -1898,7 +1962,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.atprogdialog = None summary = "" - summary += f"Successfully tagged archives: {len(match_results.good_matches)}\n" + summary += f"Successfully added {', '.join([metadata_styles[style].name() for style in self.save_data_styles])} tags to {len(match_results.good_matches)} archive(s)\n" if len(match_results.multiple_matches) > 0: summary += f"Archives with multiple matches: {len(match_results.multiple_matches)}\n" @@ -1934,7 +1998,7 @@ class TaggerWindow(QtWidgets.QMainWindow): matchdlg = AutoTagMatchWindow( self, match_results.multiple_matches, - style, + styles, self.actual_issue_data_fetch, self.config[0], self.current_talker(), diff --git a/comictaggerlib/ui/customwidgets.py b/comictaggerlib/ui/customwidgets.py new file mode 100644 index 0000000..b4efabf --- /dev/null +++ b/comictaggerlib/ui/customwidgets.py @@ -0,0 +1,114 @@ +"""Custom widgets""" + +from __future__ import annotations + +from typing import Any + +from PyQt5 import QtGui, QtWidgets +from PyQt5.QtCore import QEvent, QRect, Qt, pyqtSignal + + +# Multiselect combobox from: https://gis.stackexchange.com/a/351152 (with custom changes) +class CheckableComboBox(QtWidgets.QComboBox): + itemChecked = pyqtSignal(str, bool) + + def __init__(self, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) + # Prevent popup from closing when clicking on an item + self.view().viewport().installEventFilter(self) + + # Keeps track of when the combobox list is shown + self.justShown = False + + def resizeEvent(self, event: Any) -> None: + # Recompute text to elide as needed + super().resizeEvent(event) + self._updateText() + + def eventFilter(self, obj: Any, event: Any) -> bool: + # Allow events before the combobox list is shown + if obj == self.view().viewport(): + # We record that the combobox list has been shown + if event.type() == QEvent.Show: + self.justShown = True + # We record that the combobox list has hidden, + # this will happen if the user does not make a selection + # but clicks outside of the combobox list or presses escape + if event.type() == QEvent.Hide: + self._updateText() + self.justShown = False + # QEvent.MouseButtonPress is inconsistent on activation because double clicks are a thing + if event.type() == QEvent.MouseButtonRelease: + # If self.justShown is true it means that they clicked on the combobox to change the checked items + # This is standard behavior (on macos) but I think it is surprising when it has a multiple select + if self.justShown: + self.justShown = False + return True + + # Find the current index and item + index = self.view().indexAt(event.pos()) + self.toggleItem(index.row()) + return True + return False + + def currentData(self) -> list[Any]: + # Return the list of all checked items data + res = [] + for i in range(self.count()): + item = self.model().item(i) + if item.checkState() == Qt.Checked: + res.append(self.itemData(i)) + return res + + def addItem(self, text: str, data: Any = None) -> None: + super().addItem(text, data) + # Need to enable the checkboxes and require one checked item + # Expected that state of *all* checkboxes will be set ('adjust_save_style_combo' in taggerwindow.py) + if self.count() == 1: + self.model().item(0).setCheckState(Qt.CheckState.Checked) + + def _updateText(self) -> None: + texts = [] + for i in range(self.count()): + item = self.model().item(i) + if item.checkState() == Qt.Checked: + texts.append(item.text()) + text = ", ".join(texts) + + # Compute elided text (with "...") + + # The QStyleOptionComboBox is needed for the call to subControlRect + so = QtWidgets.QStyleOptionComboBox() + # init with the current widget + so.initFrom(self) + + # Ask the style for the size of the text field + rect = self.style().subControlRect(QtWidgets.QStyle.CC_ComboBox, so, QtWidgets.QStyle.SC_ComboBoxEditField) + + # Compute the elided text + elidedText = self.fontMetrics().elidedText(text, Qt.ElideRight, rect.width()) + + # This CheckableComboBox does not use the index, so we clear it and set the placeholder text + self.setCurrentIndex(-1) + self.setPlaceholderText(elidedText) + + def setItemChecked(self, index: Any, state: bool) -> None: + qt_state = Qt.Checked if state else Qt.Unchecked + item = self.model().item(index) + current = self.currentData() + # If we have at least one item checked emit itemChecked with the current check state and update text + # Require at least one item to be checked and provide a tooltip + if len(current) == 1 and not state and item.checkState() == Qt.Checked: + QtWidgets.QToolTip.showText(QtGui.QCursor.pos(), self.toolTip(), self, QRect(), 3000) + return + + if len(current) > 0: + item.setCheckState(qt_state) + self.itemChecked.emit(self.itemData(index), state) + self._updateText() + + def toggleItem(self, index: int) -> None: + if self.model().item(index).checkState() == Qt.Checked: + self.setItemChecked(index, False) + else: + self.setItemChecked(index, True) diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui index 33e1663..9e571b9 100644 --- a/comictaggerlib/ui/settingswindow.ui +++ b/comictaggerlib/ui/settingswindow.ui @@ -6,8 +6,8 @@ 0 0 - 702 - 559 + 703 + 574 @@ -41,7 +41,7 @@ - + @@ -54,23 +54,7 @@ - - - - - 0 - 0 - - - - Revert to default settings - - - true - - - - + @@ -83,7 +67,7 @@ - + @@ -99,6 +83,22 @@ + + + + + 0 + 0 + + + + Revert to default settings + + + true + + + @@ -106,6 +106,16 @@ + + + + Use the short name for the metadata styles (CBI, CR, etc.) + + + Use "short" names for metadata styles + + + @@ -411,7 +421,7 @@ 11 21 251 - 199 + 206 diff --git a/comictaggerlib/ui/taggerwindow.ui b/comictaggerlib/ui/taggerwindow.ui index 0033a28..4d253a3 100644 --- a/comictaggerlib/ui/taggerwindow.ui +++ b/comictaggerlib/ui/taggerwindow.ui @@ -7,7 +7,7 @@ 0 0 1096 - 621 + 658 @@ -52,43 +52,49 @@ - + QFormLayout::AllNonFixedFieldsGrow + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignHCenter|Qt::AlignTop - - - - Read Style - - - - - - - - - - Modify Style - - - - - - - Metadata Source + Data Source + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + + Read Style + + + + + + + Modify Styles + + + @@ -825,9 +831,6 @@ Primary - - AlignCenter - @@ -1166,7 +1169,7 @@ 0 0 1096 - 21 + 28 @@ -1383,7 +1386,7 @@ - Remove Current 'Modify' Tag Style + Remove Current 'Modify' Tag Style(s) @@ -1458,6 +1461,13 @@ + + + CheckableComboBox + QComboBox +
comictaggerlib.ui.customwidgets
+
+