update page selection
All checks were successful
Python CI / test (push) Successful in 1m24s
Lint / lint (push) Successful in 1m22s
Tests / test (3.10) (push) Successful in 1m2s
Tests / test (3.11) (push) Successful in 1m3s
Tests / test (3.9) (push) Successful in 59s

This commit is contained in:
Duncan Tourolle 2025-11-22 08:22:01 +01:00
parent 353b0c4aff
commit ca2b3545ee
4 changed files with 89 additions and 67 deletions

View File

@ -54,21 +54,58 @@ class ApplicationStateMixin:
return self._template_manager return self._template_manager
# Common helper methods # Common helper methods
def _get_most_visible_page_index(self):
"""
Determine which page is most visible in the current viewport.
Returns:
int: Index of the most visible page
"""
if not hasattr(self.gl_widget, '_page_renderers') or not self.gl_widget._page_renderers:
return self.gl_widget.current_page_index
# Get viewport dimensions
viewport_height = self.gl_widget.height()
viewport_center_y = viewport_height / 2
# Find which page's center is closest to viewport center
min_distance = float('inf')
best_page_index = self.gl_widget.current_page_index
for renderer, page in self.gl_widget._page_renderers:
# Get page center Y position in screen coordinates
page_height_mm = page.layout.size[1]
page_height_px = page_height_mm * self.project.working_dpi / 25.4
page_center_y_offset = renderer.screen_y + (page_height_px * self.gl_widget.zoom_level / 2)
# Calculate distance from viewport center
distance = abs(page_center_y_offset - viewport_center_y)
if distance < min_distance:
min_distance = distance
# Find the page index in project.pages
try:
best_page_index = self.project.pages.index(page)
except ValueError:
pass
return best_page_index
def get_current_page(self): def get_current_page(self):
""" """
Get currently selected page. Get currently visible page (most visible in viewport).
Returns: Returns:
Page instance or None if no page is selected Page instance or None if no page is selected
""" """
if not self.project or not self.project.pages: if not self.project or not self.project.pages:
return None return None
index = self.gl_widget.current_page_index index = self._get_most_visible_page_index()
if 0 <= index < len(self.project.pages): if 0 <= index < len(self.project.pages):
return self.project.pages[index] return self.project.pages[index]
return None return None
def get_current_page_index(self) -> int: def get_current_page_index(self) -> int:

View File

@ -238,9 +238,12 @@ class PageOperationsMixin:
# Connect page selection change # Connect page selection change
page_combo.currentIndexChanged.connect(on_page_changed) page_combo.currentIndexChanged.connect(on_page_changed)
# Initialize with first page # Initialize with most visible page
on_page_changed(0) initial_page_index = self._get_most_visible_page_index()
if 0 <= initial_page_index < len(self.project.pages):
page_combo.setCurrentIndex(initial_page_index)
on_page_changed(initial_page_index if 0 <= initial_page_index < len(self.project.pages) else 0)
# Buttons # Buttons
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
@ -335,43 +338,6 @@ class PageOperationsMixin:
if set_default_checkbox.isChecked(): if set_default_checkbox.isChecked():
status_msg += " (set as default)" status_msg += " (set as default)"
self.show_status(status_msg, 2000) self.show_status(status_msg, 2000)
def _get_most_visible_page_index(self):
"""
Determine which page is most visible in the current viewport.
Returns:
int: Index of the most visible page
"""
if not hasattr(self.gl_widget, '_page_renderers') or not self.gl_widget._page_renderers:
return self.gl_widget.current_page_index
# Get viewport dimensions
viewport_height = self.gl_widget.height()
viewport_center_y = viewport_height / 2
# Find which page's center is closest to viewport center
min_distance = float('inf')
best_page_index = self.gl_widget.current_page_index
for renderer, page in self.gl_widget._page_renderers:
# Get page center Y position in screen coordinates
page_height_mm = page.layout.size[1]
page_height_px = page_height_mm * self.project.working_dpi / 25.4
page_center_y_offset = renderer.screen_y + (page_height_px * self.gl_widget.zoom_level / 2)
# Calculate distance from viewport center
distance = abs(page_center_y_offset - viewport_center_y)
if distance < min_distance:
min_distance = distance
# Find the page index in project.pages
try:
best_page_index = self.project.pages.index(page)
except ValueError:
pass
return best_page_index
@ribbon_action( @ribbon_action(
label="Toggle Spread", label="Toggle Spread",
@ -427,26 +393,35 @@ class PageOperationsMixin:
@ribbon_action( @ribbon_action(
label="Remove Page", label="Remove Page",
tooltip="Remove the last page", tooltip="Remove the currently selected page",
tab="Layout", tab="Layout",
group="Page" group="Page"
) )
def remove_page(self): def remove_page(self):
"""Remove the last page""" """Remove the currently selected page"""
if len(self.project.pages) <= 1: if len(self.project.pages) <= 1:
self.show_warning("Cannot Remove", "Must have at least one page") self.show_warning("Cannot Remove", "Must have at least one page")
print("Cannot remove page - must have at least one page") print("Cannot remove page - must have at least one page")
return return
# Remove last page # Get the most visible page in viewport
last_page = self.project.pages[-1] page_index = self._get_most_visible_page_index()
self.project.remove_page(last_page)
# Ensure index is valid
if page_index < 0 or page_index >= len(self.project.pages):
page_index = len(self.project.pages) - 1
page_to_remove = self.project.pages[page_index]
page_name = self.project.get_page_display_name(page_to_remove)
# Remove the selected page
self.project.remove_page(page_to_remove)
# Renumber remaining pages # Renumber remaining pages
for i, page in enumerate(self.project.pages): for i, page in enumerate(self.project.pages):
page.page_number = i + 1 page.page_number = i + 1
# Update display # Update display
self.update_view() self.update_view()
print(f"Removed page, now have {len(self.project.pages)} pages") print(f"Removed {page_name}, now have {len(self.project.pages)} pages")

View File

@ -103,12 +103,14 @@ class TestGetCurrentPage:
# Setup project with pages # Setup project with pages
project = Mock(spec=Project) project = Mock(spec=Project)
project.working_dpi = 96
page1 = Mock(spec=Page) page1 = Mock(spec=Page)
page2 = Mock(spec=Page) page2 = Mock(spec=Page)
project.pages = [page1, page2] project.pages = [page1, page2]
gl_widget = Mock() gl_widget = Mock()
gl_widget.current_page_index = 0 gl_widget.current_page_index = 0
gl_widget._page_renderers = [] # No renderers, so it will use current_page_index
window._project = project window._project = project
window._gl_widget = gl_widget window._gl_widget = gl_widget
@ -120,12 +122,14 @@ class TestGetCurrentPage:
qtbot.addWidget(window) qtbot.addWidget(window)
project = Mock(spec=Project) project = Mock(spec=Project)
project.working_dpi = 96
page1 = Mock(spec=Page) page1 = Mock(spec=Page)
page2 = Mock(spec=Page) page2 = Mock(spec=Page)
project.pages = [page1, page2] project.pages = [page1, page2]
gl_widget = Mock() gl_widget = Mock()
gl_widget.current_page_index = 1 gl_widget.current_page_index = 1
gl_widget._page_renderers = [] # No renderers, so it will use current_page_index
window._project = project window._project = project
window._gl_widget = gl_widget window._gl_widget = gl_widget
@ -158,10 +162,12 @@ class TestGetCurrentPage:
qtbot.addWidget(window) qtbot.addWidget(window)
project = Mock(spec=Project) project = Mock(spec=Project)
project.working_dpi = 96
project.pages = [Mock(spec=Page)] project.pages = [Mock(spec=Page)]
gl_widget = Mock() gl_widget = Mock()
gl_widget.current_page_index = 5 # Out of range gl_widget.current_page_index = 5 # Out of range
gl_widget._page_renderers = [] # No renderers, so it will use current_page_index
window._project = project window._project = project
window._gl_widget = gl_widget window._gl_widget = gl_widget
@ -280,10 +286,12 @@ class TestRequirePage:
qtbot.addWidget(window) qtbot.addWidget(window)
project = Mock(spec=Project) project = Mock(spec=Project)
project.working_dpi = 96
project.pages = [Mock(spec=Page)] project.pages = [Mock(spec=Page)]
gl_widget = Mock() gl_widget = Mock()
gl_widget.current_page_index = 0 gl_widget.current_page_index = 0
gl_widget._page_renderers = [] # No renderers, so it will use current_page_index
window._project = project window._project = project
window._gl_widget = gl_widget window._gl_widget = gl_widget

View File

@ -5,28 +5,30 @@ Tests for PageOperationsMixin
import pytest import pytest
from unittest.mock import Mock, MagicMock, patch from unittest.mock import Mock, MagicMock, patch
from PyQt6.QtWidgets import QMainWindow from PyQt6.QtWidgets import QMainWindow
from pyPhotoAlbum.mixins.base import ApplicationStateMixin
from pyPhotoAlbum.mixins.operations.page_ops import PageOperationsMixin from pyPhotoAlbum.mixins.operations.page_ops import PageOperationsMixin
from pyPhotoAlbum.project import Project, Page from pyPhotoAlbum.project import Project, Page
from pyPhotoAlbum.page_layout import PageLayout from pyPhotoAlbum.page_layout import PageLayout
class TestPageOpsWindow(PageOperationsMixin, QMainWindow): class TestPageOpsWindow(PageOperationsMixin, ApplicationStateMixin, QMainWindow):
"""Test window with page operations mixin""" """Test window with page operations mixin"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.gl_widget = Mock() self._gl_widget = Mock()
self.gl_widget.current_page_index = 0 self._gl_widget.current_page_index = 0
self.gl_widget.zoom_level = 1.0 self._gl_widget.zoom_level = 1.0
self.gl_widget.pan_offset = [0, 0] self._gl_widget.pan_offset = [0, 0]
self.gl_widget._page_renderers = [] self._gl_widget._page_renderers = []
self.gl_widget.width = Mock(return_value=800) self._gl_widget.width = Mock(return_value=800)
self.gl_widget.height = Mock(return_value=600) self._gl_widget.height = Mock(return_value=600)
self.project = Project(name="Test") self._project = Project(name="Test")
self.project.working_dpi = 96 self._project.working_dpi = 96
self.project.page_size_mm = (210, 297) self._project.page_size_mm = (210, 297)
self._update_view_called = False self._update_view_called = False
self._status_message = None self._status_message = None
self._status_bar = Mock()
def update_view(self): def update_view(self):
self._update_view_called = True self._update_view_called = True