Fix installer
Added navigation tools
This commit is contained in:
parent
ca2b3545ee
commit
3bfe2fa654
14
install.sh
14
install.sh
@ -72,6 +72,11 @@ install_dependencies() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check if running in a virtual environment
|
||||||
|
in_virtualenv() {
|
||||||
|
[ -n "$VIRTUAL_ENV" ] || [ -n "$CONDA_DEFAULT_ENV" ]
|
||||||
|
}
|
||||||
|
|
||||||
# Install Python package
|
# Install Python package
|
||||||
install_package() {
|
install_package() {
|
||||||
local install_mode=$1
|
local install_mode=$1
|
||||||
@ -80,8 +85,13 @@ install_package() {
|
|||||||
print_info "Installing pyPhotoAlbum system-wide..."
|
print_info "Installing pyPhotoAlbum system-wide..."
|
||||||
sudo pip install .
|
sudo pip install .
|
||||||
else
|
else
|
||||||
print_info "Installing pyPhotoAlbum for current user..."
|
if in_virtualenv; then
|
||||||
pip install --user .
|
print_info "Installing pyPhotoAlbum in virtual environment..."
|
||||||
|
pip install .
|
||||||
|
else
|
||||||
|
print_info "Installing pyPhotoAlbum for current user..."
|
||||||
|
pip install --user .
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ from pyPhotoAlbum.mixins.element_selection import ElementSelectionMixin
|
|||||||
from pyPhotoAlbum.mixins.mouse_interaction import MouseInteractionMixin
|
from pyPhotoAlbum.mixins.mouse_interaction import MouseInteractionMixin
|
||||||
from pyPhotoAlbum.mixins.interaction_undo import UndoableInteractionMixin
|
from pyPhotoAlbum.mixins.interaction_undo import UndoableInteractionMixin
|
||||||
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
|
from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin
|
||||||
|
from pyPhotoAlbum.mixins.keyboard_navigation import KeyboardNavigationMixin
|
||||||
|
|
||||||
|
|
||||||
class GLWidget(
|
class GLWidget(
|
||||||
@ -30,6 +31,7 @@ class GLWidget(
|
|||||||
ElementSelectionMixin,
|
ElementSelectionMixin,
|
||||||
MouseInteractionMixin,
|
MouseInteractionMixin,
|
||||||
UndoableInteractionMixin,
|
UndoableInteractionMixin,
|
||||||
|
KeyboardNavigationMixin,
|
||||||
QOpenGLWidget
|
QOpenGLWidget
|
||||||
):
|
):
|
||||||
"""OpenGL widget for pyPhotoAlbum rendering and user interaction
|
"""OpenGL widget for pyPhotoAlbum rendering and user interaction
|
||||||
@ -60,6 +62,10 @@ class GLWidget(
|
|||||||
self.setMouseTracking(True)
|
self.setMouseTracking(True)
|
||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
|
|
||||||
|
# Enable keyboard focus
|
||||||
|
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
|
self.setFocus()
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""Handle widget close event."""
|
"""Handle widget close event."""
|
||||||
# Cleanup async loading
|
# Cleanup async loading
|
||||||
@ -93,5 +99,26 @@ class GLWidget(
|
|||||||
else:
|
else:
|
||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
|
|
||||||
|
elif event.key() == Qt.Key.Key_PageDown:
|
||||||
|
# Navigate to next page
|
||||||
|
self._navigate_to_next_page()
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
elif event.key() == Qt.Key.Key_PageUp:
|
||||||
|
# Navigate to previous page
|
||||||
|
self._navigate_to_previous_page()
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
elif event.key() in (Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Left, Qt.Key.Key_Right):
|
||||||
|
# Arrow key handling
|
||||||
|
if self.selected_elements:
|
||||||
|
# Move selected elements
|
||||||
|
self._move_selected_elements_with_arrow_keys(event.key())
|
||||||
|
event.accept()
|
||||||
|
else:
|
||||||
|
# Move viewport
|
||||||
|
self._move_viewport_with_arrow_keys(event.key())
|
||||||
|
event.accept()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
|
|||||||
@ -8,10 +8,10 @@ This version uses the mixin architecture with auto-generated ribbon configuratio
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QApplication, QMainWindow, QVBoxLayout, QWidget,
|
QApplication, QMainWindow, QVBoxLayout, QWidget,
|
||||||
QStatusBar
|
QStatusBar, QScrollBar, QHBoxLayout
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt, QSize
|
||||||
from PyQt6.QtGui import QIcon
|
from PyQt6.QtGui import QIcon
|
||||||
|
|
||||||
from pyPhotoAlbum.project import Project
|
from pyPhotoAlbum.project import Project
|
||||||
@ -113,10 +113,40 @@ class MainWindow(
|
|||||||
# Create ribbon with auto-generated config
|
# Create ribbon with auto-generated config
|
||||||
self.ribbon = RibbonWidget(self, ribbon_config)
|
self.ribbon = RibbonWidget(self, ribbon_config)
|
||||||
main_layout.addWidget(self.ribbon, 0)
|
main_layout.addWidget(self.ribbon, 0)
|
||||||
|
|
||||||
|
# Create canvas area with GL widget and scroll bars
|
||||||
|
canvas_widget = QWidget()
|
||||||
|
canvas_layout = QVBoxLayout()
|
||||||
|
canvas_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
canvas_layout.setSpacing(0)
|
||||||
|
|
||||||
|
# Top row: GL widget + vertical scrollbar
|
||||||
|
top_layout = QHBoxLayout()
|
||||||
|
top_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
top_layout.setSpacing(0)
|
||||||
|
|
||||||
# Create OpenGL widget
|
# Create OpenGL widget
|
||||||
self._gl_widget = GLWidget(self)
|
self._gl_widget = GLWidget(self)
|
||||||
main_layout.addWidget(self._gl_widget, 1)
|
top_layout.addWidget(self._gl_widget, 1)
|
||||||
|
|
||||||
|
# Vertical scrollbar
|
||||||
|
self._v_scrollbar = QScrollBar(Qt.Orientation.Vertical)
|
||||||
|
self._v_scrollbar.setRange(-10000, 10000)
|
||||||
|
self._v_scrollbar.setValue(0)
|
||||||
|
self._v_scrollbar.valueChanged.connect(self._on_vertical_scroll)
|
||||||
|
top_layout.addWidget(self._v_scrollbar, 0)
|
||||||
|
|
||||||
|
canvas_layout.addLayout(top_layout, 1)
|
||||||
|
|
||||||
|
# Bottom row: horizontal scrollbar
|
||||||
|
self._h_scrollbar = QScrollBar(Qt.Orientation.Horizontal)
|
||||||
|
self._h_scrollbar.setRange(-10000, 10000)
|
||||||
|
self._h_scrollbar.setValue(0)
|
||||||
|
self._h_scrollbar.valueChanged.connect(self._on_horizontal_scroll)
|
||||||
|
canvas_layout.addWidget(self._h_scrollbar, 0)
|
||||||
|
|
||||||
|
canvas_widget.setLayout(canvas_layout)
|
||||||
|
main_layout.addWidget(canvas_widget, 1)
|
||||||
|
|
||||||
self.setCentralWidget(main_widget)
|
self.setCentralWidget(main_widget)
|
||||||
|
|
||||||
@ -126,7 +156,62 @@ class MainWindow(
|
|||||||
|
|
||||||
# Register keyboard shortcuts
|
# Register keyboard shortcuts
|
||||||
self._register_shortcuts()
|
self._register_shortcuts()
|
||||||
|
|
||||||
|
# Track scrollbar updates to prevent feedback loops
|
||||||
|
self._updating_scrollbars = False
|
||||||
|
|
||||||
|
def _on_vertical_scroll(self, value):
|
||||||
|
"""Handle vertical scrollbar changes"""
|
||||||
|
if not self._updating_scrollbars:
|
||||||
|
# Invert scrollbar value to pan offset (scrolling down = negative pan)
|
||||||
|
self._gl_widget.pan_offset[1] = -value
|
||||||
|
self._gl_widget.update()
|
||||||
|
|
||||||
|
def _on_horizontal_scroll(self, value):
|
||||||
|
"""Handle horizontal scrollbar changes"""
|
||||||
|
if not self._updating_scrollbars:
|
||||||
|
# Invert scrollbar value to pan offset (scrolling right = negative pan)
|
||||||
|
self._gl_widget.pan_offset[0] = -value
|
||||||
|
self._gl_widget.update()
|
||||||
|
|
||||||
|
def update_scrollbars(self):
|
||||||
|
"""Update scrollbar positions and ranges based on current content and pan offset"""
|
||||||
|
self._updating_scrollbars = True
|
||||||
|
|
||||||
|
# Get content bounds
|
||||||
|
bounds = self._gl_widget.get_content_bounds()
|
||||||
|
viewport_width = self._gl_widget.width()
|
||||||
|
viewport_height = self._gl_widget.height()
|
||||||
|
|
||||||
|
content_height = bounds['height']
|
||||||
|
content_width = bounds['width']
|
||||||
|
|
||||||
|
# Vertical scrollbar
|
||||||
|
# Scrollbar value 0 = top of content
|
||||||
|
# Scrollbar value max = bottom of content
|
||||||
|
# Pan offset is inverted: positive pan = content moved down = view at top
|
||||||
|
# negative pan = content moved up = view at bottom
|
||||||
|
v_range = int(max(0, content_height - viewport_height))
|
||||||
|
self._v_scrollbar.setRange(0, v_range)
|
||||||
|
self._v_scrollbar.setPageStep(int(viewport_height))
|
||||||
|
# Invert pan_offset for scrollbar position
|
||||||
|
self._v_scrollbar.setValue(int(max(0, min(v_range, -self._gl_widget.pan_offset[1]))))
|
||||||
|
|
||||||
|
# Show/hide vertical scrollbar based on whether scrolling is needed
|
||||||
|
self._v_scrollbar.setVisible(v_range > 0)
|
||||||
|
|
||||||
|
# Horizontal scrollbar
|
||||||
|
h_range = int(max(0, content_width - viewport_width))
|
||||||
|
self._h_scrollbar.setRange(0, h_range)
|
||||||
|
self._h_scrollbar.setPageStep(int(viewport_width))
|
||||||
|
# Invert pan_offset for scrollbar position
|
||||||
|
self._h_scrollbar.setValue(int(max(0, min(h_range, -self._gl_widget.pan_offset[0]))))
|
||||||
|
|
||||||
|
# Show/hide horizontal scrollbar based on whether scrolling is needed
|
||||||
|
self._h_scrollbar.setVisible(h_range > 0)
|
||||||
|
|
||||||
|
self._updating_scrollbars = False
|
||||||
|
|
||||||
def _register_shortcuts(self):
|
def _register_shortcuts(self):
|
||||||
"""Register keyboard shortcuts from decorated methods"""
|
"""Register keyboard shortcuts from decorated methods"""
|
||||||
from PyQt6.QtGui import QShortcut, QKeySequence
|
from PyQt6.QtGui import QShortcut, QKeySequence
|
||||||
|
|||||||
@ -209,3 +209,7 @@ class ApplicationStateMixin:
|
|||||||
"""Trigger GL widget update to refresh the view"""
|
"""Trigger GL widget update to refresh the view"""
|
||||||
if self.gl_widget:
|
if self.gl_widget:
|
||||||
self.gl_widget.update()
|
self.gl_widget.update()
|
||||||
|
|
||||||
|
# Update scrollbars to reflect new content
|
||||||
|
if hasattr(self, 'update_scrollbars'):
|
||||||
|
self.update_scrollbars()
|
||||||
|
|||||||
176
pyPhotoAlbum/mixins/keyboard_navigation.py
Normal file
176
pyPhotoAlbum/mixins/keyboard_navigation.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
"""
|
||||||
|
Keyboard navigation mixin for GLWidget - handles keyboard-based navigation
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
|
||||||
|
class KeyboardNavigationMixin:
|
||||||
|
"""
|
||||||
|
Mixin providing keyboard navigation functionality.
|
||||||
|
|
||||||
|
This mixin handles Page Up/Down navigation between pages,
|
||||||
|
arrow key viewport movement, and arrow key element movement.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _navigate_to_next_page(self):
|
||||||
|
"""Navigate to the next page using Page Down key"""
|
||||||
|
main_window = self.window()
|
||||||
|
if not hasattr(main_window, 'project') or not main_window.project or not main_window.project.pages:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_index = main_window._get_most_visible_page_index()
|
||||||
|
if current_index < len(main_window.project.pages) - 1:
|
||||||
|
next_page = main_window.project.pages[current_index + 1]
|
||||||
|
self._scroll_to_page(next_page, current_index + 1)
|
||||||
|
|
||||||
|
if hasattr(main_window, 'show_status'):
|
||||||
|
page_name = main_window.project.get_page_display_name(next_page)
|
||||||
|
main_window.show_status(f"Navigated to {page_name}", 2000)
|
||||||
|
|
||||||
|
def _navigate_to_previous_page(self):
|
||||||
|
"""Navigate to the previous page using Page Up key"""
|
||||||
|
main_window = self.window()
|
||||||
|
if not hasattr(main_window, 'project') or not main_window.project or not main_window.project.pages:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_index = main_window._get_most_visible_page_index()
|
||||||
|
if current_index > 0:
|
||||||
|
prev_page = main_window.project.pages[current_index - 1]
|
||||||
|
self._scroll_to_page(prev_page, current_index - 1)
|
||||||
|
|
||||||
|
if hasattr(main_window, 'show_status'):
|
||||||
|
page_name = main_window.project.get_page_display_name(prev_page)
|
||||||
|
main_window.show_status(f"Navigated to {page_name}", 2000)
|
||||||
|
|
||||||
|
def _scroll_to_page(self, page, page_index):
|
||||||
|
"""
|
||||||
|
Scroll the viewport to center a specific page.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: The page to scroll to
|
||||||
|
page_index: The index of the page in the project
|
||||||
|
"""
|
||||||
|
main_window = self.window()
|
||||||
|
if not hasattr(main_window, 'project'):
|
||||||
|
return
|
||||||
|
|
||||||
|
dpi = main_window.project.working_dpi
|
||||||
|
PAGE_MARGIN = 50
|
||||||
|
PAGE_SPACING = 50
|
||||||
|
|
||||||
|
# Calculate the Y offset for this page
|
||||||
|
y_offset = PAGE_MARGIN
|
||||||
|
for i in range(page_index):
|
||||||
|
prev_page = main_window.project.pages[i]
|
||||||
|
prev_height_mm = prev_page.layout.size[1]
|
||||||
|
prev_height_px = prev_height_mm * dpi / 25.4
|
||||||
|
y_offset += prev_height_px * self.zoom_level + PAGE_SPACING
|
||||||
|
|
||||||
|
# Get page height
|
||||||
|
page_height_mm = page.layout.size[1]
|
||||||
|
page_height_px = page_height_mm * dpi / 25.4
|
||||||
|
screen_page_height = page_height_px * self.zoom_level
|
||||||
|
|
||||||
|
# Center the page in the viewport
|
||||||
|
viewport_height = self.height()
|
||||||
|
target_pan_y = viewport_height / 2 - y_offset - screen_page_height / 2
|
||||||
|
|
||||||
|
self.pan_offset[1] = target_pan_y
|
||||||
|
|
||||||
|
# Clamp pan offset to content bounds
|
||||||
|
if hasattr(self, 'clamp_pan_offset'):
|
||||||
|
self.clamp_pan_offset()
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
# Update scrollbars if available
|
||||||
|
main_window = self.window()
|
||||||
|
if hasattr(main_window, 'update_scrollbars'):
|
||||||
|
main_window.update_scrollbars()
|
||||||
|
|
||||||
|
def _move_viewport_with_arrow_keys(self, key):
|
||||||
|
"""
|
||||||
|
Move the viewport using arrow keys when no objects are selected.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The Qt key code (Up, Down, Left, Right)
|
||||||
|
"""
|
||||||
|
# Movement amount in pixels
|
||||||
|
move_amount = 50
|
||||||
|
|
||||||
|
if key == Qt.Key.Key_Up:
|
||||||
|
self.pan_offset[1] += move_amount
|
||||||
|
elif key == Qt.Key.Key_Down:
|
||||||
|
self.pan_offset[1] -= move_amount
|
||||||
|
elif key == Qt.Key.Key_Left:
|
||||||
|
self.pan_offset[0] += move_amount
|
||||||
|
elif key == Qt.Key.Key_Right:
|
||||||
|
self.pan_offset[0] -= move_amount
|
||||||
|
|
||||||
|
# Clamp pan offset to content bounds
|
||||||
|
if hasattr(self, 'clamp_pan_offset'):
|
||||||
|
self.clamp_pan_offset()
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
# Update scrollbars if available
|
||||||
|
main_window = self.window()
|
||||||
|
if hasattr(main_window, 'update_scrollbars'):
|
||||||
|
main_window.update_scrollbars()
|
||||||
|
|
||||||
|
def _move_selected_elements_with_arrow_keys(self, key):
|
||||||
|
"""
|
||||||
|
Move selected elements using arrow keys.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The Qt key code (Up, Down, Left, Right)
|
||||||
|
"""
|
||||||
|
main_window = self.window()
|
||||||
|
if not hasattr(main_window, 'project'):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Movement amount in mm
|
||||||
|
move_amount_mm = 1.0 # 1mm per keypress
|
||||||
|
|
||||||
|
# Calculate movement delta
|
||||||
|
dx, dy = 0, 0
|
||||||
|
if key == Qt.Key.Key_Up:
|
||||||
|
dy = -move_amount_mm
|
||||||
|
elif key == Qt.Key.Key_Down:
|
||||||
|
dy = move_amount_mm
|
||||||
|
elif key == Qt.Key.Key_Left:
|
||||||
|
dx = -move_amount_mm
|
||||||
|
elif key == Qt.Key.Key_Right:
|
||||||
|
dx = move_amount_mm
|
||||||
|
|
||||||
|
# Move all selected elements
|
||||||
|
for element in self.selected_elements:
|
||||||
|
current_x, current_y = element.position
|
||||||
|
new_x = current_x + dx
|
||||||
|
new_y = current_y + dy
|
||||||
|
|
||||||
|
# Apply snapping if element has a parent page
|
||||||
|
if hasattr(element, '_parent_page') and element._parent_page:
|
||||||
|
page = element._parent_page
|
||||||
|
snap_sys = page.layout.snapping_system
|
||||||
|
page_size = page.layout.size
|
||||||
|
dpi = main_window.project.working_dpi
|
||||||
|
|
||||||
|
snapped_pos = snap_sys.snap_position(
|
||||||
|
position=(new_x, new_y),
|
||||||
|
size=element.size,
|
||||||
|
page_size=page_size,
|
||||||
|
dpi=dpi,
|
||||||
|
project=main_window.project
|
||||||
|
)
|
||||||
|
element.position = snapped_pos
|
||||||
|
else:
|
||||||
|
element.position = (new_x, new_y)
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
if hasattr(main_window, 'show_status'):
|
||||||
|
count = len(self.selected_elements)
|
||||||
|
elem_text = "element" if count == 1 else "elements"
|
||||||
|
main_window.show_status(f"Moved {count} {elem_text}", 1000)
|
||||||
@ -25,6 +25,9 @@ class MouseInteractionMixin:
|
|||||||
|
|
||||||
def mousePressEvent(self, event):
|
def mousePressEvent(self, event):
|
||||||
"""Handle mouse press events"""
|
"""Handle mouse press events"""
|
||||||
|
# Ensure widget has focus for keyboard events
|
||||||
|
self.setFocus()
|
||||||
|
|
||||||
if event.button() == Qt.MouseButton.LeftButton:
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
x, y = event.position().x(), event.position().y()
|
x, y = event.position().x(), event.position().y()
|
||||||
ctrl_pressed = event.modifiers() & Qt.KeyboardModifier.ControlModifier
|
ctrl_pressed = event.modifiers() & Qt.KeyboardModifier.ControlModifier
|
||||||
@ -123,8 +126,18 @@ class MouseInteractionMixin:
|
|||||||
self.pan_offset[0] += dx
|
self.pan_offset[0] += dx
|
||||||
self.pan_offset[1] += dy
|
self.pan_offset[1] += dy
|
||||||
|
|
||||||
|
# Clamp pan offset to content bounds
|
||||||
|
if hasattr(self, 'clamp_pan_offset'):
|
||||||
|
self.clamp_pan_offset()
|
||||||
|
|
||||||
self.drag_start_pos = (x, y)
|
self.drag_start_pos = (x, y)
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
# Update scrollbars if available
|
||||||
|
main_window = self.window()
|
||||||
|
if hasattr(main_window, 'update_scrollbars'):
|
||||||
|
main_window.update_scrollbars()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.is_dragging or not self.drag_start_pos:
|
if not self.is_dragging or not self.drag_start_pos:
|
||||||
@ -272,17 +285,35 @@ class MouseInteractionMixin:
|
|||||||
self.pan_offset[0] = mouse_x - world_x * self.zoom_level
|
self.pan_offset[0] = mouse_x - world_x * self.zoom_level
|
||||||
self.pan_offset[1] = mouse_y - world_y * self.zoom_level
|
self.pan_offset[1] = mouse_y - world_y * self.zoom_level
|
||||||
|
|
||||||
|
# Clamp pan offset to content bounds
|
||||||
|
if hasattr(self, 'clamp_pan_offset'):
|
||||||
|
self.clamp_pan_offset()
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
main_window = self.window()
|
main_window = self.window()
|
||||||
if hasattr(main_window, 'status_bar'):
|
if hasattr(main_window, 'status_bar'):
|
||||||
main_window.status_bar.showMessage(f"Zoom: {int(self.zoom_level * 100)}%", 2000)
|
main_window.status_bar.showMessage(f"Zoom: {int(self.zoom_level * 100)}%", 2000)
|
||||||
|
|
||||||
|
# Update scrollbars if available
|
||||||
|
if hasattr(main_window, 'update_scrollbars'):
|
||||||
|
main_window.update_scrollbars()
|
||||||
else:
|
else:
|
||||||
# Regular wheel: Vertical scroll
|
# Regular wheel: Vertical scroll
|
||||||
scroll_amount = delta * 0.5
|
scroll_amount = delta * 0.5
|
||||||
self.pan_offset[1] += scroll_amount
|
self.pan_offset[1] += scroll_amount
|
||||||
|
|
||||||
|
# Clamp pan offset to content bounds
|
||||||
|
if hasattr(self, 'clamp_pan_offset'):
|
||||||
|
self.clamp_pan_offset()
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
# Update scrollbars if available
|
||||||
|
main_window = self.window()
|
||||||
|
if hasattr(main_window, 'update_scrollbars'):
|
||||||
|
main_window.update_scrollbars()
|
||||||
|
|
||||||
def _edit_text_element(self, text_element):
|
def _edit_text_element(self, text_element):
|
||||||
"""Open dialog to edit text element"""
|
"""Open dialog to edit text element"""
|
||||||
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
||||||
|
|||||||
@ -33,6 +33,11 @@ class RenderingMixin:
|
|||||||
self.pan_offset = self._calculate_center_pan_offset(self.zoom_level)
|
self.pan_offset = self._calculate_center_pan_offset(self.zoom_level)
|
||||||
self.initial_zoom_set = True
|
self.initial_zoom_set = True
|
||||||
|
|
||||||
|
# Update scrollbars now that we have content bounds
|
||||||
|
main_window = self.window()
|
||||||
|
if hasattr(main_window, 'update_scrollbars'):
|
||||||
|
main_window.update_scrollbars()
|
||||||
|
|
||||||
dpi = main_window.project.working_dpi
|
dpi = main_window.project.working_dpi
|
||||||
|
|
||||||
# Calculate page positions with ghosts
|
# Calculate page positions with ghosts
|
||||||
|
|||||||
@ -41,6 +41,11 @@ class ViewportMixin:
|
|||||||
|
|
||||||
self.update()
|
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):
|
def _calculate_fit_to_screen_zoom(self):
|
||||||
"""
|
"""
|
||||||
Calculate zoom level to fit first page to screen.
|
Calculate zoom level to fit first page to screen.
|
||||||
@ -109,3 +114,77 @@ class ViewportMixin:
|
|||||||
y_offset = (window_height - screen_page_height) / 2
|
y_offset = (window_height - screen_page_height) / 2
|
||||||
|
|
||||||
return [x_offset, y_offset]
|
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]))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user