Compare commits

...

13 Commits

Author SHA1 Message Date
785c987ba6 Run tests in parallel
Some checks failed
CI / lint (ubuntu-latest, 3.9) (push) Failing after 16s
Contributions / A job to automate contrib in readme (push) Failing after 14s
CI / build-and-test (macos-13, 3.13) (push) Has been cancelled
CI / build-and-test (macos-13, 3.9) (push) Has been cancelled
CI / build-and-test (macos-14, 3.13) (push) Has been cancelled
CI / build-and-test (macos-14, 3.9) (push) Has been cancelled
CI / build-and-test (ubuntu-22.04, 3.13) (push) Has been cancelled
CI / build-and-test (ubuntu-22.04, 3.9) (push) Has been cancelled
CI / build-and-test (windows-latest, 3.13) (push) Has been cancelled
CI / build-and-test (windows-latest, 3.9) (push) Has been cancelled
2025-06-18 21:08:27 -07:00
8ecf70bca2 Require pyicu 2025-06-18 21:08:27 -07:00
eda794ac09 Bump comicinfoxml 2025-06-18 21:08:27 -07:00
c36e4703d0 Use zipremove 2025-06-18 21:08:27 -07:00
818c3768ad Fix isort 2025-06-18 21:08:27 -07:00
5100c9640e Fix 7z 2025-06-18 21:08:27 -07:00
0a0c8f32fe Update build for linux arm64 release 2025-06-18 21:08:27 -07:00
f4e2b5305c Fix enabling original hash widgets 2025-06-18 21:08:27 -07:00
11e2dea0b1 Test python 3.9 and 3.13 publish 3.13 binaries 2025-06-18 21:08:27 -07:00
0c28572fbc Download appimage for the current platform 2025-06-18 21:08:27 -07:00
653e792bfd Switch to PyQt6 2025-06-18 17:24:37 -07:00
94f325a088 Fix error when parsing metadata from the CLI 2025-05-24 11:49:56 -07:00
ebd7fae059 Fix setting the issue to "1" when not searching online 2025-05-24 11:49:38 -07:00
39 changed files with 227 additions and 314 deletions

View File

@ -46,7 +46,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: [3.9]
python-version: [3.9, 3.13]
os: [ubuntu-22.04, macos-13, macos-14, windows-latest]
steps:
@ -70,7 +70,7 @@ jobs:
- name: Install linux dependencies
run: |
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install pkg-config libicu-dev libqt5gui5 libfuse2 desktop-file-utils
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install pkg-config libicu-dev libqt6gui6 libfuse2 desktop-file-utils
if: runner.os == 'Linux'
- name: Build and install PyPi packages
@ -90,7 +90,9 @@ jobs:
dist/binary/*.tar.gz
dist/binary/*.dmg
dist/binary/*.AppImage
if: matrix.python == 3.12
- name: PyTest
run: |
python -m tox r
python -m tox p -e py${{ matrix.python-version }}-none,py${{ matrix.python-version }}-gui,py${{ matrix.python-version }}-7z,py${{ matrix.python-version }}-cbr,py${{ matrix.python-version }}-all
shell: bash

View File

@ -15,8 +15,8 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: [3.9]
os: [ubuntu-22.04, macos-13, macos-14, windows-latest]
python-version: [3.13]
os: [ubuntu-22.04, ubuntu-22.04-arm, macos-13, macos-14, windows-latest]
steps:
- uses: actions/checkout@v4
@ -39,19 +39,22 @@ jobs:
- name: Install linux dependencies
run: |
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install pkg-config libicu-dev libqt5gui5 libfuse2 desktop-file-utils
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install pkg-config libicu-dev libqt6gui6 libfuse2 desktop-file-utils
if: runner.os == 'Linux'
- name: Build, Install and Test PyPi packages
run: |
export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig:/opt/homebrew/opt/icu4c/lib/pkgconfig${PKG_CONFIG_PATH+:$PKG_CONFIG_PATH}";
export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin${PATH+:$PATH}"
python -m tox r
python -m tox r -m release
python -m tox p
- name: Release PyPi package
run: |
python -m tox r -e pypi-upload
shell: bash
if: matrix.os == 'ubuntu-22.04'
- name: Get release name
if: startsWith(github.ref, 'refs/tags/')
shell: bash
run: |
git fetch --depth=1 origin +refs/tags/*:refs/tags/* # github is dumb
@ -70,4 +73,4 @@ jobs:
dist/binary/*.tar.gz
dist/binary/*.dmg
dist/binary/*.AppImage
dist/*${{ fromJSON('["never", ""]')[runner.os == 'Linux'] }}.whl
dist/*${{ fromJSON('["never", ""]')[matrix.os == 'ubuntu-22.04'] }}.whl

View File

@ -41,7 +41,7 @@ Please open a [GitHub Pull Request](https://github.com/comictagger/comictagger/p
Currently only python 3.9 is supported however 3.10 will probably work if you try it
Those on linux should install `Pillow` from the system package manager if possible and if the GUI `pyqt5` should be installed from the system package manager
Those on linux should install `Pillow` from the system package manager if possible and if the GUI `PyQt6` should be installed from the system package manager
Those on macOS will need to ensure that you are using python3 in x86 mode either by installing an x86 only version of python or using the universal installer and using `python3-intel64` instead of `python3`

View File

@ -3,14 +3,16 @@ from __future__ import annotations
import argparse
import os
import pathlib
import platform
try:
import niquests as requests
except ImportError:
import requests
arch = platform.machine()
parser = argparse.ArgumentParser()
parser.add_argument("APPIMAGETOOL", default="build/appimagetool-x86_64.AppImage", type=pathlib.Path, nargs="?")
parser.add_argument("APPIMAGETOOL", default=f"build/appimagetool-{arch}.AppImage", type=pathlib.Path, nargs="?")
opts = parser.parse_args()
opts.APPIMAGETOOL = opts.APPIMAGETOOL.absolute()
@ -27,7 +29,8 @@ if opts.APPIMAGETOOL.exists():
raise SystemExit(0)
urlretrieve(
"https://github.com/AppImage/appimagetool/releases/latest/download/appimagetool-x86_64.AppImage", opts.APPIMAGETOOL
f"https://github.com/AppImage/appimagetool/releases/latest/download/appimagetool-{arch}.AppImage",
opts.APPIMAGETOOL,
)
os.chmod(opts.APPIMAGETOOL, 0o0700)

View File

@ -55,20 +55,15 @@ def Tar(tar_file: pathlib.Path, path: pathlib.Path) -> None:
if __name__ == "__main__":
app = "ComicTagger"
exe = app.casefold()
final_name = f"{app}-{__version__}-{platform.system()}-{platform.machine()}"
if platform.system() == "Windows":
os_version = f"win-{platform.machine()}"
app_name = f"{exe}.exe"
final_name = f"{app}-{__version__}-{os_version}.exe"
exe = f"{exe}.exe"
elif platform.system() == "Darwin":
exe = f"{app}.app"
ver = platform.mac_ver()
os_version = f"osx-{ver[0]}-{ver[2]}"
app_name = f"{app}.app"
final_name = f"{app}-{__version__}-{os_version}"
else:
app_name = exe
final_name = f"ComicTagger-{__version__}-{platform.system()}"
final_name = f"{app}-{__version__}-macOS-{ver[0]}-{ver[2]}"
path = pathlib.Path(f"dist/{app_name}")
path = pathlib.Path(f"dist/{exe}")
binary_path = pathlib.Path("dist/binary")
binary_path.mkdir(parents=True, exist_ok=True)
archive_destination = binary_path / final_name

View File

@ -271,7 +271,7 @@ class RarArchiver(Archiver):
@classmethod
@functools.cache
def _log_not_writeable(cls, exe: str) -> None:
logger.warning("Unable to find a useable copy of %r, will not be able to write rar files", str)
logger.warning("Unable to find a useable copy of %r, will not be able to write rar files", exe)
def is_writable(self) -> bool:
return bool(self._writeable and bool(self.exe and (os.path.exists(self.exe) or shutil.which(self.exe))))

View File

@ -9,102 +9,13 @@ import zipfile
from typing import cast
import chardet
from zipremove import ZipFile
from comicapi.archivers import Archiver
logger = logging.getLogger(__name__)
class ZipFile(zipfile.ZipFile):
def remove(self, zinfo_or_arcname): # type: ignore
"""Remove a member from the archive."""
if self.mode not in ("w", "x", "a"):
raise ValueError("remove() requires mode 'w', 'x', or 'a'")
if not self.fp:
raise ValueError("Attempt to write to ZIP archive that was already closed")
if self._writing: # type: ignore[attr-defined]
raise ValueError("Can't write to ZIP archive while an open writing handle exists")
# Make sure we have an existing info object
if isinstance(zinfo_or_arcname, zipfile.ZipInfo):
zinfo = zinfo_or_arcname
# make sure zinfo exists
if zinfo not in self.filelist:
raise KeyError("There is no item %r in the archive" % zinfo_or_arcname)
else:
# get the info object
zinfo = self.getinfo(zinfo_or_arcname)
return self._remove_members({zinfo})
def _remove_members(self, members, *, remove_physical=True, chunk_size=2**20): # type: ignore
"""Remove members in a zip file.
All members (as zinfo) should exist in the zip; otherwise the zip file
will erroneously end in an inconsistent state.
"""
fp = self.fp
assert fp
entry_offset = 0
member_seen = False
# get a sorted filelist by header offset, in case the dir order
# doesn't match the actual entry order
filelist = sorted(self.filelist, key=lambda x: x.header_offset)
for i in range(len(filelist)):
info = filelist[i]
is_member = info in members
if not (member_seen or is_member):
continue
# get the total size of the entry
try:
offset = filelist[i + 1].header_offset
except IndexError:
offset = self.start_dir
entry_size = offset - info.header_offset
if is_member:
member_seen = True
entry_offset += entry_size
# update caches
self.filelist.remove(info)
try:
del self.NameToInfo[info.filename]
except KeyError:
pass
continue
# update the header and move entry data to the new position
if remove_physical:
old_header_offset = info.header_offset
info.header_offset -= entry_offset
read_size = 0
while read_size < entry_size:
fp.seek(old_header_offset + read_size)
data = fp.read(min(entry_size - read_size, chunk_size))
fp.seek(info.header_offset + read_size)
fp.write(data)
fp.flush()
read_size += len(data)
# Avoid missing entry if entries have a duplicated name.
# Reverse the order as NameToInfo normally stores the last added one.
for info in reversed(self.filelist):
self.NameToInfo.setdefault(info.filename, info)
# update state
if remove_physical:
self.start_dir -= entry_offset
self._didModify = True
# seek to the start of the central dir
fp.seek(self.start_dir)
class ZipArchiver(Archiver):
"""ZIP implementation"""
@ -149,7 +60,7 @@ class ZipArchiver(Archiver):
try:
with ZipFile(self.path, mode="a", allowZip64=True, compression=zipfile.ZIP_DEFLATED) as zf:
if archive_file in files:
zf.remove(archive_file)
zf.repack([zf.remove(archive_file)])
return True
except (zipfile.BadZipfile, OSError) as e:
logger.error("Error writing zip archive [%s]: %s :: %s", e, self.path, archive_file)
@ -163,7 +74,7 @@ class ZipArchiver(Archiver):
# now just add the archive file as a new one
with ZipFile(self.path, mode="a", allowZip64=True, compression=zipfile.ZIP_DEFLATED) as zf:
if archive_file in files:
zf.remove(archive_file)
zf.repack([zf.remove(archive_file)])
zf.writestr(archive_file, data)
return True
except (zipfile.BadZipfile, OSError) as e:

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import logging
import pathlib
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from comictaggerlib.ui import ui_path
@ -35,7 +35,7 @@ class ApplicationLogWindow(QtWidgets.QDialog):
self.log_handler.qlog.connect(self.textEdit.append)
f = QtGui.QFont("menlo")
f.setStyleHint(QtGui.QFont.Monospace)
f.setStyleHint(QtGui.QFont.StyleHint.Monospace)
self.setFont(f)
self._button = QtWidgets.QPushButton(self)
self._button.setText("Test Me")

View File

@ -20,7 +20,7 @@ import logging
import os
from typing import Callable
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from comicapi.comicarchive import ComicArchive, tags
from comicapi.genericmetadata import GenericMetadata

View File

@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt5 import QtCore, QtWidgets, uic
from PyQt6 import QtCore, QtWidgets, uic
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.ui import ui_path

View File

@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt5 import QtCore, QtWidgets, uic
from PyQt6 import QtCore, QtWidgets, uic
from comictaggerlib.ctsettings import ct_ns
from comictaggerlib.ui import ui_path

View File

@ -590,9 +590,6 @@ class CLI:
self.output(f"Processing {utils.path_to_short_str(ca.path)}...")
md, tags_read = self.create_local_metadata(ca, self.config.Runtime_Options__tags_read)
if md.issue is None or md.issue == "":
if self.config.Auto_Tag__assume_issue_one:
md.issue = "1"
matches: list[IssueResult] = []
# now, search online
@ -629,11 +626,15 @@ class CLI:
return res, match_results
else:
qt_md = self.try_quick_tag(ca, md)
query_md = md.copy()
qt_md = self.try_quick_tag(ca, query_md)
if query_md.issue is None or query_md.issue == "":
if self.config.Auto_Tag__assume_issue_one:
query_md.issue = "1"
if qt_md is None or qt_md.is_empty:
if qt_md is not None:
self.output("Failed to find match via quick tag")
ct_md, matches, res, match_results = self.normal_tag(ca, tags_read, md, match_results) # type: ignore[assignment]
ct_md, matches, res, match_results = self.normal_tag(ca, tags_read, query_md, match_results) # type: ignore[assignment]
if res is not None:
return res, match_results
else:

View File

@ -1,4 +1,4 @@
"""A PyQt5 widget to display cover images
"""A PyQt6 widget to display cover images
Display cover images from either a local archive, or from comic source metadata.
TODO: This should be re-factored using subclasses!
@ -23,7 +23,7 @@ from __future__ import annotations
import logging
import pathlib
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from comicapi.comicarchive import ComicArchive
from comictaggerlib.imagefetcher import ImageFetcher

View File

@ -20,7 +20,7 @@ import logging
import operator
import natsort
from PyQt5 import QtCore, QtWidgets, uic
from PyQt6 import QtCore, QtWidgets, uic
from comicapi import utils
from comicapi.genericmetadata import Credit

View File

@ -194,7 +194,7 @@ def parse_metadata_from_string(mdstr: str) -> GenericMetadata:
else:
value = t(value)
except (ValueError, TypeError):
raise argparse.ArgumentTypeError(f"Invalid syntax for tag '{key}': {value}")
raise argparse.ArgumentTypeError(f"Invalid syntax for tag {key!r}: {value!r}")
return value
md = GenericMetadata()
@ -240,6 +240,8 @@ def parse_metadata_from_string(mdstr: str) -> GenericMetadata:
else:
raise argparse.ArgumentTypeError(f"'{key}' is not a valid tag name")
md.is_empty = empty
except argparse.ArgumentTypeError as e:
raise e
except Exception as e:
logger.exception("Unable to read metadata from the commandline '%s'", mdstr)
raise Exception("Unable to read metadata from the commandline") from e

View File

@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt5 import QtCore, QtWidgets, uic
from PyQt6 import QtCore, QtWidgets, uic
from comictaggerlib.ui import ui_path

View File

@ -1,4 +1,4 @@
"""A PyQt5 widget for managing list of comic archive files"""
"""A PyQt6 widget for managing list of comic archive files"""
#
# Copyright 2012-2014 ComicTagger Authors
@ -22,7 +22,7 @@ import pathlib
import platform
from typing import Callable, cast
from PyQt5 import QtCore, QtWidgets, uic
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from comicapi import utils
from comicapi.comicarchive import ComicArchive
@ -64,9 +64,9 @@ class FileSelectionList(QtWidgets.QWidget):
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.ActionsContextMenu)
self.dirty_flag = False
select_all_action = QtWidgets.QAction("Select All", self)
remove_action = QtWidgets.QAction("Remove Selected Items", self)
self.separator = QtWidgets.QAction("", self)
select_all_action = QtGui.QAction("Select All", self)
remove_action = QtGui.QAction("Remove Selected Items", self)
self.separator = QtGui.QAction("", self)
self.separator.setSeparator(True)
select_all_action.setShortcut("Ctrl+A")
@ -86,14 +86,14 @@ class FileSelectionList(QtWidgets.QWidget):
def get_sorting(self) -> tuple[int, int]:
col = self.twList.horizontalHeader().sortIndicatorSection()
order = self.twList.horizontalHeader().sortIndicatorOrder()
order = self.twList.horizontalHeader().sortIndicatorOrder().value
return int(col), int(order)
def set_sorting(self, col: int, order: QtCore.Qt.SortOrder) -> None:
self.twList.horizontalHeader().setSortIndicator(col, order)
def add_app_action(self, action: QtWidgets.QAction) -> None:
self.insertAction(QtWidgets.QAction(), action)
def add_app_action(self, action: QtGui.QAction) -> None:
self.insertAction(QtGui.QAction(), action)
def set_modified_flag(self, modified: bool) -> None:
self.dirty_flag = modified

View File

@ -2,11 +2,11 @@
# Resource object code
#
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.11)
# Created by: The Resource Compiler for PyQt6 (Qt v5.15.11)
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore
from PyQt6 import QtCore
qt_resource_data = b"\
\x00\x00\x16\x0b\

View File

@ -16,7 +16,7 @@ from comictalker.comictalker import ComicTalker
logger = logging.getLogger("comictagger")
try:
qt_available = True
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt6 import QtCore, QtGui, QtWidgets
def show_exception_box(log_msg: str) -> None:
"""Checks if a QApplication instance is available and shows a messagebox with the exception message.
@ -70,7 +70,7 @@ try:
try:
# needed here to initialize QWebEngine
from PyQt5.QtWebEngineWidgets import QWebEngineView # noqa: F401
from PyQt6.QtWebEngineWidgets import QWebEngineView # noqa: F401
qt_webengine_available = True
except ImportError:
@ -81,7 +81,7 @@ try:
# Handles "Open With" from Finder on macOS
def event(self, event: QtCore.QEvent) -> bool:
if event.type() == QtCore.QEvent.FileOpen:
if event.type() == QtCore.QEvent.Type.FileOpen:
logger.info("file open recieved: %s", event.url().toLocalFile())
self.openFileRequest.emit(event.url())
return True

View File

@ -33,7 +33,7 @@ except ImportError:
from comictaggerlib import ctversion
if TYPE_CHECKING:
from PyQt5 import QtCore, QtNetwork
from PyQt6 import QtCore, QtNetwork
logger = logging.getLogger(__name__)
@ -57,7 +57,7 @@ class ImageFetcher:
if self.qt_available:
try:
from PyQt5 import QtNetwork
from PyQt6 import QtNetwork
self.qt_available = True
except ImportError:
@ -99,7 +99,7 @@ class ImageFetcher:
return image_data
if self.qt_available:
from PyQt5 import QtCore, QtNetwork
from PyQt6 import QtCore, QtNetwork
# if we found it, just emit the signal asap
if image_data:

View File

@ -19,7 +19,7 @@ from __future__ import annotations
import logging
import platform
from PyQt5 import QtCore, QtGui, QtWidgets, sip, uic
from PyQt6 import QtCore, QtGui, QtWidgets, sip, uic
from comictaggerlib.ui import ui_path
@ -82,7 +82,7 @@ class ImagePopup(QtWidgets.QDialog):
screen_size.width(),
screen_size.height(),
QtCore.Qt.AspectRatioMode.IgnoreAspectRatio,
QtCore.Qt.SmoothTransformation,
QtCore.Qt.TransformationMode.SmoothTransformation,
)
self.setMask(self.clientBgPixmap.mask())
@ -104,7 +104,10 @@ class ImagePopup(QtWidgets.QDialog):
if self.imagePixmap.width() > win_w or self.imagePixmap.height() > win_h:
# scale the pixmap to fit in the frame
display_pixmap = self.imagePixmap.scaled(
win_w, win_h, QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.SmoothTransformation
win_w,
win_h,
QtCore.Qt.AspectRatioMode.KeepAspectRatio,
QtCore.Qt.TransformationMode.SmoothTransformation,
)
self.lblImage.setPixmap(display_pixmap)
else:

View File

@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from comicapi.genericmetadata import GenericMetadata
from comicapi.issuestring import IssueString

View File

@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt5 import QtCore, QtWidgets, uic
from PyQt6 import QtCore, QtWidgets, uic
from comictaggerlib.ui import qtutils, ui_path

View File

@ -301,7 +301,7 @@ class App:
return gui.open_tagger_window(self.talkers, self.config, error)
except ImportError:
self.config[0].Runtime_Options__no_gui = True
logger.warning("PyQt5 is not available. ComicTagger is limited to command-line mode.")
logger.warning("PyQt6 is not available. ComicTagger is limited to command-line mode.")
# GUI mode is not available or CLI mode was requested
if error and error[1]:

View File

@ -19,7 +19,7 @@ from __future__ import annotations
import logging
import os
from PyQt5 import QtCore, QtWidgets, uic
from PyQt6 import QtCore, QtWidgets, uic
from comicapi.comicarchive import ComicArchive
from comictaggerlib.coverimagewidget import CoverImageWidget

View File

@ -1,4 +1,4 @@
"""A PyQt5 dialog to show a message and let the user check a box
"""A PyQt6 dialog to show a message and let the user check a box
Example usage:
@ -29,7 +29,7 @@ from __future__ import annotations
import logging
from PyQt5 import QtCore, QtWidgets
from PyQt6 import QtCore, QtWidgets
logger = logging.getLogger(__name__)

View File

@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from comicapi.comicarchive import ComicArchive
from comicapi.genericmetadata import GenericMetadata

View File

@ -1,4 +1,4 @@
"""A PyQt5 widget for editing the page list info"""
"""A PyQt6 widget for editing the page list info"""
#
# Copyright 2012-2014 ComicTagger Authors
@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt5 import QtCore, QtWidgets, uic
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from comicapi.comicarchive import ComicArchive, tags
from comicapi.genericmetadata import GenericMetadata, PageMetadata, PageType
@ -145,7 +145,7 @@ class PageListEditor(QtWidgets.QWidget):
if show_shortcut:
text = text + " (" + shortcut + ")"
self.cbPageType.addItem(text, user_data)
action_item = QtWidgets.QAction(shortcut, self)
action_item = QtGui.QAction(shortcut, self)
action_item.triggered.connect(lambda: self.select_page_type_item(self.cbPageType.findData(user_data)))
action_item.setShortcut(shortcut)
self.addAction(action_item)

View File

@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt5 import QtCore
from PyQt6 import QtCore
from comicapi.comicarchive import ComicArchive

View File

@ -1,4 +1,4 @@
"""A PyQt5 dialog to show ID log and progress"""
"""A PyQt6 dialog to show ID log and progress"""
#
# Copyright 2012-2014 ComicTagger Authors
@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt5 import QtCore, QtWidgets, uic
from PyQt6 import QtCore, QtWidgets, uic
from comictaggerlib.ui import ui_path

View File

@ -19,7 +19,7 @@ from __future__ import annotations
import logging
import settngs
from PyQt5 import QtCore, QtWidgets, uic
from PyQt6 import QtCore, QtWidgets, uic
from comicapi import utils
from comicapi.comicarchive import ComicArchive, tags

View File

@ -21,8 +21,8 @@ import logging
from collections import deque
import natsort
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtCore import QUrl, pyqtSignal
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from PyQt6.QtCore import QUrl, pyqtSignal
from comicapi import utils
from comicapi.comicarchive import ComicArchive

View File

@ -26,7 +26,7 @@ import urllib.parse
from typing import Any, cast
import settngs
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt6 import QtCore, QtGui, QtWidgets, uic
import comictaggerlib.ui.talkeruigenerator
from comicapi import merge, utils

View File

@ -31,7 +31,7 @@ from typing import Any, Callable, cast
import natsort
import settngs
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets, uic
from PyQt6 import QtCore, QtGui, QtNetwork, QtWidgets, uic
import comicapi.merge
import comictaggerlib.graphics.resources
@ -370,9 +370,9 @@ class TaggerWindow(QtWidgets.QMainWindow):
def enabled_tags(self) -> Sequence[str]:
return [tag.id for tag in tags.values() if tag.enabled]
def tag_actions(self) -> tuple[dict[str, QtWidgets.QAction], dict[str, QtWidgets.QAction]]:
view_raw_tags: dict[str, QtWidgets.QAction] = {}
remove_raw_tags: dict[str, QtWidgets.QAction] = {}
def tag_actions(self) -> tuple[dict[str, QtGui.QAction], dict[str, QtGui.QAction]]:
view_raw_tags: dict[str, QtGui.QAction] = {}
remove_raw_tags: dict[str, QtGui.QAction] = {}
for tag in tags.values():
view_raw_tags[tag.id] = self.menuViewRawTags.addAction(f"View Raw {tag.name()} Tags")
view_raw_tags[tag.id].setEnabled(tag.enabled)
@ -449,7 +449,15 @@ class TaggerWindow(QtWidgets.QMainWindow):
def toggle_enable_embedding_hashes(self) -> None:
self.config[0].Runtime_Options__enable_embedding_hashes = self.actionEnableEmbeddingHashes.isChecked()
enable_widget(self.md_attributes["original_hash"], self.config[0].Runtime_Options__enable_embedding_hashes)
enabled_widgets = set()
for tag_id in self.selected_write_tags:
if not tags[tag_id].enabled:
continue
enabled_widgets.update(tags[tag_id].supported_attributes)
enable_widget(
self.md_attributes["original_hash"],
self.config[0].Runtime_Options__enable_embedding_hashes and "original_hash" in enabled_widgets,
)
if not self.leOriginalHash.text().strip():
self.cbHashName.setCurrentText(self.config[0].internal__embedded_hash_type)
if self.config[0].Runtime_Options__enable_embedding_hashes:
@ -480,7 +488,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.actionParse_Filename_split_words.triggered.connect(self.use_filename_split)
self.actionReCalcArchiveInfo.triggered.connect(self.recalc_archive_info)
self.actionSearchOnline.triggered.connect(self.query_online)
self.actionEnableEmbeddingHashes: QtWidgets.QAction
self.actionEnableEmbeddingHashes: QtGui.QAction
self.actionEnableEmbeddingHashes.triggered.connect(self.toggle_enable_embedding_hashes)
self.actionEnableEmbeddingHashes.setChecked(self.config[0].Runtime_Options__enable_embedding_hashes)
# Window Menu

View File

@ -6,8 +6,8 @@ from enum import auto
from sys import platform
from typing import Any
from PyQt5 import QtGui, QtWidgets
from PyQt5.QtCore import QEvent, QModelIndex, QPoint, QRect, QSize, Qt, pyqtSignal
from PyQt6 import QtGui, QtWidgets
from PyQt6.QtCore import QEvent, QModelIndex, QPoint, QRect, QSize, Qt, pyqtSignal
from comicapi.utils import StrEnum
@ -30,41 +30,41 @@ class ModifyStyleItemDelegate(QtWidgets.QStyledItemDelegate):
# Draw background with the same color as other widgets
palette = self.combobox.palette()
background_color = palette.color(QtGui.QPalette.Window)
background_color = palette.color(QtGui.QPalette.ColorRole.Window)
painter.fillRect(options.rect, background_color)
style.drawPrimitive(QtWidgets.QStyle.PE_PanelItemViewItem, options, painter, self.combobox)
style.drawPrimitive(QtWidgets.QStyle.PrimitiveElement.PE_PanelItemViewItem, options, painter, self.combobox)
painter.save()
# Checkbox drawing logic
checked = index.data(Qt.CheckStateRole)
checked = index.data(Qt.ItemDataRole.CheckStateRole)
opts = QtWidgets.QStyleOptionButton()
opts.state |= QtWidgets.QStyle.State_Active
opts.state |= QtWidgets.QStyle.StateFlag.State_Active
opts.rect = self.getCheckBoxRect(options)
opts.state |= QtWidgets.QStyle.State_ReadOnly
opts.state |= QtWidgets.QStyle.StateFlag.State_ReadOnly
if checked:
opts.state |= QtWidgets.QStyle.State_On
opts.state |= QtWidgets.QStyle.StateFlag.State_On
style.drawPrimitive(
QtWidgets.QStyle.PrimitiveElement.PE_IndicatorMenuCheckMark, opts, painter, self.combobox
)
else:
opts.state |= QtWidgets.QStyle.State_Off
opts.state |= QtWidgets.QStyle.StateFlag.State_Off
if platform != "darwin":
style.drawControl(QtWidgets.QStyle.CE_CheckBox, opts, painter, self.combobox)
style.drawControl(QtWidgets.QStyle.ControlElement.CE_CheckBox, opts, painter, self.combobox)
label = index.data(Qt.DisplayRole)
label = index.data(Qt.ItemDataRole.DisplayRole)
rectangle = options.rect
rectangle.setX(opts.rect.width() + 10)
painter.drawText(rectangle, Qt.AlignVCenter, label)
# We need the restore here so that text is colored properly
painter.restore()
painter.drawText(rectangle, Qt.AlignmentFlag.AlignVCenter, label)
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)
checkBoxRect = style.subElementRect(QtWidgets.QStyle.SubElement.SE_CheckBoxIndicator, opts, None)
y = option.rect.y()
h = option.rect.height()
checkBoxTopLeftCorner = QPoint(5, int(y + h / 2 - checkBoxRect.height() / 2))
@ -99,23 +99,23 @@ class CheckableComboBox(QtWidgets.QComboBox):
# https://stackoverflow.com/questions/65826378/how-do-i-use-qcombobox-setplaceholdertext/65830989#65830989
def paintEvent(self, event: QEvent) -> None:
painter = QtWidgets.QStylePainter(self)
painter.setPen(self.palette().color(QtGui.QPalette.Text))
painter.setPen(self.palette().color(QtGui.QPalette.ColorRole.Text))
# draw the combobox frame, focusrect and selected etc.
opt = QtWidgets.QStyleOptionComboBox()
self.initStyleOption(opt)
painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, opt)
painter.drawComplexControl(QtWidgets.QStyle.ComplexControl.CC_ComboBox, opt)
if self.currentIndex() < 0:
opt.palette.setBrush(
QtGui.QPalette.ButtonText,
opt.palette.brush(QtGui.QPalette.ButtonText).color(),
QtGui.QPalette.ColorRole.ButtonText,
opt.palette.brush(QtGui.QPalette.ColorRole.ButtonText).color(),
)
if self.placeholderText():
opt.currentText = self.placeholderText()
# draw the icon and text
painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, opt)
painter.drawControl(QtWidgets.QStyle.ControlElement.CE_ComboBoxLabel, opt)
def resizeEvent(self, event: Any) -> None:
# Recompute text to elide as needed
@ -126,16 +126,16 @@ class CheckableComboBox(QtWidgets.QComboBox):
# 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:
if event.type() == QEvent.Type.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:
if event.type() == QEvent.Type.Hide:
self._updateText()
self.justShown = False
# QEvent.MouseButtonPress is inconsistent on activation because double clicks are a thing
if event.type() == QEvent.MouseButtonRelease:
# QEvent.Type.MouseButtonPress is inconsistent on activation because double clicks are a thing
if event.type() == QEvent.Type.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:
@ -153,7 +153,7 @@ class CheckableComboBox(QtWidgets.QComboBox):
res = []
for i in range(self.count()):
item = self.model().item(i)
if item.checkState() == Qt.Checked:
if item.checkState() == Qt.CheckState.Checked:
res.append(self.itemData(i))
return res
@ -168,7 +168,7 @@ class CheckableComboBox(QtWidgets.QComboBox):
texts = []
for i in range(self.count()):
item = self.model().item(i)
if item.checkState() == Qt.Checked:
if item.checkState() == Qt.CheckState.Checked:
texts.append(item.text())
text = ", ".join(texts)
@ -180,22 +180,24 @@ class CheckableComboBox(QtWidgets.QComboBox):
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)
rect = self.style().subControlRect(
QtWidgets.QStyle.ComplexControl.CC_ComboBox, so, QtWidgets.QStyle.SubControl.SC_ComboBoxEditField
)
# Compute the elided text
elidedText = self.fontMetrics().elidedText(text, Qt.ElideRight, rect.width())
elidedText = self.fontMetrics().elidedText(text, Qt.TextElideMode.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
qt_state = Qt.CheckState.Checked if state else Qt.CheckState.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:
if len(current) == 1 and not state and item.checkState() == Qt.CheckState.Checked:
QtWidgets.QToolTip.showText(QtGui.QCursor.pos(), self.toolTip(), self, QRect(), 3000)
return
@ -205,7 +207,7 @@ class CheckableComboBox(QtWidgets.QComboBox):
self._updateText()
def toggleItem(self, index: int) -> None:
if self.model().item(index).checkState() == Qt.Checked:
if self.model().item(index).checkState() == Qt.CheckState.Checked:
self.setItemChecked(index, False)
else:
self.setItemChecked(index, True)
@ -240,44 +242,44 @@ class ReadStyleItemDelegate(QtWidgets.QStyledItemDelegate):
# Draw background with the same color as other widgets
palette = self.combobox.palette()
background_color = palette.color(QtGui.QPalette.Window)
background_color = palette.color(QtGui.QPalette.ColorRole.Window)
painter.fillRect(options.rect, background_color)
style.drawPrimitive(QtWidgets.QStyle.PE_PanelItemViewItem, options, painter, self.combobox)
style.drawPrimitive(QtWidgets.QStyle.PrimitiveElement.PE_PanelItemViewItem, options, painter, self.combobox)
painter.save()
# Checkbox drawing logic
checked = index.data(Qt.CheckStateRole)
checked = index.data(Qt.ItemDataRole.CheckStateRole)
opts = QtWidgets.QStyleOptionButton()
opts.state |= QtWidgets.QStyle.State_Active
opts.state |= QtWidgets.QStyle.StateFlag.State_Active
opts.rect = self.getCheckBoxRect(options)
opts.state |= QtWidgets.QStyle.State_ReadOnly
opts.state |= QtWidgets.QStyle.StateFlag.State_ReadOnly
if checked:
opts.state |= QtWidgets.QStyle.State_On
opts.state |= QtWidgets.QStyle.StateFlag.State_On
style.drawPrimitive(
QtWidgets.QStyle.PrimitiveElement.PE_IndicatorMenuCheckMark, opts, painter, self.combobox
)
else:
opts.state |= QtWidgets.QStyle.State_Off
opts.state |= QtWidgets.QStyle.StateFlag.State_Off
if platform != "darwin":
style.drawControl(QtWidgets.QStyle.CE_CheckBox, opts, painter, self.combobox)
style.drawControl(QtWidgets.QStyle.ControlElement.CE_CheckBox, opts, painter, self.combobox)
label = index.data(Qt.DisplayRole)
label = index.data(Qt.ItemDataRole.DisplayRole)
rectangle = options.rect
rectangle.setX(opts.rect.width() + 10)
painter.drawText(rectangle, Qt.AlignVCenter, label)
# We need the restore here so that text is colored properly
painter.restore()
painter.drawText(rectangle, Qt.AlignmentFlag.AlignVCenter, label)
# Draw buttons
if checked and (options.state & QtWidgets.QStyle.State_Selected):
if checked and (options.state & QtWidgets.QStyle.StateFlag.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),
@ -298,7 +300,7 @@ class ReadStyleItemDelegate(QtWidgets.QStyledItemDelegate):
# Get size of a standard checkbox.
opts = QtWidgets.QStyleOptionButton()
style = option.widget.style()
checkBoxRect = style.subElementRect(QtWidgets.QStyle.SE_CheckBoxIndicator, opts, None)
checkBoxRect = style.subElementRect(QtWidgets.QStyle.SubElement.SE_CheckBoxIndicator, opts, None)
y = option.rect.y()
h = option.rect.height()
checkBoxTopLeftCorner = QPoint(5, int(y + h / 2 - checkBoxRect.height() / 2))
@ -307,7 +309,7 @@ class ReadStyleItemDelegate(QtWidgets.QStyledItemDelegate):
def itemClicked(self, index: QModelIndex, pos: QPoint) -> None:
item_rect = self.combobox.view().visualRect(index)
checked = index.data(Qt.CheckStateRole)
checked = index.data(Qt.ItemDataRole.CheckStateRole)
button_up_rect = self._button_up_rect(item_rect)
button_down_rect = self._button_down_rect(item_rect)
@ -336,11 +338,11 @@ class ReadStyleItemDelegate(QtWidgets.QStyledItemDelegate):
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)
checked = index.data(Qt.ItemDataRole.CheckStateRole)
if checked == Qt.Checked and button_up_rect.contains(event.pos()):
if checked == Qt.CheckState.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()):
elif checked == Qt.CheckState.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)
@ -380,23 +382,23 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
# https://stackoverflow.com/questions/65826378/how-do-i-use-qcombobox-setplaceholdertext/65830989#65830989
def paintEvent(self, event: QEvent) -> None:
painter = QtWidgets.QStylePainter(self)
painter.setPen(self.palette().color(QtGui.QPalette.Text))
painter.setPen(self.palette().color(QtGui.QPalette.ColorRole.Text))
# draw the combobox frame, focusrect and selected etc.
opt = QtWidgets.QStyleOptionComboBox()
self.initStyleOption(opt)
painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, opt)
painter.drawComplexControl(QtWidgets.QStyle.ComplexControl.CC_ComboBox, opt)
if self.currentIndex() < 0:
opt.palette.setBrush(
QtGui.QPalette.ButtonText,
opt.palette.brush(QtGui.QPalette.ButtonText).color(),
QtGui.QPalette.ColorRole.ButtonText,
opt.palette.brush(QtGui.QPalette.ColorRole.ButtonText).color(),
)
if self.placeholderText():
opt.currentText = self.placeholderText()
# draw the icon and text
painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, opt)
painter.drawControl(QtWidgets.QStyle.ControlElement.CE_ComboBoxLabel, opt)
def buttonClicked(self, index: QModelIndex, button: ClickedButtonEnum) -> None:
if button == ClickedButtonEnum.up:
@ -415,18 +417,18 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
# 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:
if event.type() == QEvent.Type.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:
if event.type() == QEvent.Type.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:
# QEvent.Type.MouseButtonPress is inconsistent on activation because double clicks are a thing
if event.type() == QEvent.Type.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:
@ -446,7 +448,7 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
res = []
for i in range(self.count()):
item = self.model().item(i)
if item.checkState() == Qt.Checked:
if item.checkState() == Qt.CheckState.Checked:
res.append(self.itemData(i))
return res
@ -458,7 +460,7 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
self.model().item(0).setCheckState(Qt.CheckState.Checked)
# Add room for "move" arrows
text_width = self.fontMetrics().width(text)
text_width = self.fontMetrics().boundingRect(text).width()
checkbox_width = 40
total_width = text_width + checkbox_width + (self.itemDelegate().button_width * 2)
if total_width > self.view().minimumWidth():
@ -477,19 +479,19 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
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)
cur_data = self.model().item(index).data(Qt.ItemDataRole.UserRole)
cur_title = self.model().item(index).data(Qt.ItemDataRole.DisplayRole)
cur_state = self.model().item(index).checkState()
swap_data = self.model().item(row).data(Qt.UserRole)
swap_title = self.model().item(row).data(Qt.DisplayRole)
swap_data = self.model().item(row).data(Qt.ItemDataRole.UserRole)
swap_title = self.model().item(row).data(Qt.ItemDataRole.DisplayRole)
swap_state = self.model().item(row).checkState()
self.model().item(row).setData(cur_data, Qt.UserRole)
self.model().item(row).setData(cur_data, Qt.ItemDataRole.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).setData(swap_data, Qt.ItemDataRole.UserRole)
self.model().item(index).setCheckState(swap_state)
self.model().item(index).setText(swap_title)
@ -497,7 +499,7 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
texts = []
for i in range(self.count()):
item = self.model().item(i)
if item.checkState() == Qt.Checked:
if item.checkState() == Qt.CheckState.Checked:
texts.append(item.text())
text = ", ".join(texts)
@ -509,22 +511,24 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
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)
rect = self.style().subControlRect(
QtWidgets.QStyle.ComplexControl.CC_ComboBox, so, QtWidgets.QStyle.SubControl.SC_ComboBoxEditField
)
# Compute the elided text
elidedText = self.fontMetrics().elidedText(text, Qt.ElideRight, rect.width())
elidedText = self.fontMetrics().elidedText(text, Qt.TextElideMode.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
qt_state = Qt.CheckState.Checked if state else Qt.CheckState.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:
if len(current) == 1 and not state and item.checkState() == Qt.CheckState.Checked:
QtWidgets.QToolTip.showText(QtGui.QCursor.pos(), self.toolTip(), self, QRect(), 3000)
return
@ -533,7 +537,7 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
self._updateText()
def toggleItem(self, index: int) -> None:
if self.model().item(index).checkState() == Qt.Checked:
if self.model().item(index).checkState() == Qt.CheckState.Checked:
self.setItemChecked(index, False)
else:
self.setItemChecked(index, True)

View File

@ -8,15 +8,15 @@ import traceback
import webbrowser
from collections.abc import Collection, Sequence
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import QWidget
from PyQt6.QtCore import QUrl
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import QWidget
logger = logging.getLogger(__name__)
try:
from PyQt5 import QtGui, QtWidgets
from PyQt5.QtCore import Qt
from PyQt6 import QtGui, QtWidgets
from PyQt6.QtCore import Qt
qt_available = True
except ImportError:
@ -31,7 +31,8 @@ if qt_available:
pil_available = False
active_palette: QPalette | None = None
try:
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineView
from PyQt6.QtWebEngineCore import QWebEnginePage
from PyQt6.QtWebEngineWidgets import QWebEngineView
class WebPage(QWebEnginePage):
def acceptNavigationRequest(
@ -54,6 +55,7 @@ if qt_available:
webengine.setPage(WebPage(parent))
webengine.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
settings = webengine.settings()
assert settings is not None
settings.setAttribute(settings.WebAttribute.AutoLoadImages, True)
settings.setAttribute(settings.WebAttribute.JavascriptEnabled, False)
settings.setAttribute(settings.WebAttribute.JavascriptCanOpenWindows, False)
@ -194,7 +196,7 @@ if qt_available:
if isinstance(widget, (QtWidgets.QTextEdit, QtWidgets.QLineEdit, QtWidgets.QAbstractSpinBox)):
widget.setReadOnly(False)
elif isinstance(widget, QtWidgets.QListWidget):
widget.setMovement(QtWidgets.QListWidget.Free)
widget.setMovement(QtWidgets.QListWidget.Movement.Free)
else:
if isinstance(widget, QtWidgets.QTableWidgetItem):
widget.setBackground(inactive_brush)
@ -211,7 +213,7 @@ if qt_available:
elif isinstance(widget, QtWidgets.QListWidget):
inactive_palette = palettes()
widget.setPalette(inactive_palette[0])
widget.setMovement(QtWidgets.QListWidget.Static)
widget.setMovement(QtWidgets.QListWidget.Movement.Static)
def replaceWidget(
layout: QtWidgets.QLayout | QtWidgets.QSplitter, old_widget: QtWidgets.QWidget, new_widget: QtWidgets.QWidget

View File

@ -6,7 +6,7 @@ from pathlib import Path
from typing import Any, NamedTuple, cast
import settngs
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt6 import QtCore, QtGui, QtWidgets
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.ctsettings import ct_ns, group_for_plugin
@ -39,11 +39,13 @@ class PasswordEdit(QtWidgets.QLineEdit):
self.visibleIcon = QtGui.QIcon(":/graphics/eye.svg")
self.hiddenIcon = QtGui.QIcon(":/graphics/hidden.svg")
self.setEchoMode(QtWidgets.QLineEdit.Password)
self.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
if show_visibility:
# Add the password hide/shown toggle at the end of the edit box.
self.togglepasswordAction = self.addAction(self.visibleIcon, QtWidgets.QLineEdit.TrailingPosition)
self.togglepasswordAction = self.addAction(
self.visibleIcon, QtWidgets.QLineEdit.ActionPosition.TrailingPosition
)
self.togglepasswordAction.setToolTip("Show password")
self.togglepasswordAction.triggered.connect(self.on_toggle_password_action)
@ -51,12 +53,12 @@ class PasswordEdit(QtWidgets.QLineEdit):
def on_toggle_password_action(self) -> None:
if not self.password_shown:
self.setEchoMode(QtWidgets.QLineEdit.Normal)
self.setEchoMode(QtWidgets.QLineEdit.EchoMode.Normal)
self.password_shown = True
self.togglepasswordAction.setIcon(self.hiddenIcon)
self.togglepasswordAction.setToolTip("Hide password")
else:
self.setEchoMode(QtWidgets.QLineEdit.Password)
self.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
self.password_shown = False
self.togglepasswordAction.setIcon(self.visibleIcon)
self.togglepasswordAction.setToolTip("Show password")
@ -125,7 +127,7 @@ def generate_spinbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) ->
widget = QtWidgets.QSpinBox()
widget.setRange(0, 9999)
widget.setToolTip(option.help)
layout.addWidget(widget, row, 1, alignment=QtCore.Qt.AlignLeft)
layout.addWidget(widget, row, 1, alignment=QtCore.Qt.AlignmentFlag.AlignLeft)
return widget
@ -138,7 +140,7 @@ def generate_doublespinbox(option: settngs.Setting, layout: QtWidgets.QGridLayou
widget = QtWidgets.QDoubleSpinBox()
widget.setRange(0, 9999.99)
widget.setToolTip(option.help)
layout.addWidget(widget, row, 1, alignment=QtCore.Qt.AlignLeft)
layout.addWidget(widget, row, 1, alignment=QtCore.Qt.AlignmentFlag.AlignLeft)
return widget
@ -223,8 +225,8 @@ def generate_talker_info(talker: ComicTalker, config: settngs.Config[ct_ns], lay
# Add horizontal divider
line = QtWidgets.QFrame()
line.setFrameShape(QtWidgets.QFrame.HLine)
line.setFrameShadow(QtWidgets.QFrame.Sunken)
line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
layout.addWidget(line, row + 3, 0, 1, -1)
@ -352,15 +354,15 @@ def generate_source_option_tabs(
talker_layout = QtWidgets.QGridLayout()
lbl_select_talker = QtWidgets.QLabel("Metadata Source:")
line = QtWidgets.QFrame()
line.setFrameShape(QtWidgets.QFrame.HLine)
line.setFrameShadow(QtWidgets.QFrame.Sunken)
line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
talker_tabs = QtWidgets.QTabWidget()
# Store all widgets as to allow easier access to their values vs. using findChildren etc. on the tab widget
sources: Sources = Sources(QtWidgets.QComboBox(), [])
talker_layout.addWidget(lbl_select_talker, 0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum)
talker_layout.addWidget(sources[0], 0, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum)
talker_layout.addWidget(lbl_select_talker, 0, 0)
talker_layout.addWidget(sources[0], 0, 1)
talker_layout.addWidget(line, 1, 0, 1, -1)
talker_layout.addWidget(talker_tabs, 2, 0, 1, -1)
@ -440,7 +442,9 @@ def generate_source_option_tabs(
generate_api_widgets(talker, tab, key_option, url_option, layout_grid, definitions=config.definitions)
# Add vertical spacer
vspacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
vspacer = QtWidgets.QSpacerItem(
20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding
)
layout_grid.addItem(vspacer, layout_grid.rowCount() + 1, 0)
# Display the new widgets
tab.tab.setLayout(layout_grid)

View File

@ -5,7 +5,7 @@ extend-exclude = "comictaggerlib/graphics/resources.py"
[tool.isort]
line_length = 120
extend_skip = ["scripts", "comictaggerlib/graphics/resources.py"]
extend_skip_glob = ["scripts", "comictaggerlib/graphics/resources.py"]
profile = "black"
[build-system]
@ -23,4 +23,4 @@ disable = "C0330, C0326, C0115, C0116, C0103"
max-line-length=120
[tool.pylint.master]
extension-pkg-whitelist="PyQt5"
extension-pkg-whitelist="PyQt6"

View File

@ -50,6 +50,8 @@ install_requires =
text2digits
typing-extensions>=4.3.0
wordninja
zipremove
pyicu;sys_platform != 'windows'
python_requires = >=3.9
[options.packages.find]
@ -73,18 +75,17 @@ pyinstaller40 =
[options.extras_require]
7z =
py7zr
py7zr<1
all =
PyQt5
PyQtWebEngine
comicinfoxml==0.4.*
PyQt6
PyQt6-WebEngine
comicinfoxml==0.5.*
gcd-talker>0.1.0
metron-talker>0.1.5
pillow-avif-plugin>=1.4.1
pillow-jxl-plugin>=1.2.5
py7zr
py7zr<1
rarfile>=4.0
pyicu;sys_platform == 'linux' or sys_platform == 'darwin'
archived_tags =
ct-archived-tags
avif =
@ -92,11 +93,11 @@ avif =
cbr =
rarfile>=4.0
cix =
comicinfoxml==0.4.*
comicinfoxml==0.5.*
gcd =
gcd-talker>0.1.0
gui =
PyQt5
PyQt6
icu =
pyicu;sys_platform == 'linux' or sys_platform == 'darwin'
jxl =
@ -104,17 +105,16 @@ jxl =
metron =
metron-talker>0.1.5
pyinstaller =
PyQt5
PyQtWebEngine
comicinfoxml==0.4.*
PyQt6
PyQt6-WebEngine
comicinfoxml==0.5.*
pillow-avif-plugin>=1.4.1
pillow-jxl-plugin>=1.2.5
py7zr
py7zr<1
rarfile>=4.0
pyicu;sys_platform == 'linux' or sys_platform == 'darwin'
qtw =
PyQt5
PyQtWebEngine
PyQt6
PyQt6-WebEngine
[options.package_data]
comicapi =
@ -126,7 +126,7 @@ comictaggerlib =
[tox:tox]
env_list =
format
py3.9-{none,gui,7z,cbr,icu,all}
py3.9-{none,gui,7z,cbr,all}
minversion = 4.4.12
basepython = {env:tox_python:python3.9}
@ -139,28 +139,10 @@ extras =
7z: 7Z
cbr: CBR
gui: GUI
icu: ICU
all: all
commands =
python -m pytest {tty:--color=yes} {posargs}
icu,all: python -c 'import importlib,platform; importlib.import_module("icu") if platform.system() != "Windows" else ...' # Sanity check for icu
[m1env]
description = run the tests with pytest
package = wheel
deps =
pytest>=7
icu,all: pyicu-binary
extras =
7z: 7Z
cbr: CBR
gui: GUI
all: 7Z,CBR,GUI
commands =
python -m pytest {tty:--color=yes} {posargs}
[testenv:py3.9-{icu,all}]
base = {env:tox_env:testenv}
python -c 'import importlib,platform; importlib.import_module("icu") if platform.system() != "Windows" else ...' # Sanity check for icu
[testenv:format]
labels =
@ -212,7 +194,6 @@ commands =
[testenv:wheel]
description = Generate wheel and tar.gz
labels =
release
build
depends = clean
skip_install = true
@ -224,8 +205,6 @@ commands =
[testenv:pypi-upload]
description = Upload wheel to PyPi
platform = linux
labels =
release
skip_install = true
depends = wheel
deps =
@ -246,7 +225,6 @@ commands =
description = Generate pyinstaller executable
labels =
build
release
base = {env:tox_env:testenv}
depends =
clean
@ -255,7 +233,6 @@ deps =
extras =
pyinstaller
commands =
pyrcc5 comictaggerlib/graphics/graphics.qrc -o comictaggerlib/graphics/resources.py
pyinstaller -y build-tools/comictagger.spec
python -c 'import importlib,platform; importlib.import_module("icu") if platform.system() != "Windows" else ...' # Sanity check for icu
@ -265,7 +242,6 @@ skip_install = true
platform = linux
base = {env:tox_env:testenv}
labels =
release
build
depends =
clean
@ -273,11 +249,11 @@ depends =
deps =
requests
allowlist_externals =
{tox_root}/build/appimagetool-x86_64.AppImage
{tox_root}/build/appimagetool.AppImage
change_dir = {tox_root}/dist/binary
commands_pre =
-python -c 'import shutil; shutil.rmtree("{tox_root}/build/", ignore_errors=True)'
python {tox_root}/build-tools/get_appimage.py {tox_root}/build/appimagetool-x86_64.AppImage
python {tox_root}/build-tools/get_appimage.py {tox_root}/build/appimagetool.AppImage
commands =
python -c 'import shutil,pathlib; shutil.copytree("{tox_root}/dist/comictagger/", "{tox_root}/build/appimage", dirs_exist_ok=True); \
shutil.copy("{tox_root}/comictaggerlib/graphics/app.png", "{tox_root}/build/appimage/app.png"); \
@ -285,12 +261,11 @@ commands =
pathlib.Path("{tox_root}/build/appimage/AppRun.desktop").write_text( \
pathlib.Path("{tox_root}/build-tools/ComicTagger.desktop").read_text() \
.replace("/usr/local/share/comictagger/app.png", "app"))'
{tox_root}/build/appimagetool-x86_64.AppImage {tox_root}/build/appimage
{tox_root}/build/appimagetool.AppImage {tox_root}/build/appimage
[testenv:zip_artifacts]
description = Zip release artifacts
labels =
release
build
depends =
wheel