From 4f3a1e5f9c210f2d2f1ffe9eb18aed77d5ca2ddb Mon Sep 17 00:00:00 2001 From: dynobo Date: Wed, 27 Nov 2024 14:40:29 +0100 Subject: [PATCH 1/2] docs: fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1dd8699c5..f474d6a49 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ uv run python -m normcap Please use [Weblate](https://hosted.weblate.org/projects/normcap/ui/) to complement or correct text for existing language as well as for adding new languages. -(If you prefer to not use Weblate, you can also [do it manually](./normcap/resources/locales/README.md), but be aware, that this more more cumbersome.) +(If you prefer to not use Weblate, you can also [do it manually](./normcap/resources/locales/README.md), but be aware, that this more cumbersome.) ## Credits From 0c96efc8fcc5507caa356f155c87848c92bd9998 Mon Sep 17 00:00:00 2001 From: dynobo Date: Tue, 26 Nov 2024 11:02:52 +0100 Subject: [PATCH 2/2] feat: prepare detection settings for more --- CHANGELOG | 5 +++ docs/usage.md | 1 + normcap/gui/menu_button.py | 47 ++++++++----------------- normcap/gui/models.py | 9 +---- normcap/gui/notification.py | 6 ++-- normcap/gui/settings.py | 28 +++++++++++---- normcap/gui/tray.py | 6 ++-- normcap/gui/window.py | 30 +++++++--------- normcap/ocr/structures.py | 2 +- tests/conftest.py | 6 ++-- tests/integration/test_normcap.py | 2 +- tests/integration/test_settings_menu.py | 2 +- tests/integration/test_tray_menu.py | 2 +- tests/test_app.py | 11 ++++-- tests/test_utils.py | 2 +- tests/tests_gui/test_menu_button.py | 11 +++--- tests/tests_gui/test_notification.py | 6 ++-- tests/tests_gui/test_settings.py | 28 +++++++-------- tests/tests_gui/test_window.py | 31 ---------------- 19 files changed, 101 insertions(+), 134 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 356e88d5e..f7ecc75c9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,11 @@ # Changelog +## v0.6.0 (upcoming) + +- All: Breaking: Commandline argument `--mode {parse,raw}` is removed in favor of a new +argument `--parse-text {True, False}`. + ## v0.5.9 (2024-11-10) - All: Add Chinese translation. Thanks, [@mofazhe](https://github.com/mofazhe)! ([#661](https://github.com/dynobo/normcap/pull/661)) diff --git a/docs/usage.md b/docs/usage.md index 0f31cfa91..ae04758c3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -31,6 +31,7 @@ hide: - The icons or next to the selection-rectangle indicate the active "capture mode" (see below). - To abort a capture or quit NormCap press `` + ## Capture Modes The settings menu allows switching between the two capture modes: "parse" and "raw": diff --git a/normcap/gui/menu_button.py b/normcap/gui/menu_button.py index ecd344214..b106d0ada 100644 --- a/normcap/gui/menu_button.py +++ b/normcap/gui/menu_button.py @@ -171,14 +171,10 @@ def on_item_click(self, action: QtGui.QAction) -> None: return # Menu items which change settings - - if group_name == "settings_group": + if group_name in ["settings_group", "detection_group"]: setting = action_name value = action.isChecked() - elif group_name == "mode_group": - setting = "mode" - value = action_name - elif group_name == "language_group": + if group_name == "language_group": setting = "language" languages = [a.objectName() for a in group.actions() if a.isChecked()] if not languages: @@ -200,8 +196,8 @@ def populate_menu_entries(self) -> None: self._add_settings_section(menu) menu.addSeparator() # L10N: Section title in Main Menu - self._add_title(menu, _("Capture mode")) - self._add_mode_section(menu) + self._add_title(menu, _("Detection")) + self._add_detection_section(menu) menu.addSeparator() # L10N: Section title in Main Menu self._add_title(menu, _("Languages")) @@ -272,37 +268,24 @@ def _add_settings_section(self, menu: QtWidgets.QMenu) -> None: ) menu.addAction(action) - def _add_mode_section(self, menu: QtWidgets.QMenu) -> None: - mode_group = QtGui.QActionGroup(menu) - mode_group.setObjectName("mode_group") - mode_group.setExclusive(True) + def _add_detection_section(self, menu: QtWidgets.QMenu) -> None: + detection_group = QtGui.QActionGroup(menu) + detection_group.setObjectName("detection_group") + detection_group.setExclusive(False) - # L10N: Entry in main menu's 'Capture mode' section - action = QtGui.QAction(_("parse"), mode_group) - action.setObjectName("parse") + # L10N: Entry in main menu's 'Detection' section + action = QtGui.QAction(_("Parse text"), detection_group) + action.setObjectName("parse_text") action.setCheckable(True) - action.setChecked(self.settings.value("mode") == "parse") - # L10N: Tooltip of main menu's 'parse' entry. Use <56 chars p. line. + action.setChecked(bool(self.settings.value("parse-text", type=bool))) + # L10N: Tooltip of main menu's 'parse text' entry. Use <56 chars p. line. action.setToolTip( _( "Tries to determine the text's type (e.g. line,\n" "paragraph, URL, email) and formats the output\n" "accordingly.\n" - "If the result is unexpected, try 'raw' mode instead." - ) - ) - menu.addAction(action) - - # L10N: Entry in main menu's 'Capture mode' section - action = QtGui.QAction(_("raw"), mode_group) - action.setObjectName("raw") - action.setCheckable(True) - action.setChecked(self.settings.value("mode") == "raw") - # L10N: Tooltip of main menu's 'raw' entry. Use <56 chars p. line. - action.setToolTip( - _( - "Returns the text exactly as detected by the Optical\n" - "Character Recognition Software." + "Turn it off to return the text exactly as detected\n" + "by the Optical Character Recognition Software." ) ) menu.addAction(action) diff --git a/normcap/gui/models.py b/normcap/gui/models.py index 7565a3ef4..711dcd71d 100644 --- a/normcap/gui/models.py +++ b/normcap/gui/models.py @@ -40,13 +40,6 @@ class DesktopEnvironment(enum.IntEnum): AWESOME = enum.auto() -class CaptureMode(enum.IntEnum): - """Available transformation modes.""" - - RAW = enum.auto() - PARSE = enum.auto() - - @dataclass class Urls: """URLs used on various places.""" @@ -153,7 +146,7 @@ def scale(self, factor: Optional[float] = None): # noqa: ANN201 class Capture: """Store all information like screenshot and selected region.""" - mode: CaptureMode = CaptureMode.PARSE + parse_text: bool = True # Image of selected region image: QtGui.QImage = field(default_factory=QtGui.QImage) diff --git a/normcap/gui/notification.py b/normcap/gui/notification.py index fe4a542b7..478d7104b 100644 --- a/normcap/gui/notification.py +++ b/normcap/gui/notification.py @@ -13,7 +13,7 @@ from normcap import ocr from normcap.gui import system_info from normcap.gui.localization import _, translate -from normcap.gui.models import Capture, CaptureMode +from normcap.gui.models import Capture logger = logging.getLogger(__name__) @@ -85,7 +85,7 @@ def _compose_notification(capture: Capture) -> tuple[str, str]: title = translate.ngettext( "1 URL captured", "{count} URLs captured", count ).format(count=count) - elif capture.mode == CaptureMode.RAW: + else: count = len(capture.ocr_text) # Count linesep only as single char: count -= (len(os.linesep) - 1) * capture.ocr_text.count(os.linesep) @@ -94,8 +94,6 @@ def _compose_notification(capture: Capture) -> tuple[str, str]: title = translate.ngettext( "1 character captured", "{count} characters captured", count ).format(count=count) - else: - title = "" return title, text diff --git a/normcap/gui/settings.py b/normcap/gui/settings.py index 44e72f60d..4eb0a1d6a 100644 --- a/normcap/gui/settings.py +++ b/normcap/gui/settings.py @@ -41,12 +41,15 @@ def _parse_str_to_bool(string: str) -> bool: nargs="+", ), Setting( - key="mode", - flag="m", - type_=str, - value="parse", - help_="Set capture mode", - choices=("raw", "parse"), + key="parse-text", + flag="p", + type_=_parse_str_to_bool, + value=True, + help_=( + "Try to determine the text's type (e.g. line, paragraph, URL, email) and " + "format the output accordingly." + ), + choices=(True, False), cli_arg=True, nargs=None, ), @@ -145,10 +148,23 @@ def __init__( self._prepare_and_sync() def _prepare_and_sync(self) -> None: + self._migrate_deprecated() self._set_missing_to_default() self._update_from_init_settings() self.sync() + def _migrate_deprecated(self) -> None: + # Migrations to v0.6.0 + # ONHOLD: Delete in 2025/11 + if self.value("mode", None): + mode = self.value("mode") + parse_text = mode == "parse" + self.setValue("parse-text", parse_text) + self.remove("mode") + logger.debug( + "Migrated setting 'mode=%s' to 'parse_text=%s'.", mode, parse_text + ) + def _set_missing_to_default(self) -> None: for d in self.default_settings: key, value = d.key, d.value diff --git a/normcap/gui/tray.py b/normcap/gui/tray.py index f3a8083e2..545092dc8 100644 --- a/normcap/gui/tray.py +++ b/normcap/gui/tray.py @@ -26,7 +26,7 @@ from normcap.gui.language_manager import LanguageManager from normcap.gui.localization import _ from normcap.gui.menu_button import MenuButton -from normcap.gui.models import Capture, CaptureMode, Days, Rect, Screen, Seconds +from normcap.gui.models import Capture, Days, Rect, Screen, Seconds from normcap.gui.notification import Notifier from normcap.gui.settings import Settings from normcap.gui.update_check import UpdateChecker @@ -246,7 +246,7 @@ def _crop_image(self, grab_info: tuple[Rect, int]) -> None: if not screenshot: raise TypeError("Screenshot is None!") - self.capture.mode = CaptureMode[str(self.settings.value("mode")).upper()] + self.capture.parse_text = bool(self.settings.value("parse-text", type=bool)) self.capture.rect = rect self.capture.screen = self.screens[screen_idx] self.capture.image = screenshot.copy(QtCore.QRect(*rect.geometry)) @@ -273,7 +273,7 @@ def _capture_to_ocr(self) -> None: languages=language, image=self.capture.image, tessdata_path=system_info.get_tessdata_path(), - parse=self.capture.mode is CaptureMode.PARSE, + parse=self.capture.parse_text, resize_factor=2, padding_size=80, ) diff --git a/normcap/gui/window.py b/normcap/gui/window.py index c57bc7498..57ff27f2b 100644 --- a/normcap/gui/window.py +++ b/normcap/gui/window.py @@ -17,7 +17,7 @@ from PySide6 import QtCore, QtGui, QtWidgets from normcap.gui import dbus, system_info -from normcap.gui.models import CaptureMode, DesktopEnvironment, Rect, Screen +from normcap.gui.models import DesktopEnvironment, Rect, Screen from normcap.gui.settings import Settings logger = logging.getLogger(__name__) @@ -87,7 +87,9 @@ def _add_image_container(self) -> None: def _add_ui_container(self) -> None: """Add widget for showing selection rectangle and settings button.""" self.ui_container = UiContainerLabel( - parent=self, color=self.color, capture_mode_func=self.get_capture_mode + parent=self, + color=self.color, + parse_text_func=lambda: bool(self.settings.value("parse-text", type=bool)), ) if logger.getEffectiveLevel() is logging.DEBUG: @@ -183,16 +185,6 @@ def clear_selection(self) -> None: self.ui_container.rect = self.selection_rect self.update() - def get_capture_mode(self) -> CaptureMode: - """Read current capture mode from application settings.""" - mode_setting = str(self.settings.value("mode")) - try: - mode = CaptureMode[mode_setting.upper()] - except KeyError: - logger.warning("Unknown capture mode: %s. Fallback to PARSE.", mode_setting) - mode = CaptureMode.PARSE - return mode - def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa: N802 """Handle ESC key pressed. @@ -270,7 +262,7 @@ def __init__( self, parent: QtWidgets.QWidget, color: QtGui.QColor, - capture_mode_func: Callable, + parse_text_func: Callable, ) -> None: super().__init__(parent) @@ -280,7 +272,7 @@ def __init__( self.rect: QtCore.QRect = QtCore.QRect() self.rect_pen = QtGui.QPen(self.color, 2, QtCore.Qt.PenStyle.DashLine) - self.get_capture_mode = capture_mode_func + self.get_parse_text = parse_text_func self.setObjectName("ui_container") self.setStyleSheet(f"#ui_container {{border: 3px solid {self.color.name()};}}") @@ -349,10 +341,12 @@ def paintEvent(self, event: QtGui.QPaintEvent) -> None: # noqa: N802 painter.setPen(self.rect_pen) painter.drawRect(self.rect) - if self.get_capture_mode() is CaptureMode.PARSE: - mode_icon = QtGui.QIcon(":parse") + if self.get_parse_text(): + selection_icon = QtGui.QIcon(":parse") else: - mode_icon = QtGui.QIcon(":raw") - mode_icon.paint(painter, self.rect.right() - 24, self.rect.top() - 30, 24, 24) + selection_icon = QtGui.QIcon(":raw") + selection_icon.paint( + painter, self.rect.right() - 24, self.rect.top() - 30, 24, 24 + ) painter.end() diff --git a/normcap/ocr/structures.py b/normcap/ocr/structures.py index eae45915d..27f20ef06 100644 --- a/normcap/ocr/structures.py +++ b/normcap/ocr/structures.py @@ -123,7 +123,7 @@ def text(self) -> str: """Provides the resulting text of the OCR. If parsed text (compiled by a transformer) is available, return that one, - otherwise fallback to "raw". + otherwise fallback to un-parseds. """ return self.parsed or self.add_linebreaks() diff --git a/tests/conftest.py b/tests/conftest.py index 4727dd36b..319aadbee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,7 @@ from normcap import app from normcap.clipboard import system_info as clipboard_system_info from normcap.gui import menu_button, system_info -from normcap.gui.models import Capture, CaptureMode, Rect +from normcap.gui.models import Capture, Rect from normcap.ocr.structures import OEM, PSM, OcrResult, TessArgs from normcap.ocr.transformers import email, url from normcap.screengrab import system_info as screengrab_system_info @@ -73,7 +73,7 @@ def capture() -> Capture: image.fill(QtGui.QColor("#ff0000")) return Capture( - mode=CaptureMode.PARSE, + parse_text=True, rect=Rect(20, 30, 220, 330), ocr_text="one two three", ocr_transformer=None, @@ -146,7 +146,7 @@ def basic_cli_args(): """NormCap configuration used by most tests.""" return [ sys.argv[0], - "--mode=parse", + "--parse-text=True", "--notification=False", "--verbosity=debug", "--update=False", diff --git a/tests/integration/test_normcap.py b/tests/integration/test_normcap.py index b8c1cdee8..5d7b0f44f 100644 --- a/tests/integration/test_normcap.py +++ b/tests/integration/test_normcap.py @@ -15,7 +15,7 @@ def test_normcap_ocr_testcases( """Tests complete OCR workflow.""" # GIVEN NormCap is started with "language" set to english - # and "parse"-mode + # and --parse-text True (default) # and a certain test image as screenshot monkeypatch.setattr(screengrab, "capture", lambda: [testcase.screenshot]) monkeypatch.setattr(sys, "exit", test_signal.on_event.emit) diff --git a/tests/integration/test_settings_menu.py b/tests/integration/test_settings_menu.py index 37bbc1b2b..963846a38 100644 --- a/tests/integration/test_settings_menu.py +++ b/tests/integration/test_settings_menu.py @@ -29,7 +29,7 @@ def test_settings_menu_creates_actions(monkeypatch, qtbot, run_normcap, test_sig texts = [a.text().lower() for a in actions] assert "show notification" in texts - assert "parse" in texts + assert "parse text" in texts assert "languages" in texts assert "about" in texts assert "close" in texts diff --git a/tests/integration/test_tray_menu.py b/tests/integration/test_tray_menu.py index cd504ff3d..29336560f 100644 --- a/tests/integration/test_tray_menu.py +++ b/tests/integration/test_tray_menu.py @@ -34,7 +34,7 @@ def test_tray_menu_capture(monkeypatch, qtbot, run_normcap, select_region): # GIVEN NormCap is started to tray via "background-mode" # and with a certain test image as screenshot tray = run_normcap( - extra_cli_args=["--language=eng", "--mode=parse", "--background-mode"] + extra_cli_args=["--language=eng", "--parse-text=True", "--background-mode"] ) assert not tray.windows diff --git a/tests/test_app.py b/tests/test_app.py index 3c54da7e1..1b427a3be 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -26,11 +26,18 @@ def test_get_args(monkeypatch): m.setattr( sys, "argv", - [sys.argv[0], "--language", "eng", "deu", "--mode=raw", "--tray=True"], + [ + sys.argv[0], + "--language", + "eng", + "deu", + "--parse-text=False", + "--tray=True", + ], ) args = app._get_args() - assert args.mode == "raw" + assert args.parse_text is False assert args.language == ["eng", "deu"] assert args.tray is True diff --git a/tests/test_utils.py b/tests/test_utils.py index e44faf3b8..cb8737f60 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -48,7 +48,7 @@ def test_argparser_defaults_are_complete(): "color", "cli_mode", "language", - "mode", + "parse_text", "notification", "reset", "tray", diff --git a/tests/tests_gui/test_menu_button.py b/tests/tests_gui/test_menu_button.py index d7c7ff204..4a17af491 100644 --- a/tests/tests_gui/test_menu_button.py +++ b/tests/tests_gui/test_menu_button.py @@ -111,13 +111,14 @@ def test_language_group(menu_btn): assert menu_btn.settings.value("language") == [langs[-1]] -def test_mode_group(menu_btn): +def test_detection_group(menu_btn): menu_btn.menu().aboutToShow.emit() - settings_group = menu_btn.findChild(QtGui.QActionGroup, "mode_group") + settings_group = menu_btn.findChild(QtGui.QActionGroup, "detection_group") for action in settings_group.children(): action.trigger() - assert menu_btn.settings.value("mode") == action.objectName() + setting_value = bool(menu_btn.settings.value(action.objectName(), type=bool)) + assert setting_value == action.isChecked() def test_settings_group(menu_btn): @@ -126,8 +127,8 @@ def test_settings_group(menu_btn): settings_group = menu_btn.findChild(QtGui.QActionGroup, "settings_group") for action in settings_group.children(): action.trigger() - setting_value = menu_btn.settings.value(action.objectName()) - assert str(setting_value).lower() == str(action.isChecked()).lower() + setting_value = menu_btn.settings.value(action.objectName(), type=bool) + assert setting_value == action.isChecked() def test_show_message_box(qapp, monkeypatch, menu_btn): diff --git a/tests/tests_gui/test_notification.py b/tests/tests_gui/test_notification.py index 253a2094d..4b261bc9c 100644 --- a/tests/tests_gui/test_notification.py +++ b/tests/tests_gui/test_notification.py @@ -7,7 +7,7 @@ from PySide6 import QtGui, QtWidgets from normcap.gui import notification -from normcap.gui.models import Capture, CaptureMode, Rect +from normcap.gui.models import Capture, Rect from normcap.ocr.structures import Transformer @@ -52,7 +52,7 @@ def test_compose_notification(ocr_transform, ocr_text, output_title, output_text capture = Capture( ocr_text=ocr_text, ocr_transformer=ocr_transform, - mode=CaptureMode.PARSE if ocr_transform != "RAW" else CaptureMode.RAW, + parse_text=ocr_transform != "RAW", image=QtGui.QImage(), screen=None, scale_factor=1, @@ -244,7 +244,7 @@ def mocked_qt_tray(cls, title, message, ocr_text, ocr_transformer): capture = Capture( ocr_text="text", ocr_transformer=Transformer.SINGLE_LINE, - mode=CaptureMode.PARSE, + parse_text=True, image=QtGui.QImage(), screen=None, scale_factor=1, diff --git a/tests/tests_gui/test_settings.py b/tests/tests_gui/test_settings.py index 06b40bb54..631be56d8 100644 --- a/tests/tests_gui/test_settings.py +++ b/tests/tests_gui/test_settings.py @@ -7,28 +7,28 @@ def test_reset_settings(): - default = "parse" - non_default = "raw" + default = True + non_default = False try: settings = Settings(organization="normcap_TEST") - settings.setValue("mode", non_default) - assert settings.value("mode") == non_default + settings.setValue("parse-text", non_default) + assert bool(settings.value("parse-text", type=bool)) == non_default settings.reset() - assert settings.value("mode") == default + assert bool(settings.value("parse-text", type=bool)) == default finally: settings.clear() def test_update_from_init_settings(caplog): - init_setting = "raw" + initial_parse_text = False try: with caplog.at_level(logging.DEBUG): settings = Settings( organization="normcap_TEST", - init_settings={"mode": init_setting, "non_existing": True}, + init_settings={"parse-text": initial_parse_text, "non_existing": True}, ) - assert settings.value("mode") == init_setting + assert bool(settings.value("parse-text", type=bool)) == initial_parse_text assert settings.value("non_existing", False) is False assert caplog.records[0].msg finally: @@ -36,16 +36,16 @@ def test_update_from_init_settings(caplog): def test_set_missing_to_default(caplog): - default_mode = "parse" - non_default_mode = "raw" + default_parse_text = True + non_default_parse_text = False default_language = "eng" try: settings = Settings(organization="normcap_TEST") - assert settings.value("mode") == default_mode + assert bool(settings.value("parse-text", type=bool)) == default_parse_text - settings.setValue("mode", non_default_mode) - assert settings.value("mode") == non_default_mode + settings.setValue("parse-text", non_default_parse_text) + assert bool(settings.value("parse-text", type=bool)) == non_default_parse_text assert settings.value("language") == default_language settings.remove("language") @@ -55,7 +55,7 @@ def test_set_missing_to_default(caplog): with caplog.at_level(logging.DEBUG): settings = Settings(organization="normcap_TEST") - assert settings.value("mode") == non_default_mode + assert bool(settings.value("parse-text", type=bool)) == non_default_parse_text assert settings.value("language") == default_language assert "Reset settings to" in caplog.records[0].msg assert caplog.records[0].args == ("language", default_language) diff --git a/tests/tests_gui/test_window.py b/tests/tests_gui/test_window.py index 9c17f592c..2899d764e 100644 --- a/tests/tests_gui/test_window.py +++ b/tests/tests_gui/test_window.py @@ -1,5 +1,3 @@ -import sys - import pytest from PySide6 import QtCore, QtGui @@ -114,32 +112,3 @@ def test_window_esc_key_pressed_while_selecting(qtbot, temp_settings): # THEN the selection should be cleared assert not win.selection_rect - - -@pytest.mark.skipif(sys.platform == "win32", reason="Fails for unknown reason") # FIXME -def test_window_get_capture_mode_fallback_to_parse(temp_settings, caplog): - # GIVEN a window with an invalid mode setting - image = QtGui.QImage(600, 400, QtGui.QImage.Format.Format_RGB32) - screen = models.Screen( - device_pixel_ratio=1.0, - left=0, - top=0, - right=600, - bottom=400, - index=0, - screenshot=image, - ) - invalid_mode = "some_deprecated_mode" - temp_settings.setValue("mode", invalid_mode) - - win = window.Window(screen=screen, settings=temp_settings, parent=None) - - # WHEN the capture mode is read - mode = win.get_capture_mode() - - # THEN a warning should be logged - # and "parse" mode should be returned as fallback - assert "warning" in caplog.text.lower() - assert "unknown capture mode" in caplog.text.lower() - assert invalid_mode in caplog.text.lower() - assert mode == models.CaptureMode.PARSE