v0.9.11
This commit is contained in:
@@ -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 ---
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -1194,7 +1196,7 @@ class ImagePane(QWidget):
|
|||||||
def __init__(self, parent_viewer, cache, image_list, index, initial_tags=None,
|
def __init__(self, parent_viewer, cache, image_list, index, initial_tags=None,
|
||||||
initial_rating=0):
|
initial_rating=0):
|
||||||
super().__init__(parent_viewer)
|
super().__init__(parent_viewer)
|
||||||
self.viewer = parent_viewer # Reference to main ImageViewer
|
self.viewer = parent_viewer # Reference to main ImageViewer
|
||||||
self.main_win = parent_viewer.main_win
|
self.main_win = parent_viewer.main_win
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -1553,7 +1556,7 @@ class ImageViewer(QWidget):
|
|||||||
self.active_pane.scrolled.disconnect(self._sync_scroll)
|
self.active_pane.scrolled.disconnect(self._sync_scroll)
|
||||||
self.active_pane.zoom_manager.zoomed.disconnect(self._sync_zoom)
|
self.active_pane.zoom_manager.zoomed.disconnect(self._sync_zoom)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass # Signal wasn't connected
|
pass # Signal wasn't connected
|
||||||
|
|
||||||
self.active_pane = pane
|
self.active_pane = pane
|
||||||
|
|
||||||
@@ -1622,7 +1625,7 @@ class ImageViewer(QWidget):
|
|||||||
img_list = base_controller.image_list
|
img_list = base_controller.image_list
|
||||||
for i in range(count - current_panes):
|
for i in range(count - current_panes):
|
||||||
new_idx = (start_idx + i + 1) % len(img_list)
|
new_idx = (start_idx + i + 1) % len(img_list)
|
||||||
pane = self.add_pane(img_list, new_idx, None, 0) # Metadata will load
|
pane = self.add_pane(img_list, new_idx, None, 0) # Metadata will load
|
||||||
pane.load_and_fit_image()
|
pane.load_and_fit_image()
|
||||||
else:
|
else:
|
||||||
# Remove panes (keep active if possible, else keep first)
|
# Remove panes (keep active if possible, else keep first)
|
||||||
@@ -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
|
||||||
@@ -2637,14 +2650,19 @@ class ImageViewer(QWidget):
|
|||||||
"icon": "zoom-fit-best"},
|
"icon": "zoom-fit-best"},
|
||||||
"separator",
|
"separator",
|
||||||
{"text": UITexts.VIEWER_MENU_CROP, "action": "toggle_crop",
|
{"text": UITexts.VIEWER_MENU_CROP, "action": "toggle_crop",
|
||||||
"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())
|
||||||
@@ -2855,7 +2874,7 @@ class ImageViewer(QWidget):
|
|||||||
h = self.canvas.height() if self.canvas else 0
|
h = self.canvas.height() if self.canvas else 0
|
||||||
if self.scroll_area:
|
if self.scroll_area:
|
||||||
self.scroll_area.ensureVisible(int(new_face.get('x', 0) * w),
|
self.scroll_area.ensureVisible(int(new_face.get('x', 0) * w),
|
||||||
int(new_face.get('y', 0) * h), 50, 50)
|
int(new_face.get('y', 0) * h), 50, 50)
|
||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
|
|
||||||
history = self.main_win.face_names_history if self.main_win else []
|
history = self.main_win.face_names_history if self.main_win else []
|
||||||
@@ -2912,7 +2931,7 @@ class ImageViewer(QWidget):
|
|||||||
h = self.canvas.height() if self.canvas else 0
|
h = self.canvas.height() if self.canvas else 0
|
||||||
if self.scroll_area:
|
if self.scroll_area:
|
||||||
self.scroll_area.ensureVisible(int(new_pet.get('x', 0) * w),
|
self.scroll_area.ensureVisible(int(new_pet.get('x', 0) * w),
|
||||||
int(new_pet.get('y', 0) * h), 50, 50)
|
int(new_pet.get('y', 0) * h), 50, 50)
|
||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
|
|
||||||
history = self.main_win.pet_names_history if self.main_win else []
|
history = self.main_win.pet_names_history if self.main_win else []
|
||||||
@@ -2968,7 +2987,7 @@ class ImageViewer(QWidget):
|
|||||||
h = self.canvas.height() if self.canvas else 0
|
h = self.canvas.height() if self.canvas else 0
|
||||||
if self.scroll_area:
|
if self.scroll_area:
|
||||||
self.scroll_area.ensureVisible(int(new_body.get('x', 0) * w),
|
self.scroll_area.ensureVisible(int(new_body.get('x', 0) * w),
|
||||||
int(new_body.get('y', 0) * h), 50, 50)
|
int(new_body.get('y', 0) * h), 50, 50)
|
||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
|
|
||||||
# For bodies, we typically don't ask for a name immediately unless desired
|
# For bodies, we typically don't ask for a name immediately unless desired
|
||||||
|
|||||||
Reference in New Issue
Block a user