Duncan Tourolle 3bfe2fa654
Some checks failed
Python CI / test (push) Successful in 1m19s
Lint / lint (push) Successful in 1m24s
Tests / test (3.10) (push) Failing after 1m2s
Tests / test (3.11) (push) Failing after 58s
Tests / test (3.9) (push) Failing after 59s
Fix installer
Added navigation tools
2025-11-22 08:50:36 +01:00

191 lines
6.5 KiB
Python

"""
Viewport mixin for GLWidget - handles zoom and pan
"""
from OpenGL.GL import *
class ViewportMixin:
"""
Mixin providing viewport zoom and pan functionality.
This mixin manages the zoom level and pan offset for the OpenGL canvas,
including fit-to-screen calculations and OpenGL initialization.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Zoom and pan state
self.zoom_level = 1.0
self.pan_offset = [0, 0]
self.initial_zoom_set = False # Track if we've set initial fit-to-screen zoom
def initializeGL(self):
"""Initialize OpenGL resources"""
glClearColor(1.0, 1.0, 1.0, 1.0)
glEnable(GL_DEPTH_TEST)
def resizeGL(self, w, h):
"""Handle window resizing"""
glViewport(0, 0, w, h)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(0, w, h, 0, -1, 1)
glMatrixMode(GL_MODELVIEW)
# Recalculate centering if we have a project loaded
if self.initial_zoom_set:
# Maintain current zoom level, just recenter
self.pan_offset = self._calculate_center_pan_offset(self.zoom_level)
self.update()
# Update scrollbars when viewport size changes
main_window = self.window()
if hasattr(main_window, 'update_scrollbars'):
main_window.update_scrollbars()
def _calculate_fit_to_screen_zoom(self):
"""
Calculate zoom level to fit first page to screen.
Returns:
float: Zoom level (1.0 = 100%, 0.5 = 50%, etc.)
"""
main_window = self.window()
if not hasattr(main_window, 'project') or not main_window.project or not main_window.project.pages:
return 1.0
window_width = self.width()
window_height = self.height()
# Get first page dimensions in mm
first_page = main_window.project.pages[0]
page_width_mm, page_height_mm = first_page.layout.size
# Convert to pixels
dpi = main_window.project.working_dpi
page_width_px = page_width_mm * dpi / 25.4
page_height_px = page_height_mm * dpi / 25.4
# Calculate zoom to fit with margins
margin = 100 # pixels
zoom_w = (window_width - margin * 2) / page_width_px
zoom_h = (window_height - margin * 2) / page_height_px
# Use the smaller zoom to ensure entire page fits
return min(zoom_w, zoom_h, 1.0) # Don't zoom in beyond 100%
def _calculate_center_pan_offset(self, zoom_level):
"""
Calculate pan offset to center the first page in the viewport.
Args:
zoom_level: The current zoom level to use for calculations
Returns:
list: [x_offset, y_offset] to center the page
"""
main_window = self.window()
if not hasattr(main_window, 'project') or not main_window.project or not main_window.project.pages:
return [0, 0]
window_width = self.width()
window_height = self.height()
# Get first page dimensions in mm
first_page = main_window.project.pages[0]
page_width_mm, page_height_mm = first_page.layout.size
# Convert to pixels
dpi = main_window.project.working_dpi
page_width_px = page_width_mm * dpi / 25.4
page_height_px = page_height_mm * dpi / 25.4
# Apply zoom to get screen dimensions
screen_page_width = page_width_px * zoom_level
screen_page_height = page_height_px * zoom_level
# Calculate offsets to center the page
# PAGE_MARGIN from rendering.py is 50
PAGE_MARGIN = 50
x_offset = (window_width - screen_page_width) / 2 - PAGE_MARGIN
y_offset = (window_height - screen_page_height) / 2
return [x_offset, y_offset]
def get_content_bounds(self):
"""
Calculate the total bounds of all content (pages).
Returns:
dict: {'min_x', 'max_x', 'min_y', 'max_y', 'width', 'height'} in pixels
"""
main_window = self.window()
if not hasattr(main_window, 'project') or not main_window.project or not main_window.project.pages:
return {'min_x': 0, 'max_x': 800, 'min_y': 0, 'max_y': 600, 'width': 800, 'height': 600}
dpi = main_window.project.working_dpi
PAGE_MARGIN = 50
PAGE_SPACING = 50
# Calculate total dimensions
total_height = PAGE_MARGIN
max_width = 0
for page in main_window.project.pages:
page_width_mm, page_height_mm = page.layout.size
page_width_px = page_width_mm * dpi / 25.4
page_height_px = page_height_mm * dpi / 25.4
screen_page_width = page_width_px * self.zoom_level
screen_page_height = page_height_px * self.zoom_level
total_height += screen_page_height + PAGE_SPACING
max_width = max(max_width, screen_page_width)
total_width = max_width + PAGE_MARGIN * 2
total_height += PAGE_MARGIN
return {
'min_x': 0,
'max_x': total_width,
'min_y': 0,
'max_y': total_height,
'width': total_width,
'height': total_height
}
def clamp_pan_offset(self):
"""
Clamp pan offset to prevent scrolling beyond content bounds.
Pan offset semantics:
- Positive pan_offset = content moved right/down (viewing top-left)
- Negative pan_offset = content moved left/up (viewing bottom-right)
"""
bounds = self.get_content_bounds()
viewport_width = self.width()
viewport_height = self.height()
content_width = bounds['width']
content_height = bounds['height']
# Only clamp if content is larger than viewport
# If content is smaller, allow any pan offset (for centering)
# Horizontal clamping
if content_width > viewport_width:
# Content is wider than viewport - restrict panning
max_pan_left = 0 # Can't pan beyond left edge
min_pan_left = -(content_width - viewport_width) # Can't pan beyond right edge
self.pan_offset[0] = max(min_pan_left, min(max_pan_left, self.pan_offset[0]))
# Vertical clamping
if content_height > viewport_height:
# Content is taller than viewport - restrict panning
max_pan_up = 0 # Can't pan beyond top edge
min_pan_up = -(content_height - viewport_height) # Can't pan beyond bottom edge
self.pan_offset[1] = max(min_pan_up, min(max_pan_up, self.pan_offset[1]))