v0.9.23
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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: {}",
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "bagheeraview"
|
||||
version = "0.9.22"
|
||||
version = "0.9.23"
|
||||
authors = [
|
||||
{ name = "Ignacio Serantes" }
|
||||
]
|
||||
|
||||
2
setup.py
2
setup.py
@@ -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 "
|
||||
|
||||
Reference in New Issue
Block a user