Merge branch 'mizaki/multi_read' into develop
This commit is contained in:
commit
851339d4e3
@ -39,7 +39,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
self,
|
||||
parent: QtWidgets.QWidget,
|
||||
match_set_list: list[Result],
|
||||
styles: list[str],
|
||||
load_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._styles = styles
|
||||
self.load_data_styles = load_styles
|
||||
self.fetch_func = fetch_func
|
||||
|
||||
self.current_match_set_idx = 0
|
||||
@ -229,8 +229,17 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
def save_match(self) -> None:
|
||||
match = self.current_match()
|
||||
ca = ComicArchive(self.current_match_set.original_path)
|
||||
md, error = self.parent().overlay_ca_read_style(self.load_data_styles, ca)
|
||||
if error is not None:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, error)
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
"Read Failed!",
|
||||
f"One or more of the read styles failed to load for {ca.path}, check log for details",
|
||||
)
|
||||
return
|
||||
|
||||
md = ca.read_metadata(self.config.internal__load_data_style)
|
||||
if md.is_empty:
|
||||
md = ca.metadata_from_filename(
|
||||
self.config.Filename_Parsing__filename_parser,
|
||||
@ -254,7 +263,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
|
||||
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
|
||||
md.overlay(ct_md)
|
||||
for style in self._styles:
|
||||
for style in self.load_data_styles:
|
||||
success = ca.write_metadata(md, style)
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
if not success:
|
||||
@ -265,4 +274,4 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
)
|
||||
break
|
||||
|
||||
ca.load_cache(list(metadata_styles))
|
||||
ca.reset_cache()
|
||||
|
@ -134,7 +134,7 @@ class CLI:
|
||||
|
||||
def actual_metadata_save(self, ca: ComicArchive, md: GenericMetadata) -> bool:
|
||||
if not self.config.Runtime_Options__dryrun:
|
||||
for style in self.config.Runtime_Options__type:
|
||||
for style in self.config.Runtime_Options__type_modify:
|
||||
# write out the new data
|
||||
if not ca.write_metadata(md, style):
|
||||
logger.error("The tag save seemed to fail for style: %s!", md_styles[style].name())
|
||||
@ -249,12 +249,11 @@ class CLI:
|
||||
|
||||
md.overlay(f_md)
|
||||
|
||||
for style in self.config.Runtime_Options__type:
|
||||
for style in self.config.Runtime_Options__type_read:
|
||||
if ca.has_metadata(style):
|
||||
try:
|
||||
t_md = ca.read_metadata(style)
|
||||
md.overlay(t_md)
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
|
||||
@ -264,7 +263,7 @@ class CLI:
|
||||
return md
|
||||
|
||||
def print(self, ca: ComicArchive) -> Result:
|
||||
if not self.config.Runtime_Options__type:
|
||||
if not self.config.Runtime_Options__type_read:
|
||||
page_count = ca.get_number_of_pages()
|
||||
|
||||
brief = ""
|
||||
@ -290,7 +289,7 @@ class CLI:
|
||||
|
||||
md = None
|
||||
for style, style_obj in md_styles.items():
|
||||
if not self.config.Runtime_Options__type or style in self.config.Runtime_Options__type:
|
||||
if not self.config.Runtime_Options__type_read or style in self.config.Runtime_Options__type_read:
|
||||
if ca.has_metadata(style):
|
||||
self.output(f"--------- {style_obj.name()} tags ---------")
|
||||
try:
|
||||
@ -322,7 +321,7 @@ class CLI:
|
||||
|
||||
def delete(self, ca: ComicArchive) -> Result:
|
||||
res = Result(Action.delete, Status.success, ca.path)
|
||||
for style in self.config.Runtime_Options__type:
|
||||
for style in self.config.Runtime_Options__type_modify:
|
||||
status = self.delete_style(ca, style)
|
||||
if status == Status.success:
|
||||
res.tags_deleted.append(style)
|
||||
@ -367,7 +366,9 @@ class CLI:
|
||||
except Exception as e:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
return res
|
||||
for style in self.config.Runtime_Options__type:
|
||||
for style in self.config.Runtime_Options__type_modify:
|
||||
if style == src_style_name:
|
||||
continue
|
||||
status = self.copy_style(ca, res.md, style)
|
||||
if status == Status.success:
|
||||
res.tags_written.append(style)
|
||||
@ -377,7 +378,7 @@ class CLI:
|
||||
|
||||
def save(self, ca: ComicArchive, match_results: OnlineMatchResults) -> tuple[Result, OnlineMatchResults]:
|
||||
if not self.config.Runtime_Options__overwrite:
|
||||
for style in self.config.Runtime_Options__type:
|
||||
for style in self.config.Runtime_Options__type_modify:
|
||||
if ca.has_metadata(style):
|
||||
self.output(f"{ca.path}: Already has {md_styles[style].name()} tags. Not overwriting.")
|
||||
return (
|
||||
@ -385,7 +386,7 @@ class CLI:
|
||||
Action.save,
|
||||
original_path=ca.path,
|
||||
status=Status.existing_tags,
|
||||
tags_written=self.config.Runtime_Options__type,
|
||||
tags_written=self.config.Runtime_Options__type_modify,
|
||||
),
|
||||
match_results,
|
||||
)
|
||||
@ -413,7 +414,7 @@ class CLI:
|
||||
Action.save,
|
||||
original_path=ca.path,
|
||||
status=Status.fetch_data_failure,
|
||||
tags_written=self.config.Runtime_Options__type,
|
||||
tags_written=self.config.Runtime_Options__type_modify,
|
||||
)
|
||||
match_results.fetch_data_failures.append(res)
|
||||
return res, match_results
|
||||
@ -425,7 +426,7 @@ class CLI:
|
||||
status=Status.match_failure,
|
||||
original_path=ca.path,
|
||||
match_status=MatchStatus.no_match,
|
||||
tags_written=self.config.Runtime_Options__type,
|
||||
tags_written=self.config.Runtime_Options__type_modify,
|
||||
)
|
||||
match_results.no_matches.append(res)
|
||||
return res, match_results
|
||||
@ -438,7 +439,7 @@ class CLI:
|
||||
status=Status.match_failure,
|
||||
original_path=ca.path,
|
||||
match_status=MatchStatus.no_match,
|
||||
tags_written=self.config.Runtime_Options__type,
|
||||
tags_written=self.config.Runtime_Options__type_modify,
|
||||
)
|
||||
match_results.no_matches.append(res)
|
||||
return res, match_results
|
||||
@ -481,7 +482,7 @@ class CLI:
|
||||
original_path=ca.path,
|
||||
online_results=matches,
|
||||
match_status=MatchStatus.low_confidence_match,
|
||||
tags_written=self.config.Runtime_Options__type,
|
||||
tags_written=self.config.Runtime_Options__type_modify,
|
||||
)
|
||||
match_results.low_confidence_matches.append(res)
|
||||
return res, match_results
|
||||
@ -493,7 +494,7 @@ class CLI:
|
||||
original_path=ca.path,
|
||||
online_results=matches,
|
||||
match_status=MatchStatus.multiple_match,
|
||||
tags_written=self.config.Runtime_Options__type,
|
||||
tags_written=self.config.Runtime_Options__type_modify,
|
||||
)
|
||||
match_results.multiple_matches.append(res)
|
||||
return res, match_results
|
||||
@ -505,7 +506,7 @@ class CLI:
|
||||
original_path=ca.path,
|
||||
online_results=matches,
|
||||
match_status=MatchStatus.low_confidence_match,
|
||||
tags_written=self.config.Runtime_Options__type,
|
||||
tags_written=self.config.Runtime_Options__type_modify,
|
||||
)
|
||||
match_results.low_confidence_matches.append(res)
|
||||
return res, match_results
|
||||
@ -517,7 +518,7 @@ class CLI:
|
||||
original_path=ca.path,
|
||||
online_results=matches,
|
||||
match_status=MatchStatus.no_match,
|
||||
tags_written=self.config.Runtime_Options__type,
|
||||
tags_written=self.config.Runtime_Options__type_modify,
|
||||
)
|
||||
match_results.no_matches.append(res)
|
||||
return res, match_results
|
||||
@ -533,7 +534,7 @@ class CLI:
|
||||
original_path=ca.path,
|
||||
online_results=matches,
|
||||
match_status=MatchStatus.good_match,
|
||||
tags_written=self.config.Runtime_Options__type,
|
||||
tags_written=self.config.Runtime_Options__type_modify,
|
||||
)
|
||||
match_results.fetch_data_failures.append(res)
|
||||
return res, match_results
|
||||
@ -545,7 +546,7 @@ class CLI:
|
||||
online_results=matches,
|
||||
match_status=MatchStatus.good_match,
|
||||
md=prepare_metadata(md, ct_md, self.config),
|
||||
tags_written=self.config.Runtime_Options__type,
|
||||
tags_written=self.config.Runtime_Options__type_modify,
|
||||
)
|
||||
assert res.md
|
||||
# ok, done building our metadata. time to save
|
||||
|
@ -160,14 +160,21 @@ def register_runtime(parser: settngs.Manager) -> None:
|
||||
parser.add_setting(
|
||||
"--json", "-j", action="store_true", help="Output json on stdout. Ignored in interactive mode.", file=False
|
||||
)
|
||||
|
||||
parser.add_setting(
|
||||
"-t",
|
||||
"--type",
|
||||
"--type-modify",
|
||||
metavar=f"{{{','.join(metadata_styles).upper()}}}",
|
||||
default=[],
|
||||
type=metadata_type,
|
||||
help="""Specify TYPE as either CR, CBL or COMET\n(as either ComicRack, ComicBookLover,\nor CoMet style tags, respectively).\nUse commas for multiple types.\nFor searching the metadata will use the first listed:\neg '-t cbl,cr' with no CBL tags, CR will be used if they exist\n\n""",
|
||||
help="""Specify the type of tags to write.\nUse commas for multiple types.\nRead types will be used if unspecified\nSee --list-plugins for the available types.\n\n""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"-t",
|
||||
"--type-read",
|
||||
metavar=f"{{{','.join(metadata_styles).upper()}}}",
|
||||
default=[],
|
||||
type=metadata_type,
|
||||
help="""Specify the type of tags to read.\nUse commas for multiple types.\nSee --list-plugins for the available types.\nThe tag use will be 'overlayed' in order:\ne.g. '-t cbl,cr' with no CBL tags, CR will be used if they exist and CR will overwrite any shared CBL tags.\n\n""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
@ -190,7 +197,7 @@ def register_commands(parser: settngs.Manager) -> None:
|
||||
dest="command",
|
||||
action="store_const",
|
||||
const=Action.print,
|
||||
help="""Print out tag info from file. Specify type\n(via -t) to get only info of that tag type.\n\n""",
|
||||
help="""Print out tag info from file. Specify type\n(via --type-read) to get only info of that tag type.\n\n""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
@ -199,7 +206,7 @@ def register_commands(parser: settngs.Manager) -> None:
|
||||
dest="command",
|
||||
action="store_const",
|
||||
const=Action.delete,
|
||||
help="Deletes the tag block of specified type (via -t).\n",
|
||||
help="Deletes the tag block of specified type (via --type-modify).\n",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
@ -207,7 +214,7 @@ def register_commands(parser: settngs.Manager) -> None:
|
||||
"--copy",
|
||||
type=metadata_type_single,
|
||||
metavar=f"{{{','.join(metadata_styles).upper()}}}",
|
||||
help="Copy the specified source tag block to\ndestination style specified via -t\n(potentially lossy operation).\n\n",
|
||||
help="Copy the specified source tag block to\ndestination style specified via --type-modify\n(potentially lossy operation).\n\n",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
@ -216,7 +223,7 @@ def register_commands(parser: settngs.Manager) -> None:
|
||||
dest="command",
|
||||
action="store_const",
|
||||
const=Action.save,
|
||||
help="Save out tags as specified type (via -t).\nMust specify also at least -o, -f, or -m.\n\n",
|
||||
help="Save out tags as specified type (via --type-modify).\nMust specify also at least -o, -f, or -m.\n\n",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
@ -284,6 +291,9 @@ def validate_commandline_settings(config: settngs.Config[ct_ns], parser: settngs
|
||||
if config[0].Runtime_Options__json and config[0].Runtime_Options__interactive:
|
||||
config[0].Runtime_Options__json = False
|
||||
|
||||
if config[0].Runtime_Options__type_read and not config[0].Runtime_Options__type_modify:
|
||||
config[0].Runtime_Options__type_modify = config[0].Runtime_Options__type_read
|
||||
|
||||
if (
|
||||
config[0].Commands__command not in (Action.save_config, Action.list_plugins)
|
||||
and config[0].Runtime_Options__no_gui
|
||||
@ -291,16 +301,16 @@ def validate_commandline_settings(config: settngs.Config[ct_ns], parser: settngs
|
||||
):
|
||||
parser.exit(message="Command requires at least one filename!\n", status=1)
|
||||
|
||||
if config[0].Commands__command == Action.delete and not config[0].Runtime_Options__type:
|
||||
parser.exit(message="Please specify the type to delete with -t\n", status=1)
|
||||
if config[0].Commands__command == Action.delete and not config[0].Runtime_Options__type_modify:
|
||||
parser.exit(message="Please specify the type to delete with --type-modify\n", status=1)
|
||||
|
||||
if config[0].Commands__command == Action.save and not config[0].Runtime_Options__type:
|
||||
parser.exit(message="Please specify the type to save with -t\n", status=1)
|
||||
if config[0].Commands__command == Action.save and not config[0].Runtime_Options__type_modify:
|
||||
parser.exit(message="Please specify the type to save with --type-modify\n", status=1)
|
||||
|
||||
if config[0].Commands__copy:
|
||||
config[0].Commands__command = Action.copy
|
||||
if not config[0].Runtime_Options__type:
|
||||
parser.exit(message="Please specify the type to copy to with -t\n", status=1)
|
||||
if not config[0].Runtime_Options__type_modify:
|
||||
parser.exit(message="Please specify the type to copy to with --type-modify\n", status=1)
|
||||
|
||||
if config[0].Runtime_Options__recursive:
|
||||
config[0].Runtime_Options__files = utils.get_recursive_filelist(config[0].Runtime_Options__files)
|
||||
|
@ -32,7 +32,7 @@ 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("load_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)
|
||||
parser.add_setting("window_height", default=0, cmdline=False)
|
||||
@ -279,6 +279,15 @@ def migrate_settings(config: settngs.Config[ct_ns]) -> settngs.Config[ct_ns]:
|
||||
else:
|
||||
config[0].internal__save_data_style = ["cbi"]
|
||||
|
||||
load_style = config[0].internal__load_data_style
|
||||
if not isinstance(load_style, list):
|
||||
if isinstance(load_style, int) and load_style in (0, 1, 2):
|
||||
config[0].internal__load_data_style = [original_types[load_style]]
|
||||
elif isinstance(load_style, str):
|
||||
config[0].internal__load_data_style = [load_style]
|
||||
else:
|
||||
config[0].internal__load_data_style = ["cbi"]
|
||||
|
||||
return config
|
||||
|
||||
|
||||
|
@ -34,14 +34,15 @@ class SettngsNS(settngs.TypedNS):
|
||||
Runtime_Options__glob: bool
|
||||
Runtime_Options__quiet: bool
|
||||
Runtime_Options__json: bool
|
||||
Runtime_Options__type: list[str]
|
||||
Runtime_Options__type_modify: list[str]
|
||||
Runtime_Options__type_read: list[str]
|
||||
Runtime_Options__overwrite: bool
|
||||
Runtime_Options__no_gui: bool
|
||||
Runtime_Options__files: list[str]
|
||||
|
||||
internal__install_id: str
|
||||
internal__save_data_style: list[str]
|
||||
internal__load_data_style: str
|
||||
internal__load_data_style: list[str]
|
||||
internal__last_opened_folder: str
|
||||
internal__window_width: int
|
||||
internal__window_height: int
|
||||
@ -149,7 +150,7 @@ class Runtime_Options(typing.TypedDict):
|
||||
class internal(typing.TypedDict):
|
||||
install_id: str
|
||||
save_data_style: list[str]
|
||||
load_data_style: str
|
||||
load_data_style: list[str]
|
||||
last_opened_folder: str
|
||||
window_width: int
|
||||
window_height: int
|
||||
|
BIN
comictaggerlib/graphics/down.png
Normal file
BIN
comictaggerlib/graphics/down.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
comictaggerlib/graphics/up.png
Normal file
BIN
comictaggerlib/graphics/up.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
@ -39,7 +39,7 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
self,
|
||||
parent: QtWidgets.QWidget,
|
||||
comic_archive_list: list[ComicArchive],
|
||||
data_style: str,
|
||||
load_data_styles: list[str],
|
||||
config: settngs.Config[ct_ns],
|
||||
talkers: dict[str, ComicTalker],
|
||||
) -> None:
|
||||
@ -48,7 +48,9 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
with (ui_path / "renamewindow.ui").open(encoding="utf-8") as uifile:
|
||||
uic.loadUi(uifile, self)
|
||||
|
||||
self.label.setText(f"Preview (based on {metadata_styles[data_style].name()} tags):")
|
||||
self.label.setText(
|
||||
f"Preview (based on {', '.join(metadata_styles[style].name() for style in load_data_styles)} tags):"
|
||||
)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowType(
|
||||
@ -61,7 +63,7 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
self.config = config
|
||||
self.talkers = talkers
|
||||
self.comic_archive_list = comic_archive_list
|
||||
self.data_style = data_style
|
||||
self.load_data_styles = load_data_styles
|
||||
self.rename_list: list[str] = []
|
||||
|
||||
self.btnSettings.clicked.connect(self.modify_settings)
|
||||
@ -70,7 +72,7 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
|
||||
self.do_preview()
|
||||
|
||||
def config_renamer(self, ca: ComicArchive, md: GenericMetadata | None = None) -> str:
|
||||
def config_renamer(self, ca: ComicArchive, md: GenericMetadata = GenericMetadata()) -> str:
|
||||
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)
|
||||
@ -82,7 +84,15 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
new_ext = ca.extension()
|
||||
|
||||
if md is None or md.is_empty:
|
||||
md = ca.read_metadata(self.data_style)
|
||||
md, error = self.parent().overlay_ca_read_style(self.load_data_styles, ca)
|
||||
if error is not None:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, error)
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self,
|
||||
"Read Failed!",
|
||||
f"One or more of the read styles failed to load for {ca.path}, check log for details",
|
||||
)
|
||||
|
||||
if md.is_empty:
|
||||
md = ca.metadata_from_filename(
|
||||
self.config[0].Filename_Parsing__filename_parser,
|
||||
|
@ -523,6 +523,7 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
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_load_style_combo()
|
||||
self.parent().adjust_save_style_combo()
|
||||
|
||||
self.config[0].Issue_Identifier__series_match_identify_thresh = self.sbNameMatchIdentifyThresh.value()
|
||||
|
@ -211,18 +211,20 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
|
||||
self.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
|
||||
|
||||
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
|
||||
config[0].internal__load_data_style = config[0].Runtime_Options__type[0]
|
||||
# respect the command line option tag type
|
||||
if config[0].Runtime_Options__type_modify:
|
||||
config[0].internal__save_data_style = config[0].Runtime_Options__type_modify
|
||||
if config[0].Runtime_Options__type_read:
|
||||
config[0].internal__load_data_style = config[0].Runtime_Options__type_read
|
||||
|
||||
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]
|
||||
for style in config[0].internal__load_data_style:
|
||||
if style not in metadata_styles:
|
||||
config[0].internal__load_data_style.remove(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.load_data_styles: list[str] = config[0].internal__load_data_style
|
||||
|
||||
self.setAcceptDrops(True)
|
||||
self.view_tag_actions, self.remove_tag_actions = self.tag_actions()
|
||||
@ -271,7 +273,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.cbMaturityRating.lineEdit().setAcceptDrops(False)
|
||||
|
||||
# hook up the callbacks
|
||||
self.cbLoadDataStyle.currentIndexChanged.connect(self.set_load_data_style)
|
||||
self.cbLoadDataStyle.dropdownClosed.connect(self.set_load_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)
|
||||
@ -433,7 +435,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.actionAutoTag.triggered.connect(self.auto_tag)
|
||||
|
||||
self.actionCopyTags.setShortcut("Ctrl+C")
|
||||
self.actionCopyTags.setStatusTip("Copy one tag style to another")
|
||||
self.actionCopyTags.setStatusTip("Copy one tag style tags to enabled modify style(s)")
|
||||
self.actionCopyTags.triggered.connect(self.copy_tags)
|
||||
|
||||
self.actionRemoveAuto.setShortcut("Ctrl+D")
|
||||
@ -1188,7 +1190,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
failed_style = metadata_styles[style].name()
|
||||
break
|
||||
|
||||
self.comic_archive.load_cache(list(metadata_styles))
|
||||
self.comic_archive.load_cache(set(metadata_styles))
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
|
||||
if failed_style:
|
||||
@ -1201,26 +1203,37 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.clear_dirty_flag()
|
||||
self.update_info_box()
|
||||
self.update_menus()
|
||||
self.fileSelectionList.update_current_row()
|
||||
|
||||
self.metadata = self.comic_archive.read_metadata(self.load_data_style)
|
||||
# Only try to read if write was successful
|
||||
self.metadata, error = self.overlay_ca_read_style(self.load_data_styles, self.comic_archive)
|
||||
if error is not None:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self,
|
||||
"Read Failed!",
|
||||
f"One or more of the read styles 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()
|
||||
else:
|
||||
QtWidgets.QMessageBox.information(self, "Whoops!", "No data to commit!")
|
||||
|
||||
def set_load_data_style(self, s: str) -> None:
|
||||
def set_load_data_style(self, load_data_styles: list[str]) -> None:
|
||||
"""Should only be called from the combobox signal"""
|
||||
if self.dirty_flag_verification(
|
||||
"Change Tag Read Style", "If you change read tag style now, data in the form will be lost. Are you sure?"
|
||||
"Change Tag Read Style",
|
||||
"If you change read tag style(s) now, data in the form will be lost. Are you sure?",
|
||||
):
|
||||
self.load_data_style = self.cbLoadDataStyle.itemData(s)
|
||||
self.config[0].internal__load_data_style = self.load_data_style
|
||||
self.load_data_styles = list(reversed(load_data_styles))
|
||||
self.config[0].internal__load_data_style = self.load_data_styles
|
||||
self.update_menus()
|
||||
if self.comic_archive is not None:
|
||||
self.load_archive(self.comic_archive)
|
||||
else:
|
||||
self.cbLoadDataStyle.currentIndexChanged.disconnect(self.set_load_data_style)
|
||||
self.cbLoadDataStyle.itemChanged.disconnect()
|
||||
self.adjust_load_style_combo()
|
||||
self.cbLoadDataStyle.currentIndexChanged.connect(self.set_load_data_style)
|
||||
self.cbLoadDataStyle.itemChanged.connect(self.set_load_data_style)
|
||||
|
||||
def set_save_data_style(self) -> None:
|
||||
self.save_data_styles = self.cbSaveDataStyle.currentData()
|
||||
@ -1392,28 +1405,38 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.cbx_sources.setCurrentIndex(self.cbx_sources.findData(self.config[0].Sources__source))
|
||||
|
||||
def adjust_load_style_combo(self) -> None:
|
||||
# select the current style
|
||||
self.cbLoadDataStyle.setCurrentIndex(self.cbLoadDataStyle.findData(self.load_data_style))
|
||||
"""Select the enabled styles. Since metadata is merged in an overlay fashion the last item in the list takes priority. We reverse the order for display to the user"""
|
||||
unchecked = set(metadata_styles.keys()) - set(self.load_data_styles)
|
||||
for i, style in enumerate(reversed(self.load_data_styles)):
|
||||
item_idx = self.cbLoadDataStyle.findData(style)
|
||||
self.cbLoadDataStyle.setItemChecked(item_idx, True)
|
||||
# Order matters, move items to list order
|
||||
if item_idx != i:
|
||||
self.cbLoadDataStyle.moveItem(item_idx, row=i)
|
||||
for style in unchecked:
|
||||
self.cbLoadDataStyle.setItemChecked(self.cbLoadDataStyle.findData(style), False)
|
||||
|
||||
def adjust_save_style_combo(self) -> None:
|
||||
# select the current 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)
|
||||
self.cbSaveDataStyle.setItemChecked(self.cbSaveDataStyle.findData(style), True)
|
||||
for style in unchecked:
|
||||
self.cbSaveDataStyle.setItemChecked(self.cbLoadDataStyle.findData(style), False)
|
||||
self.cbSaveDataStyle.setItemChecked(self.cbSaveDataStyle.findData(style), False)
|
||||
self.update_metadata_style_tweaks()
|
||||
|
||||
def populate_style_names(self) -> None:
|
||||
# First clear all entries (called from settingswindow.py)
|
||||
self.cbSaveDataStyle.clear()
|
||||
self.cbLoadDataStyle.clear()
|
||||
# Add the entries to the tag style combobox
|
||||
for style in metadata_styles.values():
|
||||
self.cbLoadDataStyle.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)
|
||||
self.cbLoadDataStyle.addItem(style.short_name.upper(), style.short_name)
|
||||
else:
|
||||
self.cbSaveDataStyle.addItem(style.name(), style.short_name)
|
||||
self.cbLoadDataStyle.addItem(style.name(), style.short_name)
|
||||
|
||||
def populate_combo_boxes(self) -> None:
|
||||
self.populate_style_names()
|
||||
@ -1580,7 +1603,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
# Abandon any further tag removals to prevent any greater damage to archive
|
||||
break
|
||||
ca.reset_cache()
|
||||
ca.load_cache(list(metadata_styles))
|
||||
ca.load_cache(set(metadata_styles))
|
||||
|
||||
progdialog.hide()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
@ -1604,12 +1627,12 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
ca_list = self.fileSelectionList.get_selected_archive_list()
|
||||
has_src_count = 0
|
||||
|
||||
src_style = self.load_data_style
|
||||
dest_styles = self.save_data_styles
|
||||
src_styles: list[str] = self.load_data_styles
|
||||
dest_styles: list[str] = self.save_data_styles
|
||||
|
||||
# Remove the read style from the write style
|
||||
if src_style in dest_styles:
|
||||
dest_styles.remove(src_style)
|
||||
if len(src_styles) == 1 and src_styles[0] in dest_styles:
|
||||
# Remove the read style from the write style
|
||||
dest_styles.remove(src_styles[0])
|
||||
|
||||
if not dest_styles:
|
||||
QtWidgets.QMessageBox.information(
|
||||
@ -1618,12 +1641,16 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
return
|
||||
|
||||
for ca in ca_list:
|
||||
if ca.has_metadata(src_style):
|
||||
has_src_count += 1
|
||||
for style in src_styles:
|
||||
if ca.has_metadata(style):
|
||||
has_src_count += 1
|
||||
continue
|
||||
|
||||
if has_src_count == 0:
|
||||
QtWidgets.QMessageBox.information(
|
||||
self, "Copy Tags", f"No archives with {metadata_styles[src_style].name()} tags selected!"
|
||||
self,
|
||||
"Copy Tags",
|
||||
f"No archives with {', '.join([metadata_styles[style].name() for style in src_styles])} tags selected!",
|
||||
)
|
||||
return
|
||||
|
||||
@ -1636,8 +1663,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 {', '.join([metadata_styles[style].name() for style in dest_styles])} tags in "
|
||||
f"Are you sure you wish to copy the combined (with overlay order) tags of "
|
||||
f"{', '.join([metadata_styles[style].name() for style in src_styles])} "
|
||||
f"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,
|
||||
@ -1655,10 +1683,11 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
success_count = 0
|
||||
for prog_idx, ca in enumerate(ca_list, 1):
|
||||
ca_saved = False
|
||||
|
||||
if ca.has_metadata(src_style) and ca.is_writable():
|
||||
md = ca.read_metadata(src_style)
|
||||
else:
|
||||
md, error = self.overlay_ca_read_style(src_styles, ca)
|
||||
if error is not None:
|
||||
failed_list.append(ca.path)
|
||||
continue
|
||||
if md.is_empty:
|
||||
continue
|
||||
|
||||
for style in dest_styles:
|
||||
@ -1683,7 +1712,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
failed_list.append(ca.path)
|
||||
|
||||
ca.reset_cache()
|
||||
ca.load_cache([self.load_data_style, *self.save_data_styles])
|
||||
ca.load_cache({*self.load_data_styles, *self.save_data_styles})
|
||||
|
||||
prog_dialog.hide()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
@ -1727,11 +1756,16 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
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.load_data_style)
|
||||
except Exception as e:
|
||||
md = GenericMetadata()
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
md, error = self.overlay_ca_read_style(self.load_data_styles, ca)
|
||||
if error is not None:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self,
|
||||
"Aborting...",
|
||||
f"One or more of the read styles failed to load for {ca.path}. Aborting to prevent any possible further damage. Check log for details.",
|
||||
)
|
||||
logger.error("Failed to load metadata for %s: %s", self.ca.path, error)
|
||||
return False, match_results
|
||||
|
||||
if md.is_empty:
|
||||
md = ca.metadata_from_filename(
|
||||
self.config[0].Filename_Parsing__filename_parser,
|
||||
@ -1888,7 +1922,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
match_results.write_failures.append(res)
|
||||
|
||||
ca.reset_cache()
|
||||
ca.load_cache([self.load_data_style] + self.save_data_styles)
|
||||
ca.load_cache({*self.load_data_styles, *self.save_data_styles})
|
||||
|
||||
return success, match_results
|
||||
|
||||
@ -1937,7 +1971,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(self.load_data_style).get_cover_page_index_list()[0]
|
||||
cover_idx = ca.read_metadata(self.load_data_styles[0]).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)
|
||||
@ -2132,7 +2166,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
if self.dirty_flag_verification(
|
||||
"File Rename", "If you rename files now, unsaved data in the form will be lost. Are you sure?"
|
||||
):
|
||||
dlg = RenameWindow(self, ca_list, self.load_data_style, self.config, self.talkers)
|
||||
dlg = RenameWindow(self, ca_list, self.load_data_styles, self.config, self.talkers)
|
||||
dlg.setModal(True)
|
||||
if dlg.exec() and self.comic_archive is not None:
|
||||
self.fileSelectionList.update_selected_rows()
|
||||
@ -2151,15 +2185,28 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
|
||||
self.config[0].internal__last_opened_folder = os.path.abspath(os.path.split(comic_archive.path)[0])
|
||||
self.comic_archive = comic_archive
|
||||
try:
|
||||
self.metadata = self.comic_archive.read_metadata(self.load_data_style)
|
||||
except Exception as e:
|
||||
logger.error("Failed to load metadata for %s: %s", self.comic_archive.path, e)
|
||||
self.exception(f"Failed to load metadata for {self.comic_archive.path}:\n\n{e}")
|
||||
self.metadata = GenericMetadata()
|
||||
|
||||
self.metadata, error = self.overlay_ca_read_style(self.load_data_styles, self.comic_archive)
|
||||
if error is not None:
|
||||
logger.error("Failed to load metadata for %s: %s", self.comic_archive.path, error)
|
||||
self.exception(f"Failed to load metadata for {self.comic_archive.path}, see log for details\n\n")
|
||||
|
||||
self.update_ui_for_archive()
|
||||
|
||||
def overlay_ca_read_style(
|
||||
self, load_data_styles: list[str], ca: ComicArchive
|
||||
) -> tuple[GenericMetadata, Exception | None]:
|
||||
md = GenericMetadata()
|
||||
error = None
|
||||
try:
|
||||
for style in load_data_styles:
|
||||
metadata = ca.read_metadata(style)
|
||||
md.overlay(metadata)
|
||||
except Exception as e:
|
||||
error = e
|
||||
|
||||
return md, error
|
||||
|
||||
def file_list_cleared(self) -> None:
|
||||
self.reset_app()
|
||||
|
||||
|
@ -2,10 +2,21 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import auto
|
||||
from sys import platform
|
||||
from typing import Any
|
||||
|
||||
from PyQt5 import QtGui, QtWidgets
|
||||
from PyQt5.QtCore import QEvent, QRect, Qt, pyqtSignal
|
||||
from PyQt5.QtCore import QEvent, QModelIndex, QPoint, QRect, QSize, Qt, pyqtSignal
|
||||
|
||||
from comicapi.utils import StrEnum
|
||||
from comictaggerlib.graphics import graphics_path
|
||||
|
||||
|
||||
class ClickedButtonEnum(StrEnum):
|
||||
up = auto()
|
||||
down = auto()
|
||||
main = auto()
|
||||
|
||||
|
||||
# Multiselect combobox from: https://gis.stackexchange.com/a/351152 (with custom changes)
|
||||
@ -112,3 +123,309 @@ class CheckableComboBox(QtWidgets.QComboBox):
|
||||
self.setItemChecked(index, False)
|
||||
else:
|
||||
self.setItemChecked(index, True)
|
||||
|
||||
|
||||
# Inspiration from https://github.com/marcel-goldschen-ohm/ModelViewPyQt and https://github.com/zxt50330/qitemdelegate-example
|
||||
class ReadStyleItemDelegate(QtWidgets.QStyledItemDelegate):
|
||||
buttonClicked = pyqtSignal(QModelIndex, ClickedButtonEnum)
|
||||
|
||||
def __init__(self, parent: QtWidgets.QWidget):
|
||||
super().__init__()
|
||||
self.combobox = parent
|
||||
|
||||
self.down_icon = QtGui.QImage(str(graphics_path / "down.png"))
|
||||
self.up_icon = QtGui.QImage(str(graphics_path / "up.png"))
|
||||
|
||||
self.button_width = self.down_icon.width()
|
||||
self.button_padding = 5
|
||||
|
||||
# Tooltip messages
|
||||
self.item_help: str = ""
|
||||
self.up_help: str = ""
|
||||
self.down_help: str = ""
|
||||
|
||||
# Connect the signal to a slot in the delegate
|
||||
self.combobox.itemClicked.connect(self.itemClicked)
|
||||
|
||||
def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QModelIndex) -> None:
|
||||
options = QtWidgets.QStyleOptionViewItem(option)
|
||||
self.initStyleOption(options, index)
|
||||
style = self.combobox.style()
|
||||
|
||||
# Draw background with the same color as other widgets
|
||||
palette = self.combobox.palette()
|
||||
background_color = palette.color(QtGui.QPalette.Window)
|
||||
painter.fillRect(options.rect, background_color)
|
||||
|
||||
style.drawPrimitive(QtWidgets.QStyle.PE_PanelItemViewItem, options, painter, self.combobox)
|
||||
|
||||
painter.save()
|
||||
|
||||
# Checkbox drawing logic
|
||||
checked = index.data(Qt.CheckStateRole)
|
||||
opts = QtWidgets.QStyleOptionButton()
|
||||
opts.state |= QtWidgets.QStyle.State_Active
|
||||
opts.rect = self.getCheckBoxRect(options)
|
||||
opts.state |= QtWidgets.QStyle.State_ReadOnly
|
||||
if checked:
|
||||
opts.state |= QtWidgets.QStyle.State_On
|
||||
style.drawPrimitive(
|
||||
QtWidgets.QStyle.PrimitiveElement.PE_IndicatorMenuCheckMark, opts, painter, self.combobox
|
||||
)
|
||||
else:
|
||||
opts.state |= QtWidgets.QStyle.State_Off
|
||||
if platform != "darwin":
|
||||
style.drawControl(QtWidgets.QStyle.CE_CheckBox, opts, painter, self.combobox)
|
||||
|
||||
label = index.data(Qt.DisplayRole)
|
||||
rectangle = options.rect
|
||||
rectangle.setX(opts.rect.width() + 10)
|
||||
painter.drawText(rectangle, Qt.AlignVCenter, label)
|
||||
|
||||
# Draw buttons
|
||||
if checked and (options.state & QtWidgets.QStyle.State_Selected):
|
||||
up_rect = self._button_up_rect(options.rect)
|
||||
down_rect = self._button_down_rect(options.rect)
|
||||
|
||||
painter.drawImage(up_rect, self.up_icon)
|
||||
painter.drawImage(down_rect, self.down_icon)
|
||||
|
||||
painter.restore()
|
||||
|
||||
def _button_up_rect(self, rect: QRect) -> QRect:
|
||||
return QRect(
|
||||
self.combobox.view().width() - (self.button_width * 2) - (self.button_padding * 2),
|
||||
rect.top() + (rect.height() - self.button_width) // 2,
|
||||
self.button_width,
|
||||
self.button_width,
|
||||
)
|
||||
|
||||
def _button_down_rect(self, rect: QRect = QRect(10, 1, 12, 12)) -> QRect:
|
||||
return QRect(
|
||||
self.combobox.view().width() - self.button_padding - self.button_width,
|
||||
rect.top() + (rect.height() - self.button_width) // 2,
|
||||
self.button_width,
|
||||
self.button_width,
|
||||
)
|
||||
|
||||
def getCheckBoxRect(self, option: QtWidgets.QStyleOptionViewItem) -> QRect:
|
||||
# Get size of a standard checkbox.
|
||||
opts = QtWidgets.QStyleOptionButton()
|
||||
style = option.widget.style()
|
||||
checkBoxRect = style.subElementRect(QtWidgets.QStyle.SE_CheckBoxIndicator, opts, None)
|
||||
y = option.rect.y()
|
||||
h = option.rect.height()
|
||||
checkBoxTopLeftCorner = QPoint(5, int(y + h / 2 - checkBoxRect.height() / 2))
|
||||
|
||||
return QRect(checkBoxTopLeftCorner, checkBoxRect.size())
|
||||
|
||||
def itemClicked(self, index: QModelIndex, pos: QPoint) -> None:
|
||||
item_rect = self.combobox.view().visualRect(index)
|
||||
checked = index.data(Qt.CheckStateRole)
|
||||
button_up_rect = self._button_up_rect(item_rect)
|
||||
button_down_rect = self._button_down_rect(item_rect)
|
||||
|
||||
if checked and button_up_rect.contains(pos):
|
||||
self.buttonClicked.emit(index, ClickedButtonEnum.up)
|
||||
elif checked and button_down_rect.contains(pos):
|
||||
self.buttonClicked.emit(index, ClickedButtonEnum.down)
|
||||
else:
|
||||
self.buttonClicked.emit(index, ClickedButtonEnum.main)
|
||||
|
||||
def setToolTip(self, item: str = "", up: str = "", down: str = "") -> None:
|
||||
if item:
|
||||
self.item_help = item
|
||||
if up:
|
||||
self.up_help = up
|
||||
if down:
|
||||
self.down_help = down
|
||||
|
||||
def helpEvent(
|
||||
self,
|
||||
event: QtGui.QHelpEvent,
|
||||
view: QtWidgets.QAbstractItemView,
|
||||
option: QtWidgets.QStyleOptionViewItem,
|
||||
index: QModelIndex,
|
||||
) -> bool:
|
||||
item_rect = view.visualRect(index)
|
||||
button_up_rect = self._button_up_rect(item_rect)
|
||||
button_down_rect = self._button_down_rect(item_rect)
|
||||
checked = index.data(Qt.CheckStateRole)
|
||||
|
||||
if checked == Qt.Checked and button_up_rect.contains(event.pos()):
|
||||
QtWidgets.QToolTip.showText(event.globalPos(), self.up_help, self.combobox, QRect(), 3000)
|
||||
elif checked == Qt.Checked and button_down_rect.contains(event.pos()):
|
||||
QtWidgets.QToolTip.showText(event.globalPos(), self.down_help, self.combobox, QRect(), 3000)
|
||||
else:
|
||||
QtWidgets.QToolTip.showText(event.globalPos(), self.item_help, self.combobox, QRect(), 3000)
|
||||
return True
|
||||
|
||||
def sizeHint(self, option: QtWidgets.QStyleOptionViewItem, index: QModelIndex) -> QSize:
|
||||
# Reimpliment standard combobox sizeHint. Only height is used by view, width is ignored
|
||||
menu_option = QtWidgets.QStyleOptionMenuItem()
|
||||
return self.combobox.style().sizeFromContents(
|
||||
QtWidgets.QStyle.ContentsType.CT_MenuItem, menu_option, option.rect.size(), self.combobox
|
||||
)
|
||||
|
||||
|
||||
# Multiselect combobox from: https://gis.stackexchange.com/a/351152 (with custom changes)
|
||||
class CheckableOrderComboBox(QtWidgets.QComboBox):
|
||||
itemClicked = pyqtSignal(QModelIndex, QPoint)
|
||||
dropdownClosed = pyqtSignal(list)
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
super().__init__(*args, **kwargs)
|
||||
itemDelegate = ReadStyleItemDelegate(self)
|
||||
itemDelegate.setToolTip(
|
||||
"Select which read style(s) to use", "Move item up in priority", "Move item down in priority"
|
||||
)
|
||||
self.setItemDelegate(itemDelegate)
|
||||
|
||||
# Prevent popup from closing when clicking on an item
|
||||
self.view().viewport().installEventFilter(self)
|
||||
|
||||
# Go on a bit of a merry-go-round with the signals to avoid custom model/view
|
||||
self.itemDelegate().buttonClicked.connect(self.buttonClicked)
|
||||
|
||||
# Keeps track of when the combobox list is shown
|
||||
self.justShown = False
|
||||
|
||||
def buttonClicked(self, index: QModelIndex, button: ClickedButtonEnum) -> None:
|
||||
if button == ClickedButtonEnum.up:
|
||||
self.moveItem(index.row(), up=True)
|
||||
elif button == ClickedButtonEnum.down:
|
||||
self.moveItem(index.row(), up=False)
|
||||
else:
|
||||
self.toggleItem(index.row())
|
||||
|
||||
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
|
||||
# Reverse as the display order is in "priority" order for the user whereas overlay requires reversed
|
||||
self.dropdownClosed.emit(self.currentData())
|
||||
# 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())
|
||||
if index.isValid():
|
||||
self.itemClicked.emit(index, event.pos())
|
||||
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)
|
||||
|
||||
# Add room for "move" arrows
|
||||
text_width = self.fontMetrics().width(text)
|
||||
checkbox_width = 40
|
||||
total_width = text_width + checkbox_width + (self.itemDelegate().button_width * 2)
|
||||
if total_width > self.view().minimumWidth():
|
||||
self.view().setMinimumWidth(total_width)
|
||||
|
||||
def moveItem(self, index: int, up: bool = False, row: int | None = None) -> None:
|
||||
"""'Move' an item. Really swap the data and titles around on the two items"""
|
||||
if row is None:
|
||||
adjust = -1 if up else 1
|
||||
row = index + adjust
|
||||
|
||||
# TODO Disable buttons at top and bottom. Do a check here for now
|
||||
if up and index == 0:
|
||||
return
|
||||
if up is False and row == self.count():
|
||||
return
|
||||
|
||||
# Grab values for the rows to swap
|
||||
cur_data = self.model().item(index).data(Qt.UserRole)
|
||||
cur_title = self.model().item(index).data(Qt.DisplayRole)
|
||||
cur_state = self.model().item(index).data(Qt.CheckStateRole)
|
||||
|
||||
swap_data = self.model().item(row).data(Qt.UserRole)
|
||||
swap_title = self.model().item(row).data(Qt.DisplayRole)
|
||||
swap_state = self.model().item(row).checkState()
|
||||
|
||||
self.model().item(row).setData(cur_data, Qt.UserRole)
|
||||
self.model().item(row).setCheckState(cur_state)
|
||||
self.model().item(row).setText(cur_title)
|
||||
|
||||
self.model().item(index).setData(swap_data, Qt.UserRole)
|
||||
self.model().item(index).setCheckState(swap_state)
|
||||
self.model().item(index).setText(swap_title)
|
||||
|
||||
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._updateText()
|
||||
|
||||
def toggleItem(self, index: int) -> None:
|
||||
if self.model().item(index).checkState() == Qt.Checked:
|
||||
self.setItemChecked(index, False)
|
||||
else:
|
||||
self.setItemChecked(index, True)
|
||||
|
@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1096</width>
|
||||
<height>658</height>
|
||||
<height>660</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
@ -76,7 +76,11 @@
|
||||
<widget class="QComboBox" name="cbx_sources"/>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="cbLoadDataStyle"/>
|
||||
<widget class="CheckableOrderComboBox" name="cbLoadDataStyle">
|
||||
<property name="toolTip">
|
||||
<string>At least one read style must be selected</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="CheckableComboBox" name="cbSaveDataStyle"/>
|
||||
@ -1524,6 +1528,11 @@
|
||||
<extends>QComboBox</extends>
|
||||
<header>comictaggerlib.ui.customwidgets</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>CheckableOrderComboBox</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>comictaggerlib.ui.customwidgets</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
|
@ -39,8 +39,9 @@ def test_save(
|
||||
config[0].Runtime_Options__online = True
|
||||
# Use the temporary comic we created
|
||||
config[0].Runtime_Options__files = [tmp_comic.path]
|
||||
# Save ComicRack tags
|
||||
config[0].Runtime_Options__type = ["cr"]
|
||||
# Read and save ComicRack tags
|
||||
config[0].Runtime_Options__type_read = ["cr"]
|
||||
config[0].Runtime_Options__type_modify = ["cr"]
|
||||
# Search using the correct series since we just put the wrong series name in the CBZ
|
||||
config[0].Runtime_Options__metadata = comicapi.genericmetadata.GenericMetadata(series=md_saved.series)
|
||||
# Run ComicTagger
|
||||
@ -89,7 +90,7 @@ def test_delete(
|
||||
# Use the temporary comic we created
|
||||
config[0].Runtime_Options__files = [tmp_comic.path]
|
||||
# Delete ComicRack tags
|
||||
config[0].Runtime_Options__type = ["cr"]
|
||||
config[0].Runtime_Options__type_modify = ["cr"]
|
||||
# Run ComicTagger
|
||||
CLI(config[0], talkers).run()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user