This commit is contained in:
Ignacio Serantes
2026-04-19 12:18:27 +02:00
parent 9d286112b6
commit b5b70326b1
7 changed files with 153 additions and 34 deletions

View File

@@ -14,7 +14,7 @@ Classes:
MainWindow: The main application window containing the thumbnail grid and docks.
"""
__appname__ = "BagheeraView"
__version__ = "0.9.22"
__version__ = "0.9.23"
__author__ = "Ignacio Serantes"
__email__ = "kde@aynoa.net"
__license__ = "LGPL"

View File

@@ -29,7 +29,7 @@ if FORCE_X11:
# --- CONFIGURATION ---
PROG_NAME = "Bagheera Image Viewer"
PROG_ID = "bagheeraview"
PROG_VERSION = "0.9.22"
PROG_VERSION = "0.9.23"
PROG_AUTHOR = "Ignacio Serantes"
# --- CACHE SETTINGS ---
@@ -852,6 +852,7 @@ _UI_TEXTS = {
"PROPERTIES_TABLE_HEADER": ["Property", "Value"],
"PROPERTIES_ADD_ATTR": "Add Attribute",
"PROPERTIES_ADD_ATTR_NAME": "Attribute Name (e.g. user.comment):",
"PROPERTIES_DELETE_ALL": "Delete All",
"PROPERTIES_ADD_ATTR_VALUE": "Value for {}:",
"PROPERTIES_ERROR_SET_ATTR": "Failed to set xattr: {}",
"PROPERTIES_ERROR_ADD_ATTR": "Failed to add xattr: {}",
@@ -1403,6 +1404,7 @@ _UI_TEXTS = {
"PROPERTIES_TABLE_HEADER": ["Propiedad", "Valor"],
"PROPERTIES_ADD_ATTR": "Añadir Atributo",
"PROPERTIES_ADD_ATTR_NAME": "Nombre del Atributo (ej. user.comment):",
"PROPERTIES_DELETE_ALL": "Borrar Todo",
"PROPERTIES_ADD_ATTR_VALUE": "Valor para {}:",
"PROPERTIES_ERROR_SET_ATTR": "Fallo al establecer xattr: {}",
"PROPERTIES_ERROR_ADD_ATTR": "Fallo al añadir xattr: {}",
@@ -1954,6 +1956,7 @@ _UI_TEXTS = {
"PROPERTIES_TABLE_HEADER": ["Propiedade", "Valor"],
"PROPERTIES_ADD_ATTR": "Engadir Atributo",
"PROPERTIES_ADD_ATTR_NAME": "Nome do Atributo (ex. user.comment):",
"PROPERTIES_DELETE_ALL": "Borrar Todo",
"PROPERTIES_ADD_ATTR_VALUE": "Valor para {}:",
"PROPERTIES_ERROR_SET_ATTR": "Fallo ao establecer xattr: {}",
"PROPERTIES_ERROR_ADD_ATTR": "Fallo ao engadir xattr: {}",

View File

@@ -3331,7 +3331,7 @@ class ImageViewer(QWidget):
# Speed 1 (slowest) requires a full 120 delta.
# Speed 10 (fastest) requires 120/10 = 12 delta.
# Still too fast so speed / 2.
threshold = 120 / speed / 2
threshold = 120 / speed * 2
self._wheel_scroll_accumulator += event.angleDelta().y()

View File

@@ -121,6 +121,43 @@ class MetadataManager:
return all_metadata
@staticmethod
def write_metadata(path, metadata_dict):
"""
Writes EXIF, IPTC, and XMP metadata back to a file.
Args:
path (str): The path to the image file.
metadata_dict (dict): A dictionary of metadata keys and values.
"""
if not HAVE_EXIV2:
return
try:
image = exiv2.ImageFactory.open(path)
image.readMetadata()
exif = image.exifData()
iptc = image.iptcData()
xmp = image.xmpData()
for key, value in metadata_dict.items():
try:
if key.startswith("Exif."):
exif[key] = str(value)
elif key.startswith("Iptc."):
iptc[key] = str(value)
elif key.startswith("Xmp."):
xmp[key] = str(value)
except Exception as e:
print(f"Error setting metadata key {key}: {e}")
image.writeMetadata()
notify_baloo(path)
mark_app_modified(path)
except Exception as e:
print(f"Error writing metadata for {path}: {e}")
class XattrManager:
"""A manager class to handle reading and writing extended attributes (xattrs)."""

View File

@@ -12,7 +12,7 @@ Classes:
from PySide6.QtWidgets import (
QWidget, QLabel, QVBoxLayout, QMessageBox, QMenu, QInputDialog,
QDialog, QTableWidget, QTableWidgetItem, QHeaderView, QTabWidget,
QFormLayout, QDialogButtonBox, QApplication
QFormLayout, QDialogButtonBox, QApplication, QToolBar, QAbstractItemView
)
from PySide6.QtGui import (
QImageReader, QIcon, QColor
@@ -76,6 +76,8 @@ class PropertiesDialog(QDialog):
self.setWindowTitle(UITexts.PROPERTIES_TITLE)
self._initial_tags = initial_tags if initial_tags is not None else []
self._initial_rating = initial_rating
self.original_xattrs = {}
self.original_exif = {}
self.loader = None
self.resize(400, 500)
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
@@ -136,6 +138,11 @@ class PropertiesDialog(QDialog):
meta_widget = QWidget()
meta_layout = QVBoxLayout(meta_widget)
self.meta_toolbar = QToolBar()
self._setup_table_toolbar(self.meta_toolbar, self.on_add_meta, self.on_delete_meta,
self.on_delete_all_meta, self.on_save_meta, self.on_cancel_meta)
meta_layout.addWidget(self.meta_toolbar)
self.table = QTableWidget()
self.table.setColumnCount(2)
self.table.setHorizontalHeaderLabels(UITexts.PROPERTIES_TABLE_HEADER)
@@ -145,12 +152,12 @@ class PropertiesDialog(QDialog):
self.table.setColumnWidth(0, self.width() * 0.4)
self.table.verticalHeader().setVisible(False)
self.table.setAlternatingRowColors(True)
self.table.setEditTriggers(QTableWidget.DoubleClicked |
QTableWidget.EditKeyPressed |
QTableWidget.SelectedClicked)
self.table.setEditTriggers(QAbstractItemView.DoubleClicked |
QAbstractItemView.EditKeyPressed |
QAbstractItemView.AnyKeyPressed)
self.table.setSelectionBehavior(QTableWidget.SelectRows)
self.table.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.table.itemChanged.connect(self.on_item_changed)
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
self.table.customContextMenuRequested.connect(self.show_context_menu)
@@ -164,6 +171,11 @@ class PropertiesDialog(QDialog):
exif_widget = QWidget()
exif_layout = QVBoxLayout(exif_widget)
self.exif_toolbar = QToolBar()
self._setup_table_toolbar(self.exif_toolbar, self.on_add_exif, self.on_delete_exif,
self.on_delete_all_exif, self.on_save_exif, self.on_cancel_exif)
exif_layout.addWidget(self.exif_toolbar)
self.exif_table = QTableWidget()
# This table will display EXIF/XMP/IPTC data.
# Reading this data involves opening the file with exiv2, which is a disk read.
@@ -180,8 +192,11 @@ class PropertiesDialog(QDialog):
1, QHeaderView.ResizeToContents)
self.exif_table.verticalHeader().setVisible(False)
self.exif_table.setAlternatingRowColors(True)
self.exif_table.setEditTriggers(QTableWidget.NoEditTriggers)
self.exif_table.setEditTriggers(QAbstractItemView.DoubleClicked |
QAbstractItemView.EditKeyPressed |
QAbstractItemView.AnyKeyPressed)
self.exif_table.setSelectionBehavior(QTableWidget.SelectRows)
self.exif_table.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.exif_table.setContextMenuPolicy(Qt.CustomContextMenu)
# This is a disk read.
self.exif_table.customContextMenuRequested.connect(self.show_exif_context_menu)
@@ -204,6 +219,87 @@ class PropertiesDialog(QDialog):
# Start background loading
self.reload_metadata()
def _setup_table_toolbar(self, toolbar, add_slot, del_slot, del_all_slot, save_slot, cancel_slot):
"""Helper to populate toolbars with buttons."""
toolbar.addAction(QIcon.fromTheme("list-add"), UITexts.CREATE, add_slot)
toolbar.addAction(QIcon.fromTheme("list-remove"), UITexts.DELETE, del_slot)
toolbar.addAction(QIcon.fromTheme("edit-clear-all"), UITexts.PROPERTIES_DELETE_ALL, del_all_slot)
toolbar.addSeparator()
toolbar.addAction(QIcon.fromTheme("document-save"), UITexts.SAVE, save_slot)
toolbar.addAction(QIcon.fromTheme("edit-undo"), UITexts.CANCEL, cancel_slot)
def on_add_meta(self):
key, ok = QInputDialog.getText(self, UITexts.PROPERTIES_ADD_ATTR, UITexts.PROPERTIES_ADD_ATTR_NAME)
if ok and key:
row = self.table.rowCount()
self.table.insertRow(row)
self.table.setItem(row, 0, QTableWidgetItem(key))
v_item = QTableWidgetItem("")
self.table.setItem(row, 1, v_item)
self.table.setCurrentItem(v_item)
self.table.editItem(v_item)
def on_delete_meta(self):
rows = sorted(set(index.row() for index in self.table.selectedIndexes()), reverse=True)
for row in rows:
self.table.removeRow(row)
def on_delete_all_meta(self):
self.table.setRowCount(0)
def on_save_meta(self):
new_attrs = {}
for r in range(self.table.rowCount()):
k_item, v_item = self.table.item(r, 0), self.table.item(r, 1)
if k_item and v_item:
new_attrs[k_item.text()] = v_item.text()
try:
for k in self.original_xattrs:
if k not in new_attrs:
XattrManager.set_attribute(self.path, k, None)
for k, v in new_attrs.items():
XattrManager.set_attribute(self.path, k, v)
self.reload_metadata()
except Exception as e:
QMessageBox.warning(self, UITexts.ERROR, f"Error: {e}")
def on_cancel_meta(self):
self.update_metadata_table(self.original_xattrs)
def on_add_exif(self):
key, ok = QInputDialog.getText(self, UITexts.PROPERTIES_ADD_ATTR, UITexts.PROPERTIES_ADD_ATTR_NAME)
if ok and key:
row = self.exif_table.rowCount()
self.exif_table.insertRow(row)
self.exif_table.setItem(row, 0, QTableWidgetItem(key))
v_item = QTableWidgetItem("")
self.exif_table.setItem(row, 1, v_item)
self.exif_table.setCurrentItem(v_item)
self.exif_table.editItem(v_item)
def on_delete_exif(self):
rows = sorted(set(index.row() for index in self.exif_table.selectedIndexes()), reverse=True)
for row in rows:
self.exif_table.removeRow(row)
def on_delete_all_exif(self):
self.exif_table.setRowCount(0)
def on_save_exif(self):
new_exif = {}
for r in range(self.exif_table.rowCount()):
k_item, v_item = self.exif_table.item(r, 0), self.exif_table.item(r, 1)
if k_item and v_item:
new_exif[k_item.text()] = v_item.text()
try:
MetadataManager.write_metadata(self.path, new_exif)
self.reload_metadata()
except Exception as e:
QMessageBox.warning(self, UITexts.ERROR, f"Error: {e}")
def on_cancel_exif(self):
self.update_exif_table(self.original_exif)
def done(self, r):
if self.loader and self.loader.isRunning():
self.loader.stop()
@@ -232,6 +328,7 @@ class PropertiesDialog(QDialog):
# Combine preloaded and newly read xattrs
all_xattrs = preloaded_xattrs.copy()
if not initial_only and disk_xattrs:
self.original_xattrs = disk_xattrs.copy()
# Disk data takes precedence or adds to it
all_xattrs.update(disk_xattrs)
@@ -242,9 +339,9 @@ class PropertiesDialog(QDialog):
for key, val in all_xattrs.items():
# QImageReader.textKeys() is not used here as it's not xattr.
k_item = QTableWidgetItem(key)
k_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
k_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable)
v_item = QTableWidgetItem(val)
v_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
v_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable)
self.table.setItem(row, 0, k_item)
self.table.setItem(row, 1, v_item)
row += 1
@@ -303,6 +400,7 @@ class PropertiesDialog(QDialog):
self.exif_table.blockSignals(False)
return
self.original_exif = exif_data.copy()
self.exif_table.setRowCount(len(exif_data))
error_color = QColor("red")
error_text_lower = UITexts.ERROR.lower()
@@ -310,9 +408,9 @@ class PropertiesDialog(QDialog):
for row, (key, value) in enumerate(sorted(exif_data.items())):
k_item = QTableWidgetItem(str(key))
k_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
k_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable)
v_item = QTableWidgetItem(str(value))
v_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
v_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable)
key_str_lower = str(key).lower()
val_str_lower = str(value).lower()
@@ -328,25 +426,6 @@ class PropertiesDialog(QDialog):
self.exif_table.blockSignals(False)
def on_item_changed(self, item):
"""
Slot that triggers when an item in the metadata table is changed.
Args:
item (QTableWidgetItem): The item that was changed.
"""
if item.column() == 1:
key = self.table.item(item.row(), 0).text()
val = item.text()
# Treat empty or whitespace-only values as removal to match previous
# behavior
val_to_set = val if val.strip() else None
try:
XattrManager.set_attribute(self.path, key, val_to_set)
except Exception as e:
QMessageBox.warning(self, UITexts.ERROR,
UITexts.PROPERTIES_ERROR_SET_ATTR.format(e))
def show_context_menu(self, pos):
"""
Displays a context menu in the metadata table.

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "bagheeraview"
version = "0.9.22"
version = "0.9.23"
authors = [
{ name = "Ignacio Serantes" }
]

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="bagheeraview",
version="0.9.22",
version="0.9.23",
author="Ignacio Serantes",
description="Bagheera Image Viewer - An image viewer for KDE with Baloo in mind",
long_description="A fast image viewer built with PySide6, featuring search and "