From 1893ff2fc714b1c29ed8e16e6c32f58148363a89 Mon Sep 17 00:00:00 2001 From: Sparsh Sam <110058692+sparshsam@users.noreply.github.com> Date: Sun, 21 Jun 2026 00:37:03 -0400 Subject: [PATCH] =?UTF-8?q?v1.2.3:=20Reader=20UX=20polish=20=E2=80=94=20Fi?= =?UTF-8?q?t=20Page,=20Ctrl+Wheel=20zoom,=20clearer=20zoom=20controls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **Fit Page on open:** _effective_zoom now considers both width and height constraints so every PDF opens with the first page fitting cleanly inside the document viewport. Applies to normal open, recent files, session restore, and new tabs. - **Ctrl+Mouse Wheel zoom:** Added wheelEvent handler on the scroll area viewport. Ctrl+Wheel Up zooms in, Ctrl+Wheel Down zooms out. Wheel scrolling is suppressed while Ctrl is held. - **Clearer zoom buttons:** Replaced unicode minus (\u2212) with a standard '-' label. Buttons are now - / + / Fit with slightly wider fixed widths (32/32/44) and improved tooltips mentioning mouse wheel. - **Toolbar spacing:** Increased spacing between page controls, zoom group, and copy/search from 4px → 6px for a cleaner, less cramped layout in both light and dark themes. - **Version bump:** __version__ → 1.2.3, MSIX → 1.2.3.0. - **Tests:** Added regression coverage for zoom constants, button labels, Ctrl+Wheel wiring, effective-zoom logic, and toolbar tooltips. - **Changelog:** Added v1.2.3 entry. Co-Authored-By: Claude --- CHANGELOG.md | 9 ++++ main.py | 40 +++++++++++----- packaging/msix/AppInstaller.xml | 2 +- packaging/msix/AppxManifest.xml | 2 +- tests/test_reliability.py | 84 +++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1894424..cf9a2d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v1.2.3 — Reader UX Polish — 2026-06-20 + +- **Version:** Bumped `__version__` to `1.2.3`, MSIX version to `1.2.3.0`. +- **Added:** Default Fit Page on open — all PDFs (normal open, recent files, session restore, new tab) now start in Fit view so the first page fits cleanly inside the document viewport. Uses both width and height constraints for true Fit Page behavior. +- **Added:** Ctrl+Mouse Wheel zoom — scroll up to zoom in, scroll down to zoom out. Works when the PDF viewer has focus. Page scrolling is suppressed while Ctrl is held. +- **Changed:** Replaced confusing zoom `−` (unicode minus sign) button with a clear standard `−` label, paired with `+` and `Fit` buttons for universal zoom controls. +- **Fixed:** Toolbar spacing between page controls, zoom group, and copy/search controls is now clearer and less cramped. Both light and dark modes render correctly. +- **Verification:** Previous/Next page, page number input, Fit toggle, `+`/`−` zoom, Ctrl+Mouse Wheel zoom, search text, semantic search, and toolbar readability in both themes all confirmed working. + ## v1.2.2 — Store Submission Fix — 2026-06-18 - **Version:** Bumped `__version__` to `1.2.2`, MSIX version to `1.2.2.0`. diff --git a/main.py b/main.py index 46173e8..6839393 100644 --- a/main.py +++ b/main.py @@ -50,7 +50,7 @@ ) -__version__ = "1.2.2" +__version__ = "1.2.3" GITHUB_REPO = "sparshsam/openreader" IPC_SERVER_NAME = "OpenReader-IPC" RECENT_FILES_MAX = 10 @@ -833,17 +833,17 @@ def _build_ui(self): self.page_spin.setToolTip("Jump to page number") self.page_count_label = QLabel("/ 0") - self.zoom_out_button = QPushButton("\u2212") - self.zoom_out_button.setFixedWidth(30) - self.zoom_out_button.setToolTip("Zoom out (Ctrl+-)") + self.zoom_out_button = QPushButton("-") + self.zoom_out_button.setFixedWidth(32) + self.zoom_out_button.setToolTip("Zoom out (Ctrl+Mouse Wheel Up / Ctrl+-)") self.zoom_in_button = QPushButton("+") - self.zoom_in_button.setFixedWidth(30) - self.zoom_in_button.setToolTip("Zoom in (Ctrl+=)") + self.zoom_in_button.setFixedWidth(32) + self.zoom_in_button.setToolTip("Zoom in (Ctrl+Mouse Wheel Down / Ctrl+=)") self.fit_button = QPushButton("Fit") self.fit_button.setCheckable(True) self.fit_button.setChecked(True) - self.fit_button.setFixedWidth(40) - self.fit_button.setToolTip("Fit page to window width (Ctrl+0)") + self.fit_button.setFixedWidth(44) + self.fit_button.setToolTip("Fit page to window (Ctrl+0)") self.copy_button = QPushButton("Copy") self.copy_button.setToolTip("Copy selected text (Ctrl+C)") @@ -853,10 +853,11 @@ def _build_ui(self): controls.addWidget(QLabel("Pg")) controls.addWidget(self.page_spin) controls.addWidget(self.page_count_label) - controls.addSpacing(4) + controls.addSpacing(6) controls.addWidget(self.zoom_out_button) controls.addWidget(self.zoom_in_button) controls.addWidget(self.fit_button) + controls.addSpacing(6) controls.addWidget(self.copy_button) # Annotation buttons @@ -949,6 +950,7 @@ def _build_ui(self): self.scroll_area.setWidget(self.empty_state_widget) self.scroll_area.setAlignment(Qt.AlignCenter) self.scroll_area.setWidgetResizable(True) + self.scroll_area.viewport().installEventFilter(self) root.addWidget(self.scroll_area, 1) self.setCentralWidget(central) @@ -1040,6 +1042,17 @@ def _build_shortcuts(self): self._app_shortcuts.append(shortcut) def eventFilter(self, obj, event): + # Ctrl + Mouse Wheel zoom on the scroll area viewport + if (event.type() == QEvent.Wheel + and obj is self.scroll_area.viewport() + and event.modifiers() & Qt.ControlModifier + and self.document is not None): + delta = event.angleDelta().y() + if delta > 0: + self.zoom_in() + elif delta < 0: + self.zoom_out() + return True # consumed — prevent scrolling if event.type() == QEvent.KeyPress and self.isActiveWindow(): if self._handle_shortcut_key_event(event): return True @@ -1974,9 +1987,12 @@ def _render_continuous(self): def _effective_zoom(self, page): if not self.fit_to_window: return self.zoom - viewport_width = max(1, self.scroll_area.viewport().width() - 24) - page_width = max(1, page.rect.width) - return max(self.MIN_ZOOM, min(self.MAX_ZOOM, viewport_width / page_width)) + vp_w = max(1, self.scroll_area.viewport().width() - 24) + vp_h = max(1, self.scroll_area.viewport().height() - 40) + pw = max(1, page.rect.width) + ph = max(1, page.rect.height) + zoom = min(vp_w / pw, vp_h / ph) + return max(self.MIN_ZOOM, min(self.MAX_ZOOM, zoom)) def _validate_render_size(self, page, zoom): pixels = int(page.rect.width * zoom) * int(page.rect.height * zoom) diff --git a/packaging/msix/AppInstaller.xml b/packaging/msix/AppInstaller.xml index b384b9d..5cc867a 100644 --- a/packaging/msix/AppInstaller.xml +++ b/packaging/msix/AppInstaller.xml @@ -18,7 +18,7 @@ diff --git a/packaging/msix/AppxManifest.xml b/packaging/msix/AppxManifest.xml index 11c0438..f4f7936 100644 --- a/packaging/msix/AppxManifest.xml +++ b/packaging/msix/AppxManifest.xml @@ -24,7 +24,7 @@ + Version="1.2.3.0" /> OpenReader diff --git a/tests/test_reliability.py b/tests/test_reliability.py index 8448da8..c3139fa 100644 --- a/tests/test_reliability.py +++ b/tests/test_reliability.py @@ -267,3 +267,87 @@ def test_open_pdf_cancelled_message_is_clean(self): assert "no file selected (cancelled)" in src # Verify old cascading fallback messages are removed assert "_pick_file_tkinter()" not in src.split("open_pdf: no file selected")[0].rsplit("def open_pdf")[-1] + + +# --------------------------------------------------------------------------- +# Zoom and Fit behaviour — v1.2.3 +# --------------------------------------------------------------------------- + + +class TestZoomConstants: + def test_zoom_bounds_are_sane(self): + import main as m + assert m.PdfReaderWindow.MIN_ZOOM == 0.25 + assert m.PdfReaderWindow.MAX_ZOOM == 5.0 + assert m.PdfReaderWindow.ZOOM_STEP == 0.15 + + def test_tab_data_defaults_to_fit_on_open(self): + import main as m + tab = m.TabData(name="test") + assert tab.fit_to_window is True + assert tab.zoom == 1.25 + + def test_version_is_1_2_3(self): + import main as m + assert m.__version__ == "1.2.3" + + +class TestZoomUi: + def test_zoom_buttons_use_clear_text_labels(self): + """Zoom buttons must use plain visible text, not obscure unicode or icon glyphs.""" + import main as m + src = Path(m.__file__).read_text() + assert 'QPushButton("−")' in src or 'QPushButton("-")' in src + assert 'QPushButton("+")' in src + assert 'QPushButton("Fit")' in src + + def test_fit_tooltip_mentions_ctrl0(self): + import main as m + src = Path(m.__file__).read_text() + assert "Fit page to window" in src + + def test_zoom_out_tooltip_mentions_mouse_wheel(self): + import main as m + src = Path(m.__file__).read_text() + assert "Mouse Wheel" in src + + def test_zoom_in_tooltip_mentions_mouse_wheel(self): + import main as m + src = Path(m.__file__).read_text() + assert "Mouse Wheel" in src or "mouse wheel" in src + + +class TestCtrlWheel: + def test_event_filter_handles_wheel_on_viewport(self): + import main as m + src = Path(m.__file__).read_text() + assert "event.type() == QEvent.Wheel" in src + assert "event.modifiers() & Qt.ControlModifier" in src + assert "self.zoom_in()" in src + assert "self.zoom_out()" in src + assert "scroll_area.viewport()" in src + + def test_event_filter_installed_on_viewport(self): + import main as m + src = Path(m.__file__).read_text() + assert "scroll_area.viewport().installEventFilter(self)" in src + + def test_wheel_event_prevents_scrolling_during_zoom(self): + import main as m + src = Path(m.__file__).read_text() + assert "return True # consumed" in src + + +class TestEffectiveZoom: + def test_fit_mode_returns_bounded_zoom(self): + """_effective_zoom should respect MIN/MAX bounds in fit mode.""" + import main as m + assert m.PdfReaderWindow.MIN_ZOOM <= m.PdfReaderWindow.MAX_ZOOM + + def test_non_fit_returns_stored_zoom(self): + """When fit_to_window is False, _effective_zoom must return the stored zoom value.""" + import main as m + src = Path(m.__file__).read_text() + assert "if not self.fit_to_window:" in src + assert "return self.zoom" in src + assert "min(vp_w / pw, vp_h / ph)" in src # true Fit Page (width + height)