MTGProxyPrinter

Changes On Branch port_pyside6
Login

Changes On Branch port_pyside6

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Changes In Branch port_pyside6 Excluding Merge-Ins

This is equivalent to a diff from 31d1829412 to 73d0989b15

2025-05-31
11:50
Dependencies: Capitalize the Pint library package name to match the case on PyPI. Also specify a version range for typing_extensions Leaf check-in: 244c5fdb16 user: thomas tags: trunk
2025-05-28
13:52
Merge with trunk check-in: ec162d04f8 user: thomas tags: enum_based_paper_size
2025-05-26
16:15
Release v0.32.0 Leaf check-in: 73d0989b15 user: thomas tags: port_pyside6
16:09
Release v0.32.0 check-in: 31d1829412 user: thomas tags: trunk, release, v0.32.0
10:15
Merge with trunk check-in: 2e4501ff9d user: thomas tags: port_pyside6
10:07
changelog.md: Add GitHub note. Fixed grammar issues. check-in: 1a0aaeee16 user: thomas tags: trunk

Changes to README.md.

109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
- Python >= 3.8

These external libraries are used in the code. They can be installed from PyPI.

- `platformdirs`
- `ijson`
- `pint`
- `PyQt5`
- `delegateto`
- `PyHamcrest`
- `cx_Freeze` (Stand-alone bundles only. Used by the installer for Windows®-based platforms.)
- Either `truststore` (Py >= 3.10) or `certifi` (Py < 3.10)
- `typing_extensions` (Py < 3.11)

### System libraries







|







109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
- Python >= 3.8

These external libraries are used in the code. They can be installed from PyPI.

- `platformdirs`
- `ijson`
- `pint`
- `PySide6`
- `delegateto`
- `PyHamcrest`
- `cx_Freeze` (Stand-alone bundles only. Used by the installer for Windows®-based platforms.)
- Either `truststore` (Py >= 3.10) or `certifi` (Py < 3.10)
- `typing_extensions` (Py < 3.11)

### System libraries

Changes to build_MTGProxyPrinter_packages.bat.

1
2
3
4
5
6
7
8
9
10
11
12
:: Generate an application bundle using cx_Freeze

:: Create or activate the build environment
IF EXIST "venv" (
  call venv\Scripts\activate.bat
) ELSE (
  call create_development_environment.bat
)

:: Create a platform-dependent, portable build in the build directory
:: and an MSI-based installer in the dist directory.
:: Also creates a cross-platform Python sdist and wheel package in the dist directory.



|
|







1
2
3
4
5
6
7
8
9
10
11
12
:: Generate an application bundle using cx_Freeze

:: Create or activate the build environment
IF EXIST "venv-PySide6" (
  call venv-PySide6\Scripts\activate.bat
) ELSE (
  call create_development_environment.bat
)

:: Create a platform-dependent, portable build in the build directory
:: and an MSI-based installer in the dist directory.
:: Also creates a cross-platform Python sdist and wheel package in the dist directory.

Changes to build_MTGProxyPrinter_packages.sh.

1
2
3
4
5
6
7
8
9
#!/bin/bash
ENVIRONMENT_NAME="venv"
# Generate an application bundle using cx_Freeze for Linux.

if [ ! -e "${ENVIRONMENT_NAME}" ]; then
  ./create_development_environment.sh
fi

source "${ENVIRONMENT_NAME}/bin/activate"

|







1
2
3
4
5
6
7
8
9
#!/bin/bash
ENVIRONMENT_NAME="venv-PySide6"
# Generate an application bundle using cx_Freeze for Linux.

if [ ! -e "${ENVIRONMENT_NAME}" ]; then
  ./create_development_environment.sh
fi

source "${ENVIRONMENT_NAME}/bin/activate"

Changes to create_development_environment.bat.

1
2
3
4
5
6
7
8
9
10
python -m venv venv

call venv\Scripts\activate.bat

python -m pip install --upgrade pip setuptools
python -m pip install wheel "pip-tools >= 7"

echo Creating requirements.txt from pyproject.toml. This takes a while.
python scripts\rebuild_requirements.py
echo "Installing dependencies into the virtual environment"
|

|







1
2
3
4
5
6
7
8
9
10
python -m venv venv-PySide6

call venv-PySide6\Scripts\activate.bat

python -m pip install --upgrade pip setuptools
python -m pip install wheel "pip-tools >= 7"

echo Creating requirements.txt from pyproject.toml. This takes a while.
python scripts\rebuild_requirements.py
echo "Installing dependencies into the virtual environment"

Changes to create_development_environment.sh.

1
2
3
4
5
6
7
8
9
#!/bin/bash
ENVIRONMENT_NAME="venv"

if [ -e "${ENVIRONMENT_NAME}" ]; then
  echo "Removing already existing virtual environment."
  rm -r "${ENVIRONMENT_NAME}"
fi

python -m venv "${ENVIRONMENT_NAME}"

|







1
2
3
4
5
6
7
8
9
#!/bin/bash
ENVIRONMENT_NAME="venv-PySide6"

if [ -e "${ENVIRONMENT_NAME}" ]; then
  echo "Removing already existing virtual environment."
  rm -r "${ENVIRONMENT_NAME}"
fi

python -m venv "${ENVIRONMENT_NAME}"

Changes to doc/ThirdPartyLicenses.md.

507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

&nbsp;

# PyQt5

Copyright (c) [Riverbank Computing Limited](https://www.riverbankcomputing.com/).

The included copy is licensed under the terms of the 
GNU General Public License version 3 as published by the Free Software Foundation.

See LICENSE.md (when viewing from the source code archive) or the License tab in the
MTGProxyPrinter About dialog for details.

&nbsp;

# cx_Freeze

Note: This section only applies to application bundles and installers created 
using cx_Freeze, like the stand-alone installer provided for Windows®-based platforms.  







|

|

|
<
|
<
<







507
508
509
510
511
512
513
514
515
516
517
518

519


520
521
522
523
524
525
526
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

&nbsp;

# Qt6, PySide6

Qt and PySide6 are available under the GNU Lesser General Public License version 3.

The Qt Toolkit is Copyright (C) 2018 The Qt Company Ltd. and other contributors.

Contact: https://www.qt.io/licensing/



&nbsp;

# cx_Freeze

Note: This section only applies to application bundles and installers created 
using cx_Freeze, like the stand-alone installer provided for Windows®-based platforms.  

Changes to mtg_proxy_printer/__main__.py.

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Import and implicitly load the settings first, before importing any modules that pull in GUI classes.
import mtg_proxy_printer.settings

import os
import platform
import sys

from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import QApplication

import mtg_proxy_printer.app_dirs
from mtg_proxy_printer.argument_parser import parse_args
import mtg_proxy_printer.logger
from mtg_proxy_printer.application import Application
import mtg_proxy_printer.natsort








|
<







17
18
19
20
21
22
23
24

25
26
27
28
29
30
31
# Import and implicitly load the settings first, before importing any modules that pull in GUI classes.
import mtg_proxy_printer.settings

import os
import platform
import sys

from PySide6.QtCore import QTimer


import mtg_proxy_printer.app_dirs
from mtg_proxy_printer.argument_parser import parse_args
import mtg_proxy_printer.logger
from mtg_proxy_printer.application import Application
import mtg_proxy_printer.natsort

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

def main():
    global _app
    arguments = parse_args()
    mtg_proxy_printer.app_dirs.migrate_from_old_appdirs()
    mtg_proxy_printer.logger.configure_root_logger()
    handle_ssl_certificates()
    # According to https://doc.qt.io/qt-5/qt.html#ApplicationAttribute-enum,
    # Qt.AA_EnableHighDpiScaling has to be set prior to creating the QApplication instance
    QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling)
    _app = Application(arguments, sys.argv)
    if arguments.test_exit_on_launch:
        logger.info("Skipping startup tasks, because immediate application exit was requested.")
        QTimer.singleShot(0, _app.main_window.on_action_quit_triggered)
    else:
        logger.debug("Enqueueing startup tasks.")
        _app.enqueue_startup_tasks(arguments)
    logger.debug("Initialisation done. Starting event loop.")
    _app.exec_()
    logger.debug("Left event loop.")


if __name__ == "__main__":
    main()







<
<
<








|





56
57
58
59
60
61
62



63
64
65
66
67
68
69
70
71
72
73
74
75
76

def main():
    global _app
    arguments = parse_args()
    mtg_proxy_printer.app_dirs.migrate_from_old_appdirs()
    mtg_proxy_printer.logger.configure_root_logger()
    handle_ssl_certificates()



    _app = Application(arguments, sys.argv)
    if arguments.test_exit_on_launch:
        logger.info("Skipping startup tasks, because immediate application exit was requested.")
        QTimer.singleShot(0, _app.main_window.on_action_quit_triggered)
    else:
        logger.debug("Enqueueing startup tasks.")
        _app.enqueue_startup_tasks(arguments)
    logger.debug("Initialisation done. Starting event loop.")
    _app.exec()
    logger.debug("Left event loop.")


if __name__ == "__main__":
    main()

Changes to mtg_proxy_printer/application.py.

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import pathlib
import platform
import shutil
import sys
from tempfile import mkdtemp
import typing

from PyQt5.QtCore import pyqtSlot as Slot, Qt, QTimer, QStringListModel, QThreadPool, QTranslator, QLocale, QLibraryInfo
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QIcon

from mtg_proxy_printer.argument_parser import Namespace
from mtg_proxy_printer import meta_data
import mtg_proxy_printer.model.carddb
import mtg_proxy_printer.carddb_migrations
import mtg_proxy_printer.model.document
import mtg_proxy_printer.model.imagedb







|
|
|







20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import pathlib
import platform
import shutil
import sys
from tempfile import mkdtemp
import typing

from PySide6.QtCore import Slot, QTimer, QStringListModel, QThreadPool, QTranslator, QLocale, QLibraryInfo
from PySide6.QtWidgets import QApplication
from PySide6.QtGui import QIcon

from mtg_proxy_printer.argument_parser import Namespace
from mtg_proxy_printer import meta_data
import mtg_proxy_printer.model.carddb
import mtg_proxy_printer.carddb_migrations
import mtg_proxy_printer.model.document
import mtg_proxy_printer.model.imagedb
55
56
57
58
59
60
61
62
63
64
65
66
67
68







69
70
71
72
73
74
75

class Application(QApplication):

    def __init__(self, args: Namespace, argv: typing.List[str] = None):
        if argv is None:
            argv = sys.argv
        logger.info(f"Starting MTGProxyPrinter version {meta_data.__version__}")
        if not os.getenv("QT_QPA_PLUGIN") and "-platform" not in argv and platform.system() == "Windows":
            logger.info("Running on Windows without explicit platform override. Enabling dark mode rendering.")
            # The explicit set platform and parameters overwrite the environment, so set these options iff neither
            # present as parameters nor environment variables.
            argv.append("-platform")
            argv.append("windows:darkmode=1")
        super().__init__(argv)







        # Used by the with_database_write_lock decorator to not start un-frozen,
        # waiting tasks when the application is about to exit
        self.should_run = True
        self.args = args
        self._setup_translations()
        self._setup_icons()
        self.language_model = self._create_language_model()  # TODO: Can this be removed?







<
<
<
<
<
<

>
>
>
>
>
>
>







55
56
57
58
59
60
61






62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

class Application(QApplication):

    def __init__(self, args: Namespace, argv: typing.List[str] = None):
        if argv is None:
            argv = sys.argv
        logger.info(f"Starting MTGProxyPrinter version {meta_data.__version__}")






        super().__init__(argv)
        # Note: The sub-expression '"-style" not in argv' breaks, if "-style" is passed as a value for another
        # argument or as a positional argument.
        # (For example, if the user wants to load a document with file name "-style" via command line argument.)
        if platform.system() == "Windows" and "-style" not in argv and not os.getenv("QT_STYLE_OVERRIDE"):
            logger.info("Running on Windows without explicit style set. Use 'fusion', which supports dark mode.")
            # Set a dark-mode compatible style, if on Windows and the user does not override the style.
            self.setStyle("fusion")
        # Used by the with_database_write_lock decorator to not start un-frozen,
        # waiting tasks when the application is about to exit
        self.should_run = True
        self.args = args
        self._setup_translations()
        self._setup_icons()
        self.language_model = self._create_language_model()  # TODO: Can this be removed?
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
        logger.info(
            f"Loading localisations. System locale: {system_locale.name()}, selected locale: {locale.name()}. "
            f"Possible display languages are: {locale.uiLanguages()}")
        path = ":" if mtg_proxy_printer.ui.common.HAS_COMPILED_RESOURCES \
            else str(pathlib.Path(mtg_proxy_printer.__file__).parent / "resources")
        path += "/translations"
        logger.debug(f"Locale search path is '{path}'")
        self._load_translator(locale, "qtbase", QLibraryInfo.location(QLibraryInfo.LibraryLocation.TranslationsPath))
        self._load_translator(locale, "mtgproxyprinter", path)

    def _load_translator(self, locale: QLocale, component: str, path: str):
        translator = QTranslator(self)
        if translator.load(locale, component, '_', path):
            logger.debug(f"{component} translation loaded successfully, installing it")
            self.installTranslator(translator)







|







226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
        logger.info(
            f"Loading localisations. System locale: {system_locale.name()}, selected locale: {locale.name()}. "
            f"Possible display languages are: {locale.uiLanguages()}")
        path = ":" if mtg_proxy_printer.ui.common.HAS_COMPILED_RESOURCES \
            else str(pathlib.Path(mtg_proxy_printer.__file__).parent / "resources")
        path += "/translations"
        logger.debug(f"Locale search path is '{path}'")
        self._load_translator(locale, "qtbase", QLibraryInfo.location(QLibraryInfo.LibraryPath.TranslationsPath))
        self._load_translator(locale, "mtgproxyprinter", path)

    def _load_translator(self, locale: QLocale, component: str, path: str):
        translator = QTranslator(self)
        if translator.load(locale, component, '_', path):
            logger.debug(f"{component} translation loaded successfully, installing it")
            self.installTranslator(translator)
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
                theme_search_paths = QIcon.themeSearchPaths()
                theme_search_paths.append(mtg_proxy_printer.ui.common.ICON_PATH_PREFIX)
                QIcon.setThemeSearchPaths(theme_search_paths)
            QIcon.setThemeName("breeze")
        else:
            logger.debug(f"Using system-provided icon theme '{fallback_icon_theme}'")

        self.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps)

    @Slot()
    def quit(self):
        logger.info("About to exit.")
        self.should_run = False
        self.main_window.hide()
        self.main_window.close()
        self.closeAllWindows()







<
<







263
264
265
266
267
268
269


270
271
272
273
274
275
276
                theme_search_paths = QIcon.themeSearchPaths()
                theme_search_paths.append(mtg_proxy_printer.ui.common.ICON_PATH_PREFIX)
                QIcon.setThemeSearchPaths(theme_search_paths)
            QIcon.setThemeName("breeze")
        else:
            logger.debug(f"Using system-provided icon theme '{fallback_icon_theme}'")



    @Slot()
    def quit(self):
        logger.info("About to exit.")
        self.should_run = False
        self.main_window.hide()
        self.main_window.close()
        self.closeAllWindows()

Changes to mtg_proxy_printer/card_info_downloader.py.

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import socket
import typing
import urllib.error
import urllib.parse
import urllib.request

import ijson
from PyQt5.QtCore import pyqtSignal as Signal, QObject, Qt, QThreadPool

from mtg_proxy_printer.downloader_base import DownloaderBase
from mtg_proxy_printer.model.carddb import CardDatabase, SCHEMA_NAME, with_database_write_lock
from mtg_proxy_printer.sqlite_helpers import cached_dedent
from mtg_proxy_printer.printing_filter_updater import PrintingFilterUpdater
import mtg_proxy_printer.metered_file
from mtg_proxy_printer.logger import get_logger







|







26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import socket
import typing
import urllib.error
import urllib.parse
import urllib.request

import ijson
from PySide6.QtCore import Signal, QObject, Qt, QThreadPool

from mtg_proxy_printer.downloader_base import DownloaderBase
from mtg_proxy_printer.model.carddb import CardDatabase, SCHEMA_NAME, with_database_write_lock
from mtg_proxy_printer.sqlite_helpers import cached_dedent
from mtg_proxy_printer.printing_filter_updater import PrintingFilterUpdater
import mtg_proxy_printer.metered_file
from mtg_proxy_printer.logger import get_logger

Changes to mtg_proxy_printer/carddb_migrations.py.

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import time
import typing
import urllib.error
import urllib.parse
from textwrap import dedent
from typing import List, Dict, Union, Tuple, Any, Generator, Callable, Iterable

from PyQt5.QtCore import QCoreApplication, Qt

try:
    from typing import LiteralString
except ImportError:
    from typing_extensions import LiteralString

from mtg_proxy_printer.progress_meter import ProgressMeter







|







29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import time
import typing
import urllib.error
import urllib.parse
from textwrap import dedent
from typing import List, Dict, Union, Tuple, Any, Generator, Callable, Iterable

from PySide6.QtCore import QCoreApplication, Qt

try:
    from typing import LiteralString
except ImportError:
    from typing_extensions import LiteralString

from mtg_proxy_printer.progress_meter import ProgressMeter

Changes to mtg_proxy_printer/decklist_downloader.py.

25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import urllib.parse
from io import StringIO
import platform
import re
import typing

import ijson
from PyQt5.QtGui import QValidator

from mtg_proxy_printer.downloader_base import DownloaderBase
from mtg_proxy_printer.decklist_parser.common import ParserBase
from mtg_proxy_printer.decklist_parser.csv_parsers import ScryfallCSVParser, TappedOutCSVParser
from mtg_proxy_printer.decklist_parser.re_parsers import MTGArenaParser, MagicWorkstationDeckDataFormatParser, \
    XMageParser
from mtg_proxy_printer.logger import get_logger







|







25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import urllib.parse
from io import StringIO
import platform
import re
import typing

import ijson
from PySide6.QtGui import QValidator

from mtg_proxy_printer.downloader_base import DownloaderBase
from mtg_proxy_printer.decklist_parser.common import ParserBase
from mtg_proxy_printer.decklist_parser.csv_parsers import ScryfallCSVParser, TappedOutCSVParser
from mtg_proxy_printer.decklist_parser.re_parsers import MTGArenaParser, MagicWorkstationDeckDataFormatParser, \
    XMageParser
from mtg_proxy_printer.logger import get_logger

Changes to mtg_proxy_printer/decklist_parser/common.py.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from abc import abstractmethod
import typing

from PyQt5.QtCore import QObject, pyqtSignal as Signal

from mtg_proxy_printer.model.carddb import CardDatabase, CardIdentificationData
from mtg_proxy_printer.model.card import Card, AnyCardType
from mtg_proxy_printer.model.imagedb import ImageDatabase
import mtg_proxy_printer.settings
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)







|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from abc import abstractmethod
import typing

from PySide6.QtCore import QObject, Signal

from mtg_proxy_printer.model.carddb import CardDatabase, CardIdentificationData
from mtg_proxy_printer.model.card import Card, AnyCardType
from mtg_proxy_printer.model.imagedb import ImageDatabase
import mtg_proxy_printer.settings
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)

Changes to mtg_proxy_printer/decklist_parser/csv_parsers.py.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29


import abc
import collections
import csv
import typing

from PyQt5.QtCore import QObject, QCoreApplication

from mtg_proxy_printer.model.carddb import CardDatabase, CardIdentificationData
from ..model.card import Card
from mtg_proxy_printer.model.imagedb import ImageDatabase

from .common import ParsedDeck, ParserBase
from mtg_proxy_printer.logger import get_logger







|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29


import abc
import collections
import csv
import typing

from PySide6.QtCore import QObject, QCoreApplication

from mtg_proxy_printer.model.carddb import CardDatabase, CardIdentificationData
from ..model.card import Card
from mtg_proxy_printer.model.imagedb import ImageDatabase

from .common import ParsedDeck, ParserBase
from mtg_proxy_printer.logger import get_logger

Changes to mtg_proxy_printer/decklist_parser/re_parsers.py.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#  along with this program. If not, see <http://www.gnu.org/licenses/>.

import copy
from collections import Counter
import re
import typing

from PyQt5.QtCore import QObject, QCoreApplication

from mtg_proxy_printer.decklist_parser.common import ParsedDeck, ParserBase
from mtg_proxy_printer.model.carddb import CardDatabase, CardIdentificationData
from mtg_proxy_printer.model.card import Card
from mtg_proxy_printer.model.imagedb import ImageDatabase
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)







|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#  along with this program. If not, see <http://www.gnu.org/licenses/>.

import copy
from collections import Counter
import re
import typing

from PySide6.QtCore import QObject, QCoreApplication

from mtg_proxy_printer.decklist_parser.common import ParsedDeck, ParserBase
from mtg_proxy_printer.model.carddb import CardDatabase, CardIdentificationData
from mtg_proxy_printer.model.card import Card
from mtg_proxy_printer.model.imagedb import ImageDatabase
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
175
176
177
178
179
180
181

182
183
184
185
186
187
188

    @staticmethod
    def supported_file_types() -> typing.Dict[str, typing.List[str]]:
        return {
            QCoreApplication.translate(
                "MagicWorkstationDeckDataFormatParser", "Magic Workstation Deck Data Format"): ["mwDeck"],
        }

    PREFIXES_TO_SKIP = frozenset({"//"})

    def __init__(self, card_db: CardDatabase, image_db: ImageDatabase, parent: QObject = None):
        super().__init__(
            card_db, image_db,
            re.compile(r"(SB: {1,2})?(?P<copies>\d+) \[(?P<set_code>\w+)?] (?P<name>.+)"), parent
        )







>







175
176
177
178
179
180
181
182
183
184
185
186
187
188
189

    @staticmethod
    def supported_file_types() -> typing.Dict[str, typing.List[str]]:
        return {
            QCoreApplication.translate(
                "MagicWorkstationDeckDataFormatParser", "Magic Workstation Deck Data Format"): ["mwDeck"],
        }

    PREFIXES_TO_SKIP = frozenset({"//"})

    def __init__(self, card_db: CardDatabase, image_db: ImageDatabase, parent: QObject = None):
        super().__init__(
            card_db, image_db,
            re.compile(r"(SB: {1,2})?(?P<copies>\d+) \[(?P<set_code>\w+)?] (?P<name>.+)"), parent
        )

Changes to mtg_proxy_printer/document_controller/_interface.py.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

from abc import abstractmethod
from functools import partial
import itertools
import operator
import typing

from PyQt5.QtCore import QCoreApplication

from mtg_proxy_printer.units_and_sizes import StringList

if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.document import Document

try:







|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

from abc import abstractmethod
from functools import partial
import itertools
import operator
import typing

from PySide6.QtCore import QCoreApplication

from mtg_proxy_printer.units_and_sizes import StringList

if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.document import Document

try:

Changes to mtg_proxy_printer/document_controller/edit_custom_card.py.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import functools
import typing

from PyQt5.QtCore import QModelIndex, Qt
if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.model.document_page import CardContainer, PageColumns
from ._interface import DocumentAction, Self

from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)







|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import functools
import typing

from PySide6.QtCore import QModelIndex, Qt
if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.model.document_page import CardContainer, PageColumns
from ._interface import DocumentAction, Self

from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)

Changes to mtg_proxy_printer/document_controller/move_cards.py.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import functools
import itertools
import typing

from PyQt5.QtCore import QModelIndex

from ._interface import DocumentAction, IllegalStateError, Self
from mtg_proxy_printer.logger import get_logger

if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.document_page import Page
    from mtg_proxy_printer.model.document import Document







|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import functools
import itertools
import typing

from PySide6.QtCore import QModelIndex

from ._interface import DocumentAction, IllegalStateError, Self
from mtg_proxy_printer.logger import get_logger

if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.document_page import Page
    from mtg_proxy_printer.model.document import Document

Changes to mtg_proxy_printer/document_controller/replace_card.py.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import functools
import typing

from PyQt5.QtCore import Qt

from ..model.card import Card

if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.document_page import CardContainer
    from mtg_proxy_printer.model.document import Document








|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import functools
import typing

from PySide6.QtCore import Qt

from ..model.card import Card

if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.document_page import CardContainer
    from mtg_proxy_printer.model.document import Document

Changes to mtg_proxy_printer/document_controller/shuffle_document.py.

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    from random import randbytes
except ImportError:
    # Compatibility with Py 3.8
    from secrets import token_bytes as randbytes

import typing

from PyQt5.QtCore import Qt, QModelIndex

from ._interface import DocumentAction, IllegalStateError, Self
from ..model.card import Card
from mtg_proxy_printer.model.document_page import CardContainer, PageColumns
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.units_and_sizes import PageType
__all__ = [







|







19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    from random import randbytes
except ImportError:
    # Compatibility with Py 3.8
    from secrets import token_bytes as randbytes

import typing

from PySide6.QtCore import Qt, QModelIndex

from ._interface import DocumentAction, IllegalStateError, Self
from ..model.card import Card
from mtg_proxy_printer.model.document_page import CardContainer, PageColumns
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.units_and_sizes import PageType
__all__ = [

Changes to mtg_proxy_printer/downloader_base.py.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import gzip

from PyQt5.QtCore import QObject, pyqtSignal as Signal

import mtg_proxy_printer.http_file
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger

# Offer accepting gzip, as that is supported by the Scryfall server and reduces network data use by 80-90%







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import gzip

from PySide6.QtCore import QObject, Signal

import mtg_proxy_printer.http_file
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger

# Offer accepting gzip, as that is supported by the Scryfall server and reduces network data use by 80-90%

Changes to mtg_proxy_printer/http_file.py.

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import http.client
import socket
import time
from typing import List, Optional, Dict, Callable
import urllib.error
import urllib.request

from PyQt5.QtCore import QObject, pyqtSignal as Signal
import delegateto

from mtg_proxy_printer.meta_data import USER_AGENT
from mtg_proxy_printer.logger import get_logger

logger = get_logger(__name__)
del get_logger







|







20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import http.client
import socket
import time
from typing import List, Optional, Dict, Callable
import urllib.error
import urllib.request

from PySide6.QtCore import QObject, Signal
import delegateto

from mtg_proxy_printer.meta_data import USER_AGENT
from mtg_proxy_printer.logger import get_logger

logger = get_logger(__name__)
del get_logger

Changes to mtg_proxy_printer/meta_data.py.

11
12
13
14
15
16
17
18
19
20
21
22
23
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


PROGRAMNAME = "MTGProxyPrinter"
__version__ = "0.32.0"
COPYRIGHT = "(C) 2020-2025 Thomas Hess"
HOME_PAGE = "https://chiselapp.com/user/luziferius/repository/MTGProxyPrinter"

DOWNLOAD_WEB_PAGE = f"{HOME_PAGE}/uv/download.html"
USER_AGENT = f"{PROGRAMNAME}/{__version__} ({HOME_PAGE})"







|





11
12
13
14
15
16
17
18
19
20
21
22
23
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


PROGRAMNAME = "MTGProxyPrinter"
__version__ = "0.32.0+PySide6"
COPYRIGHT = "(C) 2020-2025 Thomas Hess"
HOME_PAGE = "https://chiselapp.com/user/luziferius/repository/MTGProxyPrinter"

DOWNLOAD_WEB_PAGE = f"{HOME_PAGE}/uv/download.html"
USER_AGENT = f"{PROGRAMNAME}/{__version__} ({HOME_PAGE})"

Changes to mtg_proxy_printer/metered_file.py.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#  along with this program. If not, see <http://www.gnu.org/licenses/>.



from typing import Iterable, List, Optional, BinaryIO, Union
from io import BufferedIOBase

from PyQt5.QtCore import QObject, pyqtSignal as Signal
from delegateto import delegate

from mtg_proxy_printer.logger import get_logger

logger = get_logger(__name__)
del get_logger
__all__ = [







|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#  along with this program. If not, see <http://www.gnu.org/licenses/>.



from typing import Iterable, List, Optional, BinaryIO, Union
from io import BufferedIOBase

from PySide6.QtCore import QObject, Signal
from delegateto import delegate

from mtg_proxy_printer.logger import get_logger

logger = get_logger(__name__)
del get_logger
__all__ = [

Changes to mtg_proxy_printer/missing_images_manager.py.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import typing

from PyQt5.QtCore import QObject, pyqtSignal as Signal, pyqtSlot as Slot

from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger

__all__ = [







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import typing

from PySide6.QtCore import QObject, Signal, Slot

from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger

__all__ = [

Changes to mtg_proxy_printer/model/card.py.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

31
32
33
34
35
36
37

import dataclasses
import hashlib
import enum
import functools
import typing

from PyQt5.QtCore import QRect, QPoint, QSize, Qt, QPointF
from PyQt5.QtGui import QPixmap, QColor, QColorConstants, QPainter, QTransform

from mtg_proxy_printer.units_and_sizes import CardSize, PageType, CardSizes, UUID

ItemDataRole = Qt.ItemDataRole
RenderHint = QPainter.RenderHint
SmoothTransformation = Qt.TransformationMode.SmoothTransformation
IgnoreAspectRatio = Qt.AspectRatioMode.IgnoreAspectRatio


@dataclasses.dataclass(frozen=True)
class MTGSet:
    code: str
    name: str

    def data(self, role: ItemDataRole):







|
|







>







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

import dataclasses
import hashlib
import enum
import functools
import typing

from PySide6.QtCore import QRect, QPoint, QSize, Qt, QPointF
from PySide6.QtGui import QPixmap, QColor, QColorConstants, QPainter, QTransform

from mtg_proxy_printer.units_and_sizes import CardSize, PageType, CardSizes, UUID

ItemDataRole = Qt.ItemDataRole
RenderHint = QPainter.RenderHint
SmoothTransformation = Qt.TransformationMode.SmoothTransformation
IgnoreAspectRatio = Qt.AspectRatioMode.IgnoreAspectRatio


@dataclasses.dataclass(frozen=True)
class MTGSet:
    code: str
    name: str

    def data(self, role: ItemDataRole):
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
        # Cards thus can’t be scaled using a singular factor of sqrt(2) on both axis.
        # The scaled cards get a bit compressed horizontally.
        vertical_scaling_factor = card_size.width() / card_size.height()
        horizontal_scaling_factor = card_size.height() / (2 * card_size.width())
        combined_image = QPixmap(card_size)
        combined_image.fill(QColorConstants.Transparent)
        painter = QPainter(combined_image)
        painter.setRenderHints(RenderHint.SmoothPixmapTransform | RenderHint.HighQualityAntialiasing)
        transformation = QTransform()
        transformation.rotate(90)
        transformation.scale(horizontal_scaling_factor, vertical_scaling_factor)
        painter.setTransform(transformation)
        painter.drawPixmap(QPointF(card_size.width(), -card_size.height()), self.back.image_file)
        painter.drawPixmap(QPointF(0, -card_size.height()), self.front.image_file)








|







262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
        # Cards thus can’t be scaled using a singular factor of sqrt(2) on both axis.
        # The scaled cards get a bit compressed horizontally.
        vertical_scaling_factor = card_size.width() / card_size.height()
        horizontal_scaling_factor = card_size.height() / (2 * card_size.width())
        combined_image = QPixmap(card_size)
        combined_image.fill(QColorConstants.Transparent)
        painter = QPainter(combined_image)
        painter.setRenderHints(RenderHint.SmoothPixmapTransform)
        transformation = QTransform()
        transformation.rotate(90)
        transformation.scale(horizontal_scaling_factor, vertical_scaling_factor)
        painter.setTransform(transformation)
        painter.drawPixmap(QPointF(card_size.width(), -card_size.height()), self.back.image_file)
        painter.drawPixmap(QPointF(0, -card_size.height()), self.front.image_file)

Changes to mtg_proxy_printer/model/card_list.py.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

from collections import Counter
import dataclasses
import enum
import itertools
import typing

from PyQt5.QtCore import QAbstractTableModel, QModelIndex, Qt, pyqtSignal as Signal, QItemSelection
from PyQt5.QtGui import QIcon

from mtg_proxy_printer.document_controller import DocumentAction
from mtg_proxy_printer.document_controller.edit_custom_card import ActionEditCustomCard
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.model.document_page import PageColumns
from mtg_proxy_printer.ui.common import get_card_image_tooltip
from mtg_proxy_printer.decklist_parser.common import CardCounter







|
|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

from collections import Counter
import dataclasses
import enum
import itertools
import typing

from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal, QItemSelection
from PySide6.QtGui import QIcon

from mtg_proxy_printer.document_controller import DocumentAction
from mtg_proxy_printer.document_controller.edit_custom_card import ActionEditCustomCard
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.model.document_page import PageColumns
from mtg_proxy_printer.ui.common import get_card_image_tooltip
from mtg_proxy_printer.decklist_parser.common import CardCounter
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
            return get_card_image_tooltip(card.source_image_file)
        elif card.is_oversized and role == ItemDataRole.ToolTipRole:
            return self.tr("Beware: Potentially oversized card!\nThis card may not fit in your deck.")
        if card.is_oversized and role == ItemDataRole.DecorationRole:
            return self._oversized_icon
        return None

    def flags(self, index: QModelIndex) -> Qt.ItemFlags:
        flags = super().flags(index)
        if index.column() in self.EDITABLE_COLUMNS or self.rows[index.row()].card.is_custom_card:
            flags |= ItemFlag.ItemIsEditable
        return flags

    def setData(self, index: QModelIndex, value: typing.Any, role: ItemDataRole = ItemDataRole.EditRole) -> bool:
        row, column = index.row(), index.column()







|







132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
            return get_card_image_tooltip(card.source_image_file)
        elif card.is_oversized and role == ItemDataRole.ToolTipRole:
            return self.tr("Beware: Potentially oversized card!\nThis card may not fit in your deck.")
        if card.is_oversized and role == ItemDataRole.DecorationRole:
            return self._oversized_icon
        return None

    def flags(self, index: QModelIndex) -> ItemFlag:
        flags = super().flags(index)
        if index.column() in self.EDITABLE_COLUMNS or self.rows[index.row()].card.is_custom_card:
            flags |= ItemFlag.ItemIsEditable
        return flags

    def setData(self, index: QModelIndex, value: typing.Any, role: ItemDataRole = ItemDataRole.EditRole) -> bool:
        row, column = index.row(), index.column()

Changes to mtg_proxy_printer/model/carddb.py.

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import threading
try:
    from typing import LiteralString
except ImportError:
    from typing_extensions import LiteralString
from typing import NamedTuple, TypeVar, Set, Optional, Dict, Literal, List, Sequence, Any, Union, Tuple

from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QObject, pyqtSignal as Signal, pyqtSlot as Slot

from mtg_proxy_printer.model.card import MTGSet, Card, CheckCard, OptionalCard, CardList, CustomCard
from mtg_proxy_printer.model.imagedb_files import CacheContent
import mtg_proxy_printer.app_dirs
from mtg_proxy_printer.natsort import natural_sorted
import mtg_proxy_printer.meta_data
from mtg_proxy_printer.sqlite_helpers import cached_dedent, open_database, validate_database_schema







|
|







24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import threading
try:
    from typing import LiteralString
except ImportError:
    from typing_extensions import LiteralString
from typing import NamedTuple, TypeVar, Set, Optional, Dict, Literal, List, Sequence, Any, Union, Tuple

from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QObject, Signal, Slot

from mtg_proxy_printer.model.card import MTGSet, Card, CheckCard, OptionalCard, CardList, CustomCard
from mtg_proxy_printer.model.imagedb_files import CacheContent
import mtg_proxy_printer.app_dirs
from mtg_proxy_printer.natsort import natural_sorted
import mtg_proxy_printer.meta_data
from mtg_proxy_printer.sqlite_helpers import cached_dedent, open_database, validate_database_schema

Changes to mtg_proxy_printer/model/document.py.

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import enum
import itertools
import math
import pathlib
import sys
import typing

from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt, pyqtSlot as Slot, pyqtSignal as Signal, \
    QPersistentModelIndex

from mtg_proxy_printer.model.imagedb_files import ImageKey
from mtg_proxy_printer.natsort import to_list_of_ranges
from mtg_proxy_printer.document_controller.edit_custom_card import ActionEditCustomCard
from mtg_proxy_printer.model.document_page import CardContainer, Page, PageColumns
from mtg_proxy_printer.units_and_sizes import PageType, CardSizes, CardSize







|







18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import enum
import itertools
import math
import pathlib
import sys
import typing

from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Slot, Signal, \
    QPersistentModelIndex

from mtg_proxy_printer.model.imagedb_files import ImageKey
from mtg_proxy_printer.natsort import to_list_of_ranges
from mtg_proxy_printer.document_controller.edit_custom_card import ActionEditCustomCard
from mtg_proxy_printer.model.document_page import CardContainer, Page, PageColumns
from mtg_proxy_printer.units_and_sizes import PageType, CardSizes, CardSize
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
        if not index.isValid():
            return None
        if isinstance(index.internalPointer(), CardContainer):  # Card
            return self._data_card(index, role)
        else:  # Page
            return self._data_page(index, role)

    def flags(self, index: AnyIndex) -> Qt.ItemFlags:
        index = self._to_index(index)
        data = index.internalPointer()
        flags = super().flags(index)
        if isinstance(data, CardContainer) and (index.column() in self.EDITABLE_COLUMNS or data.card.is_custom_card):
            flags |= ItemFlag.ItemIsEditable
        return flags








|







219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
        if not index.isValid():
            return None
        if isinstance(index.internalPointer(), CardContainer):  # Card
            return self._data_card(index, role)
        else:  # Page
            return self._data_page(index, role)

    def flags(self, index: AnyIndex) -> Qt.ItemFlag:
        index = self._to_index(index)
        data = index.internalPointer()
        flags = super().flags(index)
        if isinstance(data, CardContainer) and (index.column() in self.EDITABLE_COLUMNS or data.card.is_custom_card):
            flags |= ItemFlag.ItemIsEditable
        return flags

Changes to mtg_proxy_printer/model/document_loader.py.

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import itertools
import pathlib
import sqlite3
import textwrap
from typing import Counter, Dict, Iterable, List, NamedTuple, Optional, Tuple, TYPE_CHECKING, TypeVar

import pint
from PyQt5.QtCore import QObject, pyqtSignal as Signal, QThreadPool, Qt
from hamcrest import assert_that, all_of, instance_of, greater_than_or_equal_to, matches_regexp, is_in, \
    has_properties, is_, any_of, none, has_item, has_property, equal_to

try:
    from hamcrest import contains_exactly
except ImportError:
    # Compatibility with PyHamcrest < 1.10







|







19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import itertools
import pathlib
import sqlite3
import textwrap
from typing import Counter, Dict, Iterable, List, NamedTuple, Optional, Tuple, TYPE_CHECKING, TypeVar

import pint
from PySide6.QtCore import QObject, Signal, QThreadPool, Qt
from hamcrest import assert_that, all_of, instance_of, greater_than_or_equal_to, matches_regexp, is_in, \
    has_properties, is_, any_of, none, has_item, has_property, equal_to

try:
    from hamcrest import contains_exactly
except ImportError:
    # Compatibility with PyHamcrest < 1.10

Changes to mtg_proxy_printer/model/imagedb.py.

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import shutil
import socket
import string
import threading
import typing
import urllib.error

from PyQt5.QtCore import QObject, pyqtSignal as Signal, pyqtSlot as Slot, QModelIndex, Qt, QThreadPool
from PyQt5.QtGui import QPixmap, QColorConstants

if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.document import Document

from mtg_proxy_printer.model.carddb import with_database_write_lock
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard
from mtg_proxy_printer.document_controller.replace_card import ActionReplaceCard







|
|







22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import shutil
import socket
import string
import threading
import typing
import urllib.error

from PySide6.QtCore import QObject, Signal, Slot, QModelIndex, Qt, QThreadPool
from PySide6.QtGui import QPixmap, QColorConstants

if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.document import Document

from mtg_proxy_printer.model.carddb import with_database_write_lock
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard
from mtg_proxy_printer.document_controller.replace_card import ActionReplaceCard

Changes to mtg_proxy_printer/model/imagedb_files.py.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import dataclasses
import pathlib

from PyQt5.QtCore import Qt
import mtg_proxy_printer.app_dirs
import mtg_proxy_printer.http_file
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger

ItemDataRole = Qt.ItemDataRole







|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import dataclasses
import pathlib

from PySide6.QtCore import Qt
import mtg_proxy_printer.app_dirs
import mtg_proxy_printer.http_file
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger

ItemDataRole = Qt.ItemDataRole

Changes to mtg_proxy_printer/model/page_layout.py.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

import dataclasses
import math
import typing
from typing import Generator, Tuple

import pint
from PyQt5.QtGui import QPageLayout, QPageSize
from PyQt5.QtCore import QMarginsF, QSizeF

try:
    from hamcrest import contains_exactly
except ImportError:
    # Compatibility with PyHamcrest < 1.10
    from hamcrest import contains as contains_exactly








|
|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

import dataclasses
import math
import typing
from typing import Generator, Tuple

import pint
from PySide6.QtGui import QPageLayout, QPageSize
from PySide6.QtCore import QMarginsF, QSizeF

try:
    from hamcrest import contains_exactly
except ImportError:
    # Compatibility with PyHamcrest < 1.10
    from hamcrest import contains as contains_exactly

Changes to mtg_proxy_printer/model/string_list.py.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import typing

from PyQt5.QtCore import QAbstractListModel, Qt, QObject, QModelIndex

from mtg_proxy_printer.model.card import MTGSet

__all__ = [
    "PrettySetListModel",
]
INVALID_INDEX = QModelIndex()







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import typing

from PySide6.QtCore import QAbstractListModel, Qt, QObject, QModelIndex

from mtg_proxy_printer.model.card import MTGSet

__all__ = [
    "PrettySetListModel",
]
INVALID_INDEX = QModelIndex()

Changes to mtg_proxy_printer/natsort.py.

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Natural sorting for lists or other iterables of strings.
"""

import itertools
import re
import typing

from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex

__all__ = [
    "natural_sorted",
    "str_less_than",
    "NaturallySortedSortFilterProxyModel",
    "to_list_of_ranges"
]







|







18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Natural sorting for lists or other iterables of strings.
"""

import itertools
import re
import typing

from PySide6.QtCore import QSortFilterProxyModel, QModelIndex

__all__ = [
    "natural_sorted",
    "str_less_than",
    "NaturallySortedSortFilterProxyModel",
    "to_list_of_ranges"
]

Changes to mtg_proxy_printer/print.py.

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    from os import process_cpu_count
except ImportError:
    from os import cpu_count as process_cpu_count
from pathlib import Path
from threading import BoundedSemaphore
import typing

from PyQt5.QtCore import QObject, QMarginsF, QSizeF, pyqtSlot as Slot, QPersistentModelIndex, QThreadPool
from PyQt5.QtGui import QPainter, QPdfWriter, QPageSize, QImage
from PyQt5.QtPrintSupport import QPrinter


from mtg_proxy_printer.runner import ProgressSignalContainer
if typing.TYPE_CHECKING:
    from mtg_proxy_printer.ui.main_window import MainWindow
from mtg_proxy_printer.units_and_sizes import RESOLUTION
import mtg_proxy_printer.meta_data







|
|
|







21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    from os import process_cpu_count
except ImportError:
    from os import cpu_count as process_cpu_count
from pathlib import Path
from threading import BoundedSemaphore
import typing

from PySide6.QtCore import QObject, QMarginsF, QSizeF, Slot, QPersistentModelIndex, QThreadPool
from PySide6.QtGui import QPainter, QPdfWriter, QPageSize, QImage
from PySide6.QtPrintSupport import QPrinter


from mtg_proxy_printer.runner import ProgressSignalContainer
if typing.TYPE_CHECKING:
    from mtg_proxy_printer.ui.main_window import MainWindow
from mtg_proxy_printer.units_and_sizes import RESOLUTION
import mtg_proxy_printer.meta_data
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
            f"Setting page layout failed! "
            f"Layout: page_size={page_layout.pageSize().size(QPageSize.Unit.Millimeter)}, "
            f"orientation={page_layout.orientation()}, "
            f"margins={layout.margin_left, layout.margin_top, layout.margin_right, layout.margin_bottom}")
    # magnitude returns a float by default, so round to int to avoid a TypeError
    printer.setResolution(round(mtg_proxy_printer.units_and_sizes.RESOLUTION.magnitude))
    # Disable duplex printing by default
    printer.setDoubleSidedPrinting(False)
    printer.setDuplex(QPrinter.DuplexMode.DuplexNone)
    printer.setOutputFormat(QPrinter.OutputFormat.NativeFormat)
    if RenderMode.IMPLICIT_MARGINS not in renderer.render_mode:
        printer.setFullPage(True)
    return printer









<







122
123
124
125
126
127
128

129
130
131
132
133
134
135
            f"Setting page layout failed! "
            f"Layout: page_size={page_layout.pageSize().size(QPageSize.Unit.Millimeter)}, "
            f"orientation={page_layout.orientation()}, "
            f"margins={layout.margin_left, layout.margin_top, layout.margin_right, layout.margin_bottom}")
    # magnitude returns a float by default, so round to int to avoid a TypeError
    printer.setResolution(round(mtg_proxy_printer.units_and_sizes.RESOLUTION.magnitude))
    # Disable duplex printing by default

    printer.setDuplex(QPrinter.DuplexMode.DuplexNone)
    printer.setOutputFormat(QPrinter.OutputFormat.NativeFormat)
    if RenderMode.IMPLICIT_MARGINS not in renderer.render_mode:
        printer.setFullPage(True)
    return printer


208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
        self.painter.end()
        logger.info("Writing document finished.")

    def _switch_to_page(self, page_number: int):
        """Render the given page on the internal scene"""
        index = QPersistentModelIndex(self.document.index(page_number, 0))
        self.scene.on_current_page_changed(index)



class Renderer(QObject):

    def __init__(self, document: Document, parent: QObject = None):
        super().__init__(parent)
        self.document = document







<







207
208
209
210
211
212
213

214
215
216
217
218
219
220
        self.painter.end()
        logger.info("Writing document finished.")

    def _switch_to_page(self, page_number: int):
        """Render the given page on the internal scene"""
        index = QPersistentModelIndex(self.document.index(page_number, 0))
        self.scene.on_current_page_changed(index)



class Renderer(QObject):

    def __init__(self, document: Document, parent: QObject = None):
        super().__init__(parent)
        self.document = document

Changes to mtg_proxy_printer/printing_filter_updater.py.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import sqlite3
import typing

from PyQt5.QtCore import Qt, QCoreApplication

import mtg_proxy_printer.settings
if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.carddb import CardDatabase
    from mtg_proxy_printer.ui.main_window import MainWindow
from mtg_proxy_printer.model.carddb import SCHEMA_NAME, with_database_write_lock
from mtg_proxy_printer.sqlite_helpers import cached_dedent, open_database







|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import sqlite3
import typing

from PySide6.QtCore import QObject, Signal, Qt, QCoreApplication

import mtg_proxy_printer.settings
if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.carddb import CardDatabase
    from mtg_proxy_printer.ui.main_window import MainWindow
from mtg_proxy_printer.model.carddb import SCHEMA_NAME, with_database_write_lock
from mtg_proxy_printer.sqlite_helpers import cached_dedent, open_database

Changes to mtg_proxy_printer/resources/ui/central_widget/columnar.ui.

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
       <verstretch>1</verstretch>
      </sizepolicy>
     </property>
     <property name="acceptDrops">
      <bool>false</bool>
     </property>
     <property name="renderHints">
      <set>QPainter::Antialiasing|QPainter::HighQualityAntialiasing</set>
     </property>
    </widget>
   </item>
   <item row="4" column="2" rowspan="2">
    <widget class="PageCardTableView" name="page_card_table_view">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Expanding" vsizetype="Expanding">







|







22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
       <verstretch>1</verstretch>
      </sizepolicy>
     </property>
     <property name="acceptDrops">
      <bool>false</bool>
     </property>
     <property name="renderHints">
      <set>QPainter::Antialiasing</set>
     </property>
    </widget>
   </item>
   <item row="4" column="2" rowspan="2">
    <widget class="PageCardTableView" name="page_card_table_view">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Expanding" vsizetype="Expanding">

Changes to mtg_proxy_printer/resources/ui/central_widget/grouped.ui.

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
       <verstretch>10</verstretch>
      </sizepolicy>
     </property>
     <property name="acceptDrops">
      <bool>false</bool>
     </property>
     <property name="renderHints">
      <set>QPainter::Antialiasing|QPainter::HighQualityAntialiasing</set>
     </property>
    </widget>
   </item>
   <item row="1" column="0" rowspan="7" colspan="2">
    <widget class="QListView" name="document_view">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Expanding" vsizetype="Expanding">







|







22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
       <verstretch>10</verstretch>
      </sizepolicy>
     </property>
     <property name="acceptDrops">
      <bool>false</bool>
     </property>
     <property name="renderHints">
      <set>QPainter::Antialiasing</set>
     </property>
    </widget>
   </item>
   <item row="1" column="0" rowspan="7" colspan="2">
    <widget class="QListView" name="document_view">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Expanding" vsizetype="Expanding">

Changes to mtg_proxy_printer/runner.py.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import typing

from PyQt5.QtCore import QRunnable, QObject, pyqtSignal as Signal

from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger

__all__ = [
    "Runnable",







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import typing

from PySide6.QtCore import QRunnable, QObject, Signal

from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger

__all__ = [
    "Runnable",

Changes to mtg_proxy_printer/settings.py.

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import math
import pathlib
import re
import typing
import tokenize

import pint
from PyQt5.QtCore import QStandardPaths

import mtg_proxy_printer.app_dirs
import mtg_proxy_printer.meta_data
import mtg_proxy_printer.natsort
from mtg_proxy_printer.units_and_sizes import CardSizes, ConfigParser, SectionProxy, unit_registry, T, QuantityT

StandardLocation = QStandardPaths.StandardLocation







|







18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import math
import pathlib
import re
import typing
import tokenize

import pint
from PySide6.QtCore import QStandardPaths

import mtg_proxy_printer.app_dirs
import mtg_proxy_printer.meta_data
import mtg_proxy_printer.natsort
from mtg_proxy_printer.units_and_sizes import CardSizes, ConfigParser, SectionProxy, unit_registry, T, QuantityT

StandardLocation = QStandardPaths.StandardLocation

Changes to mtg_proxy_printer/ui/add_card.py.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from typing import Union, Type, Optional

from PyQt5.QtCore import QStringListModel, pyqtSlot as Slot, pyqtSignal as Signal, Qt, QItemSelectionModel, QItemSelection
from PyQt5.QtWidgets import QWidget, QDialogButtonBox
from PyQt5.QtGui import QIcon

import mtg_proxy_printer.model.card
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard
import mtg_proxy_printer.model.string_list
import mtg_proxy_printer.model.carddb
import mtg_proxy_printer.model.document
import mtg_proxy_printer.settings







|
|
|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from typing import Union, Type, Optional

from PySide6.QtCore import QStringListModel, Slot, Signal, Qt, QItemSelectionModel, QItemSelection
from PySide6.QtWidgets import QWidget, QDialogButtonBox
from PySide6.QtGui import QIcon

import mtg_proxy_printer.model.card
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard
import mtg_proxy_printer.model.string_list
import mtg_proxy_printer.model.carddb
import mtg_proxy_printer.model.document
import mtg_proxy_printer.settings

Changes to mtg_proxy_printer/ui/cache_cleanup_wizard.py.

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import dataclasses
import datetime
import enum
import math
import pathlib
import typing

from PyQt5.QtCore import QAbstractTableModel, Qt, QModelIndex, QObject, QItemSelectionModel, QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QWizard, QWizardPage

import mtg_proxy_printer.settings
from mtg_proxy_printer.natsort import NaturallySortedSortFilterProxyModel
from mtg_proxy_printer.model.carddb import CardDatabase
from mtg_proxy_printer.model.card import MTGSet, Card
from mtg_proxy_printer.model.imagedb import ImageDatabase
from mtg_proxy_printer.model.imagedb_files import CacheContent as ImageCacheContent, ImageKey







|
|
|







17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import dataclasses
import datetime
import enum
import math
import pathlib
import typing

from PySide6.QtCore import QAbstractTableModel, Qt, QModelIndex, QObject, QItemSelectionModel, QSize
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QWidget, QWizard, QWizardPage

import mtg_proxy_printer.settings
from mtg_proxy_printer.natsort import NaturallySortedSortFilterProxyModel
from mtg_proxy_printer.model.carddb import CardDatabase
from mtg_proxy_printer.model.card import MTGSet, Card
from mtg_proxy_printer.model.imagedb import ImageDatabase
from mtg_proxy_printer.model.imagedb_files import CacheContent as ImageCacheContent, ImageKey
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
    BUTTON_ICONS = {
        QWizard.WizardButton.FinishButton: "edit-delete",
        QWizard.WizardButton.CancelButton: "dialog-cancel",
        QWizard.WizardButton.HelpButton: "help-contents",
    }

    def __init__(self, card_db: CardDatabase, image_db: ImageDatabase,
                 parent: QWidget = None, flags=Qt.WindowFlags()):
        super().__init__(QSize(1024, 768), parent, flags)
        self.image_db = image_db
        self.addPage(FilterSetupPage(self))
        self.addPage(CardFilterPage(card_db, image_db, self))
        self.addPage(SummaryPage(self))
        self.setWindowTitle(self.tr("Cleanup locally stored card images", "Dialog window title"))
        self.setWindowIcon(QIcon.fromTheme("edit-clear-history"))







|







445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
    BUTTON_ICONS = {
        QWizard.WizardButton.FinishButton: "edit-delete",
        QWizard.WizardButton.CancelButton: "dialog-cancel",
        QWizard.WizardButton.HelpButton: "help-contents",
    }

    def __init__(self, card_db: CardDatabase, image_db: ImageDatabase,
                 parent: QWidget = None, flags=Qt.WindowType.Window):
        super().__init__(QSize(1024, 768), parent, flags)
        self.image_db = image_db
        self.addPage(FilterSetupPage(self))
        self.addPage(CardFilterPage(card_db, image_db, self))
        self.addPage(SummaryPage(self))
        self.setWindowTitle(self.tr("Cleanup locally stored card images", "Dialog window title"))
        self.setWindowIcon(QIcon.fromTheme("edit-clear-history"))

Changes to mtg_proxy_printer/ui/card_list_table_view.py.

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.

import math

from PyQt5.QtCore import Qt, pyqtSignal as Signal, pyqtSlot as Slot
from PyQt5.QtWidgets import QTableView, QWidget

from mtg_proxy_printer.model.card_list import CardListColumns, CardListModel
from mtg_proxy_printer.natsort import NaturallySortedSortFilterProxyModel
from mtg_proxy_printer.ui.item_delegates import CollectorNumberEditorDelegate, BoundedCopiesSpinboxDelegate, \
    CardSideSelectionDelegate, SetEditorDelegate, LanguageEditorDelegate

from mtg_proxy_printer.logger import get_logger







|
|







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.

import math

from PySide6.QtCore import Qt, Signal, Slot
from PySide6.QtWidgets import QTableView, QWidget

from mtg_proxy_printer.model.card_list import CardListColumns, CardListModel
from mtg_proxy_printer.natsort import NaturallySortedSortFilterProxyModel
from mtg_proxy_printer.ui.item_delegates import CollectorNumberEditorDelegate, BoundedCopiesSpinboxDelegate, \
    CardSideSelectionDelegate, SetEditorDelegate, LanguageEditorDelegate

from mtg_proxy_printer.logger import get_logger

Changes to mtg_proxy_printer/ui/central_widget.py.

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.

from typing import Union, Type

from PyQt5.QtCore import  pyqtSlot as Slot, QItemSelectionModel, QModelIndex
from PyQt5.QtWidgets import QWidget

import mtg_proxy_printer.app_dirs
import mtg_proxy_printer.settings
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.model.carddb import CardDatabase
from mtg_proxy_printer.model.imagedb import ImageDatabase








|
|







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.

from typing import Union, Type

from PySide6.QtCore import Slot, QItemSelectionModel, QModelIndex
from PySide6.QtWidgets import QWidget

import mtg_proxy_printer.app_dirs
import mtg_proxy_printer.settings
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.model.carddb import CardDatabase
from mtg_proxy_printer.model.imagedb import ImageDatabase

Changes to mtg_proxy_printer/ui/common.py.

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    from functools import lru_cache
    cache = lru_cache

from pathlib import Path
import platform
from typing import Union, Dict

from PyQt5.QtCore import QFile, QObject, QSize, QCoreApplication, Qt, QBuffer, QIODevice
from PyQt5.QtWidgets import QWizard, QWidget, QGraphicsColorizeEffect, QTextEdit, QDialog
from PyQt5.QtGui import QIcon, QPixmap
# noinspection PyUnresolvedReferences
from PyQt5 import uic

import mtg_proxy_printer.settings
from mtg_proxy_printer.units_and_sizes import OptStr
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger








|
|
|
<
|







19
20
21
22
23
24
25
26
27
28

29
30
31
32
33
34
35
36
    from functools import lru_cache
    cache = lru_cache

from pathlib import Path
import platform
from typing import Union, Dict

from PySide6.QtCore import QFile, QObject, QSize, QCoreApplication, Qt, QBuffer, QIODevice
from PySide6.QtWidgets import QWizard, QWidget, QGraphicsColorizeEffect, QTextEdit, QDialog
from PySide6.QtGui import QIcon, QPixmap

from PySide6.QtUiTools import loadUiType

import mtg_proxy_printer.settings
from mtg_proxy_printer.units_and_sizes import OptStr
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger

121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138

139


140
141
142
143
144
145
146

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.qt_object.blockSignals(False)


def load_ui_from_file(name: str):
    """
    Returns the Ui class type from uic.loadUiType(), loading the ui file with the given name.

    :param name: Path to the UI file
    :return: class implementing the requested Ui
    :raises FileNotFoundError: If the given ui file does not exist
    """
    file_path = f"{RESOURCE_PATH_PREFIX}/ui/{name}.ui"
    if not QFile.exists(file_path):
        error_message = f"UI file not found: {file_path}"
        logger.error(error_message)
        raise FileNotFoundError(error_message)

    base_type, _ = uic.loadUiType(file_path, from_imports=True)


    return base_type

def load_icon(name: str) -> QIcon:
    """Loads a QIcon with the given name from the internal resources"""
    file_path = f"{RESOURCE_PATH_PREFIX}/icons/{name}"
    if not QFile.exists(file_path):
        error_message = f"Icon not found: {file_path}"







|
<









>
|
>
>







120
121
122
123
124
125
126
127

128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.qt_object.blockSignals(False)


def load_ui_from_file(name: str):
    """
    Returns the Ui class type as returned by PySide6.QtUiTools.loadUiType(), loading the ui file with the given name.

    :param name: Path to the UI file
    :return: class implementing the requested Ui
    :raises FileNotFoundError: If the given ui file does not exist
    """
    file_path = f"{RESOURCE_PATH_PREFIX}/ui/{name}.ui"
    if not QFile.exists(file_path):
        error_message = f"UI file not found: {file_path}"
        logger.error(error_message)
        raise FileNotFoundError(error_message)
    try:
        base_type, _ = loadUiType(file_path)
    except TypeError as e:
        raise RuntimeError(f"Ui compilation failed for path {file_path}") from e
    return base_type

def load_icon(name: str) -> QIcon:
    """Loads a QIcon with the given name from the internal resources"""
    file_path = f"{RESOURCE_PATH_PREFIX}/icons/{name}"
    if not QFile.exists(file_path):
        error_message = f"Icon not found: {file_path}"

Changes to mtg_proxy_printer/ui/compiled_resources.pyi.

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.

"""
Declares the interface created by the PyQt5 resource compiler.
This is only used for type hinting.
"""

import typing

qt_version: typing.List[int] = ...
rcc_version: int = ...







|







10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.

"""
Declares the interface created by the PySide6 resource compiler.
This is only used for type hinting.
"""

import typing

qt_version: typing.List[int] = ...
rcc_version: int = ...

Changes to mtg_proxy_printer/ui/custom_card_import_dialog.py.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from collections import Counter
from pathlib import Path
import typing

from PyQt5.QtCore import Qt, pyqtSignal as Signal, pyqtSlot as Slot
from PyQt5.QtGui import QDragEnterEvent, QDropEvent, QPixmap
from PyQt5.QtWidgets import QDialog, QWidget, QFileDialog, QPushButton

from mtg_proxy_printer.document_controller import DocumentAction
from mtg_proxy_printer.document_controller.import_deck_list import ActionImportDeckList
from mtg_proxy_printer.model.card import CustomCard
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.units_and_sizes import CardSizes








|
|
|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from collections import Counter
from pathlib import Path
import typing

from PySide6.QtCore import Qt, Signal, Slot
from PySide6.QtGui import QDragEnterEvent, QDropEvent, QPixmap
from PySide6.QtWidgets import QDialog, QWidget, QFileDialog, QPushButton

from mtg_proxy_printer.document_controller import DocumentAction
from mtg_proxy_printer.document_controller.import_deck_list import ActionImportDeckList
from mtg_proxy_printer.model.card import CustomCard
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.units_and_sizes import CardSizes

43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
EventTypes = typing.Union[QDragEnterEvent, QDropEvent]


class CustomCardImportDialog(QDialog):

    request_action = Signal(DocumentAction)

    def __init__(self, document: Document, parent: QWidget = None, flags=Qt.WindowFlags()):
        super().__init__(parent, flags)
        self.ui = ui = Ui_CustomCardImportDialog()
        ui.setupUi(self)
        self.ok_button.setEnabled(False)
        ui.remove_selected.setDisabled(True)
        self.model = model = CardListModel(document)
        model.request_action.connect(self.request_action)







|







43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
EventTypes = typing.Union[QDragEnterEvent, QDropEvent]


class CustomCardImportDialog(QDialog):

    request_action = Signal(DocumentAction)

    def __init__(self, document: Document, parent: QWidget = None, flags=Qt.WindowType.Window):
        super().__init__(parent, flags)
        self.ui = ui = Ui_CustomCardImportDialog()
        ui.setupUi(self)
        self.ok_button.setEnabled(False)
        ui.remove_selected.setDisabled(True)
        self.model = model = CardListModel(document)
        model.request_action.connect(self.request_action)

Changes to mtg_proxy_printer/ui/deck_import_wizard.py.

16
17
18
19
20
21
22
23
24
25
26

27
28
29
30
31
32
33
import itertools
import pathlib
import re
import typing
import urllib.error
import urllib.parse

from PyQt5.QtCore import pyqtSlot as Slot, pyqtSignal as Signal, pyqtProperty as Property, QStringListModel, Qt, \
    QSize, QUrl
from PyQt5.QtGui import QValidator, QIcon, QDesktopServices
from PyQt5.QtWidgets import QWizard, QFileDialog, QMessageBox, QWizardPage, QWidget, QRadioButton


from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.units_and_sizes import SectionProxy
import mtg_proxy_printer.settings
from mtg_proxy_printer.decklist_parser import re_parsers, common, csv_parsers
from mtg_proxy_printer.decklist_downloader import IsIdentifyingDeckUrlValidator, AVAILABLE_DOWNLOADERS, \
    get_downloader_class, ParserBase







|

|
|
>







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import itertools
import pathlib
import re
import typing
import urllib.error
import urllib.parse

from PySide6.QtCore import Slot, Signal, Property, QStringListModel, Qt, SIGNAL, \
    QSize, QUrl
from PySide6.QtGui import QValidator, QIcon, QDesktopServices
from PySide6.QtWidgets import QWizard, QFileDialog, QMessageBox, QWizardPage, QWidget, QRadioButton


from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.units_and_sizes import SectionProxy
import mtg_proxy_printer.settings
from mtg_proxy_printer.decklist_parser import re_parsers, common, csv_parsers
from mtg_proxy_printer.decklist_downloader import IsIdentifyingDeckUrlValidator, AVAILABLE_DOWNLOADERS, \
    get_downloader_class, ParserBase
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
        ui.deck_list_download_url_line_edit.textChanged.connect(
            lambda text: ui.deck_list_download_button.setEnabled(
                self.deck_list_url_validator.validate(text)[0] == State.Acceptable))
        supported_sites = "\n".join((downloader.APPLICABLE_WEBSITES for downloader in AVAILABLE_DOWNLOADERS.values()))
        ui.deck_list_download_url_line_edit.setToolTip(
            self.tr("Supported websites:\n{supported_sites}").format(supported_sites=supported_sites))
        ui.translate_deck_list_target_language.setModel(language_model)
        self.registerField("deck_list*", ui.deck_list, "plainText", ui.deck_list.textChanged)
        self.registerField("print-guessing-enable", ui.print_guessing_enable)
        self.registerField("print-guessing-prefer-already-downloaded", ui.print_guessing_prefer_already_downloaded)
        self.registerField("translate-deck-list-enable", ui.translate_deck_list_enable)
        self.registerField("deck-list-downloaded", self, "deck_list_downloader", self.deck_list_downloader_changed)
        self.registerField(
            "translate-deck-list-target-language", ui.translate_deck_list_target_language,
            "currentText", ui.translate_deck_list_target_language.currentTextChanged
        )
        logger.info(f"Created {self.__class__.__name__} instance.")


    @Property(str, notify=deck_list_downloader_changed)
    def deck_list_downloader(self):
        return self._deck_list_downloader







|



|


|







113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
        ui.deck_list_download_url_line_edit.textChanged.connect(
            lambda text: ui.deck_list_download_button.setEnabled(
                self.deck_list_url_validator.validate(text)[0] == State.Acceptable))
        supported_sites = "\n".join((downloader.APPLICABLE_WEBSITES for downloader in AVAILABLE_DOWNLOADERS.values()))
        ui.deck_list_download_url_line_edit.setToolTip(
            self.tr("Supported websites:\n{supported_sites}").format(supported_sites=supported_sites))
        ui.translate_deck_list_target_language.setModel(language_model)
        self.registerField("deck_list*", ui.deck_list)
        self.registerField("print-guessing-enable", ui.print_guessing_enable)
        self.registerField("print-guessing-prefer-already-downloaded", ui.print_guessing_prefer_already_downloaded)
        self.registerField("translate-deck-list-enable", ui.translate_deck_list_enable)
        self.registerField("deck-list-downloaded", self, "deck_list_downloader", "deck_list_downloader_changed(str)")
        self.registerField(
            "translate-deck-list-target-language", ui.translate_deck_list_target_language,
            "currentText", "currentTextChanged(str)"
        )
        logger.info(f"Created {self.__class__.__name__} instance.")


    @Property(str, notify=deck_list_downloader_changed)
    def deck_list_downloader(self):
        return self._deck_list_downloader
595
596
597
598
599
600
601
602
603

604
605
606
607
608
609
610
    request_action = Signal(ActionImportDeckList)
    BUTTON_ICONS = {
        QWizard.WizardButton.FinishButton: "dialog-ok",
        QWizard.WizardButton.CancelButton: "dialog-cancel",
    }

    def __init__(self, document: Document, language_model: QStringListModel,
                 parent: QWidget = None, flags=Qt.WindowFlags()):
        super().__init__(QSize(1000, 600), parent, flags)

        self.select_deck_parser_page = SelectDeckParserPage(document, self)
        self.load_list_page = LoadListPage(language_model, self)
        self.summary_page = SummaryPage(document, self)
        self.addPage(self.load_list_page)
        self.addPage(self.select_deck_parser_page)
        self.addPage(self.summary_page)
        self.setWindowIcon(QIcon.fromTheme("document-import"))







|

>







596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
    request_action = Signal(ActionImportDeckList)
    BUTTON_ICONS = {
        QWizard.WizardButton.FinishButton: "dialog-ok",
        QWizard.WizardButton.CancelButton: "dialog-cancel",
    }

    def __init__(self, document: Document, language_model: QStringListModel,
                 parent: QWidget = None, flags=Qt.WindowType.Window):
        super().__init__(QSize(1000, 600), parent, flags)
        self.setDefaultProperty("QPlainTextEdit", "plainText", SIGNAL("textChanged()"))
        self.select_deck_parser_page = SelectDeckParserPage(document, self)
        self.load_list_page = LoadListPage(language_model, self)
        self.summary_page = SummaryPage(document, self)
        self.addPage(self.load_list_page)
        self.addPage(self.select_deck_parser_page)
        self.addPage(self.summary_page)
        self.setWindowIcon(QIcon.fromTheme("document-import"))

Changes to mtg_proxy_printer/ui/dialogs.py.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

from pathlib import Path
import shutil
import sys
import typing
from typing import Tuple

from PyQt5.QtCore import QFile, pyqtSignal as Signal, pyqtSlot as Slot, QThreadPool, QObject, QEvent, Qt
from PyQt5.QtWidgets import QFileDialog, QWidget, QTextBrowser, QDialogButtonBox, QDialog
from PyQt5.QtGui import QIcon
from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrintDialog, QPrinter

import mtg_proxy_printer.app_dirs
from mtg_proxy_printer.model.carddb import CardDatabase
from mtg_proxy_printer.model.card import AnyCardType
import mtg_proxy_printer.model.imagedb
import mtg_proxy_printer.print
import mtg_proxy_printer.settings







|
|
|
|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

from pathlib import Path
import shutil
import sys
import typing
from typing import Tuple

from PySide6.QtCore import QFile, Signal, Slot, QThreadPool, QObject, QEvent, Qt
from PySide6.QtWidgets import QFileDialog, QWidget, QTextBrowser, QDialogButtonBox, QDialog
from PySide6.QtGui import QIcon
from PySide6.QtPrintSupport import QPrintPreviewDialog, QPrintDialog, QPrinter

import mtg_proxy_printer.app_dirs
from mtg_proxy_printer.model.carddb import CardDatabase
from mtg_proxy_printer.model.card import AnyCardType
import mtg_proxy_printer.model.imagedb
import mtg_proxy_printer.print
import mtg_proxy_printer.settings
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
        file_path = self._get_file_path(":/changelog.md", "/../../doc/changelog.md")
        self._set_text_browser_with_markdown_file_content(file_path, self.ui.changelog_text_browser)

    def _set_text_browser_with_markdown_file_content(self, file_path: str, text_browser: QTextBrowser):
        file = QFile(file_path, self)
        file.open(QFile.OpenModeFlag.ReadOnly)
        try:
            content = file.readAll().data().decode("utf-8")
        finally:
            file.close()
        text_browser.setMarkdown(content)


class PrintPreviewDialog(QPrintPreviewDialog):








|







303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
        file_path = self._get_file_path(":/changelog.md", "/../../doc/changelog.md")
        self._set_text_browser_with_markdown_file_content(file_path, self.ui.changelog_text_browser)

    def _set_text_browser_with_markdown_file_content(self, file_path: str, text_browser: QTextBrowser):
        file = QFile(file_path, self)
        file.open(QFile.OpenModeFlag.ReadOnly)
        try:
            content = file.readAll().toStdString()
        finally:
            file.close()
        text_browser.setMarkdown(content)


class PrintPreviewDialog(QPrintPreviewDialog):

423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
        logger.info(f"User accepted the {self.__class__.__name__}")
        action = ActionEditDocumentSettings(self.ui.page_config_container.ui.page_config_widget.page_layout)
        self.document.apply(action)
        logger.debug("Saving settings in the document done.")

    def clear_highlight(self):
        """Clears all GUI widget highlights."""
        for item in self.findChildren((QWidget,), options=Qt.FindChildOption.FindChildrenRecursively):  # type: QWidget
            item.setGraphicsEffect(None)


UNSAFE_FILE_NAME_CHARS = r'''*"/\<>:|?^'''
UNSAFE_FILE_NAME_MAPPING = str.maketrans(UNSAFE_FILE_NAME_CHARS, "_"*len(UNSAFE_FILE_NAME_CHARS))









|







423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
        logger.info(f"User accepted the {self.__class__.__name__}")
        action = ActionEditDocumentSettings(self.ui.page_config_container.ui.page_config_widget.page_layout)
        self.document.apply(action)
        logger.debug("Saving settings in the document done.")

    def clear_highlight(self):
        """Clears all GUI widget highlights."""
        for item in self.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively):  # type: QWidget
            item.setGraphicsEffect(None)


UNSAFE_FILE_NAME_CHARS = r'''*"/\<>:|?^'''
UNSAFE_FILE_NAME_MAPPING = str.maketrans(UNSAFE_FILE_NAME_CHARS, "_"*len(UNSAFE_FILE_NAME_CHARS))


Changes to mtg_proxy_printer/ui/item_delegates.py.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.

import typing
from itertools import combinations
from typing import Union

from PyQt5.QtCore import QModelIndex, Qt, QAbstractItemModel, QSortFilterProxyModel, QObject, QEvent
from PyQt5.QtGui import QKeyEvent, QFocusEvent
from PyQt5.QtWidgets import QStyledItemDelegate, QWidget, QStyleOptionViewItem, QComboBox, QSpinBox, QLineEdit, \
    QApplication

from mtg_proxy_printer.model.card import MTGSet, Card, AnyCardType
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.logger import get_logger

try:







|
|
|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.

import typing
from itertools import combinations
from typing import Union

from PySide6.QtCore import QModelIndex, Qt, QAbstractItemModel, QSortFilterProxyModel, QObject, QEvent
from PySide6.QtGui import QKeyEvent, QFocusEvent
from PySide6.QtWidgets import QStyledItemDelegate, QWidget, QStyleOptionViewItem, QComboBox, QSpinBox, QLineEdit, \
    QApplication

from mtg_proxy_printer.model.card import MTGSet, Card, AnyCardType
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.logger import get_logger

try:
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
class SetEditorDelegate(FastComboBoxDelegate):
    """
    A set editor. For official cards, use a QComboBox with valid set choices for the given card.
    For custom cards, use the embedded editor widget to allow free-form text entry.
    """
    class CustomCardSetEditor(QWidget):
        """A widget holding two line edits, allowing the user to freely edit the set name & code of custom cards."""
        def __init__(self, parent: QWidget = None, flags=Qt.WindowFlags()):
            super().__init__(parent, flags)
            self.ui = ui = Ui_SetEditor()
            ui.setupUi(self)

        def set_data(self, mtg_set: MTGSet):
            self.ui.name_editor.setText(mtg_set.name)
            self.ui.code_edit.setText(mtg_set.code)







|







112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
class SetEditorDelegate(FastComboBoxDelegate):
    """
    A set editor. For official cards, use a QComboBox with valid set choices for the given card.
    For custom cards, use the embedded editor widget to allow free-form text entry.
    """
    class CustomCardSetEditor(QWidget):
        """A widget holding two line edits, allowing the user to freely edit the set name & code of custom cards."""
        def __init__(self, parent: QWidget = None, flags=Qt.WindowType.Widget):
            super().__init__(parent, flags)
            self.ui = ui = Ui_SetEditor()
            ui.setupUi(self)

        def set_data(self, mtg_set: MTGSet):
            self.ui.name_editor.setText(mtg_set.name)
            self.ui.code_edit.setText(mtg_set.code)

Changes to mtg_proxy_printer/ui/main_window.py.

13
14
15
16
17
18
19

20
21
22

23
24
25
26
27
28
29
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import pathlib
import typing


from PyQt5.QtCore import pyqtSlot as Slot, pyqtSignal as Signal, QStringListModel, QUrl, Qt, QSize
from PyQt5.QtGui import QCloseEvent, QKeySequence, QDesktopServices, QDragEnterEvent, QDropEvent, QPixmap
from PyQt5.QtWidgets import QApplication, QMessageBox, QAction, QWidget, QMainWindow, QDialog


from mtg_proxy_printer.missing_images_manager import MissingImagesManager
from mtg_proxy_printer.card_info_downloader import CardInfoDownloader
from mtg_proxy_printer.model.carddb import CardDatabase
from mtg_proxy_printer.model.imagedb import ImageDatabase
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.document_controller.compact_document import ActionCompactDocument







>
|
|
|
>







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import pathlib
import typing


from PySide6.QtCore import Slot, Signal, QStringListModel, QUrl, Qt
from PySide6.QtGui import QCloseEvent, QKeySequence, QAction, QDesktopServices, QDragEnterEvent, QDropEvent
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget, QMainWindow, QDialog
from PySide6.QtPrintSupport import QPrintDialog

from mtg_proxy_printer.missing_images_manager import MissingImagesManager
from mtg_proxy_printer.card_info_downloader import CardInfoDownloader
from mtg_proxy_printer.model.carddb import CardDatabase
from mtg_proxy_printer.model.imagedb import ImageDatabase
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.document_controller.compact_document import ActionCompactDocument
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
            (self.ui.action_undo, StandardKey.Undo),
            (self.ui.action_redo, StandardKey.Redo),
        ]
        for action, shortcut in actions_with_shortcuts:
            action.setShortcut(shortcut)

    def _setup_central_widget(self):
        self.setCentralWidget(self.ui.central_widget)
        self.ui.central_widget.set_data(self.document, self.card_database, self.image_db)

    def _setup_loading_state_connections(self):
        for widget_or_action in self._get_widgets_and_actions_disabled_in_loading_state():
            self.loading_state_changed.connect(widget_or_action.setDisabled)

    def _setup_undo_redo_actions(self, document: Document):







<







126
127
128
129
130
131
132

133
134
135
136
137
138
139
            (self.ui.action_undo, StandardKey.Undo),
            (self.ui.action_redo, StandardKey.Redo),
        ]
        for action, shortcut in actions_with_shortcuts:
            action.setShortcut(shortcut)

    def _setup_central_widget(self):

        self.ui.central_widget.set_data(self.document, self.card_database, self.image_db)

    def _setup_loading_state_connections(self):
        for widget_or_action in self._get_widgets_and_actions_disabled_in_loading_state():
            self.loading_state_changed.connect(widget_or_action.setDisabled)

    def _setup_undo_redo_actions(self, document: Document):
278
279
280
281
282
283
284

285
286
287
288
289
290
291
292
        action_str = self.tr(
            "printing",
            "This is passed as the {action} when asking the user about compacting the document if that can save pages")
        if self._ask_user_about_compacting_document(action_str) == StandardButton.Cancel:
            return
        self.current_dialog = dialog = PrintDialog(self.document, self)
        dialog.finished.connect(self.on_dialog_finished)

        self.missing_images_manager.obtain_missing_images(dialog.open)

    @Slot()
    def on_action_print_preview_triggered(self):
        logger.info(f"User views the print preview.")
        action_str = self.tr(
            "printing",
            "This is passed as the {action} when asking the user about compacting the document if that can save pages")







>
|







279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
        action_str = self.tr(
            "printing",
            "This is passed as the {action} when asking the user about compacting the document if that can save pages")
        if self._ask_user_about_compacting_document(action_str) == StandardButton.Cancel:
            return
        self.current_dialog = dialog = PrintDialog(self.document, self)
        dialog.finished.connect(self.on_dialog_finished)
        # Use the QDialog base class open() method, because QPrintDialog.open() performs additional, unwanted actions.
        self.missing_images_manager.obtain_missing_images(super(QPrintDialog, dialog).open)

    @Slot()
    def on_action_print_preview_triggered(self):
        logger.info(f"User views the print preview.")
        action_str = self.tr(
            "printing",
            "This is passed as the {action} when asking the user about compacting the document if that can save pages")
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
                self, self.tr("Download required Card data from Scryfall?"),
                self.tr(
                    "This program requires downloading additional card data from Scryfall to operate the card search.\n"
                    "Download the required data from Scryfall now?\n"
                    "Without the data, you can only print custom cards by drag&dropping "
                    "the image files onto the main window."),
                StandardButton.Yes | StandardButton.No, StandardButton.Yes) == StandardButton.Yes:
            self.ui.action_download_card_data.trigger()

    @Slot()
    def on_action_save_document_triggered(self):
        logger.debug("User clicked on Save")
        if self.document.save_file_path is None:
            logger.debug("No save file path set. Call 'Save as' instead.")
            self.ui.action_save_as.trigger()







|







373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
                self, self.tr("Download required Card data from Scryfall?"),
                self.tr(
                    "This program requires downloading additional card data from Scryfall to operate the card search.\n"
                    "Download the required data from Scryfall now?\n"
                    "Without the data, you can only print custom cards by drag&dropping "
                    "the image files onto the main window."),
                StandardButton.Yes | StandardButton.No, StandardButton.Yes) == StandardButton.Yes:
            self.card_data_downloader.import_from_api()

    @Slot()
    def on_action_save_document_triggered(self):
        logger.debug("User clicked on Save")
        if self.document.save_file_path is None:
            logger.debug("No save file path set. Call 'Save as' instead.")
            self.ui.action_save_as.trigger()
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
                self, self.tr("New card data available"),
                self.tr(
                    "There are %n new printings available on Scryfall. Update the local data now?",
                    "", estimated_card_count),
                StandardButton.Yes | StandardButton.No, StandardButton.Yes
        ) == StandardButton.Yes:
            logger.info("User agreed to update the card data from Scryfall. Performing update")
            self.ui.action_download_card_data.trigger()
        else:
            # If the user declines to perform the update now, allow them to perform it later by enabling the action.
            self.ui.action_download_card_data.setEnabled(True)

    def ask_user_about_application_update_policy(self):
        """Executed on start when the application update policy setting is set to None, the default value."""
        name = mtg_proxy_printer.meta_data.PROGRAMNAME







|







468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
                self, self.tr("New card data available"),
                self.tr(
                    "There are %n new printings available on Scryfall. Update the local data now?",
                    "", estimated_card_count),
                StandardButton.Yes | StandardButton.No, StandardButton.Yes
        ) == StandardButton.Yes:
            logger.info("User agreed to update the card data from Scryfall. Performing update")
            self.card_data_downloader.import_from_api()
        else:
            # If the user declines to perform the update now, allow them to perform it later by enabling the action.
            self.ui.action_download_card_data.setEnabled(True)

    def ask_user_about_application_update_policy(self):
        """Executed on start when the application update policy setting is set to None, the default value."""
        name = mtg_proxy_printer.meta_data.PROGRAMNAME
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
        if mime_data.hasUrls() and len(dropped_urls := mime_data.urls()) == 1:
            url = dropped_urls[0].toLocalFile()
            path = pathlib.Path(url)
            acceptable = path.is_file() and path.suffix.casefold() == f".{DEFAULT_SAVE_SUFFIX}"
            if acceptable:
                return path
        return None

    @staticmethod
    def _to_pixmaps(event: typing.Union[QDragEnterEvent, QDropEvent]) -> typing.List[QPixmap]:
        result: typing.List[QPixmap] = []
        mime_data = event.mimeData()
        regular = mtg_proxy_printer.units_and_sizes.CardSizes.REGULAR
        width, height = regular.width.magnitude, regular.height.magnitude
        for url in mime_data.urls():
            pixmap = QPixmap(url.toLocalFile())
            if not pixmap.isNull():
                if pixmap.width() != width or pixmap.height() != height:
                    new_size = QSize(width, height)
                    pixmap = pixmap.scaled(new_size, transformMode=TransformationMode.SmoothTransformation)
                result.append(pixmap)
        return result







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
544
545
546
547
548
549
550















        if mime_data.hasUrls() and len(dropped_urls := mime_data.urls()) == 1:
            url = dropped_urls[0].toLocalFile()
            path = pathlib.Path(url)
            acceptable = path.is_file() and path.suffix.casefold() == f".{DEFAULT_SAVE_SUFFIX}"
            if acceptable:
                return path
        return None















Changes to mtg_proxy_printer/ui/page_card_table_view.py.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

import functools
import math
import operator
from pathlib import Path
from typing import Union, Optional

from PyQt5.QtCore import QPoint, Qt, pyqtSignal as Signal, pyqtSlot as Slot, QPersistentModelIndex
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QTableView, QWidget, QMenu, QAction, QInputDialog, QFileDialog

from mtg_proxy_printer.app_dirs import data_directories
from mtg_proxy_printer.document_controller import DocumentAction
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard, ActionRemoveCards
from mtg_proxy_printer.model.carddb import CardDatabase
from mtg_proxy_printer.model.card import Card, CheckCard, CardList, AnyCardType, AnyCardTypeForTypeCheck
from mtg_proxy_printer.model.document import Document







|
|
|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

import functools
import math
import operator
from pathlib import Path
from typing import Union, Optional

from PySide6.QtCore import QPoint, Qt, Signal, Slot, QPersistentModelIndex
from PySide6.QtGui import QIcon, QAction
from PySide6.QtWidgets import QTableView, QWidget, QMenu, QInputDialog, QFileDialog

from mtg_proxy_printer.app_dirs import data_directories
from mtg_proxy_printer.document_controller import DocumentAction
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard, ActionRemoveCards
from mtg_proxy_printer.model.carddb import CardDatabase
from mtg_proxy_printer.model.card import Card, CheckCard, CardList, AnyCardType, AnyCardTypeForTypeCheck
from mtg_proxy_printer.model.document import Document

Changes to mtg_proxy_printer/ui/page_config_container.py.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from functools import partial

from PyQt5.QtWidgets import QWidget

from mtg_proxy_printer.ui.common import load_ui_from_file
from mtg_proxy_printer.logger import get_logger

try:
    from mtg_proxy_printer.ui.generated.page_config_container import Ui_PageConfigContainer
except ModuleNotFoundError:







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from functools import partial

from PySide6.QtWidgets import QWidget

from mtg_proxy_printer.ui.common import load_ui_from_file
from mtg_proxy_printer.logger import get_logger

try:
    from mtg_proxy_printer.ui.generated.page_config_container import Ui_PageConfigContainer
except ModuleNotFoundError:

Changes to mtg_proxy_printer/ui/page_config_preview_area.py.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import enum
from unittest.mock import MagicMock

from PyQt5.QtCore import pyqtSlot as Slot, QPersistentModelIndex
from PyQt5.QtGui import QColorConstants, QPainter, QPixmap
from PyQt5.QtWidgets import QWidget

from mtg_proxy_printer.document_controller.page_actions import ActionNewPage
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard, ActionRemoveCards
from mtg_proxy_printer.model.page_layout import PageLayoutSettings
from mtg_proxy_printer.units_and_sizes import CardSizes, CardSize
from mtg_proxy_printer.model.document_page import PageType
from mtg_proxy_printer.model.document import Document







|
|
|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import enum
from unittest.mock import MagicMock

from PySide6.QtCore import Slot, QPersistentModelIndex
from PySide6.QtGui import QColorConstants, QPainter, QPixmap
from PySide6.QtWidgets import QWidget

from mtg_proxy_printer.document_controller.page_actions import ActionNewPage
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard, ActionRemoveCards
from mtg_proxy_printer.model.page_layout import PageLayoutSettings
from mtg_proxy_printer.units_and_sizes import CardSizes, CardSize
from mtg_proxy_printer.model.document_page import PageType
from mtg_proxy_printer.model.document import Document

Changes to mtg_proxy_printer/ui/page_config_widget.py.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30


import functools
from functools import partial
import math
import typing

from PyQt5.QtCore import pyqtSlot as Slot, Qt, pyqtSignal as Signal
from PyQt5.QtWidgets import QGroupBox, QWidget, QDoubleSpinBox, QCheckBox, QLineEdit

import mtg_proxy_printer.settings
from mtg_proxy_printer.ui.common import load_ui_from_file, BlockedSignals, highlight_widget
from mtg_proxy_printer.model.page_layout import PageLayoutSettings
from mtg_proxy_printer.units_and_sizes import CardSizes, PageType, unit_registry, ConfigParser, QuantityT

try:







|
|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30


import functools
from functools import partial
import math
import typing

from PySide6.QtCore import Slot, Qt, Signal
from PySide6.QtWidgets import QGroupBox, QWidget, QDoubleSpinBox, QCheckBox, QLineEdit

import mtg_proxy_printer.settings
from mtg_proxy_printer.ui.common import load_ui_from_file, BlockedSignals, highlight_widget
from mtg_proxy_printer.model.page_layout import PageLayoutSettings
from mtg_proxy_printer.units_and_sizes import CardSizes, PageType, unit_registry, ConfigParser, QuantityT

try:
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91



92
93
94
95
96
97
98
99

        for spinbox, _ in self._get_decimal_settings_widgets():
            layout_key = spinbox.objectName()
            spinbox.valueChanged[float].connect(
                partial(self.set_numerical_page_layout_item, page_layout, layout_key, "mm"))
            spinbox.valueChanged[float].connect(self.validate_paper_size_settings)
            spinbox.valueChanged[float].connect(self.on_page_layout_setting_changed)
            spinbox.valueChanged[float].connect(partial(self.page_layout_changed.emit, page_layout))

        for checkbox, _ in self._get_boolean_settings_widgets():
            layout_key = checkbox.objectName()
            checkbox.stateChanged.connect(partial(self.set_boolean_page_layout_item, page_layout, layout_key))
            checkbox.stateChanged.connect(partial(self.page_layout_changed.emit, page_layout))

        ui.document_name.textChanged.connect(partial(setattr, page_layout, "document_name"))
        ui.document_name.textChanged.connect(partial(self.page_layout_changed.emit, page_layout))
        return page_layout

    @staticmethod
    def set_numerical_page_layout_item(page_layout: PageLayoutSettings, layout_key: str, unit: str, value: float):
        # Implementation note: This call is placed here, because stuffing it into a lambda defined within a while loop
        # somehow uses the wrong references and will set the attribute that was processed last in the loop.
        # This method can be used via functools.partial to reduce the signature to (float) -> None,
        # which can be connected to the valueChanged[float] signal just fine.
        # Also, functools.partial does not exhibit the same issue as the lambda expression shows.
        setattr(page_layout, layout_key, value*unit_registry.parse_units(unit))

    @staticmethod
    def set_boolean_page_layout_item(page_layout: PageLayoutSettings, layout_key: str, value: CheckState):
        # Implementation note: This call is placed here, because stuffing it into a lambda defined within a while loop
        # somehow uses the wrong references and will set the attribute that was processed last in the loop.
        # This method can be used via functools.partial to reduce the signature to (CheckState) -> None,
        # which can be connected to the stateChanged signal just fine.
        # Also, functools.partial does not exhibit the same issue as the lambda expression shows.



        setattr(page_layout, layout_key, value == CheckState.Checked)

    @Slot()
    def on_page_layout_setting_changed(self):
        """
        Recomputes and updates the page capacity display, whenever any page layout widget changes.
        """
        regular_capacity = self.page_layout.compute_page_card_capacity(PageType.REGULAR)







|




|


|


















>
>
>
|







58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

        for spinbox, _ in self._get_decimal_settings_widgets():
            layout_key = spinbox.objectName()
            spinbox.valueChanged[float].connect(
                partial(self.set_numerical_page_layout_item, page_layout, layout_key, "mm"))
            spinbox.valueChanged[float].connect(self.validate_paper_size_settings)
            spinbox.valueChanged[float].connect(self.on_page_layout_setting_changed)
            spinbox.valueChanged[float].connect(lambda: self.page_layout_changed.emit(page_layout))

        for checkbox, _ in self._get_boolean_settings_widgets():
            layout_key = checkbox.objectName()
            checkbox.stateChanged.connect(partial(self.set_boolean_page_layout_item, page_layout, layout_key))
            checkbox.stateChanged.connect(lambda: self.page_layout_changed.emit(page_layout))

        ui.document_name.textChanged.connect(partial(setattr, page_layout, "document_name"))
        ui.document_name.textChanged.connect(lambda: self.page_layout_changed.emit(page_layout))
        return page_layout

    @staticmethod
    def set_numerical_page_layout_item(page_layout: PageLayoutSettings, layout_key: str, unit: str, value: float):
        # Implementation note: This call is placed here, because stuffing it into a lambda defined within a while loop
        # somehow uses the wrong references and will set the attribute that was processed last in the loop.
        # This method can be used via functools.partial to reduce the signature to (float) -> None,
        # which can be connected to the valueChanged[float] signal just fine.
        # Also, functools.partial does not exhibit the same issue as the lambda expression shows.
        setattr(page_layout, layout_key, value*unit_registry.parse_units(unit))

    @staticmethod
    def set_boolean_page_layout_item(page_layout: PageLayoutSettings, layout_key: str, value: CheckState):
        # Implementation note: This call is placed here, because stuffing it into a lambda defined within a while loop
        # somehow uses the wrong references and will set the attribute that was processed last in the loop.
        # This method can be used via functools.partial to reduce the signature to (CheckState) -> None,
        # which can be connected to the stateChanged signal just fine.
        # Also, functools.partial does not exhibit the same issue as the lambda expression shows.
        #
        # PySide6 maps the QCheckBox check states to proper Python enums, but the stateChanged Qt signal carries raw
        # integers. To get the integers for comparison, the lambdas below require accessing the CheckState enum values.
        setattr(page_layout, layout_key, value == CheckState.Checked.value)

    @Slot()
    def on_page_layout_setting_changed(self):
        """
        Recomputes and updates the page capacity display, whenever any page layout widget changes.
        """
        regular_capacity = self.page_layout.compute_page_card_capacity(PageType.REGULAR)

Changes to mtg_proxy_printer/ui/page_renderer.py.

14
15
16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import enum
import typing
from functools import partial

from PyQt5.QtCore import Qt, QEvent
from PyQt5.QtWidgets import QGraphicsView, QWidget, QAction
from PyQt5.QtGui import QWheelEvent, QKeySequence, QPalette, QResizeEvent


from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.logger import get_logger
from mtg_proxy_printer.ui.page_scene import RenderMode, PageScene

logger = get_logger(__name__)
del get_logger







|
|
|
>







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import enum
import typing
from functools import partial

from PySide6.QtCore import Qt, QEvent
from PySide6.QtWidgets import QGraphicsView, QWidget
from PySide6.QtGui import QWheelEvent, QKeySequence, QPalette, QResizeEvent, QAction


from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.logger import get_logger
from mtg_proxy_printer.ui.page_scene import RenderMode, PageScene

logger = get_logger(__name__)
del get_logger

Changes to mtg_proxy_printer/ui/page_scene.py.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

import collections
import enum
import functools
import itertools
import typing

from PyQt5.QtCore import Qt, QSizeF, QPointF, QRectF, pyqtSignal as Signal, QObject, pyqtSlot as Slot, \
    QPersistentModelIndex, QModelIndex, QRect, QPoint, QSize
from PyQt5.QtGui import QPen, QColorConstants, QBrush, QColor, QPalette, QFontMetrics, QPixmap, QTransform, QPolygonF
from PyQt5.QtWidgets import QGraphicsItemGroup, QGraphicsItem, QGraphicsPixmapItem, QGraphicsRectItem, \
    QGraphicsLineItem, QGraphicsSimpleTextItem, QGraphicsScene, QGraphicsPolygonItem

from mtg_proxy_printer.model.card import CardCorner, Card
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.model.document_page import PageColumns
from mtg_proxy_printer.model.page_layout import PageLayoutSettings
from mtg_proxy_printer.settings import settings







|

|
|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

import collections
import enum
import functools
import itertools
import typing

from PySide6.QtCore import Qt, QSizeF, QPointF, QRectF, Signal, QObject, Slot, \
    QPersistentModelIndex, QModelIndex, QRect, QPoint, QSize
from PySide6.QtGui import QPen, QColorConstants, QBrush, QColor, QPalette, QFontMetrics, QPixmap, QTransform, QPolygonF
from PySide6.QtWidgets import QGraphicsItemGroup, QGraphicsItem, QGraphicsPixmapItem, QGraphicsRectItem, \
    QGraphicsLineItem, QGraphicsSimpleTextItem, QGraphicsScene, QGraphicsPolygonItem

from mtg_proxy_printer.model.card import CardCorner, Card
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.model.document_page import PageColumns
from mtg_proxy_printer.model.page_layout import PageLayoutSettings
from mtg_proxy_printer.settings import settings

Changes to mtg_proxy_printer/ui/printing_filter_widgets.py.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31



import abc
from functools import partial
from typing import List, Tuple

from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QGroupBox, QWidget, QCheckBox, QPushButton

from mtg_proxy_printer.units_and_sizes import ConfigParser, SectionProxy
from mtg_proxy_printer.ui.common import highlight_widget

try:
    from mtg_proxy_printer.ui.generated.settings_window.format_printing_filter import Ui_FormatPrintingFilter
    from mtg_proxy_printer.ui.generated.settings_window.general_printing_filter import Ui_GeneralPrintingFilter







|
|
|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31



import abc
from functools import partial
from typing import List, Tuple

from PySide6.QtCore import QUrl
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import QGroupBox, QWidget, QCheckBox, QPushButton

from mtg_proxy_printer.units_and_sizes import ConfigParser, SectionProxy
from mtg_proxy_printer.ui.common import highlight_widget

try:
    from mtg_proxy_printer.ui.generated.settings_window.format_printing_filter import Ui_FormatPrintingFilter
    from mtg_proxy_printer.ui.generated.settings_window.general_printing_filter import Ui_GeneralPrintingFilter

Changes to mtg_proxy_printer/ui/progress_bar.py.

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from PyQt5.QtCore import pyqtSlot as Slot, Qt
from PyQt5.QtWidgets import QWidget, QLabel, QProgressBar

from mtg_proxy_printer.runner import ProgressSignalContainer

try:
    from mtg_proxy_printer.ui.generated.progress_bar import Ui_ProgressBar
except ModuleNotFoundError:
    from mtg_proxy_printer.ui.common import load_ui_from_file







|
|







10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from PySide6.QtCore import Slot, Qt
from PySide6.QtWidgets import QWidget, QLabel, QProgressBar

from mtg_proxy_printer.runner import ProgressSignalContainer

try:
    from mtg_proxy_printer.ui.generated.progress_bar import Ui_ProgressBar
except ModuleNotFoundError:
    from mtg_proxy_printer.ui.common import load_ui_from_file
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
__all__ = [
    "ProgressBar",
]


class ProgressBar(QWidget):

    def __init__(self, parent: QWidget = None, flags=Qt.WindowType()):
        super().__init__(parent, flags)
        self.ui = ui = Ui_ProgressBar()
        ui.setupUi(self)
        self.set_outer_progress = ui.outer_progress_bar.setValue
        self.set_inner_progress = ui.inner_progress_bar.setValue
        self.set_independent_progress = ui.independent_bar.setValue
        for item in (ui.inner_progress_bar, ui.inner_progress_label):







|







34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
__all__ = [
    "ProgressBar",
]


class ProgressBar(QWidget):

    def __init__(self, parent: QWidget = None, flags=Qt.WindowType.Widget):
        super().__init__(parent, flags)
        self.ui = ui = Ui_ProgressBar()
        ui.setupUi(self)
        self.set_outer_progress = ui.outer_progress_bar.setValue
        self.set_inner_progress = ui.inner_progress_bar.setValue
        self.set_independent_progress = ui.independent_bar.setValue
        for item in (ui.inner_progress_bar, ui.inner_progress_label):

Changes to mtg_proxy_printer/ui/settings_window.py.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import pathlib
import typing
from functools import partial

from PyQt5.QtCore import QStringListModel, pyqtSignal as Signal, Qt, QItemSelectionModel, QEvent, QObject, QTimer
from PyQt5.QtWidgets import QDialogButtonBox, QMessageBox, QWidget, QDialog
from PyQt5.QtGui import QIcon, QStandardItemModel, QResizeEvent

import mtg_proxy_printer.app_dirs
from mtg_proxy_printer.units_and_sizes import ConfigParser
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.document_controller import DocumentAction
from mtg_proxy_printer.document_controller.edit_document_settings import ActionEditDocumentSettings








|
|
|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import pathlib
import typing
from functools import partial

from PySide6.QtCore import QStringListModel, Signal, Qt, QItemSelectionModel, QEvent, QObject, QTimer
from PySide6.QtWidgets import QDialogButtonBox, QMessageBox, QWidget, QDialog
from PySide6.QtGui import QIcon, QStandardItemModel, QResizeEvent

import mtg_proxy_printer.app_dirs
from mtg_proxy_printer.units_and_sizes import ConfigParser
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.document_controller import DocumentAction
from mtg_proxy_printer.document_controller.edit_document_settings import ActionEditDocumentSettings

Changes to mtg_proxy_printer/ui/settings_window_pages.py.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import json
import logging
from functools import partial
import pathlib
import typing
from abc import abstractmethod

from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot, QUrl, QStandardPaths, QStringListModel, Qt, QThreadPool
from PyQt5.QtGui import QDesktopServices, QStandardItem, QIcon
from PyQt5.QtWidgets import QWidget, QCheckBox, QFileDialog, QMessageBox, QApplication, QLineEdit, QDoubleSpinBox

import mtg_proxy_printer.app_dirs
import mtg_proxy_printer.settings
from mtg_proxy_printer.printing_filter_updater import PrintingFilterUpdater
from mtg_proxy_printer.logger import get_logger
from mtg_proxy_printer.ui.common import highlight_widget, load_file
from mtg_proxy_printer.units_and_sizes import OptStr, ConfigParser, unit_registry, QuantityT







|
|
|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import json
import logging
from functools import partial
import pathlib
import typing
from abc import abstractmethod

from PySide6.QtCore import Signal, Slot, QUrl, QStandardPaths, QStringListModel, Qt, QThreadPool
from PySide6.QtGui import QDesktopServices, QStandardItem, QIcon
from PySide6.QtWidgets import QWidget, QCheckBox, QFileDialog, QMessageBox, QApplication, QLineEdit, QDoubleSpinBox

import mtg_proxy_printer.app_dirs
import mtg_proxy_printer.settings
from mtg_proxy_printer.printing_filter_updater import PrintingFilterUpdater
from mtg_proxy_printer.logger import get_logger
from mtg_proxy_printer.ui.common import highlight_widget, load_file
from mtg_proxy_printer.units_and_sizes import OptStr, ConfigParser, unit_registry, QuantityT
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84


class PageMetadata(typing.NamedTuple):
    text: str
    icon_name: OptStr
    tooltip: OptStr = None


class Page(QWidget):
    """The base class for settings page widgets. Defines the API used by the settings window"""

    def display_item(self) -> typing.Sequence[QStandardItem]:
        """Returns a list model item for this page, used to represent the page in the settings page selection UI."""
        data = self.display_metadata()
        item = QStandardItem(data.text)







<







70
71
72
73
74
75
76

77
78
79
80
81
82
83


class PageMetadata(typing.NamedTuple):
    text: str
    icon_name: OptStr
    tooltip: OptStr = None


class Page(QWidget):
    """The base class for settings page widgets. Defines the API used by the settings window"""

    def display_item(self) -> typing.Sequence[QStandardItem]:
        """Returns a list model item for this page, used to represent the page in the settings page selection UI."""
        data = self.display_metadata()
        item = QStandardItem(data.text)
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
    @abstractmethod
    def highlight_differing_settings(self, settings: ConfigParser):
        """Highlights GUI widgets with a state different from the given settings"""
        pass

    def clear_highlight(self):
        """Clears all GUI widget highlights."""
        for item in self.findChildren((QWidget,), options=Qt.FindChildOption.FindChildrenRecursively):  # type: QWidget
            item.setGraphicsEffect(None)


class DebugSettingsPage(Page):

    requested_card_download = Signal(pathlib.Path)








|







111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
    @abstractmethod
    def highlight_differing_settings(self, settings: ConfigParser):
        """Highlights GUI widgets with a state different from the given settings"""
        pass

    def clear_highlight(self):
        """Clears all GUI widget highlights."""
        for item in self.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively):  # type: QWidget
            item.setGraphicsEffect(None)


class DebugSettingsPage(Page):

    requested_card_download = Signal(pathlib.Path)

501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
        self.page_config_widget.highlight_differing_settings(settings)


class PrinterSettingsPage(Page):
    def display_metadata(self) -> PageMetadata:
        return PageMetadata(self.tr("Printer settings"), "document-print", self.tr("Configure the printer"))

    def __init__(self, parent=None, flags=Qt.WindowFlags()):
        super().__init__(parent, flags)
        self.ui = ui = Ui_PrinterSettingsPage()
        ui.setupUi(self)

    def _get_printer_settings_boolean_widgets(self):
        ui = self.ui
        widgets_with_settings: typing.List[typing.Tuple[QCheckBox, str]] = [







|







500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
        self.page_config_widget.highlight_differing_settings(settings)


class PrinterSettingsPage(Page):
    def display_metadata(self) -> PageMetadata:
        return PageMetadata(self.tr("Printer settings"), "document-print", self.tr("Configure the printer"))

    def __init__(self, parent=None, flags=Qt.WindowType.Widget):
        super().__init__(parent, flags)
        self.ui = ui = Ui_PrinterSettingsPage()
        ui.setupUi(self)

    def _get_printer_settings_boolean_widgets(self):
        ui = self.ui
        widgets_with_settings: typing.List[typing.Tuple[QCheckBox, str]] = [
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
                highlight_widget(spinbox)


class PDFSettingsPage(Page):
    def display_metadata(self) -> PageMetadata:
        return PageMetadata(self.tr("PDF export settings"), "viewpdf", self.tr("Configure the PDF export"))

    def __init__(self, parent=None, flags=Qt.WindowFlags()):
        super().__init__(parent, flags)
        self.ui = ui = Ui_PDFSettingsPage()
        ui.setupUi(self)

    def load(self, settings: ConfigParser):
        ui = self.ui
        section = settings["pdf-export"]







|







551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
                highlight_widget(spinbox)


class PDFSettingsPage(Page):
    def display_metadata(self) -> PageMetadata:
        return PageMetadata(self.tr("PDF export settings"), "viewpdf", self.tr("Configure the PDF export"))

    def __init__(self, parent=None, flags=Qt.WindowType.Widget):
        super().__init__(parent, flags)
        self.ui = ui = Ui_PDFSettingsPage()
        ui.setupUi(self)

    def load(self, settings: ConfigParser):
        ui = self.ui
        section = settings["pdf-export"]

Changes to mtg_proxy_printer/units_and_sizes.py.

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
    from typing_extensions import NotRequired

import pint
try:
    from pint.facets.plain.registry import QuantityT, UnitT
except ImportError:  # Compatibility with Pint 0.21 for Python 3.8 support
    QuantityT = UnitT = typing.Any
from PyQt5.QtCore import QSize


def _setup_units() -> typing.Tuple[pint.UnitRegistry, QuantityT]:
    registry = pint.UnitRegistry(cache_folder=":auto:")
    resolution = registry.parse_expression("300dots/inch")
    print_context = pint.Context("print")
    print_context.add_transformation("[length]", "[printing_unit]", lambda _, x: x*RESOLUTION)







|







26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
    from typing_extensions import NotRequired

import pint
try:
    from pint.facets.plain.registry import QuantityT, UnitT
except ImportError:  # Compatibility with Pint 0.21 for Python 3.8 support
    QuantityT = UnitT = typing.Any
from PySide6.QtCore import QSize


def _setup_units() -> typing.Tuple[pint.UnitRegistry, QuantityT]:
    registry = pint.UnitRegistry(cache_folder=":auto:")
    resolution = registry.parse_expression("300dots/inch")
    print_context = pint.Context("print")
    print_context.add_transformation("[length]", "[printing_unit]", lambda _, x: x*RESOLUTION)

Changes to mtg_proxy_printer/update_checker.py.

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import socket
import sqlite3
import typing
import urllib.parse
import urllib.error

import ijson
from PyQt5.QtCore import QObject, pyqtSignal as Signal, QThreadPool

from mtg_proxy_printer.argument_parser import Namespace
import mtg_proxy_printer.meta_data
from mtg_proxy_printer import settings
from mtg_proxy_printer.model.carddb import CardDatabase, SCHEMA_NAME
from mtg_proxy_printer.card_info_downloader import ApiStreamWorker, CardInfoWorkerBase
from mtg_proxy_printer.natsort import natural_sorted, str_less_than







|







19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import socket
import sqlite3
import typing
import urllib.parse
import urllib.error

import ijson
from PySide6.QtCore import QObject, Signal, QThreadPool

from mtg_proxy_printer.argument_parser import Namespace
import mtg_proxy_printer.meta_data
from mtg_proxy_printer import settings
from mtg_proxy_printer.model.carddb import CardDatabase, SCHEMA_NAME
from mtg_proxy_printer.card_info_downloader import ApiStreamWorker, CardInfoWorkerBase
from mtg_proxy_printer.natsort import natural_sorted, str_less_than

Changes to pyproject.toml.

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
]
# Minimum required ijson version is 3.1 for the use_float parameter.
# ijson adds full compatibility with Python 3.11 in version 3.2 (3.1 is really slow on Py3.11).
# and Python 3.12 support in 3.2.1. Require newer versions on those Python versions to avoid
# falling back to the slow pure Python backend.
dependencies = [
    "platformdirs >= 2.6.0",
    "PyQt5",
    "ijson >= 3.1.0; python_version < '3.11'",
    "ijson >= 3.2.0; python_version >= '3.11'",
    "ijson >= 3.2.1; python_version >= '3.12'",
    "pint < 0.22; python_version < '3.9'",  # 0.22 dropped Py 3.8 support, thus Win7 support
    "pint >= 0.22; python_version >= '3.9'",  # Requires 0.22 for the QuantityT and UnitT types
    "delegateto == 1.5",
    "PyHamcrest >= 2",







|







36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
]
# Minimum required ijson version is 3.1 for the use_float parameter.
# ijson adds full compatibility with Python 3.11 in version 3.2 (3.1 is really slow on Py3.11).
# and Python 3.12 support in 3.2.1. Require newer versions on those Python versions to avoid
# falling back to the slow pure Python backend.
dependencies = [
    "platformdirs >= 2.6.0",
    "PySide6_Essentials >= 6.7.0",
    "ijson >= 3.1.0; python_version < '3.11'",
    "ijson >= 3.2.0; python_version >= '3.11'",
    "ijson >= 3.2.1; python_version >= '3.12'",
    "pint < 0.22; python_version < '3.9'",  # 0.22 dropped Py 3.8 support, thus Win7 support
    "pint >= 0.22; python_version >= '3.9'",  # Requires 0.22 for the QuantityT and UnitT types
    "delegateto == 1.5",
    "PyHamcrest >= 2",
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
[project.optional-dependencies]
dev = [
    "pytest >= 6.0",
    "pytest-cov",
    "pytest-timeout",
    "pytest-qt >= 2.0",
    "tox >= 4.0",
    "PySide2; platform_system == 'Windows' and python_version < '3.12'",  # Used for lrelease: Compiling translations
    "PySide6_Essentials; platform_system == 'Windows' and python_version >= '3.12'",  # Used for lrelease: Compiling translations
    "pillow; platform_system == 'Windows'",  # Used to convert the app icon to .ico
]
package = [
    "build",
    "cx_Freeze >= 7.2; platform.python_implementation == 'CPython'",
    "PySide2; platform_system == 'Windows' and python_version < '3.12'",  # Used for lrelease: Compiling translations
    "PySide6_Essentials; platform_system == 'Windows' and python_version >= '3.12'",  # Used for lrelease: Compiling translations
    "pillow; platform_system == 'Windows'",  # Used to convert the app icon to .ico
]


[project.urls]
Homepage = "https://chiselapp.com/user/luziferius/repository/MTGProxyPrinter/index"
Issues = "https://chiselapp.com/user/luziferius/repository/MTGProxyPrinter/ticket"







<
<





<
<







60
61
62
63
64
65
66


67
68
69
70
71


72
73
74
75
76
77
78
[project.optional-dependencies]
dev = [
    "pytest >= 6.0",
    "pytest-cov",
    "pytest-timeout",
    "pytest-qt >= 2.0",
    "tox >= 4.0",


    "pillow; platform_system == 'Windows'",  # Used to convert the app icon to .ico
]
package = [
    "build",
    "cx_Freeze >= 7.2; platform.python_implementation == 'CPython'",


    "pillow; platform_system == 'Windows'",  # Used to convert the app icon to .ico
]


[project.urls]
Homepage = "https://chiselapp.com/user/luziferius/repository/MTGProxyPrinter/index"
Issues = "https://chiselapp.com/user/luziferius/repository/MTGProxyPrinter/ticket"
111
112
113
114
115
116
117
118
119
120
121
version = {attr= "mtg_proxy_printer.meta_data.__version__"}


[build-system]
requires = [
    "setuptools >= 61",
    "wheel >= 0.45",
    "PyQt5",
    "build",
]
build-backend = "setuptools.build_meta"







|



107
108
109
110
111
112
113
114
115
116
117
version = {attr= "mtg_proxy_printer.meta_data.__version__"}


[build-system]
requires = [
    "setuptools >= 61",
    "wheel >= 0.45",
    "PySide6_Essentials >= 6.7.0",
    "build",
]
build-backend = "setuptools.build_meta"

Changes to run_tests.bat.

1
2
3
4
5
6
7
8
9
10
:: Runs the unit tests

:: Create or activate the build environment
IF EXIST "venv" (
  call venv\Scripts\activate.bat
) ELSE (
  call create_development_environment.bat
)

tox run



|
|





1
2
3
4
5
6
7
8
9
10
:: Runs the unit tests

:: Create or activate the build environment
IF EXIST "venv-PySide6" (
  call venv-PySide6\Scripts\activate.bat
) ELSE (
  call create_development_environment.bat
)

tox run

Changes to run_tests.sh.

1
2
3
4
5
#!/bin/bash

source venv/bin/activate
tox run
deactivate


|


1
2
3
4
5
#!/bin/bash

source venv-PySide6/bin/activate
tox run
deactivate

Changes to scripts/clean_windows_build.bat.

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

63
64
65
66
67
68
69
70
71
72
73


74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

92
93
94
95
96
97
98
99
100
101
102
103
104
105
:: but WITHOUT ANY WARRANTY; without even the implied warranty of
:: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
:: GNU General Public License for more details.
::
:: You should have received a copy of the GNU General Public License
:: along with this program. If not, see <http://www.gnu.org/licenses/>.


:: Cleanup various items that bloat the application bundle. Mostly related to unused PyQt5 and Qt5 components
if "%1%"=="" (
  pushd build\exe*
) else (
  pushd "%1"
)

rmdir /S /Q PyQt5.uic.widget-plugins

pushd lib
del ijson\backends\python*.dll


pushd PyQt5
:: All DLLs here are also in Qt5\bin\
del *.dll
del QtRemoteObjects.pyd QtSerialPort.pyd QtSensors.pyd QtNetwork.pyd QtXml.pyd QtXmlPatterns.pyd pyrcc.pyd




:: The sip bindings aren't used at runtime
rmdir /S /Q bindings

pushd Qt5

:: Unused Qsci
rmdir /S /Q qsci

:: Unused Qml bindings
rmdir /S /Q qml


::  Unused DLLs
pushd bin
del d3dcompiler_47.dll libEGL.dll libGLESv2.dll opengl32sw.dll Qt5Bluetooth.dll Qt5DBus.dll Qt5Designer.dll Qt5Help.dll
del Qt5Location.dll Qt5Multimedia.dll Qt5MultimediaWidgets.dll Qt5Network.dll Qt5Nfc.dll Qt5OpenGL.dll Qt5Positioning.dll
del Qt5PositioningQuick.dll Qt5Qml.dll Qt5QmlModels.dll Qt5QmlWorkerScript.dll Qt5Quick.dll Qt5Quick3D.dll
del Qt5Quick3DAssetImport.dll Qt5Quick3DRender.dll Qt5Quick3DRuntimeRender.dll Qt5Quick3DUtils.dll Qt5QuickControls2.dll
del Qt5QuickParticles.dll Qt5QuickShapes.dll Qt5QuickTemplates2.dll Qt5QuickTest.dll Qt5QuickWidgets.dll Qt5RemoteObjects.dll
del Qt5Sensors.dll Qt5SerialPort.dll Qt5Sql.dll Qt5Test.dll Qt5WebChannel.dll
del Qt5WebSockets.dll Qt5WebView.dll Qt5XmlPatterns.dll
del libcrypto-1_1-x64.dll libssl-1_1-x64.dll
popd


:: Unused plugins
pushd plugins


:: Remove duplicated Qt5 base DLLs
FOR %%G IN ( printsupport platforms imageformats styles
) DO del %%G\Qt5*.dll


FOR %%G IN (
  assetimporters audio geometryloaders geoservices mediaservice playlistformats
  position renderers sceneparsers sensorgestures sensors sqldrivers texttospeech webview
) DO rmdir /S /Q %%G
popd




:: Unused translations (of unused modules)
pushd translations
del qtxmlpatterns_*.qm
del qtconnectivity_*.qm
del qtdeclarative_*.qm
del qtlocation_*.qm
del qtmultimedia_*.qm
del qtquickcontrols_*.qm
del qtquickcontrols2_*.qm
del qtserialport_*.qm
del qtwebsockets_*.qm
popd

:: leave Qt5
popd

:: Unused extension modules

del *.pyi
del QAxContainer.pyd QtBluetooth.pyd QtDBus.pyd QtDesigner.pyd QtHelp.pyd QtLocation.pyd QtMultimedia.pyd QtMultimediaWidgets.pyd
del QtOpenGL.pyd QtNfc.pyd QtPositioning.pyd QtQml.pyd QtQuick.pyd QtQuick3D.pyd QtQuickWidgets.pyd
del QtRemoteObjects.pydQtSensors.pydQtSerialPort.pyd QtSql.pyd QtTest.pyd QtTextToSpeech.pyd QtWebChannel.pyd
del QtWebSockets.pyd _QOpenGLFunctions_2_0.pyd _QOpenGLFunctions_2_1.pyd _QOpenGLFunctions_4_1_Core.pyd

:: leave PyQt5
popd
:: leave lib
popd
::leave build directory
popd









<
<






|

|
<

|
<
|
|
<

>
>
>
|
|
|
|

|
<
|
<
<
|
|
<
<
<
<
<
<
<
<
<
<
<

<
<
<

>
|
<
|

|
<
<
<
<


>
>

|
<
<
<
<
<
<
<
<
<
<


|


|
>
|
<
<
<
<

<
<






9
10
11
12
13
14
15


16
17
18
19
20
21
22
23
24

25
26

27
28

29
30
31
32
33
34
35
36
37
38

39


40
41











42



43
44
45

46
47
48




49
50
51
52
53
54










55
56
57
58
59
60
61
62




63


64
65
66
67
68
69
:: but WITHOUT ANY WARRANTY; without even the implied warranty of
:: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
:: GNU General Public License for more details.
::
:: You should have received a copy of the GNU General Public License
:: along with this program. If not, see <http://www.gnu.org/licenses/>.



if "%1%"=="" (
  pushd build\exe*
) else (
  pushd "%1"
)

pushd lib

pushd PySide6


:: Don't need the executables, like Qt6 Designer, etc.

:: Delete all typing stubs
del *.exe *.pyi


:: Remove unused components. Each consists of a pair of QtComponent.pyd and Qt6Component.dll
del Q*tSerialPort* Qt*DBus* Qt*Designer* Qt*JsonRpc* Qt*Labs* Qt*LanguageServer* Qt*Network*
del Qt*Qml* Qt*Quick* Qt*RemoteObjects* Qt*Sensors* Qt*Sql* Qt*Test* Qt*WebChannel*
del Qt*Bluetooth* Qt*Charts* Qt*Concurrent* Qt*DataVisualization* Qt*Graphs* Qt*HttpServer*
del Qt*Nfc* Qt*Positioning* Qt*Scxml* Qt*SerialBus* Qt*SerialPort* Qt*ShaderTools*
del Qt*StateMachine* Qt*Test* Qt*TextToSpeech* Qt*Web* Qt*3D*
del Qt*Help* Qt*Multimedia* Qt*Xml*

:: Unused audio/video codec DLLs

del avformat-*.dll avutil-*.dll swresample-*.dll swscale-*.dll


:: Unused OpenGL bindings. The Qt6OpenGL DLLs are required and thus not removed
del QtOpenGL.pyd QtOpenGLWidgets.pyd opengl32sw.dll
















pushd translations
:: Remove translations for unused/removed components

del assistant* designer* linguist* qtdeclarative*

:: leave translations




popd

pushd plugins
del /Q /S tls

:: leave plugins










popd

:: leave PySide6
popd


del shiboken6\shiboken6*.lib
del email\architecture.rst







:: leave lib
popd
::leave build directory
popd


Changes to scripts/compile_resources.py.

70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def split_iterable(iterable: typing.Iterable[T], chunk_size: int, /) -> typing.List[typing.Tuple[T, ...]]:
    """Split the given iterable into chunks of size chunk_size. Does not add padding values to the last item."""
    iterable = iter(iterable)
    return list(iter(lambda: tuple(itertools.islice(iterable, chunk_size)), ()))


def compile():
    command = ("pyrcc5", "-compress", "9", str(SOURCES_PATH))  # noqa  # "pyrcc5" is a program name, not a typo
    compiled = subprocess.check_output(command, universal_newlines=True)  # type: str
    # The resource compiler outputs > 15000 lines with extremely low line length.
    # Reduce the file size by removing a good percentage of those line breaks
    blocks = compiled.split("\\\n")
    chunks = split_iterable(blocks, 7)
    joined_chunks = ("".join(items) for items in chunks)
    compiled = "\\\n".join(joined_chunks)
    TARGET_PATH.write_text(compiled, "utf-8")


def clean():
    TARGET_PATH.unlink(missing_ok=True)


def main():
    args = parse_args()







|








<







70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

86
87
88
89
90
91
92
def split_iterable(iterable: typing.Iterable[T], chunk_size: int, /) -> typing.List[typing.Tuple[T, ...]]:
    """Split the given iterable into chunks of size chunk_size. Does not add padding values to the last item."""
    iterable = iter(iterable)
    return list(iter(lambda: tuple(itertools.islice(iterable, chunk_size)), ()))


def compile():
    command = ("pyside6-rcc", "--compress", "9", "--generator", "python", str(SOURCES_PATH))
    compiled = subprocess.check_output(command, universal_newlines=True)  # type: str
    # The resource compiler outputs > 15000 lines with extremely low line length.
    # Reduce the file size by removing a good percentage of those line breaks
    blocks = compiled.split("\\\n")
    chunks = split_iterable(blocks, 7)
    joined_chunks = ("".join(items) for items in chunks)
    compiled = "\\\n".join(joined_chunks)
    TARGET_PATH.write_text(compiled, "utf-8")


def clean():
    TARGET_PATH.unlink(missing_ok=True)


def main():
    args = parse_args()

Changes to scripts/compile_ui_files.py.

23
24
25
26
27
28
29
30
31
32
33
34

35
36
37
38
39
40
41
42
43
44
45
  like Python wheels and application bundles created via cx_Freeze.
- Creation of type hinting stubs with suffix ".pyi". These are used during development,
  to provide type hinting and autocompletion for the Ui classes defined by the UI files.
"""

import argparse
import ast
import io
import itertools
import textwrap
from pathlib import Path
import shutil

from typing import Tuple, NamedTuple, TypeVar, Iterable, Union, Type, List, Any, Dict, Set

import PyQt5.uic

SOURCE_ROOT = Path(__file__).parent.parent  # Checkout root directory
MAIN_PACKAGE = SOURCE_ROOT / "mtg_proxy_printer"
UI_SOURCE_PATH = MAIN_PACKAGE / "resources/ui"  # UI files live here
TARGET_PATH = MAIN_PACKAGE / "ui/generated"  # Package containing generated modules/type hinting stubs
T = TypeVar("T")
ClassRegistry = Dict[str, ast.ImportFrom]
UsedClasses = Set[str]







<




>


<
<







23
24
25
26
27
28
29

30
31
32
33
34
35
36


37
38
39
40
41
42
43
  like Python wheels and application bundles created via cx_Freeze.
- Creation of type hinting stubs with suffix ".pyi". These are used during development,
  to provide type hinting and autocompletion for the Ui classes defined by the UI files.
"""

import argparse
import ast

import itertools
import textwrap
from pathlib import Path
import shutil
import subprocess
from typing import Tuple, NamedTuple, TypeVar, Iterable, Union, Type, List, Any, Dict, Set



SOURCE_ROOT = Path(__file__).parent.parent  # Checkout root directory
MAIN_PACKAGE = SOURCE_ROOT / "mtg_proxy_printer"
UI_SOURCE_PATH = MAIN_PACKAGE / "resources/ui"  # UI files live here
TARGET_PATH = MAIN_PACKAGE / "ui/generated"  # Package containing generated modules/type hinting stubs
T = TypeVar("T")
ClassRegistry = Dict[str, ast.ImportFrom]
UsedClasses = Set[str]
74
75
76
77
78
79
80






81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148

149
150
151
152
153
154
155
156
157
158
    args = parser.parse_args()
    return args


def type_filter(any_: Iterable[Any], types: Union[Type[T], Tuple[Type[T], ...]]) -> Iterable[T]:
    return filter(lambda x: isinstance(x, types), any_)








def compile_ui_files(args: Namespace, target_path: Path = TARGET_PATH, source_path: Path = UI_SOURCE_PATH):
    """
    Compiles all UI files found in source_path to Python types, storing results in target_path.

    Recursively finds UI files under source_path, replicates the found directory tree as a Python package hierarchy and
    populates it with the compiled Ui types.
    """
    if args.purge_existing and target_path.is_dir():
        shutil.rmtree(target_path)

    source_path = source_path.resolve()
    target_path.mkdir(exist_ok=True)

    def map_to_output(directory, file_name):
        dir_path = Path(directory).relative_to(source_path)
        return target_path/dir_path, file_name
    import functools
    PyQt5.uic.open = functools.partial(open, encoding="utf-8")
    PyQt5.uic.compileUiDir(str(source_path), recurse=True, map=map_to_output)
    create_python_package(target_path)


def create_python_package(target_dir: Path):
    """
    Creates an empty __init__.py file in target_dir and each subdirectory, recursively.
    This marks these directories as proper Python packages.
    """
    (target_dir/"__init__.py").touch(exist_ok=True)
    for entry in target_dir.rglob("*"):
        if entry.is_dir():
            (entry/"__init__.py").touch(exist_ok=True)


def create_ui_type_stubs(args: Namespace, target_path: Path = TARGET_PATH, source_path: Path = UI_SOURCE_PATH):
    """
    Creates type hinting stubs for all UI files found in source_path, storing results in target_path.

    Recursively finds UI files under source_path, replicates the found directory tree as a Python package hierarchy and
    populates it with the created type hints.
    """
    if args.purge_existing and target_path.is_dir():
        shutil.rmtree(target_path)
    class_registry = build_class_registry(MAIN_PACKAGE)

    for ui_file in source_path.rglob("*.ui"):
        compiled = compile_ui_file(ui_file)
        stub = generate_stub(compiled, ui_file, class_registry)
        parent_dir = (target_path/ui_file.relative_to(source_path)).parent
        parent_dir.mkdir(exist_ok=True)
        (parent_dir/f"{ui_file.stem}.pyi").write_text(stub, "utf-8")
    create_python_package(target_path)


def build_class_registry(package_path: Path) -> ClassRegistry:
    """Scan the source tree for classes and build a dict from class name to import path"""
    result: ClassRegistry = {}
    for py_file in package_path.rglob("*.py"):
        module_path = ".".join((py_file.parent.relative_to(package_path.parent) / py_file.stem).parts)
        root_node = ast.parse(py_file.read_text("utf-8"), py_file)
        for class_def in type_filter(root_node.body, ast.ClassDef):
            result[class_def.name] = ast.ImportFrom(module_path, [ast.alias(class_def.name)])
    return result


def compile_ui_file(path: Path) -> str:
    buffer = io.StringIO()
    try:
        PyQt5.uic.compileUi(path, buffer, from_imports=True)

    except Exception as e:
        raise RuntimeError(f"Compilation failed for file {path}") from e
    return buffer.getvalue()


def generate_stub(compiled_ui: str, ui_file: Path, class_registry: ClassRegistry) -> str:
    root_node = ast.parse(compiled_ui)
    header = f"# Automatically generated type hinting stub for '{ui_file.name}'. Do not modify."
    # Keep all imports unmodified
    imports = "import typing\n\n"







>
>
>
>
>
>










|
|
<
|
<
|
<
<
<
<
|
|
<
<
<
<
<
<
<
<
<
<












>




|

<














<

<
>


|







72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

97

98




99
100










101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119

120
121
122
123
124
125
126
127
128
129
130
131
132
133

134

135
136
137
138
139
140
141
142
143
144
145
    args = parser.parse_args()
    return args


def type_filter(any_: Iterable[Any], types: Union[Type[T], Tuple[Type[T], ...]]) -> Iterable[T]:
    return filter(lambda x: isinstance(x, types), any_)


def create_python_package(location: Path, /):
    """Creates an empty Python package at the given Path"""
    location.mkdir(parents=True, exist_ok=True)
    (location/"__init__.py").touch(exist_ok=True)


def compile_ui_files(args: Namespace, target_path: Path = TARGET_PATH, source_path: Path = UI_SOURCE_PATH):
    """
    Compiles all UI files found in source_path to Python types, storing results in target_path.

    Recursively finds UI files under source_path, replicates the found directory tree as a Python package hierarchy and
    populates it with the compiled Ui types.
    """
    if args.purge_existing and target_path.is_dir():
        shutil.rmtree(target_path)
    create_python_package(target_path)
    for ui_file in source_path.rglob("*.ui"):

        compiled = compile_ui_file(ui_file)

        parent_dir = (target_path/ui_file.relative_to(source_path)).parent




        create_python_package(parent_dir)
        (parent_dir/f"{ui_file.stem}.py").write_text(compiled, "utf-8")












def create_ui_type_stubs(args: Namespace, target_path: Path = TARGET_PATH, source_path: Path = UI_SOURCE_PATH):
    """
    Creates type hinting stubs for all UI files found in source_path, storing results in target_path.

    Recursively finds UI files under source_path, replicates the found directory tree as a Python package hierarchy and
    populates it with the created type hints.
    """
    if args.purge_existing and target_path.is_dir():
        shutil.rmtree(target_path)
    class_registry = build_class_registry(MAIN_PACKAGE)
    create_python_package(target_path)
    for ui_file in source_path.rglob("*.ui"):
        compiled = compile_ui_file(ui_file)
        stub = generate_stub(compiled, ui_file, class_registry)
        parent_dir = (target_path/ui_file.relative_to(source_path)).parent
        create_python_package(parent_dir)
        (parent_dir/f"{ui_file.stem}.pyi").write_text(stub, "utf-8")



def build_class_registry(package_path: Path) -> ClassRegistry:
    """Scan the source tree for classes and build a dict from class name to import path"""
    result: ClassRegistry = {}
    for py_file in package_path.rglob("*.py"):
        module_path = ".".join((py_file.parent.relative_to(package_path.parent) / py_file.stem).parts)
        root_node = ast.parse(py_file.read_text("utf-8"), py_file)
        for class_def in type_filter(root_node.body, ast.ClassDef):
            result[class_def.name] = ast.ImportFrom(module_path, [ast.alias(class_def.name)])
    return result


def compile_ui_file(path: Path) -> str:

    try:

        command = ("pyside6-uic", "--generator", "python", str(path))
    except Exception as e:
        raise RuntimeError(f"Compilation failed for file {path}") from e
    return subprocess.check_output(command, encoding="utf-8")


def generate_stub(compiled_ui: str, ui_file: Path, class_registry: ClassRegistry) -> str:
    root_node = ast.parse(compiled_ui)
    header = f"# Automatically generated type hinting stub for '{ui_file.name}'. Do not modify."
    # Keep all imports unmodified
    imports = "import typing\n\n"
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
    return f"class {class_root.name}({base_classes}):"


def get_assignments(function_body: List[ast.stmt]) -> List[Assignment]:
    return [
        Assignment(
            assignment.targets[0].attr,
            get_assignment_type(assignment)
        )
        for assignment
        in type_filter(function_body, ast.Assign)
        if hasattr(assignment.targets[0], "attr")  # Filter out local variables
    ]


def get_assignment_type(assignment: ast.Assign):
    func = assignment.value.func
    if isinstance(func, ast.Attribute):
        return f"{func.value.id}.{func.attr}"  # Qualified name: module.ClassName()
    elif isinstance(func, ast.Name):
        return func.id
    raise NotImplementedError("Unknown assignment type")


def get_function_stub(function_body: ast.FunctionDef, found_class_uses: UsedClasses):
    for index, arg in enumerate(function_body.args.args):
        if arg.arg == "self":
            continue
        found_class_uses.add(arg.arg)
        arg.annotation = ast.Constant(arg.arg)







|






<
<
<
<
<
<
<
<
<







200
201
202
203
204
205
206
207
208
209
210
211
212
213









214
215
216
217
218
219
220
    return f"class {class_root.name}({base_classes}):"


def get_assignments(function_body: List[ast.stmt]) -> List[Assignment]:
    return [
        Assignment(
            assignment.targets[0].attr,
            assignment.value.func.id
        )
        for assignment
        in type_filter(function_body, ast.Assign)
        if hasattr(assignment.targets[0], "attr")  # Filter out local variables
    ]











def get_function_stub(function_body: ast.FunctionDef, found_class_uses: UsedClasses):
    for index, arg in enumerate(function_body.args.args):
        if arg.arg == "self":
            continue
        found_class_uses.add(arg.arg)
        arg.annotation = ast.Constant(arg.arg)

Changes to scripts/update_translations.py.

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31


"""
Management script for application translations
"""

import argparse
import itertools
import json
import math
import pathlib
import re
import subprocess
import sys
from typing import Callable, Dict, NamedTuple







<







17
18
19
20
21
22
23

24
25
26
27
28
29
30


"""
Management script for application translations
"""

import argparse

import json
import math
import pathlib
import re
import subprocess
import sys
from typing import Callable, Dict, NamedTuple
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
        subprocess.check_output("crowdin")
    except FileNotFoundError as e:
        raise RuntimeError("The required Crowdin CLI client is not installed in the PATH, exiting.") from e


def register_new_raw_strings():
    TRANSLATIONS_DIR.mkdir(parents=True, exist_ok=True)
    # PyQt5
    package = pathlib.Path("mtg_proxy_printer")
    files = list(itertools.chain(package.rglob("*.py"), package.rglob("*.ui")))
    subprocess.call([
        "pylupdate5",
        "-noobsolete", "-verbose",
        *files,
        "-ts", SOURCES_PATH
    ])
    ''' PySide6
    subprocess.call([
        "pyside6-lupdate",
        "-source-language", "en_US",
        "-recursive", "-no-obsolete",
        "-extensions", "py,ui",
        "mtg_proxy_printer",
        "-ts", SOURCES_PATH
    ])
    '''


def upload_raw_strings(args: Namespace):
    """Updates the sources .ts from code, then uploads it to the Crowdin API"""
    register_new_raw_strings()
    verify_crowdin_cli_present()
    subprocess.call([







<
<
<
<
<
<
<
<
<
<








<







92
93
94
95
96
97
98










99
100
101
102
103
104
105
106

107
108
109
110
111
112
113
        subprocess.check_output("crowdin")
    except FileNotFoundError as e:
        raise RuntimeError("The required Crowdin CLI client is not installed in the PATH, exiting.") from e


def register_new_raw_strings():
    TRANSLATIONS_DIR.mkdir(parents=True, exist_ok=True)










    subprocess.call([
        "pyside6-lupdate",
        "-source-language", "en_US",
        "-recursive", "-no-obsolete",
        "-extensions", "py,ui",
        "mtg_proxy_printer",
        "-ts", SOURCES_PATH
    ])



def upload_raw_strings(args: Namespace):
    """Updates the sources .ts from code, then uploads it to the Crowdin API"""
    register_new_raw_strings()
    verify_crowdin_cli_present()
    subprocess.call([
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
    try:
        subprocess.check_output(["lrelease", "-version"])
    except FileNotFoundError:
        print("lrelease not found on PATH. Falling back to the executable supplied by PySide2.")
        import sys
        exe = pathlib.Path(sys.executable)
        venv = exe.parent.parent
        lrelease5 = venv / "Lib" / "site-packages" / "PySide2" / "lrelease.exe"
        lrelease6 = venv / "Lib" / "site-packages" / "PySide6" / "lrelease.exe"
        if not lrelease5.is_file() and not lrelease6.is_file():
            raise RuntimeError("No fallback lrelease executable found")
        return lrelease5 if lrelease5.is_file() else lrelease6
    else:
        return "lrelease"


def compile_translations(args: Namespace):
    """Compiles .ts files into importable, binary translation files with .qm suffix."""
    lrelease = get_lrelease()







<

|

|







149
150
151
152
153
154
155

156
157
158
159
160
161
162
163
164
165
166
    try:
        subprocess.check_output(["lrelease", "-version"])
    except FileNotFoundError:
        print("lrelease not found on PATH. Falling back to the executable supplied by PySide2.")
        import sys
        exe = pathlib.Path(sys.executable)
        venv = exe.parent.parent

        lrelease6 = venv / "Lib" / "site-packages" / "PySide6" / "lrelease.exe"
        if not lrelease6.is_file():
            raise RuntimeError("No fallback lrelease executable found")
        return lrelease6
    else:
        return "lrelease"


def compile_translations(args: Namespace):
    """Compiles .ts files into importable, binary translation files with .qm suffix."""
    lrelease = get_lrelease()

Changes to setup_cx_freeze.py.

40
41
42
43
44
45
46


47
48
49
50


51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100


base = "Win32GUI" if sys.platform == "win32" else None

excludes  = [
    f"{main_package}.resources",  # Do not include the raw resources as individual files
    "distutils",


    "lib2to3",
    "pep517",
    "pytest",
    "pydoc_data",


    "tkinter",
    "toml",
    "sqlite3.test",  # Ignore the internal test suite
    "pint.testsuite",  # Ignore the internal test suite
    "ijson.benchmark",  # Ignore the benchmark script added after ijson 3.2.3
    "importlib_metadata",

    # All unused PyQt components
    "PyQt5.QtXmlPatterns",
    "PyQt5.QtNfc",
    "PyQt5.QtQml",
    "PyQt5.QtSql",
    "PyQt5.Qt3DAnimation",
    "PyQt5.Qt3DCore",
    "PyQt5.Qt3DExtras",
    "PyQt5.Qt3DInput",
    "PyQt5.Qt3DLogic",
    "PyQt5.Qt3DRender",
    "PyQt5.QtBluetooth",
    "PyQt5.QtChart",
    "PyQt5.QtDataVisualisation",
    "PyQt5.QtLocation",
    "PyQt5.QtMultimedia",
    "PyQt5.QtMultimediaWidgets",
    "PyQt5.QtNetwork",
    "PyQt5.QtNetworkAuth",
    "PyQt5.QtOpenGL",
    "PyQt5.QtPositioning",
    "PyQt5.QtPurchasing",
    "PyQt5.QtQuick",
    "PyQt5.QtQuickWidgets",
    "PyQt5.QtRemoteObjects",
    "PyQt5.QtSensors",

    "PyQt5.QtSerialPort",
    "PyQt5.QtTest",
    "PyQt5.QtWebChannel",
    "PyQt5.QtWebEngine",
    "PyQt5.QtWebEngineCore",
    "PyQt5.QtWebEngineWidgets",
    "PyQt5.QtWebKit",
    "PyQt5.QtWebKitWidgets",
    "PyQt5.QtWebSockets",
    "PyQt5.uic.port_v2",
]

if sys.platform == "win32":
    excludes += [
        "platformdirs.android",
        "platformdirs.macos",
        "platformdirs.unix",







>
>


|

>
>


<
<
|
<
|
|
<
|
|
|
<
|
<
|
|
|
<
|
<
<
|
<
|
<
<
|
|
|
<
<
|
>
|
|
|
|
|
|
|
<
|
<







40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56


57

58
59

60
61
62

63

64
65
66

67


68

69


70
71
72


73
74
75
76
77
78
79
80
81

82

83
84
85
86
87
88
89


base = "Win32GUI" if sys.platform == "win32" else None

excludes  = [
    f"{main_package}.resources",  # Do not include the raw resources as individual files
    "distutils",
    "ijson.benchmark",  # Ignore the benchmark script added after ijson 3.2.3
    "importlib_metadata",
    "lib2to3",
    "pep517",
    "pint.testsuite",  # Ignore the internal test suite
    "pydoc_data",
    "pytest",
    "sqlite3.test",  # Ignore the internal test suite
    "tkinter",
    "toml",


    # Empty package with readme and download scripts

    "ctypes.test",
    # Unused PySide6 components

    "PySide6.glue",
    "PySide6.include",
    "PySide6.metatypes",

    "PySide6.plugins.assetimporters",

    "PySide6.plugins.canbus",
    "PySide6.plugins.designer",
    "PySide6.plugins.geometryloaders",

    "PySide6.plugins.geoservices",


    "PySide6.plugins.multimedia",

    "PySide6.plugins.networkinformation",


    "PySide6.plugins.position",
    "PySide6.plugins.qmltooling",
    "PySide6.plugins.scxmldatamodel",


    "PySide6.plugins.sensors",
    "PySide6.plugins.sqldrivers",  # Use Python native sqlite3 module instead
    "PySide6.plugins.tls",
    "PySide6.qml",
    "PySide6.QtAsyncio",
    "PySide6.resources",
    "PySide6.scripts",
    "PySide6.support",
    "PySide6.translations.qtwebengine_locales",

    "PySide6.typesystems",

]

if sys.platform == "win32":
    excludes += [
        "platformdirs.android",
        "platformdirs.macos",
        "platformdirs.unix",

Changes to tests/conftest.py.

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
fixtures defined here are available in all test modules.
"""
import itertools
import sqlite3
import unittest.mock
from pathlib import Path

from PyQt5.QtGui import QColorConstants, QPixmap
import pytest
from hamcrest import assert_that, is_

import mtg_proxy_printer.sqlite_helpers
import mtg_proxy_printer.settings
from mtg_proxy_printer.model.page_layout import PageLayoutSettings
from mtg_proxy_printer.printing_filter_updater import PrintingFilterUpdater







|







19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
fixtures defined here are available in all test modules.
"""
import itertools
import sqlite3
import unittest.mock
from pathlib import Path

from PySide6.QtGui import QColorConstants, QPixmap
import pytest
from hamcrest import assert_that, is_

import mtg_proxy_printer.sqlite_helpers
import mtg_proxy_printer.settings
from mtg_proxy_printer.model.page_layout import PageLayoutSettings
from mtg_proxy_printer.printing_filter_updater import PrintingFilterUpdater
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
    db = mtg_proxy_printer.sqlite_helpers.open_database(":memory:", "document-v7", check_same_thread=False)
    if request.param:
        db.execute("PRAGMA reverse_unordered_selects = TRUE")
    return db


@pytest.fixture
def image_db(tmp_path: Path):
    image_db = ImageDatabase(tmp_path)
    regular = image_db.get_blank(CardSizes.REGULAR)
    large = image_db.get_blank(CardSizes.OVERSIZED)
    for scryfall_id, is_front in itertools.product(
            ["0000579f-7b35-4ed3-b44c-db2a538066fe", "b3b87bfc-f97f-4734-94f6-e3e2f335fc4d"], [True, False]):
        # Regular card images
        key = ImageKey(scryfall_id, is_front, True)
        image_db.loaded_images[key] = regular.copy()
        image_db.images_on_disk.add(key)
    for scryfall_id in ["650722b4-d72b-4745-a1a5-00a34836282b"]:
        # Oversized card images
        key = ImageKey(scryfall_id, True, True)
        image_db.loaded_images[key] = large.copy()
        image_db.images_on_disk.add(key)

    yield image_db
    image_db.__dict__.clear()


@pytest.fixture
def document(qtbot, card_db: CardDatabase, image_db: ImageDatabase) -> Document:
    fill_card_database_with_json_cards(qtbot, card_db, [
        "regular_english_card", "oversized_card", "english_double_faced_card"])
    document = Document(card_db, image_db)
    document.loader.db = card_db.db
    yield document
    document.__dict__.clear()

@pytest.fixture
def mock_imagedb():
    mock_image_db = unittest.mock.NonCallableMagicMock(spec=ImageDatabase)
    blanks = {
        CardSizes.REGULAR: QPixmap(CardSizes.REGULAR.as_qsize_px()),
        CardSizes.OVERSIZED: QPixmap(CardSizes.OVERSIZED.as_qsize_px()),







|
















<









<







56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

80
81
82
83
84
85
86
87
88

89
90
91
92
93
94
95
    db = mtg_proxy_printer.sqlite_helpers.open_database(":memory:", "document-v7", check_same_thread=False)
    if request.param:
        db.execute("PRAGMA reverse_unordered_selects = TRUE")
    return db


@pytest.fixture
def image_db(qtbot, tmp_path: Path):
    image_db = ImageDatabase(tmp_path)
    regular = image_db.get_blank(CardSizes.REGULAR)
    large = image_db.get_blank(CardSizes.OVERSIZED)
    for scryfall_id, is_front in itertools.product(
            ["0000579f-7b35-4ed3-b44c-db2a538066fe", "b3b87bfc-f97f-4734-94f6-e3e2f335fc4d"], [True, False]):
        # Regular card images
        key = ImageKey(scryfall_id, is_front, True)
        image_db.loaded_images[key] = regular.copy()
        image_db.images_on_disk.add(key)
    for scryfall_id in ["650722b4-d72b-4745-a1a5-00a34836282b"]:
        # Oversized card images
        key = ImageKey(scryfall_id, True, True)
        image_db.loaded_images[key] = large.copy()
        image_db.images_on_disk.add(key)

    yield image_db



@pytest.fixture
def document(qtbot, card_db: CardDatabase, image_db: ImageDatabase) -> Document:
    fill_card_database_with_json_cards(qtbot, card_db, [
        "regular_english_card", "oversized_card", "english_double_faced_card"])
    document = Document(card_db, image_db)
    document.loader.db = card_db.db
    yield document


@pytest.fixture
def mock_imagedb():
    mock_image_db = unittest.mock.NonCallableMagicMock(spec=ImageDatabase)
    blanks = {
        CardSizes.REGULAR: QPixmap(CardSizes.REGULAR.as_qsize_px()),
        CardSizes.OVERSIZED: QPixmap(CardSizes.OVERSIZED.as_qsize_px()),
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def document_light(qtbot, mock_imagedb) -> Document:
    mock_card_db = unittest.mock.NonCallableMagicMock()
    mock_card_db.db = mtg_proxy_printer.sqlite_helpers.create_in_memory_database(
        "carddb", check_same_thread=False)
    document = Document(mock_card_db, mock_imagedb)
    document.loader.db = mock_card_db.db
    yield document
    document.__dict__.clear()


@pytest.fixture
def page_layout() -> PageLayoutSettings:
    layout = PageLayoutSettings.create_from_settings()
    defaults = PageLayoutSettings.create_from_settings(mtg_proxy_printer.settings.DEFAULT_SETTINGS)
    assert_that(layout, is_dataclass_equal_to(defaults))
    return layout







<








103
104
105
106
107
108
109

110
111
112
113
114
115
116
117
def document_light(qtbot, mock_imagedb) -> Document:
    mock_card_db = unittest.mock.NonCallableMagicMock()
    mock_card_db.db = mtg_proxy_printer.sqlite_helpers.create_in_memory_database(
        "carddb", check_same_thread=False)
    document = Document(mock_card_db, mock_imagedb)
    document.loader.db = mock_card_db.db
    yield document



@pytest.fixture
def page_layout() -> PageLayoutSettings:
    layout = PageLayoutSettings.create_from_settings()
    defaults = PageLayoutSettings.create_from_settings(mtg_proxy_printer.settings.DEFAULT_SETTINGS)
    assert_that(layout, is_dataclass_equal_to(defaults))
    return layout

Changes to tests/document_controller/helpers.py.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29


"""
This module contains an assortment of small helper functions used in the tests for the document controller
"""
import itertools

from PyQt5.QtGui import QPixmap

import hamcrest.core.base_matcher
from hamcrest import has_properties, same_instance, all_of, instance_of, assert_that, is_, equal_to, has_property

from mtg_proxy_printer.model.card import MTGSet, Card, AnyCardType
from mtg_proxy_printer.model.document_page import CardContainer, Page
from mtg_proxy_printer.units_and_sizes import CardSizes, CardSize







|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29


"""
This module contains an assortment of small helper functions used in the tests for the document controller
"""
import itertools

from PySide6.QtGui import QPixmap

import hamcrest.core.base_matcher
from hamcrest import has_properties, same_instance, all_of, instance_of, assert_that, is_, equal_to, has_property

from mtg_proxy_printer.model.card import MTGSet, Card, AnyCardType
from mtg_proxy_printer.model.document_page import CardContainer, Page
from mtg_proxy_printer.units_and_sizes import CardSizes, CardSize

Changes to tests/document_controller/test_action_move_cards.py.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29



from functools import partial

import pytest
from hamcrest import *
from PyQt5.QtCore import QModelIndex

from mtg_proxy_printer.units_and_sizes import CardSizes
from mtg_proxy_printer.model.document_page import PageType
from mtg_proxy_printer.document_controller import IllegalStateError
from mtg_proxy_printer.document_controller.page_actions import ActionNewPage
from mtg_proxy_printer.document_controller.move_cards import ActionMoveCards








|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29



from functools import partial

import pytest
from hamcrest import *
from PySide6.QtCore import QModelIndex

from mtg_proxy_printer.units_and_sizes import CardSizes
from mtg_proxy_printer.model.document_page import PageType
from mtg_proxy_printer.document_controller import IllegalStateError
from mtg_proxy_printer.document_controller.page_actions import ActionNewPage
from mtg_proxy_printer.document_controller.move_cards import ActionMoveCards

Changes to tests/document_controller/test_action_remove_page.py.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from functools import partial

from hamcrest import *
from PyQt5.QtCore import QModelIndex

from mtg_proxy_printer.model.document_page import Page
from mtg_proxy_printer.document_controller import IllegalStateError
from mtg_proxy_printer.document_controller.page_actions import ActionRemovePage

from .helpers import append_new_pages, append_new_card_in_page, card_container_with, verify_page_index_cache_is_valid








|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from functools import partial

from hamcrest import *
from PySide6.QtCore import QModelIndex

from mtg_proxy_printer.model.document_page import Page
from mtg_proxy_printer.document_controller import IllegalStateError
from mtg_proxy_printer.document_controller.page_actions import ActionRemovePage

from .helpers import append_new_pages, append_new_card_in_page, card_container_with, verify_page_index_cache_is_valid

Changes to tests/document_controller/test_action_save_document.py.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import copy
import dataclasses
from pathlib import Path
import textwrap

from hamcrest import *
from PyQt5.QtCore import QModelIndex, Qt
import pytest
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.model.page_layout import PageLayoutSettings
from mtg_proxy_printer.sqlite_helpers import open_database, create_in_memory_database
from mtg_proxy_printer.units_and_sizes import unit_registry, UnitT
from mtg_proxy_printer.model.card import CheckCard







|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import copy
import dataclasses
from pathlib import Path
import textwrap

from hamcrest import *
from PySide6.QtCore import QModelIndex, Qt
import pytest
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.model.page_layout import PageLayoutSettings
from mtg_proxy_printer.sqlite_helpers import open_database, create_in_memory_database
from mtg_proxy_printer.units_and_sizes import unit_registry, UnitT
from mtg_proxy_printer.model.card import CheckCard

Changes to tests/model/test_card.py.

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.
import copy

import pytest
from PyQt5.QtCore import QBuffer, QIODevice
from PyQt5.QtGui import QPixmap, QColorConstants, QColor
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.model.card import Card, MTGSet, CardCorner, CustomCard, CheckCard
from mtg_proxy_printer.units_and_sizes import CardSize, CardSizes, PageType, UUID

from hamcrest import *








|
|







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.
import copy

import pytest
from PySide6.QtCore import QBuffer, QIODevice
from PySide6.QtGui import QPixmap, QColorConstants, QColor
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.model.card import Card, MTGSet, CardCorner, CustomCard, CheckCard
from mtg_proxy_printer.units_and_sizes import CardSize, CardSizes, PageType, UUID

from hamcrest import *

Changes to tests/model/test_card_list.py.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

from collections import Counter
import typing

from hamcrest import *
import pytest
from pytestqt.qtbot import QtBot
from PyQt5.QtCore import QItemSelectionModel

from mtg_proxy_printer.model.carddb import CardDatabase, CardIdentificationData
from mtg_proxy_printer.model.card_list import CardListModel, CardListModelRow, CardListColumns
from mtg_proxy_printer.model.document import Document

from tests.helpers import fill_card_database_with_json_cards








|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

from collections import Counter
import typing

from hamcrest import *
import pytest
from pytestqt.qtbot import QtBot
from PySide6.QtCore import QItemSelectionModel

from mtg_proxy_printer.model.carddb import CardDatabase, CardIdentificationData
from mtg_proxy_printer.model.card_list import CardListModel, CardListModelRow, CardListColumns
from mtg_proxy_printer.model.document import Document

from tests.helpers import fill_card_database_with_json_cards

Changes to tests/model/test_carddb.py.

197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
            "regular_english_card",
            "english_double_faced_card",
            "english_double_faced_art_series_card",
            "Flowerfoot_Swordmaster_card",
            "Flowerfoot_Swordmaster_token",
        ],
    )
    yield card_db
    card_db.__dict__.clear()


def generate_test_cases_for_test_translate_card_name():
    """Yields tuples with card data, target language and expected result."""
    # Same-language identity translation
    yield CardIdentificationData("en", "Forest"), "en", "Forest"
    yield CardIdentificationData("de", "Wald"), "de", "Wald"







|
<







197
198
199
200
201
202
203
204

205
206
207
208
209
210
211
            "regular_english_card",
            "english_double_faced_card",
            "english_double_faced_art_series_card",
            "Flowerfoot_Swordmaster_card",
            "Flowerfoot_Swordmaster_token",
        ],
    )
    return card_db



def generate_test_cases_for_test_translate_card_name():
    """Yields tuples with card data, target language and expected result."""
    # Same-language identity translation
    yield CardIdentificationData("en", "Forest"), "en", "Forest"
    yield CardIdentificationData("de", "Wald"), "de", "Wald"

Changes to tests/model/test_document.py.

14
15
16
17
18
19
20
21
22

23
24
25
26
27
28
29
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import copy
import typing
import unittest.mock

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap

from hamcrest import *

from hamcrest import contains_exactly
import pytest
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.units_and_sizes import PageType, unit_registry, UnitT, CardSizes, CardSize







|
|
>







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import copy
import typing
import unittest.mock

from PySide6.QtCore import Qt
from PySide6.QtGui import QPixmap

from hamcrest import *

from hamcrest import contains_exactly
import pytest
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.units_and_sizes import PageType, unit_registry, UnitT, CardSizes, CardSize
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
    custom_layout = PageLayoutSettings(
        page_height=300*mm, page_width=200*mm,
        margin_top=20*mm, margin_bottom=19*mm, margin_left=18*mm, margin_right=17*mm,
        row_spacing=3*mm, column_spacing=2*mm, card_bleed=1*mm,
        draw_cut_markers=True, draw_sharp_corners=False,
    )
    document.apply(ActionEditDocumentSettings(custom_layout))
    yield document
    document.__dict__.clear()


def test_document_reset_clears_modified_page_layout(qtbot: QtBot, page_layout: PageLayoutSettings, document_custom_layout: Document):
    assert_that(
        document_custom_layout,
        has_property("page_layout", not_(equal_to(page_layout)))
    )







|
<







349
350
351
352
353
354
355
356

357
358
359
360
361
362
363
    custom_layout = PageLayoutSettings(
        page_height=300*mm, page_width=200*mm,
        margin_top=20*mm, margin_bottom=19*mm, margin_left=18*mm, margin_right=17*mm,
        row_spacing=3*mm, column_spacing=2*mm, card_bleed=1*mm,
        draw_cut_markers=True, draw_sharp_corners=False,
    )
    document.apply(ActionEditDocumentSettings(custom_layout))
    return document



def test_document_reset_clears_modified_page_layout(qtbot: QtBot, page_layout: PageLayoutSettings, document_custom_layout: Document):
    assert_that(
        document_custom_layout,
        has_property("page_layout", not_(equal_to(page_layout)))
    )

Changes to tests/model/test_image_db.py.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import io

import pytest
from PyQt5.QtCore import QBuffer, QIODevice
from PyQt5.QtGui import QPixmap
from hamcrest import *
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.units_and_sizes import CardSizes, CardSize
from mtg_proxy_printer.model.imagedb import ImageDatabase
from mtg_proxy_printer.model.imagedb_files import ImageKey








|
|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import io

import pytest
from PySide6.QtCore import QBuffer, QIODevice
from PySide6.QtGui import QPixmap
from hamcrest import *
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.units_and_sizes import CardSizes, CardSize
from mtg_proxy_printer.model.imagedb import ImageDatabase
from mtg_proxy_printer.model.imagedb_files import ImageKey

Changes to tests/test___main__.py.

42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

@pytest.fixture
def main_mocks():
    with patch("mtg_proxy_printer.__main__.mtg_proxy_printer.logger.configure_root_logger") as configure_root_logger, \
            patch.multiple(
            "mtg_proxy_printer.__main__",
            _app=DEFAULT, Application=DEFAULT, handle_ssl_certificates=DEFAULT,
            parse_args=DEFAULT, QTimer=DEFAULT, logger=DEFAULT, QApplication=DEFAULT) as mocks:
        mocks["configure_root_logger"] = configure_root_logger
        yield mocks
    mocks.clear()


def test_main_calls_handle_ssl_certificates(main_mocks):
    mtg_proxy_printer.__main__.main()







|







42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

@pytest.fixture
def main_mocks():
    with patch("mtg_proxy_printer.__main__.mtg_proxy_printer.logger.configure_root_logger") as configure_root_logger, \
            patch.multiple(
            "mtg_proxy_printer.__main__",
            _app=DEFAULT, Application=DEFAULT, handle_ssl_certificates=DEFAULT,
            parse_args=DEFAULT, QTimer=DEFAULT, logger=DEFAULT) as mocks:
        mocks["configure_root_logger"] = configure_root_logger
        yield mocks
    mocks.clear()


def test_main_calls_handle_ssl_certificates(main_mocks):
    mtg_proxy_printer.__main__.main()
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def test_main_configures_logger(main_mocks):
    mtg_proxy_printer.__main__.main()
    main_mocks["configure_root_logger"].assert_called_once()


def test_main_calls_exec_on_application_instance(main_mocks):
    mtg_proxy_printer.__main__.main()
    main_mocks["Application"]().exec_.assert_called_once()


def test_enqueues_startup_tasks_on_regular_launch(main_mocks):
    main_mocks["parse_args"].return_value = Namespace(test_exit_on_launch=False)
    mtg_proxy_printer.__main__.main()
    app = main_mocks["Application"]()
    app.enqueue_startup_tasks.assert_called_once()







|







78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def test_main_configures_logger(main_mocks):
    mtg_proxy_printer.__main__.main()
    main_mocks["configure_root_logger"].assert_called_once()


def test_main_calls_exec_on_application_instance(main_mocks):
    mtg_proxy_printer.__main__.main()
    main_mocks["Application"]().exec.assert_called_once()


def test_enqueues_startup_tasks_on_regular_launch(main_mocks):
    main_mocks["parse_args"].return_value = Namespace(test_exit_on_launch=False)
    mtg_proxy_printer.__main__.main()
    app = main_mocks["Application"]()
    app.enqueue_startup_tasks.assert_called_once()

Changes to tests/test_check_card_rendering.py.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import pytest
from hamcrest import *
from PyQt5.QtGui import QPixmap, QColorConstants

from mtg_proxy_printer.model.card import MTGSet, Card, CheckCard
from mtg_proxy_printer.units_and_sizes import CardSizes


@pytest.fixture
def blank_image(qtbot) -> QPixmap:







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import pytest
from hamcrest import *
from PySide6.QtGui import QPixmap, QColorConstants

from mtg_proxy_printer.model.card import MTGSet, Card, CheckCard
from mtg_proxy_printer.units_and_sizes import CardSizes


@pytest.fixture
def blank_image(qtbot) -> QPixmap:

Changes to tests/test_known_card_image_model.py.

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
"""
Tests the KnownCardImageModel used internally by the CacheCleanupWizard.
"""

import pathlib
import typing

from PyQt5.QtCore import Qt
import pytest
from hamcrest import *

from mtg_proxy_printer.ui.cache_cleanup_wizard import KnownCardImageModel, KnownCardColumns
from mtg_proxy_printer.model.carddb import CardDatabase
from mtg_proxy_printer.model.imagedb import ImageDatabase








|







17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
"""
Tests the KnownCardImageModel used internally by the CacheCleanupWizard.
"""

import pathlib
import typing

from PySide6.QtCore import Qt
import pytest
from hamcrest import *

from mtg_proxy_printer.ui.cache_cleanup_wizard import KnownCardImageModel, KnownCardColumns
from mtg_proxy_printer.model.carddb import CardDatabase
from mtg_proxy_printer.model.imagedb import ImageDatabase

49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    front_image = image_db.db_path/"lowres_front"/"b3"/"b3b87bfc-f97f-4734-94f6-e3e2f335fc4d.png"
    back_image = image_db.db_path/"lowres_back"/"b3"/"b3b87bfc-f97f-4734-94f6-e3e2f335fc4d.png"
    front_image.parent.mkdir(parents=True)
    back_image.parent.mkdir(parents=True)
    image_db.get_blank().save(str(front_image), "PNG")
    image_db.get_blank().save(str(back_image), "PNG")
    yield Environment(card_db, image_db, front_image, back_image)
    image_db.__dict__.clear()


@pytest.mark.parametrize("is_hidden", [True, False])
@pytest.mark.parametrize("is_front", [True, False])
def test_add_row_identifies_low_resolution_images(environment: Environment, is_front: bool, is_hidden: bool):
    model = KnownCardImageModel(environment.card_db)
    card = environment.card_db.get_card_with_scryfall_id("b3b87bfc-f97f-4734-94f6-e3e2f335fc4d", is_front)







<







49
50
51
52
53
54
55

56
57
58
59
60
61
62
    front_image = image_db.db_path/"lowres_front"/"b3"/"b3b87bfc-f97f-4734-94f6-e3e2f335fc4d.png"
    back_image = image_db.db_path/"lowres_back"/"b3"/"b3b87bfc-f97f-4734-94f6-e3e2f335fc4d.png"
    front_image.parent.mkdir(parents=True)
    back_image.parent.mkdir(parents=True)
    image_db.get_blank().save(str(front_image), "PNG")
    image_db.get_blank().save(str(back_image), "PNG")
    yield Environment(card_db, image_db, front_image, back_image)



@pytest.mark.parametrize("is_hidden", [True, False])
@pytest.mark.parametrize("is_front", [True, False])
def test_add_row_identifies_low_resolution_images(environment: Environment, is_front: bool, is_hidden: bool):
    model = KnownCardImageModel(environment.card_db)
    card = environment.card_db.get_card_with_scryfall_id("b3b87bfc-f97f-4734-94f6-e3e2f335fc4d", is_front)

Changes to tests/test_page_layout_settings.py.

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

import mtg_proxy_printer.settings
import mtg_proxy_printer.model.document
import mtg_proxy_printer.model.document_loader
from mtg_proxy_printer.units_and_sizes import PageType, QuantityT, UnitT, unit_registry, StrDict
from mtg_proxy_printer.ui.page_scene import RenderMode

from PyQt5.QtGui import QPageLayout, QPageSize
from PyQt5.QtCore import QMarginsF
import pytest
from hamcrest import *
PageLayoutSettings = mtg_proxy_printer.model.document_loader.PageLayoutSettings

from tests.hasgetter import has_getters
from tests.helpers import quantity_close_to








|
|







21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

import mtg_proxy_printer.settings
import mtg_proxy_printer.model.document
import mtg_proxy_printer.model.document_loader
from mtg_proxy_printer.units_and_sizes import PageType, QuantityT, UnitT, unit_registry, StrDict
from mtg_proxy_printer.ui.page_scene import RenderMode

from PySide6.QtGui import QPageLayout, QPageSize
from PySide6.QtCore import QMarginsF
import pytest
from hamcrest import *
PageLayoutSettings = mtg_proxy_printer.model.document_loader.PageLayoutSettings

from tests.hasgetter import has_getters
from tests.helpers import quantity_close_to

Changes to tests/ui/settings/test_card_filter_widgets.py.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import typing
from unittest.mock import patch

from PyQt5.QtWidgets import QCheckBox
import pytest
from hamcrest import *

from mtg_proxy_printer.units_and_sizes import SectionProxy
import mtg_proxy_printer.settings
from mtg_proxy_printer.ui.printing_filter_widgets import AbstractPrintingFilter, GeneralPrintingFilter, \
    FormatPrintingFilter







|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import typing
from unittest.mock import patch

from PySide6.QtWidgets import QCheckBox
import pytest
from hamcrest import *

from mtg_proxy_printer.units_and_sizes import SectionProxy
import mtg_proxy_printer.settings
from mtg_proxy_printer.ui.printing_filter_widgets import AbstractPrintingFilter, GeneralPrintingFilter, \
    FormatPrintingFilter

Changes to tests/ui/settings/test_initial_page_selection.py.

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from PyQt5.QtCore import QStringListModel

from hamcrest import *
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.ui.settings_window import SettingsWindow









|







10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from PySide6.QtCore import QStringListModel

from hamcrest import *
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.ui.settings_window import SettingsWindow


Changes to tests/ui/test_add_card.py.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

import typing

import pytest
from hamcrest import *
from pytestqt.qtbot import QtBot

from PyQt5.QtCore import Qt, QPoint, QRect, QItemSelectionModel
from PyQt5.QtWidgets import QDialogButtonBox

from mtg_proxy_printer.model.carddb import CardDatabase, CardIdentificationData
from mtg_proxy_printer.ui.add_card import HorizontalAddCardWidget, VerticalAddCardWidget

from tests.helpers import fill_card_database_with_json_card

StringList = typing.List[str]







|
|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

import typing

import pytest
from hamcrest import *
from pytestqt.qtbot import QtBot

from PySide6.QtCore import Qt, QPoint, QRect, QItemSelectionModel
from PySide6.QtWidgets import QDialogButtonBox

from mtg_proxy_printer.model.carddb import CardDatabase, CardIdentificationData
from mtg_proxy_printer.ui.add_card import HorizontalAddCardWidget, VerticalAddCardWidget

from tests.helpers import fill_card_database_with_json_card

StringList = typing.List[str]
43
44
45
46
47
48
49



50

51
52
53
54
55
56

57
    """
    fill_card_database_with_json_card(qtbot, card_db, "english_double_faced_art_series_card")
    expected_card_identification_data = CardIdentificationData(
        "en", "Clearwater Pathway", "aznr", "25"
    )
    qtbot.add_widget(add_card_widget := widget_class())
    add_card_widget.set_card_database(card_db)



    add_card_widget.ui.copies_input.setValue(1)

    add_card_widget.ui.card_name_list.setSelection(QRect(1, 1, 1, 1), ClearAndSelect)
    qtbot.mouseClick(add_card_widget.ui.card_name_list, LeftButton, pos=QPoint(10, 10))
    qtbot.wait(10)
    qtbot.mouseClick(
        add_card_widget.ui.button_box.button(StandardButton.Ok), LeftButton
    )

    assert_that(add_card_widget._read_card_data_from_ui(), is_(equal_to(expected_card_identification_data)))







>
>
>
|
>






>

43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
    """
    fill_card_database_with_json_card(qtbot, card_db, "english_double_faced_art_series_card")
    expected_card_identification_data = CardIdentificationData(
        "en", "Clearwater Pathway", "aznr", "25"
    )
    qtbot.add_widget(add_card_widget := widget_class())
    add_card_widget.set_card_database(card_db)
    add_card_widget.card_name_filter_updated("")  # Populate the card name list
    add_card_widget.ui.card_name_list.setSelection(QRect(1, 1, 1, 1), ClearAndSelect)
    qtbot.mouseClick(add_card_widget.ui.card_name_list, LeftButton, pos=QPoint(10, 10))
    ok_button = add_card_widget.ui.button_box.button(StandardButton.Ok)
    qtbot.mouseClick(ok_button, LeftButton, pos=QPoint(10, 10))
    add_card_widget.ui.card_name_list.setSelection(QRect(1, 1, 1, 1), ClearAndSelect)
    qtbot.mouseClick(add_card_widget.ui.card_name_list, LeftButton, pos=QPoint(10, 10))
    qtbot.wait(10)
    qtbot.mouseClick(
        add_card_widget.ui.button_box.button(StandardButton.Ok), LeftButton
    )
    add_card_widget.ui.copies_input.setValue(1)
    assert_that(add_card_widget._read_card_data_from_ui(), is_(equal_to(expected_card_identification_data)))

Changes to tests/ui/test_card_item.py.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from hamcrest import *
import pytest
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QColorConstants, QPixmap, QImage, QPainter, QColor
from PyQt5.QtWidgets import QGraphicsItem, QGraphicsScene

from mtg_proxy_printer.units_and_sizes import CardSizes
from mtg_proxy_printer.ui.page_scene import CardItem

from tests.document_controller.helpers import append_new_card_in_page
from tests.hasgetter import has_getter








|
|
|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from hamcrest import *
import pytest
from PySide6.QtCore import QSize
from PySide6.QtGui import QColorConstants, QPixmap, QImage, QPainter, QColor
from PySide6.QtWidgets import QGraphicsItem, QGraphicsScene

from mtg_proxy_printer.units_and_sizes import CardSizes
from mtg_proxy_printer.ui.page_scene import CardItem

from tests.document_controller.helpers import append_new_card_in_page
from tests.hasgetter import has_getter

Changes to tests/ui/test_central_widget.py.

11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from pytestqt.qtbot import QtBot
from PyQt5.QtCore import Qt

from hamcrest import *

from mtg_proxy_printer.model.document_page import Page
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard

# Import dynamically used by pytest. Without this, the main_window fixture won’t be found by pytest.
from .test_main_window import main_window  # noqa







|
>







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from pytestqt.qtbot import QtBot
from PySide6.QtCore import Qt

from hamcrest import *

from mtg_proxy_printer.model.document_page import Page
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard

# Import dynamically used by pytest. Without this, the main_window fixture won’t be found by pytest.
from .test_main_window import main_window  # noqa

Changes to tests/ui/test_deck_import_wizard.py.

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import unittest.mock
from unittest.mock import MagicMock

from hamcrest import *
from pytestqt.qtbot import QtBot
import pytest

from PyQt5.QtCore import QStringListModel, Qt, QPoint, QObject
from PyQt5.QtWidgets import QCheckBox, QWizard, QTableView, QComboBox, QLineEdit
from PyQt5.QtTest import QTest

import mtg_proxy_printer.settings
from mtg_proxy_printer.model.carddb import CardDatabase, CardIdentificationData
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.ui.deck_import_wizard import DeckImportWizard
from mtg_proxy_printer.decklist_parser.re_parsers import MTGOnlineParser, MTGArenaParser, \
    GenericRegularExpressionDeckParser







|
|
|







19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import unittest.mock
from unittest.mock import MagicMock

from hamcrest import *
from pytestqt.qtbot import QtBot
import pytest

from PySide6.QtCore import QStringListModel, Qt, QPoint, QObject
from PySide6.QtWidgets import QCheckBox, QWizard, QTableView, QComboBox, QLineEdit
from PySide6.QtTest import QTest

import mtg_proxy_printer.settings
from mtg_proxy_printer.model.carddb import CardDatabase, CardIdentificationData
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.ui.deck_import_wizard import DeckImportWizard
from mtg_proxy_printer.decklist_parser.re_parsers import MTGOnlineParser, MTGArenaParser, \
    GenericRegularExpressionDeckParser

Changes to tests/ui/test_item_delegate.py.

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.

from PyQt5.QtCore import QModelIndex
from PyQt5.QtWidgets import QSpinBox, QWidget, QStyleOptionViewItem
import pytest
from hamcrest import *
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.model.card import MTGSet
from mtg_proxy_printer.ui.item_delegates import BoundedCopiesSpinboxDelegate, SetEditorDelegate








|
|







9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.

from PySide6.QtCore import QModelIndex
from PySide6.QtWidgets import QSpinBox, QWidget, QStyleOptionViewItem
import pytest
from hamcrest import *
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.model.card import MTGSet
from mtg_proxy_printer.ui.item_delegates import BoundedCopiesSpinboxDelegate, SetEditorDelegate

Changes to tests/ui/test_main_window.py.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31


from collections import Counter
import pathlib
import typing
import unittest.mock


from PyQt5.QtCore import QStringListModel, QThreadPool
from PyQt5.QtWidgets import QMessageBox
from pytestqt.qtbot import QtBot
from hamcrest import *
import pytest

import mtg_proxy_printer.http_file
import mtg_proxy_printer.downloader_base
from mtg_proxy_printer.document_controller.save_document import ActionSaveDocument







<
|
|







15
16
17
18
19
20
21

22
23
24
25
26
27
28
29
30


from collections import Counter
import pathlib
import typing
import unittest.mock


from PySide6.QtCore import QStringListModel, QThreadPool
from PySide6.QtWidgets import QMessageBox
from pytestqt.qtbot import QtBot
from hamcrest import *
import pytest

import mtg_proxy_printer.http_file
import mtg_proxy_printer.downloader_base
from mtg_proxy_printer.document_controller.save_document import ActionSaveDocument
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
        cid = CardInfoDownloader(card_db)
        main_window = MainWindow(card_db, cid, document.image_db, document, QStringListModel(["en"]))
        qtbot.add_widget(main_window)
        with qtbot.wait_exposed(main_window, timeout=1000):
            main_window.show()
        yield main_window
        main_window.hide()
        del cid
        main_window.__dict__.clear()


def test_declining_card_data_update_offer_results_in_no_action(qtbot: QtBot, main_window: MainWindow):
    ui = main_window.ui
    ui.action_download_card_data.setEnabled(False)
    with unittest.mock.patch.object(
            mtg_proxy_printer.ui.main_window.QMessageBox, "question", return_value=StandardButton.No), \
        unittest.mock.patch(
            "mtg_proxy_printer.card_info_downloader.DatabaseImportWorker.import_card_data_from_online_api") as import_from_api, \
        unittest.mock.patch.object(QThreadPool.globalInstance(), "start") as thread_pool_start, \
            qtbot.assertNotEmitted(main_window.loading_state_changed):
        main_window.show_card_data_update_available_message_box(10000)
    thread_pool_start.assert_not_called()
    import_from_api.assert_not_called()
    assert_that(ui.action_download_card_data.isEnabled(), is_(True))


def test_accepting_card_data_update_offer_results_in_performed_action(qtbot: QtBot, main_window: MainWindow):
    ui = main_window.ui
    ui.action_download_card_data.setEnabled(True)
    with unittest.mock.patch.object(
        mtg_proxy_printer.ui.main_window.QMessageBox,
            "question", return_value=StandardButton.Yes) as message_box, \
            unittest.mock.patch.object(QThreadPool.globalInstance(), "start") as thread_pool_start:
        main_window.show_card_data_update_available_message_box(10000)
    message_box.assert_called_once()
    thread_pool_start.assert_called_once()
    assert_that(ui.action_download_card_data.isEnabled(), is_(False))


def test_action_download_card_data_is_enabled_after_network_error(qtbot: QtBot, main_window: MainWindow):
    ui = main_window.ui
    ui.action_download_card_data.setEnabled(False)
    with unittest.mock.patch.object(
        mtg_proxy_printer.ui.main_window.QMessageBox, "warning", return_value=StandardButton.Ok







<
<














<












<







62
63
64
65
66
67
68


69
70
71
72
73
74
75
76
77
78
79
80
81
82

83
84
85
86
87
88
89
90
91
92
93
94

95
96
97
98
99
100
101
        cid = CardInfoDownloader(card_db)
        main_window = MainWindow(card_db, cid, document.image_db, document, QStringListModel(["en"]))
        qtbot.add_widget(main_window)
        with qtbot.wait_exposed(main_window, timeout=1000):
            main_window.show()
        yield main_window
        main_window.hide()




def test_declining_card_data_update_offer_results_in_no_action(qtbot: QtBot, main_window: MainWindow):
    ui = main_window.ui
    ui.action_download_card_data.setEnabled(False)
    with unittest.mock.patch.object(
            mtg_proxy_printer.ui.main_window.QMessageBox, "question", return_value=StandardButton.No), \
        unittest.mock.patch(
            "mtg_proxy_printer.card_info_downloader.DatabaseImportWorker.import_card_data_from_online_api") as import_from_api, \
        unittest.mock.patch.object(QThreadPool.globalInstance(), "start") as thread_pool_start, \
            qtbot.assertNotEmitted(main_window.loading_state_changed):
        main_window.show_card_data_update_available_message_box(10000)
    thread_pool_start.assert_not_called()
    import_from_api.assert_not_called()



def test_accepting_card_data_update_offer_results_in_performed_action(qtbot: QtBot, main_window: MainWindow):
    ui = main_window.ui
    ui.action_download_card_data.setEnabled(True)
    with unittest.mock.patch.object(
        mtg_proxy_printer.ui.main_window.QMessageBox,
            "question", return_value=StandardButton.Yes) as message_box, \
            unittest.mock.patch.object(QThreadPool.globalInstance(), "start") as thread_pool_start:
        main_window.show_card_data_update_available_message_box(10000)
    message_box.assert_called_once()
    thread_pool_start.assert_called_once()



def test_action_download_card_data_is_enabled_after_network_error(qtbot: QtBot, main_window: MainWindow):
    ui = main_window.ui
    ui.action_download_card_data.setEnabled(False)
    with unittest.mock.patch.object(
        mtg_proxy_printer.ui.main_window.QMessageBox, "warning", return_value=StandardButton.Ok

Changes to tests/ui/test_page_config_container.py.

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from PyQt5.QtWidgets import QCheckBox, QDoubleSpinBox, QLineEdit

import pytest
from pytestqt.qtbot import QtBot
from hamcrest import *

from mtg_proxy_printer.model.page_layout import PageLayoutSettings
from mtg_proxy_printer.units_and_sizes import QuantityT, unit_registry







|







10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from PySide6.QtWidgets import QCheckBox, QDoubleSpinBox, QLineEdit

import pytest
from pytestqt.qtbot import QtBot
from hamcrest import *

from mtg_proxy_printer.model.page_layout import PageLayoutSettings
from mtg_proxy_printer.units_and_sizes import QuantityT, unit_registry

Changes to tests/ui/test_page_config_dialog.py.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import pytest

from PyQt5.QtWidgets import QDialogButtonBox
from mtg_proxy_printer.ui.dialogs import DocumentSettingsDialog
StandardButton = QDialogButtonBox.StandardButton


def test__init__(qtbot, document_light):
    """Ensure that the dialog can be instantiated"""
    DocumentSettingsDialog(document_light)







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


import pytest

from PySide6.QtWidgets import QDialogButtonBox
from mtg_proxy_printer.ui.dialogs import DocumentSettingsDialog
StandardButton = QDialogButtonBox.StandardButton


def test__init__(qtbot, document_light):
    """Ensure that the dialog can be instantiated"""
    DocumentSettingsDialog(document_light)

Changes to tests/ui/test_page_config_widget.py.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from unittest.mock import patch

import pint
from PyQt5.QtWidgets import QDoubleSpinBox, QCheckBox, QLineEdit

from hamcrest import *
import pytest
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.model.page_layout import PageLayoutSettings
import mtg_proxy_printer.settings







|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from unittest.mock import patch

import pint
from PySide6.QtWidgets import QDoubleSpinBox, QCheckBox, QLineEdit

from hamcrest import *
import pytest
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.model.page_layout import PageLayoutSettings
import mtg_proxy_printer.settings

Changes to tests/ui/test_page_renderer.py.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from unittest.mock import patch

import pytest
from hamcrest import *
from PyQt5.QtCore import QEvent
from PyQt5.QtWidgets import QAction

from mtg_proxy_printer.ui.page_renderer import PageRenderer, ZoomDirection

PATH_PREFIX = "mtg_proxy_printer.ui.page_renderer."


@pytest.fixture







|
|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from unittest.mock import patch

import pytest
from hamcrest import *
from PySide6.QtCore import QEvent
from PySide6.QtGui import QAction

from mtg_proxy_printer.ui.page_renderer import PageRenderer, ZoomDirection

PATH_PREFIX = "mtg_proxy_printer.ui.page_renderer."


@pytest.fixture
51
52
53
54
55
56
57
58


@pytest.mark.parametrize("zoom_action, direction", [
    ("zoom_in_action", ZoomDirection.IN), ("zoom_out_action", ZoomDirection.OUT)])
def test_renderer_zoom_action_triggers_zoom(renderer: PageRenderer, zoom_action: str, direction: ZoomDirection):
    action: QAction = getattr(renderer, zoom_action)
    action.trigger()
    renderer._perform_zoom_step.assert_called_once_with(direction, False)







|
51
52
53
54
55
56
57
58


@pytest.mark.parametrize("zoom_action, direction", [
    ("zoom_in_action", ZoomDirection.IN), ("zoom_out_action", ZoomDirection.OUT)])
def test_renderer_zoom_action_triggers_zoom(renderer: PageRenderer, zoom_action: str, direction: ZoomDirection):
    action: QAction = getattr(renderer, zoom_action)
    action.trigger()
    renderer._perform_zoom_step.assert_called_once_with(direction)

Changes to tests/ui/test_page_scene.py.

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import typing
from unittest.mock import patch
from math import ceil

from hamcrest import *
import pytest

from PyQt5.QtWidgets import QGraphicsPixmapItem, QGraphicsLineItem
from PyQt5.QtGui import QPalette, QColorConstants, QPixmap, QImage, QColor, QPainter
from PyQt5.QtCore import QPoint

from mtg_proxy_printer.units_and_sizes import PageType, CardSizes, CardSize, UnitT, unit_registry, QuantityT
from mtg_proxy_printer.ui.page_scene import RenderMode, PageScene
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard, ActionRemoveCards
from mtg_proxy_printer.document_controller.compact_document import ActionCompactDocument
from mtg_proxy_printer.model.document import Document








|
|
|







20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import typing
from unittest.mock import patch
from math import ceil

from hamcrest import *
import pytest

from PySide6.QtWidgets import QGraphicsPixmapItem, QGraphicsLineItem
from PySide6.QtGui import QPalette, QColorConstants, QPixmap, QImage, QColor, QPainter
from PySide6.QtCore import QPoint

from mtg_proxy_printer.units_and_sizes import PageType, CardSizes, CardSize, UnitT, unit_registry, QuantityT
from mtg_proxy_printer.ui.page_scene import RenderMode, PageScene
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard, ActionRemoveCards
from mtg_proxy_printer.document_controller.compact_document import ActionCompactDocument
from mtg_proxy_printer.model.document import Document

Changes to tests/ui/test_progress_bar.py.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from hamcrest import *
import pytest
from PyQt5.QtWidgets import QWidget
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.ui.progress_bar import ProgressBar
from tests.hasgetter import has_getters


INNER_ELEMENTS = ["inner_progress_bar", "inner_progress_label"]







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.


from hamcrest import *
import pytest
from PySide6.QtWidgets import QWidget
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.ui.progress_bar import ProgressBar
from tests.hasgetter import has_getters


INNER_ELEMENTS = ["inner_progress_bar", "inner_progress_label"]