Commit da758584 authored by Kirill Unitsaev's avatar Kirill Unitsaev

widgets: init image_dual widget

parent e210051a
from gi.repository import Adw, Gtk, Gdk, Gio, GLib
import os
from .BaseWidget import BaseWidget
class DualImageChooserWidget(BaseWidget):
ORIENTATION_THRESHOLD = 750
IMAGE_WIDTH = 220
PLACEHOLDER_HEIGHT = 120
def __init__(self, setting):
super().__init__(setting)
self._surface_handler_id = None
self._surface = None
def create_row(self):
self.row = Adw.PreferencesRow(
activatable=False,
focusable=False
)
main_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=12,
margin_top=12,
margin_bottom=12,
margin_start=12,
margin_end=12,
)
if self.setting.name:
title_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=2,
hexpand=True,
)
title_label = Gtk.Label(
label=self.setting.name,
halign=Gtk.Align.START,
)
title_box.append(title_label)
if self.setting.help:
help_label = Gtk.Label(
label=self.setting.help,
halign=Gtk.Align.START,
wrap=True,
css_classes=["caption", "dim-label"]
)
title_box.append(help_label)
main_box.append(title_box)
self.images_box = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
spacing=12,
halign=Gtk.Align.CENTER,
valign=Gtk.Align.START,
homogeneous=True,
)
labels = self.setting.map.get('labels', [_("Light"), _(
"Dark")]) if self.setting.map else [_("Light"), _("Dark")]
self.pictures = []
self.placeholders = []
self.image_stacks = []
self.reset_revealers = []
for i in range(2):
image_container = self._create_image_container(
i, labels[i] if i < len(labels) else "")
self.images_box.append(image_container)
main_box.append(self.images_box)
self.row.set_child(main_box)
self.row.connect("realize", self._on_realize)
self.row.connect("unrealize", self._on_unrealize)
self.update_display()
self._update_reset_visibility()
return self.row
def _on_realize(self, widget):
window = widget.get_root()
if window:
surface = window.get_surface()
if surface:
self._surface = surface
self._surface_handler_id = surface.connect(
"layout", self._on_window_layout)
self._update_orientation(surface.get_width())
def _on_unrealize(self, widget):
if self._surface and self._surface_handler_id:
self._surface.disconnect(self._surface_handler_id)
self._surface_handler_id = None
self._surface = None
def _on_window_layout(self, surface, width, height):
self._update_orientation(width)
def _update_orientation(self, width):
new_orientation = (
Gtk.Orientation.VERTICAL if width < self.ORIENTATION_THRESHOLD
else Gtk.Orientation.HORIZONTAL
)
if self.images_box.get_orientation() != new_orientation:
self.images_box.set_orientation(new_orientation)
def _create_image_container(self, index, label):
container = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=6,
)
image_frame = Gtk.Frame(
css_classes=["view"],
halign=Gtk.Align.CENTER,
)
picture = Gtk.Picture(
content_fit=Gtk.ContentFit.FILL,
)
self.pictures.append(picture)
placeholder = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
halign=Gtk.Align.CENTER,
valign=Gtk.Align.CENTER,
width_request=self.IMAGE_WIDTH,
height_request=self.PLACEHOLDER_HEIGHT,
)
placeholder_icon = Gtk.Image(
icon_name="image-missing-symbolic",
pixel_size=48,
css_classes=["dim-label"],
)
placeholder.append(placeholder_icon)
self.placeholders.append(placeholder)
image_stack = Gtk.Stack(
transition_type=Gtk.StackTransitionType.CROSSFADE,
transition_duration=150,
)
image_stack.add_named(placeholder, "placeholder")
image_stack.add_named(picture, "picture")
self.image_stacks.append(image_stack)
overlay = Gtk.Overlay()
overlay.set_child(image_stack)
change_button = Gtk.Button(
icon_name="document-edit-symbolic",
valign=Gtk.Align.END,
halign=Gtk.Align.END,
margin_bottom=5,
margin_end=5,
css_classes=["circular", "secondary"],
)
change_button.connect("clicked", self._on_change_clicked, index)
image_reset_button = Gtk.Button(
icon_name="edit-undo-symbolic",
margin_top=5,
margin_end=5,
css_classes=["circular", "secondary"],
)
image_reset_button.connect(
"clicked", self._on_image_reset_clicked, index)
image_reset_revealer = Gtk.Revealer(
transition_type=Gtk.RevealerTransitionType.CROSSFADE,
transition_duration=150,
child=image_reset_button,
reveal_child=False,
valign=Gtk.Align.START,
halign=Gtk.Align.END
)
self.reset_revealers.append(image_reset_revealer)
overlay.add_overlay(change_button)
overlay.add_overlay(image_reset_revealer)
image_frame.set_child(overlay)
container.append(image_frame)
if label:
label_widget = Gtk.Label(
label=label,
halign=Gtk.Align.CENTER,
css_classes=["caption", "dim-label"]
)
container.append(label_widget)
return container
def _get_values(self):
values = self.setting._get_backend_value()
if isinstance(values, list) and len(values) >= 2:
return values[0], values[1]
return None, None
def _get_defaults(self):
default = self.setting.default
if isinstance(default, list) and len(default) >= 2:
return default[0], default[1]
return None, None
def _set_value(self, index, value):
backend_value = self.setting._get_backend_value()
if isinstance(backend_value, list) and len(backend_value) >= 2:
values = list(backend_value)
else:
values = [None, None]
values[index] = value
self.setting._set_backend_value(values)
def _normalize_path(self, path):
if not path or not isinstance(path, str):
return None
if path.startswith('"') and path.endswith('"'):
path = path[1:-1]
if path.startswith("file://"):
path = path[7:]
return os.path.expandvars(os.path.expanduser(path))
def _has_file_prefix(self, path):
return isinstance(path, str) and path.startswith("file://")
def _format_path_for_save(self, path, original):
if not path:
return path
if self._has_file_prefix(original):
path = f"file://{path}"
had_quotes = original and original.startswith(
'"') and original.endswith('"')
needs_quotes = ' ' in path or had_quotes
if needs_quotes and not path.startswith('"'):
path = f'"{path}"'
return path
def update_display(self):
for i in range(2):
self._update_image(i)
self._update_reset_visibility()
def _update_image(self, index):
values = self._get_values()
current = self._normalize_path(values[index])
if current and os.path.exists(current):
file = Gio.File.new_for_path(current)
try:
texture = Gdk.Texture.new_from_file(file)
self.pictures[index].set_paintable(texture)
width = texture.get_width()
height = texture.get_height()
if width > 0 and height > 0:
target_height = int(self.IMAGE_WIDTH * height / width)
self.pictures[index].set_size_request(
self.IMAGE_WIDTH, target_height)
self.image_stacks[index].set_visible_child_name("picture")
except Exception as e:
self.logger.error(f"Error loading image {index}: {e}")
self.pictures[index].set_paintable(None)
self.image_stacks[index].set_visible_child_name("placeholder")
else:
self.pictures[index].set_paintable(None)
self.image_stacks[index].set_visible_child_name("placeholder")
def _on_reset_clicked(self, button):
default = self.setting.default
if default is not None:
self.setting._set_backend_value(default)
self.update_display()
def _on_image_reset_clicked(self, button, index):
defaults = self._get_defaults()
if defaults[index] is not None:
self._set_value(index, defaults[index])
self.update_display()
def _update_reset_visibility(self):
defaults = self._get_defaults()
values = self._get_values()
for i in range(2):
current_value = self._normalize_path(values[i])
default_value = self._normalize_path(defaults[i])
self.reset_revealers[i].set_reveal_child(
current_value != default_value if default_value is not None
else False
)
def _on_change_clicked(self, button, index):
dialog = Gtk.FileDialog()
if self.setting.map and 'extensions' in self.setting.map:
filters = self._create_file_filters()
if filters.get_n_items() > 0:
dialog.set_filters(filters)
dialog.set_default_filter(filters.get_item(0))
values = self._get_values()
current = self._normalize_path(values[index])
if current:
try:
current_file = Gio.File.new_for_path(current)
parent = current_file.get_parent()
if parent:
dialog.set_initial_folder(parent)
except Exception as e:
self.logger.error(f"Error setting initial folder: {e}")
try:
dialog.open(
parent=button.get_root(),
callback=lambda d, r: self._on_file_selected(d, r, index)
)
except Exception as e:
self.logger.error(f"File dialog error: {e}")
def _create_file_filters(self):
filters = Gio.ListStore.new(Gtk.FileFilter)
patterns = [p for p in self.setting.map.get('extensions', [])]
if patterns:
file_filter = Gtk.FileFilter()
file_filter.set_name(_("Image files"))
for pattern in patterns:
if pattern.startswith('.'):
file_filter.add_suffix(pattern[1:])
else:
file_filter.add_pattern(pattern)
filters.append(file_filter)
return filters
def _on_file_selected(self, dialog, result, index):
try:
file = dialog.open_finish(result)
if file:
path = file.get_path()
values = self._get_values()
path = self._format_path_for_save(path, values[index])
self._set_value(index, path)
self.update_display()
except Exception as e:
if "dismissed" not in str(e).lower():
self.logger.error(f"File selection error: {e}")
...@@ -21,6 +21,7 @@ class WidgetFactory: ...@@ -21,6 +21,7 @@ class WidgetFactory:
'avatar': AvatarWidget, 'avatar': AvatarWidget,
'file': FileChooser, 'file': FileChooser,
'image': ImageChooserWidget, 'image': ImageChooserWidget,
'image_dual': DualImageChooserWidget,
'choice': ChoiceWidget, 'choice': ChoiceWidget,
'choice_radio': RadioChoiceWidget, 'choice_radio': RadioChoiceWidget,
'theme_chooser': ThemeChooserWidget, 'theme_chooser': ThemeChooserWidget,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment