From 23021ba63278c0f215a0c126274be1974afc2d6e Mon Sep 17 00:00:00 2001 From: Mizaki Date: Sat, 27 Jan 2024 01:41:48 +0000 Subject: [PATCH] Add support for saving multiple metadata styles in the GUI Unwind credit color comprehension Convert save style from a string setting to a list Use lordwelch version of Checkable combobox Improve readbility, fix label alignment in taggerwindow.ui, better report removal of tags and clearer number meanings. Unwind list comprehension for easier readability --- comictaggerlib/autotagmatchwindow.py | 18 +- comictaggerlib/ctsettings/file.py | 7 +- .../ctsettings/settngs_namespace.py | 2 +- comictaggerlib/pagelisteditor.py | 17 +- comictaggerlib/taggerwindow.py | 226 +++++++++++------- comictaggerlib/ui/customwidgets.py | 113 +++++++++ comictaggerlib/ui/taggerwindow.ui | 39 +-- 7 files changed, 297 insertions(+), 125 deletions(-) create mode 100644 comictaggerlib/ui/customwidgets.py diff --git a/comictaggerlib/autotagmatchwindow.py b/comictaggerlib/autotagmatchwindow.py index a9494d7..b47561d 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,10 @@ 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", "Saving the tags to the archive seemed to fail!") + 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..7053d1d 100644 --- a/comictaggerlib/ctsettings/file.py +++ b/comictaggerlib/ctsettings/file.py @@ -23,7 +23,7 @@ def general(parser: settngs.Manager) -> None: 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 +255,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..ad7a6e6 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 diff --git a/comictaggerlib/pagelisteditor.py b/comictaggerlib/pagelisteditor.py index 1c5fd20..4696d1c 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,16 @@ 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 = [] + for style in styles: + for attr in style.supported_attributes: + enabled_widgets.append(attr) + + 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/taggerwindow.py b/comictaggerlib/taggerwindow.py index a695f4d..ef2d96e 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -61,6 +61,7 @@ from comictaggerlib.resulttypes import Action, IssueResult, MatchStatus, OnlineM from comictaggerlib.seriesselectionwindow import SeriesSelectionWindow from comictaggerlib.settingswindow import SettingsWindow from comictaggerlib.ui import ui_path +from comictaggerlib.ui.customwidgets import CheckableComboBox from comictaggerlib.ui.qtutils import center_window_on_parent, enable_widget, reduce_widget_font_size from comictaggerlib.versionchecker import VersionChecker from comictalker.comictalker import ComicTalker, TalkerError @@ -215,15 +216,29 @@ 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 + + # Add multiselect combobox + self.cbSaveDataStyle = CheckableComboBox() + self.cbSaveDataStyle.setToolTip("At least 1 save style is required") + # Add normal combobox for read style (TODO support multiple read styles) + self.cbLoadDataStyle = QtWidgets.QComboBox() + + # Need to set minimum or source_style_formLayout will resize larger than 230px which will affect the + # file info box and cover image width underneath + self.cbLoadDataStyle.setMinimumWidth(100) + self.cbSaveDataStyle.setMinimumWidth(100) + self.source_style_formLayout.addRow("Read Style", self.cbLoadDataStyle) + self.source_style_formLayout.addRow("Modify Styles", self.cbSaveDataStyle) self.setAcceptDrops(True) self.view_tag_actions, self.remove_tag_actions = self.tag_actions() @@ -273,7 +288,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 +453,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 +1161,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 +1170,22 @@ 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) + fail_list = [] + # Save each style + for style in self.save_data_styles: + success = self.comic_archive.write_metadata(self.metadata, style) + if not success: + fail_list.append(style) + 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 fail_list: + QtWidgets.QMessageBox.warning( + self, + "Save failed", + f"The tag save operation seemed to fail for:{', '.join([metadata_styles[style].name() for style in fail_list])}", + ) else: self.clear_dirty_flag() self.update_info_box() @@ -1186,9 +1211,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 +1221,18 @@ 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 = [] + + for style in styles: + for attr in style.supported_attributes: + enabled.append(attr) + 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 +1242,17 @@ class TaggerWindow(QtWidgets.QMainWindow): def update_metadata_style_tweaks(self) -> None: # depending on the current data style, certain fields are disabled + enabled_widgets = [] + for style in self.save_data_styles: + for attr in metadata_styles[style].supported_attributes: + enabled_widgets.append(attr) - 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,7 +1386,11 @@ 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: @@ -1461,19 +1498,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 +1530,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 +1552,15 @@ 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 not ca.remove_metadata(style): + failed_list.append(ca.path) + # Abandon any further tag removals to prevent any greater damage to archive + break + else: + success_count += 1 + ca.load_cache(list(metadata_styles)) progdialog.hide() QtCore.QCoreApplication.processEvents() @@ -1521,7 +1568,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 +1585,13 @@ class TaggerWindow(QtWidgets.QMainWindow): has_src_count = 0 src_style = self.load_data_style - dest_style = self.save_data_style + dest_styles = list(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 +1602,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 +1616,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 +1634,32 @@ 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 + ca_saved = False + 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)) - QtCore.QCoreApplication.processEvents() + prog_dialog.setValue(prog_idx) + prog_dialog.setLabelText(str(ca.path)) + center_window_on_parent(prog_dialog) + QtCore.QCoreApplication.processEvents() - if ca.has_metadata(src_style) and ca.is_writable(): - md = ca.read_metadata(src_style) + if ca.has_metadata(src_style) and ca.is_writable(): + md = ca.read_metadata(src_style) - if dest_style == "cbi" and self.config[0].Comic_Book_Lover__apply_transform_on_bulk_operation: + 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) - else: - success_count += 1 + if not ca.write_metadata(md, style): + failed_list.append(ca.path) + else: + if not ca_saved: + success_count += 1 + ca_saved = True - ca.load_cache([self.load_data_style, self.save_data_style, src_style, dest_style]) + ca.load_cache([self.load_data_style, self.save_data_style, src_style, style]) prog_dialog.hide() QtCore.QCoreApplication.processEvents() @@ -1652,7 +1712,7 @@ class TaggerWindow(QtWidgets.QMainWindow): # 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 +1852,42 @@ 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, + # Save each style + for style in self.save_data_styles: + if not ca.write_metadata(md, 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, + self.auto_tag_log( + f"{metadata_styles[style].name()} save failed! Aborting any additional style saves.\n" ) - ) - success = True - self.auto_tag_log("Save complete!\n") - ca.load_cache([self.load_data_style, self.save_data_style]) + break + else: + match_results.good_matches.append( + Result( + Action.save, + Status.success, + ca.path, + online_results=matches, + match_status=MatchStatus.good_match, + ) + ) + success = True + self.auto_tag_log(f"{metadata_styles[style].name()} save complete!\n") + + 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 +1903,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 +1930,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 +1964,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.atprogdialog = None summary = "" - summary += f"Successfully tagged archives: {len(match_results.good_matches)}\n" + summary += f"Successfully added {len(match_results.good_matches)} tags to archive(s)\n" if len(match_results.multiple_matches) > 0: summary += f"Archives with multiple matches: {len(match_results.multiple_matches)}\n" @@ -1934,7 +2000,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..ff0a3c9 --- /dev/null +++ b/comictaggerlib/ui/customwidgets.py @@ -0,0 +1,113 @@ +"""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/taggerwindow.ui b/comictaggerlib/ui/taggerwindow.ui index 0033a28..d2929cb 100644 --- a/comictaggerlib/ui/taggerwindow.ui +++ b/comictaggerlib/ui/taggerwindow.ui @@ -7,7 +7,7 @@ 0 0 1096 - 621 + 658 @@ -52,37 +52,23 @@ - + 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 @@ -825,9 +811,6 @@ Primary - - AlignCenter - @@ -1166,7 +1149,7 @@ 0 0 1096 - 21 + 28 @@ -1383,7 +1366,7 @@ - Remove Current 'Modify' Tag Style + Remove Current 'Modify' Tag Style(s)