Skip to content

Commit

Permalink
feat: prepare detection settings for more
Browse files Browse the repository at this point in the history
  • Loading branch information
dynobo committed Nov 27, 2024
1 parent 4f3a1e5 commit 0c96efc
Show file tree
Hide file tree
Showing 19 changed files with 101 additions and 134 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ hide:
- The icons <span class="md-pink">★</span> or <span class="md-pink">☰</span> next to the selection-rectangle indicate the active "capture mode" (see below).
- To abort a capture or quit NormCap press `<esc>`

<!-- TODO: Adjust to new settings -->
## Capture Modes

The settings menu <span class="md-pink">⚙</span> allows switching between the two capture modes: "parse" and "raw":
Expand Down
47 changes: 15 additions & 32 deletions normcap/gui/menu_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"))
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 1 addition & 8 deletions normcap/gui/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 2 additions & 4 deletions normcap/gui/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand Down
28 changes: 22 additions & 6 deletions normcap/gui/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions normcap/gui/tray.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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,
)
Expand Down
30 changes: 12 additions & 18 deletions normcap/gui/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)

Expand All @@ -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()};}}")
Expand Down Expand Up @@ -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()
2 changes: 1 addition & 1 deletion normcap/ocr/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
6 changes: 3 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_normcap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_settings_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_tray_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 9 additions & 2 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_argparser_defaults_are_complete():
"color",
"cli_mode",
"language",
"mode",
"parse_text",
"notification",
"reset",
"tray",
Expand Down
Loading

0 comments on commit 0c96efc

Please sign in to comment.