191 lines
6.5 KiB
Python
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]))
|