Duncan Tourolle 7f32858baf
All checks were successful
Python CI / test (push) Successful in 1m7s
Lint / lint (push) Successful in 1m11s
Tests / test (3.10) (push) Successful in 50s
Tests / test (3.11) (push) Successful in 51s
Tests / test (3.9) (push) Successful in 47s
big refactor to use mixin architecture
2025-11-11 10:35:24 +01:00

303 lines
11 KiB
Python

"""
Rendering mixin for GLWidget - handles OpenGL rendering
"""
from OpenGL.GL import *
from PyQt6.QtGui import QPainter, QFont, QColor, QPen
from PyQt6.QtCore import Qt, QRectF
from pyPhotoAlbum.models import TextBoxData
class RenderingMixin:
"""
Mixin providing OpenGL rendering functionality.
This mixin handles rendering pages, elements, selection handles,
and text overlays.
"""
def paintGL(self):
"""Main rendering function - renders all pages vertically"""
from pyPhotoAlbum.page_renderer import PageRenderer
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:
return
# Set initial zoom if not done yet
if not self.initial_zoom_set:
self.zoom_level = self._calculate_fit_to_screen_zoom()
self.initial_zoom_set = True
dpi = main_window.project.working_dpi
# Calculate page positions with ghosts
page_positions = self._get_page_positions()
# Store page renderers for later use
self._page_renderers = []
# Left margin for page rendering
PAGE_MARGIN = 50
# Render all pages
for page_info in page_positions:
page_type, page_or_ghost, y_offset = page_info
if page_type == 'page':
page = page_or_ghost
page_width_mm, page_height_mm = page.layout.size
screen_x = PAGE_MARGIN + self.pan_offset[0]
screen_y = (y_offset * self.zoom_level) + self.pan_offset[1]
renderer = PageRenderer(
page_width_mm=page_width_mm,
page_height_mm=page_height_mm,
screen_x=screen_x,
screen_y=screen_y,
dpi=dpi,
zoom=self.zoom_level
)
self._page_renderers.append((renderer, page))
renderer.begin_render()
page.layout.render(dpi=dpi)
renderer.end_render()
elif page_type == 'ghost':
ghost = page_or_ghost
ghost_width_mm, ghost_height_mm = ghost.page_size
screen_x = PAGE_MARGIN + self.pan_offset[0]
screen_y = (y_offset * self.zoom_level) + self.pan_offset[1]
renderer = PageRenderer(
page_width_mm=ghost_width_mm,
page_height_mm=ghost_height_mm,
screen_x=screen_x,
screen_y=screen_y,
dpi=dpi,
zoom=self.zoom_level
)
self._render_ghost_page(ghost, renderer)
# Update PageRenderer references for selected elements
for element in self.selected_elements:
if hasattr(element, '_parent_page'):
for renderer, page in self._page_renderers:
if page is element._parent_page:
element._page_renderer = renderer
break
# Draw selection handles
if self.selected_element:
self._draw_selection_handles()
# Render text overlays
self._render_text_overlays()
def _draw_selection_handles(self):
"""Draw selection handles around the selected element"""
if not self.selected_element:
return
main_window = self.window()
if not hasattr(main_window, 'project') or not main_window.project or not main_window.project.pages:
return
if not hasattr(self.selected_element, '_page_renderer'):
return
renderer = self.selected_element._page_renderer
elem_x, elem_y = self.selected_element.position
elem_w, elem_h = self.selected_element.size
handle_size = 8
x, y = renderer.page_to_screen(elem_x, elem_y)
w = elem_w * renderer.zoom
h = elem_h * renderer.zoom
center_x = x + w / 2
center_y = y + h / 2
if self.selected_element.rotation != 0:
glPushMatrix()
glTranslatef(center_x, center_y, 0)
glRotatef(self.selected_element.rotation, 0, 0, 1)
glTranslatef(-w / 2, -h / 2, 0)
x, y = 0, 0
if self.rotation_mode:
glColor3f(1.0, 0.5, 0.0)
else:
glColor3f(0.0, 0.5, 1.0)
glLineWidth(2.0)
glBegin(GL_LINE_LOOP)
glVertex2f(x, y)
glVertex2f(x + w, y)
glVertex2f(x + w, y + h)
glVertex2f(x, y + h)
glEnd()
glLineWidth(1.0)
if self.rotation_mode:
import math
handle_radius = 6
handles = [(x, y), (x + w, y), (x, y + h), (x + w, y + h)]
glColor3f(1.0, 0.5, 0.0)
glBegin(GL_TRIANGLE_FAN)
glVertex2f(center_x, center_y)
for angle in range(0, 361, 10):
rad = math.radians(angle)
hx = center_x + 3 * math.cos(rad)
hy = center_y + 3 * math.sin(rad)
glVertex2f(hx, hy)
glEnd()
for hx, hy in handles:
glColor3f(1.0, 1.0, 1.0)
glBegin(GL_TRIANGLE_FAN)
glVertex2f(hx, hy)
for angle in range(0, 361, 30):
rad = math.radians(angle)
px = hx + handle_radius * math.cos(rad)
py = hy + handle_radius * math.sin(rad)
glVertex2f(px, py)
glEnd()
glColor3f(1.0, 0.5, 0.0)
glBegin(GL_LINE_LOOP)
for angle in range(0, 361, 30):
rad = math.radians(angle)
px = hx + handle_radius * math.cos(rad)
py = hy + handle_radius * math.sin(rad)
glVertex2f(px, py)
glEnd()
else:
handles = [
(x - handle_size/2, y - handle_size/2),
(x + w - handle_size/2, y - handle_size/2),
(x - handle_size/2, y + h - handle_size/2),
(x + w - handle_size/2, y + h - handle_size/2),
]
glColor3f(1.0, 1.0, 1.0)
for hx, hy in handles:
glBegin(GL_QUADS)
glVertex2f(hx, hy)
glVertex2f(hx + handle_size, hy)
glVertex2f(hx + handle_size, hy + handle_size)
glVertex2f(hx, hy + handle_size)
glEnd()
glColor3f(0.0, 0.5, 1.0)
for hx, hy in handles:
glBegin(GL_LINE_LOOP)
glVertex2f(hx, hy)
glVertex2f(hx + handle_size, hy)
glVertex2f(hx + handle_size, hy + handle_size)
glVertex2f(hx, hy + handle_size)
glEnd()
if self.selected_element.rotation != 0:
glPopMatrix()
def _render_text_overlays(self):
"""Render text content for TextBoxData elements using QPainter overlay"""
if not hasattr(self, '_page_renderers') or not self._page_renderers:
return
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setRenderHint(QPainter.RenderHint.TextAntialiasing)
try:
for renderer, page in self._page_renderers:
text_elements = [elem for elem in page.layout.elements if isinstance(elem, TextBoxData)]
for element in text_elements:
if not element.text_content:
continue
x, y = element.position
w, h = element.size
screen_x, screen_y = renderer.page_to_screen(x, y)
screen_w = w * renderer.zoom
screen_h = h * renderer.zoom
font_family = element.font_settings.get('family', 'Arial')
font_size = int(element.font_settings.get('size', 12) * renderer.zoom)
font = QFont(font_family, font_size)
painter.setFont(font)
font_color = element.font_settings.get('color', (0, 0, 0))
if all(isinstance(c, int) and c > 1 for c in font_color):
color = QColor(*font_color)
else:
color = QColor(int(font_color[0] * 255), int(font_color[1] * 255), int(font_color[2] * 255))
painter.setPen(QPen(color))
if element.rotation != 0:
painter.save()
center_x = screen_x + screen_w / 2
center_y = screen_y + screen_h / 2
painter.translate(center_x, center_y)
painter.rotate(element.rotation)
painter.translate(-screen_w / 2, -screen_h / 2)
rect = QRectF(0, 0, screen_w, screen_h)
else:
rect = QRectF(screen_x, screen_y, screen_w, screen_h)
alignment = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
if element.alignment == 'center':
alignment = Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop
elif element.alignment == 'right':
alignment = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop
text_flags = Qt.TextFlag.TextWordWrap
painter.drawText(rect, int(alignment | text_flags), element.text_content)
if element.rotation != 0:
painter.restore()
finally:
painter.end()
def _render_ghost_page(self, ghost_data, renderer):
"""Render a ghost page using PageRenderer"""
renderer.begin_render()
ghost_data.render()
renderer.end_render()
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setRenderHint(QPainter.RenderHint.TextAntialiasing)
try:
px, py, pw, ph = ghost_data.get_page_rect()
screen_x, screen_y = renderer.page_to_screen(px, py)
screen_w = pw * renderer.zoom
screen_h = ph * renderer.zoom
font = QFont("Arial", int(16 * renderer.zoom), QFont.Weight.Bold)
painter.setFont(font)
painter.setPen(QColor(120, 120, 120))
rect = QRectF(screen_x, screen_y, screen_w, screen_h)
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, "Click to Add Page")
finally:
painter.end()