Re-organised ribbons. Fixed bug in rendering.
All checks were successful
Python CI / test (push) Successful in 1m31s
Lint / lint (push) Successful in 1m7s
Tests / test (3.11) (push) Successful in 1m43s
Tests / test (3.12) (push) Successful in 1m44s
Tests / test (3.13) (push) Successful in 1m38s
Tests / test (3.14) (push) Successful in 1m17s
All checks were successful
Python CI / test (push) Successful in 1m31s
Lint / lint (push) Successful in 1m7s
Tests / test (3.11) (push) Successful in 1m43s
Tests / test (3.12) (push) Successful in 1m44s
Tests / test (3.13) (push) Successful in 1m38s
Tests / test (3.14) (push) Successful in 1m17s
This commit is contained in:
parent
2a087f0c9d
commit
80d7d291f3
@ -62,6 +62,7 @@ try:
|
||||
# Clear operations
|
||||
glClear,
|
||||
glClearColor,
|
||||
glFlush,
|
||||
GL_COLOR_BUFFER_BIT,
|
||||
GL_DEPTH_BUFFER_BIT,
|
||||
# Viewport
|
||||
@ -93,7 +94,7 @@ except ImportError:
|
||||
glTexParameteri = glDeleteTextures = glTexCoord2f = _gl_stub
|
||||
glPushMatrix = glPopMatrix = glScalef = glTranslatef = _gl_stub
|
||||
glLoadIdentity = glRotatef = _gl_stub
|
||||
glClear = glClearColor = _gl_stub
|
||||
glClear = glClearColor = glFlush = _gl_stub
|
||||
glViewport = glMatrixMode = glOrtho = _gl_stub
|
||||
glGetString = _gl_stub
|
||||
|
||||
|
||||
@ -53,11 +53,20 @@ class GLWidget(
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Store reference to main window for accessing project
|
||||
self._main_window = parent
|
||||
|
||||
# Initialize async loading system
|
||||
self._init_async_loading()
|
||||
|
||||
# Initialize OpenGL
|
||||
self.setFormat(self.format())
|
||||
# Set up OpenGL surface format with explicit double buffering
|
||||
from PyQt6.QtGui import QSurfaceFormat
|
||||
fmt = QSurfaceFormat()
|
||||
fmt.setSwapBehavior(QSurfaceFormat.SwapBehavior.DoubleBuffer)
|
||||
fmt.setSwapInterval(1) # Enable vsync
|
||||
self.setFormat(fmt)
|
||||
|
||||
# Force full redraws to ensure viewport updates
|
||||
self.setUpdateBehavior(QOpenGLWidget.UpdateBehavior.NoPartialUpdate)
|
||||
|
||||
# Enable mouse tracking and drag-drop
|
||||
@ -68,6 +77,20 @@ class GLWidget(
|
||||
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.setFocus()
|
||||
|
||||
def window(self):
|
||||
"""Override window() to return stored main_window reference.
|
||||
|
||||
This fixes the Qt widget hierarchy issue where window() returns None
|
||||
because the GL widget is nested in container widgets.
|
||||
"""
|
||||
return self._main_window if hasattr(self, '_main_window') else super().window()
|
||||
|
||||
def update(self):
|
||||
"""Override update to force immediate repaint"""
|
||||
super().update()
|
||||
# Force immediate processing of paint events
|
||||
self.repaint()
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle widget close event."""
|
||||
# Cleanup async loading
|
||||
|
||||
@ -74,7 +74,7 @@ class EditOperationsMixin:
|
||||
@ribbon_action(
|
||||
label="Rotate Left",
|
||||
tooltip="Rotate selected element 90° counter-clockwise",
|
||||
tab="Home",
|
||||
tab="Arrange",
|
||||
group="Transform",
|
||||
requires_selection=True,
|
||||
)
|
||||
@ -97,7 +97,7 @@ class EditOperationsMixin:
|
||||
@ribbon_action(
|
||||
label="Rotate Right",
|
||||
tooltip="Rotate selected element 90° clockwise",
|
||||
tab="Home",
|
||||
tab="Arrange",
|
||||
group="Transform",
|
||||
requires_selection=True,
|
||||
)
|
||||
@ -120,7 +120,7 @@ class EditOperationsMixin:
|
||||
@ribbon_action(
|
||||
label="Reset Rotation",
|
||||
tooltip="Reset selected element rotation to 0°",
|
||||
tab="Home",
|
||||
tab="Arrange",
|
||||
group="Transform",
|
||||
requires_selection=True,
|
||||
)
|
||||
|
||||
@ -586,7 +586,7 @@ class FileOperationsMixin:
|
||||
x, y = element.position
|
||||
element.position = (x + x_offset, y + y_offset)
|
||||
|
||||
@ribbon_action(label="Export PDF", tooltip="Export project to PDF", tab="Export", group="Export")
|
||||
@ribbon_action(label="Export PDF", tooltip="Export project to PDF", tab="Home", group="File")
|
||||
def export_pdf(self):
|
||||
"""Export project to PDF using async backend (non-blocking)"""
|
||||
# Check if we have pages to export
|
||||
|
||||
@ -19,8 +19,8 @@ class MergeOperationsMixin:
|
||||
@ribbon_action(
|
||||
label="Merge Projects",
|
||||
tooltip="Merge another project file with the current project",
|
||||
tab="File",
|
||||
group="Import/Export",
|
||||
tab="Home",
|
||||
group="File",
|
||||
)
|
||||
def merge_projects(self):
|
||||
"""
|
||||
|
||||
@ -54,7 +54,13 @@ class ViewOperationsMixin:
|
||||
self.update_view()
|
||||
self.show_status(f"Zoom: {int(self.gl_widget.zoom_level * 100)}%", 2000)
|
||||
|
||||
@ribbon_action(label="Toggle Grid Snap", tooltip="Toggle snapping to grid", tab="View", group="Snapping")
|
||||
@ribbon_action(
|
||||
label="Grid Snap",
|
||||
tooltip="Enable/disable snapping to grid (Ctrl+G)",
|
||||
tab="Insert",
|
||||
group="Snapping",
|
||||
shortcut="Ctrl+G",
|
||||
)
|
||||
def toggle_grid_snap(self):
|
||||
"""Toggle grid snapping"""
|
||||
if not self.project:
|
||||
@ -67,7 +73,13 @@ class ViewOperationsMixin:
|
||||
self.show_status(f"Grid snapping {status}", 2000)
|
||||
print(f"Grid snapping {status}")
|
||||
|
||||
@ribbon_action(label="Toggle Edge Snap", tooltip="Toggle snapping to page edges", tab="View", group="Snapping")
|
||||
@ribbon_action(
|
||||
label="Edge Snap",
|
||||
tooltip="Enable/disable snapping to page edges (Ctrl+E)",
|
||||
tab="Insert",
|
||||
group="Snapping",
|
||||
shortcut="Ctrl+E",
|
||||
)
|
||||
def toggle_edge_snap(self):
|
||||
"""Toggle edge snapping"""
|
||||
if not self.project:
|
||||
@ -80,7 +92,7 @@ class ViewOperationsMixin:
|
||||
self.show_status(f"Edge snapping {status}", 2000)
|
||||
print(f"Edge snapping {status}")
|
||||
|
||||
@ribbon_action(label="Toggle Guide Snap", tooltip="Toggle snapping to guides", tab="View", group="Snapping")
|
||||
@ribbon_action(label="Guide Snap", tooltip="Enable/disable snapping to guides", tab="Insert", group="Snapping")
|
||||
def toggle_guide_snap(self):
|
||||
"""Toggle guide snapping"""
|
||||
if not self.project:
|
||||
@ -93,7 +105,7 @@ class ViewOperationsMixin:
|
||||
self.show_status(f"Guide snapping {status}", 2000)
|
||||
print(f"Guide snapping {status}")
|
||||
|
||||
@ribbon_action(label="Show Grid", tooltip="Toggle visibility of grid lines", tab="View", group="Snapping")
|
||||
@ribbon_action(label="Show Grid", tooltip="Toggle visibility of grid lines", tab="Insert", group="Snapping")
|
||||
def toggle_show_grid(self):
|
||||
"""Toggle grid visibility"""
|
||||
if not self.project:
|
||||
@ -106,7 +118,7 @@ class ViewOperationsMixin:
|
||||
self.show_status(f"Grid {status}", 2000)
|
||||
print(f"Grid {status}")
|
||||
|
||||
@ribbon_action(label="Show Guides", tooltip="Toggle visibility of guide lines", tab="View", group="Snapping")
|
||||
@ribbon_action(label="Show Guides", tooltip="Toggle visibility of guide lines", tab="Insert", group="Snapping")
|
||||
def toggle_snap_lines(self):
|
||||
"""Toggle guide lines visibility"""
|
||||
if not self.project:
|
||||
@ -164,7 +176,7 @@ class ViewOperationsMixin:
|
||||
print(f"Cleared {guide_count} guides")
|
||||
|
||||
@ribbon_action(
|
||||
label="Set Grid Size...", tooltip="Configure grid spacing for snapping", tab="View", group="Snapping"
|
||||
label="Grid Settings...", tooltip="Configure grid size and snap threshold", tab="Insert", group="Snapping"
|
||||
)
|
||||
def set_grid_size(self):
|
||||
"""Open dialog to set grid size"""
|
||||
@ -234,50 +246,3 @@ class ViewOperationsMixin:
|
||||
self.update_view()
|
||||
self.show_status(f"Grid size: {new_grid_size:.1f}mm, Threshold: {new_threshold:.1f}mm", 2000)
|
||||
print(f"Updated grid settings - Size: {new_grid_size:.1f}mm, Threshold: {new_threshold:.1f}mm")
|
||||
|
||||
# ===== Layout Tab Snapping Controls =====
|
||||
# These provide easy access to snapping features during layout work
|
||||
|
||||
@ribbon_action(
|
||||
label="Grid Snap",
|
||||
tooltip="Enable/disable snapping to grid (Ctrl+G)",
|
||||
tab="Layout",
|
||||
group="Snapping",
|
||||
shortcut="Ctrl+G",
|
||||
)
|
||||
def layout_toggle_grid_snap(self):
|
||||
"""Toggle grid snapping (Layout tab)"""
|
||||
self.toggle_grid_snap()
|
||||
|
||||
@ribbon_action(
|
||||
label="Edge Snap",
|
||||
tooltip="Enable/disable snapping to page edges (Ctrl+E)",
|
||||
tab="Layout",
|
||||
group="Snapping",
|
||||
shortcut="Ctrl+E",
|
||||
)
|
||||
def layout_toggle_edge_snap(self):
|
||||
"""Toggle edge snapping (Layout tab)"""
|
||||
self.toggle_edge_snap()
|
||||
|
||||
@ribbon_action(label="Guide Snap", tooltip="Enable/disable snapping to guides", tab="Layout", group="Snapping")
|
||||
def layout_toggle_guide_snap(self):
|
||||
"""Toggle guide snapping (Layout tab)"""
|
||||
self.toggle_guide_snap()
|
||||
|
||||
@ribbon_action(label="Show Grid", tooltip="Toggle visibility of grid lines", tab="Layout", group="Snapping")
|
||||
def layout_toggle_show_grid(self):
|
||||
"""Toggle grid visibility (Layout tab)"""
|
||||
self.toggle_show_grid()
|
||||
|
||||
@ribbon_action(label="Show Guides", tooltip="Toggle visibility of guide lines", tab="Layout", group="Snapping")
|
||||
def layout_toggle_snap_lines(self):
|
||||
"""Toggle guide lines visibility (Layout tab)"""
|
||||
self.toggle_snap_lines()
|
||||
|
||||
@ribbon_action(
|
||||
label="Grid Settings...", tooltip="Configure grid size and snap threshold", tab="Layout", group="Snapping"
|
||||
)
|
||||
def layout_set_grid_size(self):
|
||||
"""Open grid settings dialog (Layout tab)"""
|
||||
self.set_grid_size()
|
||||
|
||||
@ -70,15 +70,23 @@ class PageNavigationMixin:
|
||||
Returns:
|
||||
List of tuples (page_type, page_or_ghost_data, y_offset)
|
||||
"""
|
||||
main_window = self.window()
|
||||
if not hasattr(main_window, "project"):
|
||||
# Use stored reference to main window
|
||||
main_window = getattr(self, '_main_window', None)
|
||||
if main_window is None:
|
||||
main_window = self.window()
|
||||
|
||||
try:
|
||||
project = main_window.project
|
||||
if not project:
|
||||
return []
|
||||
except (AttributeError, TypeError):
|
||||
return []
|
||||
|
||||
dpi = main_window.project.working_dpi
|
||||
dpi = project.working_dpi
|
||||
|
||||
# Use project's page_spacing_mm setting (default is 10mm = 1cm)
|
||||
# Convert to pixels at working DPI
|
||||
spacing_mm = main_window.project.page_spacing_mm
|
||||
spacing_mm = project.page_spacing_mm
|
||||
spacing_px = spacing_mm * dpi / 25.4
|
||||
|
||||
# Start with a small top margin (5mm)
|
||||
@ -89,7 +97,7 @@ class PageNavigationMixin:
|
||||
current_y = top_margin_px # Initial top offset in pixels (not screen pixels)
|
||||
|
||||
# First, render cover if it exists
|
||||
for page in main_window.project.pages:
|
||||
for page in project.pages:
|
||||
if page.is_cover:
|
||||
result.append(("page", page, current_y))
|
||||
|
||||
@ -102,7 +110,7 @@ class PageNavigationMixin:
|
||||
break # Only one cover allowed
|
||||
|
||||
# Get page layout with ghosts from project (this excludes cover)
|
||||
layout_with_ghosts = main_window.project.calculate_page_layout_with_ghosts()
|
||||
layout_with_ghosts = project.calculate_page_layout_with_ghosts()
|
||||
|
||||
for page_type, page_obj, logical_pos in layout_with_ghosts:
|
||||
if page_type == "page":
|
||||
@ -119,7 +127,7 @@ class PageNavigationMixin:
|
||||
|
||||
elif page_type == "ghost":
|
||||
# Ghost page - use default page size
|
||||
page_size_mm = main_window.project.page_size_mm
|
||||
page_size_mm = project.page_size_mm
|
||||
from pyPhotoAlbum.models import GhostPageData
|
||||
|
||||
# Create ghost page data with correct size
|
||||
|
||||
@ -25,8 +25,23 @@ class RenderingMixin:
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
|
||||
glLoadIdentity()
|
||||
|
||||
main_window = self.window()
|
||||
if not hasattr(main_window, "project") or not main_window.project or not main_window.project.pages:
|
||||
# Use stored reference to main window
|
||||
main_window = getattr(self, '_main_window', None)
|
||||
if main_window is None:
|
||||
# Fallback to window() if _main_window not set
|
||||
main_window = self.window()
|
||||
|
||||
if main_window is None:
|
||||
return
|
||||
|
||||
try:
|
||||
project = main_window.project
|
||||
if not project:
|
||||
return
|
||||
if not project.pages:
|
||||
return
|
||||
except AttributeError:
|
||||
# Project not yet initialized
|
||||
return
|
||||
|
||||
# Set initial zoom and center the page if not done yet
|
||||
@ -36,11 +51,10 @@ class RenderingMixin:
|
||||
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()
|
||||
if hasattr(self, '_main_window') and hasattr(self._main_window, "update_scrollbars"):
|
||||
self._main_window.update_scrollbars()
|
||||
|
||||
dpi = main_window.project.working_dpi
|
||||
dpi = project.working_dpi
|
||||
|
||||
# Calculate page positions with ghosts
|
||||
page_positions = self._get_page_positions()
|
||||
@ -52,6 +66,7 @@ class RenderingMixin:
|
||||
PAGE_MARGIN = 50
|
||||
|
||||
# Render all pages
|
||||
pages_rendered = 0
|
||||
for page_info in page_positions:
|
||||
page_type, page_or_ghost, y_offset = page_info
|
||||
|
||||
@ -76,8 +91,9 @@ class RenderingMixin:
|
||||
renderer.begin_render()
|
||||
# Pass widget reference for async loading
|
||||
page.layout._parent_widget = self
|
||||
page.layout.render(dpi=dpi, project=main_window.project)
|
||||
page.layout.render(dpi=dpi, project=project)
|
||||
renderer.end_render()
|
||||
pages_rendered += 1
|
||||
|
||||
elif page_type == "ghost":
|
||||
ghost = page_or_ghost
|
||||
@ -109,7 +125,8 @@ class RenderingMixin:
|
||||
for element in self.selected_elements:
|
||||
self._draw_selection_handles(element)
|
||||
|
||||
# Render text overlays
|
||||
# Render text overlays using QPainter
|
||||
# Qt will handle OpenGL/QPainter coordination automatically
|
||||
self._render_text_overlays()
|
||||
|
||||
def _draw_selection_handles(self, element):
|
||||
|
||||
@ -74,7 +74,7 @@ def build_ribbon_config(window_class: Type) -> Dict[str, Any]:
|
||||
ribbon_config = {}
|
||||
|
||||
# Define tab order (tabs will appear in this order)
|
||||
tab_order = ["Home", "Insert", "Layout", "Arrange", "View", "Export"]
|
||||
tab_order = ["Home", "Insert", "Layout", "Arrange", "View"]
|
||||
|
||||
# Add tabs in the defined order, then add any remaining tabs
|
||||
all_tabs = list(tabs.keys())
|
||||
@ -90,11 +90,10 @@ def build_ribbon_config(window_class: Type) -> Dict[str, Any]:
|
||||
# Define group order per tab (if needed)
|
||||
group_orders = {
|
||||
"Home": ["File", "Edit"],
|
||||
"Insert": ["Media"],
|
||||
"Layout": ["Navigation", "Page", "Templates"],
|
||||
"Arrange": ["Align", "Size", "Distribute"],
|
||||
"View": ["Zoom"],
|
||||
"Export": ["Export"],
|
||||
"Insert": ["Media", "Snapping"],
|
||||
"Layout": ["Page", "Templates"],
|
||||
"Arrange": ["Align", "Distribute", "Size", "Order", "Transform"],
|
||||
"View": ["Zoom", "Guides"],
|
||||
}
|
||||
|
||||
# Get the group order for this tab, or use alphabetical
|
||||
|
||||
@ -373,9 +373,9 @@ class TestLayoutTabDelegation:
|
||||
|
||||
window.project.snap_to_grid = False
|
||||
|
||||
window.layout_toggle_grid_snap()
|
||||
window.toggle_grid_snap()
|
||||
|
||||
# Should delegate to toggle_grid_snap
|
||||
# Should toggle snap_to_grid
|
||||
assert window.project.snap_to_grid is True
|
||||
assert window._update_view_called
|
||||
|
||||
@ -385,7 +385,7 @@ class TestLayoutTabDelegation:
|
||||
|
||||
window.project.snap_to_edges = False
|
||||
|
||||
window.layout_toggle_edge_snap()
|
||||
window.toggle_edge_snap()
|
||||
|
||||
assert window.project.snap_to_edges is True
|
||||
assert window._update_view_called
|
||||
@ -396,7 +396,7 @@ class TestLayoutTabDelegation:
|
||||
|
||||
window.project.snap_to_guides = False
|
||||
|
||||
window.layout_toggle_guide_snap()
|
||||
window.toggle_guide_snap()
|
||||
|
||||
assert window.project.snap_to_guides is True
|
||||
assert window._update_view_called
|
||||
@ -407,13 +407,13 @@ class TestLayoutTabDelegation:
|
||||
|
||||
window.project.show_snap_lines = False
|
||||
|
||||
window.layout_toggle_snap_lines()
|
||||
window.toggle_snap_lines()
|
||||
|
||||
assert window.project.show_snap_lines is True
|
||||
assert window._update_view_called
|
||||
|
||||
def test_layout_set_grid_size_delegates(self, qtbot):
|
||||
"""Test layout tab grid size delegates to main method"""
|
||||
"""Test grid size dialog works"""
|
||||
window = TestViewWindow()
|
||||
qtbot.addWidget(window)
|
||||
|
||||
@ -422,12 +422,12 @@ class TestLayoutTabDelegation:
|
||||
page.layout = layout
|
||||
window._current_page = page
|
||||
|
||||
# Mock dialog to verify delegation
|
||||
# Mock dialog to verify it runs
|
||||
mock_dialog = Mock(spec=QDialog)
|
||||
mock_dialog.exec.return_value = QDialog.DialogCode.Rejected
|
||||
|
||||
with patch("PyQt6.QtWidgets.QDialog", return_value=mock_dialog):
|
||||
window.layout_set_grid_size()
|
||||
window.set_grid_size()
|
||||
|
||||
# Verify method was called (dialog creation attempted)
|
||||
# Verify dialog was shown
|
||||
mock_dialog.exec.assert_called_once()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user