Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: prepare detection settings for more #677

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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