Stable with arrow nav

everything working with new keyboard arrow keys nav for next/prev image. Will add png chunk metadata next.
This commit is contained in:
Beak
2025-09-29 11:17:38 -07:00
parent 76fd6c28e0
commit 5a32ad4100
6 changed files with 187 additions and 414 deletions
Binary file not shown.
Binary file not shown.
-25
View File
@@ -1,25 +0,0 @@
# helpers.py
from PySide6.QtGui import QPixmap
def load_image(path: str) -> QPixmap:
"""
Load an image from disk and return a QPixmap.
In the future, this can also handle scaling, caching, etc.
"""
pixmap = QPixmap(path)
if pixmap.isNull():
raise FileNotFoundError(f"Could not load image: {path}")
return pixmap
def get_fake_metadata(path: str) -> dict:
"""
Temporary stub: returns fake metadata.
Replace later with real EXIF/PNG chunk parsing.
"""
return {
"Filename": path,
"Width": 1920,
"Height": 1080,
"Format": "PNG"
}
+15 -2
View File
@@ -1,9 +1,10 @@
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
from PySide6.QtGui import QPixmap, QDragEnterEvent, QDropEvent, QPainter
from PySide6.QtCore import Qt, Signal
from PySide6.QtOpenGLWidgets import QOpenGLWidget # ← ADD THIS IMPORT
from PySide6.QtOpenGLWidgets import QOpenGLWidget
import os
"""this defines the 'canvas' class, meaning the area where images are dropped and displayed"""
class ImageCanvas(QGraphicsView):
# Signal emitted when a valid file is dropped
@@ -33,9 +34,21 @@ class ImageCanvas(QGraphicsView):
self.setRenderHint(QPainter.Antialiasing, True)
self.setRenderHint(QPainter.SmoothPixmapTransform, True)
# ← ADD OPENGL ACCELERATION HERE
self.setViewport(QOpenGLWidget()) # Hardware-accelerated rendering
# Enable keyboard focus for the canvas
self.setFocusPolicy(Qt.StrongFocus)
def keyPressEvent(self, event):
"""Forward arrow keys to main window for navigation"""
if event.key() in (Qt.Key_Right, Qt.Key_Left):
main_window = self.window() # Get the actual main window, not just the immediate parent
if main_window:
main_window.keyPressEvent(event)
event.accept()
else:
super().keyPressEvent(event)
# --- Drag & Drop Events ---
def dragEnterEvent(self, event: QDragEnterEvent):
if event.mimeData().hasUrls():
+89 -192
View File
@@ -5,35 +5,36 @@ from PySide6.QtWidgets import (
)
from PySide6.QtCore import Qt
from image_canvas import ImageCanvas
from image_folder import ImageFolder
from PySide6.QtWidgets import QLabel
from PySide6.QtGui import QPixmap
from PySide6.QtCore import QTimer
import os
from PySide6.QtCore import Qt, QTimer, QEvent
import sys
import helpers
class MetaViewApp(QMainWindow):
from metaview_logic import MetaViewLogicMixin
class MetaViewApp(MetaViewLogicMixin, QMainWindow):
# Now inherits from both QMainWindow and our logic mixin
def __init__(self):
super().__init__()
self.setWindowTitle("Image Viewer Layout")
self.resize(1400, 900)
self.folder_model = None
self.setWindowTitle("Image Viewer GL Layout")
# Image cache
self.image_cache = {} # Dictionary to cache loaded images
self.cache_size = 5 # Number of images to keep cached
# --- 4:3 Optimized Window Size ---
# Calculate for 1024x768 image + UI elements
self.resize(1400, 900) # Wider window to accommodate right sidebar
self.folder_model = None
self.image_cache = {}
self.cache_size = 5
# --- Design reference sizes (for proportional scaling) ---
self.design_canvas_w = 672
self.design_canvas_h = 504
self.design_thumb_w = self.design_canvas_w // 5 # 134
self.design_thumb_h = self.design_thumb_w * 3 // 4 # 100
self.design_total_h = (
self.design_canvas_h + self.design_thumb_h + 40 + 20 + 20
)
# --- Design reference sizes for 4:3 ---
self.design_canvas_w = 1024 # Target image width
self.design_canvas_h = 768 # Target image height
self.design_thumb_w = 120 # Thumbnail width
self.design_thumb_h = 90 # Thumbnail height (4:3)
self.scale_factor = 1.0
# ---- Central Widget ----
@@ -42,34 +43,49 @@ class MetaViewApp(QMainWindow):
# ---- Master Grid Layout ----
self.main_layout = QGridLayout(central)
self.main_layout.setContentsMargins(10, 20, 30, 20)
self.main_layout.setContentsMargins(10, 20, 10, 20)
self.main_layout.setSpacing(10)
# Column weights like Tkinter's columnconfigure
self.main_layout.setColumnStretch(0, 0) # left sidebar
self.main_layout.setColumnStretch(1, 5) # center area
self.main_layout.setColumnStretch(2, 3) # right panel future
# Column weights - center area for image, right for metadata
self.main_layout.setColumnStretch(0, 0) # left aesthetic spacer (fixed)
self.main_layout.setColumnStretch(1, 3) # center image area (main content)
self.main_layout.setColumnStretch(2, 1) # right metadata panel (~1/4 width)
self.main_layout.setRowStretch(0, 1) # main row expands
# --- Left Sidebar ---
self.left_margin_frame = QFrame()
left_layout = QVBoxLayout(self.left_margin_frame)
left_layout.addWidget(QLabel("Sidebar"))
self.main_layout.addWidget(self.left_margin_frame, 0, 0)
# --- Left Spacer (Aesthetic) ---
self.left_spacer = QFrame()
self.left_spacer.setFixedWidth(40) # Small fixed width spacer
self.left_spacer.setStyleSheet("background: transparent;")
self.main_layout.addWidget(self.left_spacer, 0, 0)
# --- Center Image Area ---
self.center_group = QGroupBox("Image View and Controls")
self.center_group = QGroupBox("Image View")
self.center_group.setStyleSheet("""
QGroupBox {
border: 2px solid #333333;
border-radius: 4px;
margin-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left;
left: 10px;
padding: 0 5px 0 5px;
background: palette(window);
}
""")
self.main_layout.addWidget(self.center_group, 0, 1)
center_layout = QGridLayout(self.center_group)
center_layout.setRowStretch(0, 5) # image view
center_layout.setRowStretch(0, 5) # image view (most space)
center_layout.setRowStretch(1, 1) # filmstrip
center_layout.setRowStretch(2, 0) # buttons
center_layout.setRowStretch(3, 0) # path display later
center_layout.setColumnStretch(0, 1)
# ---- Image Canvas Equivalent ----
# ---- Image Canvas ----
self.image_view = ImageCanvas()
self.image_view.fileDropped.connect(self.load_image_from_path)
self.image_view.setMinimumSize(800, 600) # 4:3 minimum
center_layout.addWidget(self.image_view, 0, 0)
# ---- Filmstrip Frame ----
@@ -85,11 +101,12 @@ class MetaViewApp(QMainWindow):
# Thumbnail scroller
self.thumb_scroll_area = QScrollArea()
self.thumb_scroll_area.setWidgetResizable(True)
self.thumb_scroll_area.setMinimumHeight(100)
self.thumb_container = QWidget()
self.thumb_layout = QHBoxLayout(self.thumb_container)
self.thumb_scroll_area.setWidget(self.thumb_container)
filmstrip_layout.addWidget(self.thumb_scroll_area)
center_layout.addWidget(self.filmstrip_frame, 1, 0)
# ---- Button Frame ----
@@ -106,176 +123,56 @@ class MetaViewApp(QMainWindow):
center_layout.addWidget(self.button_frame, 2, 0)
open_btn.clicked.connect(self.open_test_image)
# --- Right Metadata Panel ---
self.right_metadata = QGroupBox("Metadata")
self.right_metadata.setMinimumWidth(300) # ~1/4 of window width
metadata_layout = QVBoxLayout(self.right_metadata)
# Placeholder metadata content
metadata_layout.addWidget(QLabel("File Information:"))
self.file_info_label = QLabel("No image loaded")
metadata_layout.addWidget(self.file_info_label)
metadata_layout.addWidget(QLabel("AI Parameters:"))
self.ai_params_label = QLabel("Waiting for image...")
metadata_layout.addWidget(self.ai_params_label)
metadata_layout.addStretch() # Push content to top
self.main_layout.addWidget(self.right_metadata, 0, 2)
# ---- Connect Signals ----
open_btn.clicked.connect(self.open_image_file)
next_btn.clicked.connect(self.show_next_image)
prev_btn.clicked.connect(self.show_prev_image)
# REMOVED THE PROBLEMATIC LINE: self.resizeEvent(None)
# Instead, use a timer to initialize after UI is fully built
# Initialize after UI is built
QTimer.singleShot(100, self.initialize_scale)
def initialize_scale(self):
"""Initialize scale factor after UI is fully built"""
self.update_scale_factor()
if self.folder_model:
self.populate_filmstrip()
def update_scale_factor(self):
"""Calculate scale factor based on current sizes"""
if not hasattr(self, "image_view") or not hasattr(self, "filmstrip_frame"):
return
# Install event filter on THIS window only
self.installEventFilter(self)
# Current available width and height
avail_w = self.image_view.width()
avail_h = self.image_view.height()
print(f"Available space for image: {avail_w}x{avail_h}")
# Compute scale factors relative to design sizes
scale_w = avail_w / self.design_canvas_w
scale_h = avail_h / self.design_total_h
self.scale_factor = min(scale_w, scale_h)
print(f"Scale factor updated: {self.scale_factor}")
def eventFilter(self, obj, event):
if event.type() == QEvent.KeyPress:
print(f"🎹 EVENT FILTER: Key {event.key()} in {type(obj).__name__}")
if event.key() in (Qt.Key_Right, Qt.Key_Left):
print("🎹 EVENT FILTER: Calling keyPressEvent directly")
result = self.keyPressEvent(event)
print(f"🎹 EVENT FILTER: keyPressEvent returned {result}")
return True # Event handled, stop propagation
return super().eventFilter(obj, event)
# --- Example method using helpers ---
def open_test_image(self):
"""
Demo function: load a test image and print metadata.
"""
path = "test.png" # placeholder
try:
pixmap = helpers.load_image(path)
meta = helpers.get_fake_metadata(path)
print("Metadata:", meta)
except FileNotFoundError as e:
print(e)
#load image with path
def load_image_from_path(self, file_path):
folder = os.path.dirname(file_path)
self.folder_model = ImageFolder(folder, start_file=file_path)
# Clear cache when folder changes
self.image_cache.clear()
current = self.folder_model.current()
if current:
print("Displaying:", current["name"])
self.display_image(current["path"])
self.populate_filmstrip()
self.preload_adjacent_images()
# New method for displaying images
def display_image(self, path):
"""Display image from cache or load it"""
if path in self.image_cache:
# Use cached pixmap
print(f"Using cached image: {os.path.basename(path)}")
self.image_view.load_image_from_pixmap(self.image_cache[path])
else:
# Load and cache
pixmap = QPixmap(path)
if not pixmap.isNull():
self.image_cache[path] = pixmap
self.image_view.load_image(path)
# New method for preloading
def preload_adjacent_images(self):
"""Pre-load images around current index"""
if not self.folder_model or not self.folder_model.files:
return
files = self.folder_model.files
current_idx = self.folder_model.index
# Pre-load 2 images on each side
for offset in range(-2, 3):
if offset == 0:
continue # Skip current (already loaded)
idx = (current_idx + offset) % len(files)
path = files[idx]["path"]
if path not in self.image_cache:
# Load into cache but don't display
print(f"Pre-loading: {os.path.basename(path)}")
pixmap = QPixmap(path)
if not pixmap.isNull():
self.image_cache[path] = pixmap
def show_next_image(self):
if self.folder_model:
current = self.folder_model.next()
if current:
self.display_image(current["path"])
self.populate_filmstrip()
self.preload_adjacent_images()
def show_prev_image(self):
if self.folder_model:
current = self.folder_model.prev()
if current:
self.display_image(current["path"])
self.populate_filmstrip()
self.preload_adjacent_images()
# Mouse wheel handler
def wheelEvent(self, event):
if event.angleDelta().y() > 0:
# wheel scrolled up
self.show_prev_image()
else:
# wheel scrolled down
self.show_next_image()
event.accept()
def resizeEvent(self, event):
super().resizeEvent(event)
self.update_scale_factor()
# Rebuild thumbs at new scale
if self.folder_model:
self.populate_filmstrip()
def populate_filmstrip(self):
# Clear old thumbnails
for i in reversed(range(self.thumb_layout.count())):
item = self.thumb_layout.takeAt(i)
if widget := item.widget():
widget.deleteLater()
if not self.folder_model:
return
# Scale design size by current factor
thumb_w = max(64, int(self.design_thumb_w * self.scale_factor))
thumb_h = max(48, int(self.design_thumb_h * self.scale_factor))
# Show 5 thumbnails centered on current index
center_index = self.folder_model.index
files = self.folder_model.files
if not files:
return
for offset in range(-2, 3):
idx = (center_index + offset) % len(files)
file_path = files[idx]["path"]
pix = QPixmap(file_path).scaled(
thumb_w, thumb_h,
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
lbl = QLabel()
lbl.setPixmap(pix)
lbl.setAlignment(Qt.AlignCenter)
self.thumb_layout.addWidget(lbl)
def check_focus(self):
"""Check what has focus"""
print(f"🎯 Current focus widget: {self.focusWidget()}")
print(f"🎯 Main window has focus: {self.hasFocus()}")
print(f"🎯 Image view has focus: {self.image_view.hasFocus()}")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MetaViewApp()
window.show()
sys.exit(app.exec())
+83 -195
View File
@@ -1,169 +1,55 @@
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QFrame, QGroupBox,
QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QPushButton,
QScrollArea
)
# metaview_logic.py
from PySide6.QtCore import Qt
from image_canvas import ImageCanvas
from image_folder import ImageFolder
from PySide6.QtWidgets import QLabel
from PySide6.QtGui import QPixmap
from PySide6.QtCore import QTimer
from PySide6.QtGui import QPainter
from ui_styles import create_styled_groupbox
from PySide6.QtWidgets import QFileDialog
from pathlib import Path
import struct
import zlib
import os
import sys
import helpers
class MetaViewLogicMixin:
"""All the behavior logic goes here - no UI creation"""
# Mouse wheel handler
def wheelEvent(self, event):
if event.angleDelta().y() > 0:
# wheel scrolled up
self.show_prev_image()
else:
# wheel scrolled down
self.show_next_image()
class MetaViewApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Image Viewer GL Layout")
# --- 4:3 Optimized Window Size ---
# Calculate for 1024x768 image + UI elements
self.resize(1400, 900) # Wider window to accommodate right sidebar
self.folder_model = None
self.image_cache = {}
self.cache_size = 5
# --- Design reference sizes for 4:3 ---
self.design_canvas_w = 1024 # Target image width
self.design_canvas_h = 768 # Target image height
self.design_thumb_w = 120 # Thumbnail width
self.design_thumb_h = 90 # Thumbnail height (4:3)
self.scale_factor = 1.0
# ---- Central Widget ----
central = QWidget()
self.setCentralWidget(central)
# ---- Master Grid Layout ----
self.main_layout = QGridLayout(central)
self.main_layout.setContentsMargins(10, 20, 10, 20)
self.main_layout.setSpacing(10)
# Column weights - center area for image, right for metadata
self.main_layout.setColumnStretch(0, 0) # left aesthetic spacer (fixed)
self.main_layout.setColumnStretch(1, 3) # center image area (main content)
self.main_layout.setColumnStretch(2, 1) # right metadata panel (~1/4 width)
self.main_layout.setRowStretch(0, 1) # main row expands
# --- Left Spacer (Aesthetic) ---
self.left_spacer = QFrame()
self.left_spacer.setFixedWidth(40) # Small fixed width spacer
self.left_spacer.setStyleSheet("background: transparent;")
self.main_layout.addWidget(self.left_spacer, 0, 0)
# --- Center Image Area ---
self.center_group = QGroupBox("Image View")
# Keep everything default Qt style, just make borders darker
#self.center_group.setStyleSheet("QGroupBox { border: 2px solid red; }")
self.center_group.setStyleSheet("""
QGroupBox {
border: 2px solid #333333;
border-radius: 4px;
margin-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left;
left: 10px;
padding: 0 5px 0 5px;
background: palette(window);
}
""")
#self.right_metadata.setStyleSheet("QGroupBox { border-color: #222222; }")
self.main_layout.addWidget(self.center_group, 0, 1)
center_layout = QGridLayout(self.center_group)
center_layout.setRowStretch(0, 5) # image view (most space)
center_layout.setRowStretch(1, 1) # filmstrip
center_layout.setRowStretch(2, 0) # buttons
center_layout.setColumnStretch(0, 1)
# ---- Image Canvas ----
self.image_view = ImageCanvas()
self.image_view.fileDropped.connect(self.load_image_from_path)
self.image_view.setMinimumSize(800, 600) # 4:3 minimum
center_layout.addWidget(self.image_view, 0, 0)
# ---- Filmstrip Frame ----
self.filmstrip_frame = QFrame()
filmstrip_layout = QVBoxLayout(self.filmstrip_frame)
# Top separator
sep = QFrame()
sep.setFrameShape(QFrame.HLine)
sep.setStyleSheet("color: #252222;")
filmstrip_layout.addWidget(sep)
# Thumbnail scroller
self.thumb_scroll_area = QScrollArea()
self.thumb_scroll_area.setWidgetResizable(True)
self.thumb_scroll_area.setMinimumHeight(100)
self.thumb_container = QWidget()
self.thumb_layout = QHBoxLayout(self.thumb_container)
self.thumb_scroll_area.setWidget(self.thumb_container)
filmstrip_layout.addWidget(self.thumb_scroll_area)
center_layout.addWidget(self.filmstrip_frame, 1, 0)
# ---- Button Frame ----
self.button_frame = QFrame()
btn_layout = QHBoxLayout(self.button_frame)
next_btn = QPushButton("Next Image")
prev_btn = QPushButton("Previous Image")
open_btn = QPushButton("Open Image")
btn_layout.addWidget(next_btn)
btn_layout.addWidget(prev_btn)
btn_layout.addWidget(open_btn)
center_layout.addWidget(self.button_frame, 2, 0)
# --- Right Metadata Panel ---
self.right_metadata = QGroupBox("Metadata")
self.right_metadata.setMinimumWidth(300) # ~1/4 of window width
metadata_layout = QVBoxLayout(self.right_metadata)
# Placeholder metadata content
metadata_layout.addWidget(QLabel("File Information:"))
self.file_info_label = QLabel("No image loaded")
metadata_layout.addWidget(self.file_info_label)
metadata_layout.addWidget(QLabel("AI Parameters:"))
self.ai_params_label = QLabel("Waiting for image...")
metadata_layout.addWidget(self.ai_params_label)
metadata_layout.addStretch() # Push content to top
self.main_layout.addWidget(self.right_metadata, 0, 2)
# ---- Connect Signals ----
open_btn.clicked.connect(self.open_test_image)
next_btn.clicked.connect(self.show_next_image)
prev_btn.clicked.connect(self.show_prev_image)
# Initialize after UI is built
QTimer.singleShot(100, self.initialize_scale)
event.accept()
return super().wheelEvent(event) # ← FIXED: Only call super() once
# arrow / key nav
def keyPressEvent(self, event):
if event.key() == Qt.Key_Right:
self.show_next_image()
event.accept()
elif event.key() == Qt.Key_Left:
self.show_prev_image()
event.accept()
else:
super().keyPressEvent(event)
def initialize_scale(self):
"""Initialize after UI is fully built"""
self.update_scale_factor()
if self.folder_model:
self.populate_filmstrip()
def update_scale_factor(self):
"""Calculate scale factor based on current image view size"""
if not hasattr(self, "image_view"):
return
# Use only the image view's available space
avail_w = self.image_view.width()
avail_h = self.image_view.height()
@@ -172,20 +58,15 @@ class MetaViewApp(QMainWindow):
scale_h = avail_h / self.design_canvas_h
self.scale_factor = min(scale_w, scale_h)
# --- Image Handling ---
def load_image_from_path(self, file_path):
folder = os.path.dirname(file_path)
self.folder_model = ImageFolder(folder, start_file=file_path)
self.image_cache.clear()
current = self.folder_model.current()
if current:
print("Displaying:", current["name"])
self.display_image(current["path"])
self.update_metadata(current) # Update metadata panel
self.populate_filmstrip()
self.preload_adjacent_images()
def resizeEvent(self, event):
super().resizeEvent(event)
self.update_scale_factor()
if self.folder_model:
self.populate_filmstrip()
#kind of a placeholder here for png chunks later
def update_metadata(self, file_info):
"""Update the right metadata panel with file information"""
self.file_info_label.setText(
@@ -202,15 +83,18 @@ class MetaViewApp(QMainWindow):
"CFG: 7.5"
)
def display_image(self, path):
"""Display image from cache or load it"""
if path in self.image_cache:
self.image_view.load_image_from_pixmap(self.image_cache[path])
else:
pixmap = QPixmap(path)
if not pixmap.isNull():
self.image_cache[path] = pixmap
self.image_view.load_image(path)
def load_image_from_path(self, file_path):
folder = os.path.dirname(file_path)
self.folder_model = ImageFolder(folder, start_file=file_path)
self.image_cache.clear()
current = self.folder_model.current()
if current:
print("Displaying:", current["name"])
self.display_image(current["path"])
self.update_metadata(current) # Update metadata panel
self.populate_filmstrip()
self.preload_adjacent_images()
def preload_adjacent_images(self):
"""Pre-load images around current index"""
@@ -230,6 +114,16 @@ class MetaViewApp(QMainWindow):
if not pixmap.isNull():
self.image_cache[path] = pixmap
def display_image(self, path):
"""Display image from cache or load it"""
if path in self.image_cache:
self.image_view.load_image_from_pixmap(self.image_cache[path])
else:
pixmap = QPixmap(path)
if not pixmap.isNull():
self.image_cache[path] = pixmap
self.image_view.load_image(path)
def show_next_image(self):
if self.folder_model:
current = self.folder_model.next()
@@ -247,20 +141,8 @@ class MetaViewApp(QMainWindow):
self.update_metadata(current)
self.populate_filmstrip()
self.preload_adjacent_images()
def wheelEvent(self, event):
if event.angleDelta().y() > 0:
self.show_prev_image()
else:
self.show_next_image()
event.accept()
def resizeEvent(self, event):
super().resizeEvent(event)
self.update_scale_factor()
if self.folder_model:
self.populate_filmstrip()
def populate_filmstrip(self):
# Clear old thumbnails
for i in reversed(range(self.thumb_layout.count())):
@@ -324,18 +206,24 @@ class MetaViewApp(QMainWindow):
self.thumb_layout.addWidget(lbl)
def open_test_image(self):
path = "test.png"
try:
pixmap = helpers.load_image(path)
meta = helpers.get_fake_metadata(path)
print("Metadata:", meta)
except FileNotFoundError as e:
print(e)
from PySide6.QtWidgets import QFileDialog
def open_image_file(self):
"""Open file dialog to select an image"""
file_dialog = QFileDialog(self)
file_dialog.setWindowTitle("Open Image File")
file_dialog.setNameFilter("Images (*.png *.jpg *.jpeg *.bmp *.gif *.tiff *.webp)")
file_dialog.setFileMode(QFileDialog.ExistingFile)
if file_dialog.exec():
selected_files = file_dialog.selectedFiles()
if selected_files:
file_path = selected_files[0]
self.load_image_from_path(file_path)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MetaViewApp()
window.show()
sys.exit(app.exec())