diff --git a/CHANGELOG b/CHANGELOG index 356e88d5..f7ecc75c 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 0f31cfa9..ae04758c 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 ecd34421..7acb0544 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"))) + # 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 7565a3ef..711dcd71 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 fe4a542b..f8b6a3de 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: + elif capture.parse_text: count = len(capture.ocr_text) # Count linesep only as single char: count -= (len(os.linesep) - 1) * capture.ocr_text.count(os.linesep) diff --git a/normcap/gui/settings.py b/normcap/gui/settings.py index 44e72f60..1b18900f 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,19 @@ 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") + self.setValue("parse-text", mode == "parse") + self.remoce("mode") + 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 f3a8083e..ab4a9d03 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")) 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 c57bc749..8ee8f51a 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")), ) 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 eae45915..27f20ef0 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/normcap/resources/locales/README.md b/normcap/resources/locales/README.md index d1f61c4c..af5289bd 100644 --- a/normcap/resources/locales/README.md +++ b/normcap/resources/locales/README.md @@ -20,6 +20,7 @@ | [fr_FR](./fr_FR/LC_MESSAGES/messages.po) | 100% | 68 of 68 | | [hi_IN](./hi_IN/LC_MESSAGES/messages.po) | 8% | 6 of 68 | | [it_IT](./it_IT/LC_MESSAGES/messages.po) | 100% | 68 of 68 | +| [ja_JP](./ja_JP/LC_MESSAGES/messages.po) | 100% | 68 of 68 | | [pl_PL](./pl_PL/LC_MESSAGES/messages.po) | 8% | 6 of 68 | | [pt_BR](./pt_BR/LC_MESSAGES/messages.po) | 100% | 68 of 68 | | [pt_PT](./pt_PT/LC_MESSAGES/messages.po) | 100% | 68 of 68 | diff --git a/normcap/resources/locales/messages.pot b/normcap/resources/locales/messages.pot index a37abcca..389b41a3 100644 --- a/normcap/resources/locales/messages.pot +++ b/normcap/resources/locales/messages.pot @@ -421,4 +421,3 @@ msgid "" "\n" "Do you want to view the changelog on GitHub now?" msgstr "" - diff --git a/tests/conftest.py b/tests/conftest.py index 4727dd36..6c4f7872 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, diff --git a/tests/integration/test_normcap.py b/tests/integration/test_normcap.py index b8c1cdee..5d7b0f44 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 37bbc1b2..f3b7d81e 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/test_utils.py b/tests/test_utils.py index e44faf3b..dacf28a5 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_notification.py b/tests/tests_gui/test_notification.py index 253a2094..4b261bc9 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 06b40bb5..b83bf77e 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 settings.value("parse-text") == non_default settings.reset() - assert settings.value("mode") == default + assert settings.value("parse-text") == 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 settings.value("parse-text") == 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 settings.value("parse-text") == 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 settings.value("parse-text") == 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 settings.value("parse-text") == 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 9c17f592..2899d764 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