Skip to content
Merged
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
40 changes: 28 additions & 12 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)")

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion packaging/msix/AppInstaller.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<MainPackage
Name="SparshSam.OpenReader"
Publisher="CN=E6186421-BF8A-47E0-A89C-0F513DFF91C0"
Version="1.2.2.0"
Version="1.2.3.0"
ProcessorArchitecture="x64"
Uri="https://downloads.openreader.app/stable/OpenReader.msix" />

Expand Down
2 changes: 1 addition & 1 deletion packaging/msix/AppxManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<Identity
Name="SparshSam.OpenReader"
Publisher="CN=E6186421-BF8A-47E0-A89C-0F513DFF91C0"
Version="1.2.2.0" />
Version="1.2.3.0" />

<Properties>
<DisplayName>OpenReader</DisplayName>
Expand Down
84 changes: 84 additions & 0 deletions tests/test_reliability.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)