Compare commits

..

1 Commits

Author SHA1 Message Date
Ignacio Serantes
0349155fd2 v0.9.11 2026-03-24 09:14:57 +01:00
3 changed files with 51 additions and 29 deletions

View File

@@ -29,7 +29,7 @@ if FORCE_X11:
# --- CONFIGURATION --- # --- CONFIGURATION ---
PROG_NAME = "Bagheera Image Viewer" PROG_NAME = "Bagheera Image Viewer"
PROG_ID = "bagheeraview" PROG_ID = "bagheeraview"
PROG_VERSION = "0.9.11-dev" PROG_VERSION = "0.9.11"
PROG_AUTHOR = "Ignacio Serantes" PROG_AUTHOR = "Ignacio Serantes"
# --- CACHE SETTINGS --- # --- CACHE SETTINGS ---

View File

@@ -64,7 +64,8 @@ class ThreadPoolManager:
) )
self.pool.setMaxThreadCount(self.default_thread_count) self.pool.setMaxThreadCount(self.default_thread_count)
self.is_user_active = False self.is_user_active = False
logger.info(f"ThreadPoolManager initialized with {self.default_thread_count} threads.") logger.info(f"ThreadPoolManager initialized with "
f"{self.default_thread_count} threads.")
def get_pool(self): def get_pool(self):
"""Returns the managed QThreadPool instance.""" """Returns the managed QThreadPool instance."""
@@ -87,7 +88,8 @@ class ThreadPoolManager:
else: else:
# User is idle, restore to default thread count. # User is idle, restore to default thread count.
self.pool.setMaxThreadCount(self.default_thread_count) self.pool.setMaxThreadCount(self.default_thread_count)
logger.debug(f"User is idle, restoring thread pool to {self.default_thread_count}.") logger.debug(f"User is idle, restoring thread pool to "
f"{self.default_thread_count}.")
def update_default_thread_count(self): def update_default_thread_count(self):
"""Updates the default thread count from application settings.""" """Updates the default thread count from application settings."""
@@ -1329,6 +1331,7 @@ class ImageScanner(QThread):
progress_percent = Signal(int) progress_percent = Signal(int)
finished_scan = Signal(int) # Total images found finished_scan = Signal(int) # Total images found
more_files_available = Signal(int, int) # Last loaded index, remainder more_files_available = Signal(int, int) # Last loaded index, remainder
def __init__(self, cache, paths, is_file_list=False, viewers=None, def __init__(self, cache, paths, is_file_list=False, viewers=None,
thread_pool_manager=None): thread_pool_manager=None):
# is_file_list is not used # is_file_list is not used

View File

@@ -15,8 +15,8 @@ import json
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QScrollArea, QLabel, QHBoxLayout, QListWidget, QWidget, QVBoxLayout, QScrollArea, QLabel, QHBoxLayout, QListWidget,
QListWidgetItem, QAbstractItemView, QMenu, QInputDialog, QDialog, QDialogButtonBox, QGridLayout, QListWidgetItem, QAbstractItemView, QMenu, QInputDialog, QDialog, QDialogButtonBox,
QApplication, QMessageBox, QLineEdit, QFileDialog QGridLayout, QApplication, QMessageBox, QLineEdit, QFileDialog
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QPixmap, QIcon, QTransform, QDrag, QPainter, QPen, QColor, QAction, QCursor, QPixmap, QIcon, QTransform, QDrag, QPainter, QPen, QColor, QAction, QCursor,
@@ -39,6 +39,7 @@ from imagecontroller import ImageController
from widgets import FaceNameInputWidget from widgets import FaceNameInputWidget
from propertiesdialog import PropertiesDialog from propertiesdialog import PropertiesDialog
class HighlightWidget(QWidget): class HighlightWidget(QWidget):
"""Widget to show a highlight border around the active pane.""" """Widget to show a highlight border around the active pane."""
def __init__(self, parent=None): def __init__(self, parent=None):
@@ -47,6 +48,7 @@ class HighlightWidget(QWidget):
self.setStyleSheet("border: 2px solid #3498db; background: transparent;") self.setStyleSheet("border: 2px solid #3498db; background: transparent;")
self.hide() self.hide()
class FaceNameDialog(QDialog): class FaceNameDialog(QDialog):
"""A dialog to get a face name using the FaceNameInputWidget.""" """A dialog to get a face name using the FaceNameInputWidget."""
def __init__(self, parent=None, history=None, current_name="", main_win=None, def __init__(self, parent=None, history=None, current_name="", main_win=None,
@@ -1329,6 +1331,7 @@ class ImagePane(QWidget):
if self.controller.get_current_path() != current_path: if self.controller.get_current_path() != current_path:
self.load_and_fit_image() self.load_and_fit_image()
class ImageViewer(QWidget): class ImageViewer(QWidget):
""" """
A standalone window for viewing and manipulating a single image. A standalone window for viewing and manipulating a single image.
@@ -1410,7 +1413,6 @@ class ImageViewer(QWidget):
# self.canvas = FaceCanvas(self) ... Moved to ImagePane # self.canvas = FaceCanvas(self) ... Moved to ImagePane
# self.scroll_area.setWidget(self.canvas) # self.scroll_area.setWidget(self.canvas)
self.filmstrip = FilmStripWidget(self.controller) self.filmstrip = FilmStripWidget(self.controller)
self.filmstrip.setSpacing(2) self.filmstrip.setSpacing(2)
self.filmstrip.itemClicked.connect(self.on_filmstrip_clicked) self.filmstrip.itemClicked.connect(self.on_filmstrip_clicked)
@@ -1540,7 +1542,8 @@ class ImageViewer(QWidget):
return self.active_pane.movie if self.active_pane else None return self.active_pane.movie if self.active_pane else None
def add_pane(self, image_list, index, initial_tags, initial_rating): def add_pane(self, image_list, index, initial_tags, initial_rating):
pane = ImagePane(self, self.cache, image_list, index, initial_tags, initial_rating) pane = ImagePane(self, self.cache, image_list, index, initial_tags,
initial_rating)
self.panes.append(pane) self.panes.append(pane)
self.update_grid_layout() self.update_grid_layout()
return pane return pane
@@ -1636,8 +1639,10 @@ class ImageViewer(QWidget):
# Restore default behavior (auto-resize) if we go back to single view # Restore default behavior (auto-resize) if we go back to single view
if count == 1 and self.active_pane: if count == 1 and self.active_pane:
# Allow layout to settle before resizing window to ensure accurate sizing # Allow layout to settle before resizing window to ensure accurate
QTimer.singleShot(0, lambda: self.active_pane.update_view(resize_win=True)) # sizing
QTimer.singleShot(
0, lambda: self.active_pane.update_view(resize_win=True))
def toggle_link_panes(self): def toggle_link_panes(self):
"""Toggles the synchronized zoom/scroll for comparison mode.""" """Toggles the synchronized zoom/scroll for comparison mode."""
@@ -1883,7 +1888,8 @@ class ImageViewer(QWidget):
""" """
kwinoutputconfig.json kwinoutputconfig.json
""" """
return self.screen().availableGeometry().width(), self.screen().availableGeometry().height() return self.screen().availableGeometry().width(), \
self.screen().availableGeometry().height()
# We run kscreen-doctor and look for the primary monitor line. # We run kscreen-doctor and look for the primary monitor line.
if FORCE_X11: if FORCE_X11:
if os.path.exists(KWINOUTPUTCONFIG_PATH): if os.path.exists(KWINOUTPUTCONFIG_PATH):
@@ -1986,7 +1992,8 @@ class ImageViewer(QWidget):
if self.filmstrip.isVisible(): if self.filmstrip.isVisible():
self.populate_filmstrip() self.populate_filmstrip()
pane.update_view(resize_win=False) pane.update_view(resize_win=False)
QTimer.singleShot(0, lambda: self.restore_scroll_for_pane(pane, restore_config)) QTimer.singleShot(
0, lambda: self.restore_scroll_for_pane(pane, restore_config))
elif reloaded: elif reloaded:
# Calculate zoom to fit the image on the screen # Calculate zoom to fit the image on the screen
if self.isFullScreen(): if self.isFullScreen():
@@ -2004,7 +2011,8 @@ class ImageViewer(QWidget):
else: else:
# Tried to guess # Tried to guess
screen_width, screen_height = self.get_desktop_resolution() screen_width, screen_height = self.get_desktop_resolution()
if pane == self.panes[0]: self._first_load = False if pane == self.panes[0]:
self._first_load = False
else: else:
screen_geo = self.screen().availableGeometry() screen_geo = self.screen().availableGeometry()
screen_width = screen_geo.width() screen_width = screen_geo.width()
@@ -2274,11 +2282,13 @@ class ImageViewer(QWidget):
def save_cropped_image(self): def save_cropped_image(self):
"""Saves the area currently selected in crop mode as a new image.""" """Saves the area currently selected in crop mode as a new image."""
if not self.active_pane or not self.active_pane.crop_mode or self.active_pane.canvas.crop_rect.isNull(): if not self.active_pane or not self.active_pane.crop_mode \
or self.active_pane.canvas.crop_rect.isNull():
return return
# Get normalized coordinates from the canvas rect # Get normalized coordinates from the canvas rect
nx, ny, nw, nh = self.active_pane.canvas.map_to_source(self.active_pane.canvas.crop_rect) nx, ny, nw, nh = self.active_pane.canvas.map_to_source(
self.active_pane.canvas.crop_rect)
# Use original pixmap to extract high-quality crop # Use original pixmap to extract high-quality crop
orig = self.active_pane.controller.pixmap_original orig = self.active_pane.controller.pixmap_original
@@ -2403,8 +2413,10 @@ class ImageViewer(QWidget):
"show_faces": self.controller.show_faces, "show_faces": self.controller.show_faces,
"flip_h": self.controller.flip_h, "flip_h": self.controller.flip_h,
"flip_v": self.controller.flip_v, "flip_v": self.controller.flip_v,
"scroll_x": self.scroll_area.horizontalScrollBar().value() if self.scroll_area else 0, "scroll_x": self.scroll_area.horizontalScrollBar().value()
"scroll_y": self.scroll_area.verticalScrollBar().value() if self.scroll_area else 0, if self.scroll_area else 0,
"scroll_y": self.scroll_area.verticalScrollBar().value()
if self.scroll_area else 0,
"status_bar_visible": self.status_bar_container.isVisible(), "status_bar_visible": self.status_bar_container.isVisible(),
"filmstrip_visible": self.filmstrip.isVisible() "filmstrip_visible": self.filmstrip.isVisible()
} }
@@ -2493,7 +2505,8 @@ class ImageViewer(QWidget):
return False return False
pos = self.canvas.mapFromGlobal(event.globalPos()) if self.canvas else QPoint() pos = self.canvas.mapFromGlobal(event.globalPos()) if self.canvas else QPoint()
clicked_face = self.active_pane._get_clicked_face(pos) if self.active_pane else None clicked_face = self.active_pane._get_clicked_face(pos) \
if self.active_pane else None
if not clicked_face: if not clicked_face:
return False return False
@@ -2640,11 +2653,16 @@ class ImageViewer(QWidget):
"icon": "transform-crop", "checkable": True}, # checked updated later "icon": "transform-crop", "checkable": True}, # checked updated later
"separator", "separator",
{"text": UITexts.VIEWER_MENU_COMPARE, "icon": "view-grid", "submenu": [ {"text": UITexts.VIEWER_MENU_COMPARE, "icon": "view-grid", "submenu": [
{"text": UITexts.VIEWER_MENU_COMPARE_1, "action": "compare_1", "icon": "view-restore"}, {"text": UITexts.VIEWER_MENU_COMPARE_1,
{"text": UITexts.VIEWER_MENU_COMPARE_2, "action": "compare_2", "icon": "view-split-left-right"}, "action": "compare_1", "icon": "view-restore"},
{"text": UITexts.VIEWER_MENU_COMPARE_4, "action": "compare_4", "icon": "view-grid"}, {"text": UITexts.VIEWER_MENU_COMPARE_2,
"action": "compare_2", "icon": "view-split-left-right"},
{"text": UITexts.VIEWER_MENU_COMPARE_4,
"action": "compare_4", "icon": "view-grid"},
"separator", "separator",
{"text": UITexts.VIEWER_MENU_LINK_PANES, "action": "link_panes", "icon": "object-link", "checkable": True, "checked": self.panes_linked} {"text": UITexts.VIEWER_MENU_LINK_PANES,
"action": "link_panes", "icon": "object-link",
"checkable": True, "checked": self.panes_linked}
]}, ]},
"separator", "separator",
] ]
@@ -2808,7 +2826,8 @@ class ImageViewer(QWidget):
Args: Args:
event (QContextMenuEvent): The context menu event. event (QContextMenuEvent): The context menu event.
""" """
if self.active_pane and self.active_pane.crop_mode and not self.active_pane.canvas.crop_rect.isNull(): if self.active_pane and self.active_pane.crop_mode \
and not self.active_pane.canvas.crop_rect.isNull():
pos = self.active_pane.canvas.mapFromGlobal(event.globalPos()) pos = self.active_pane.canvas.mapFromGlobal(event.globalPos())
if self.active_pane.canvas.crop_rect.contains(pos): if self.active_pane.canvas.crop_rect.contains(pos):
self.show_crop_menu(event.globalPos()) self.show_crop_menu(event.globalPos())