Commit ad03fe58 authored by Roman Alifanov's avatar Roman Alifanov

Merge branch 'tuneit_next' into 'master'

Tuneit next See merge request !4 feat: graph widgets, system monitor, async improvements - Add info_graph, info_graph_large, info_graph_multi widgets - Add system_monitor module with CPU/RAM/Swap monitoring - Rework async value loading with ThreadPoolExecutor - Pause update timer when page is hidden (map/unmap) - Rework Entry/FileChooser reset button behavior - Fix NumStepper int conversion when digits=0 - Update app icon and add artists to About (GNOME Design)
parents da758584 1c726c10
......@@ -163,6 +163,14 @@
lower: 0
step: 1
digits: 0
- name: "Actions"
weight: 10
page: "Boot"
type: custom
settings:
- name: Update GRUB
type: button
set_command: "pkexec /usr/sbin/update-grub 2>&1 | sed 's/:/-/g' | while read line; do echo \"NOTIFY:$line\"; done"
- name: "Main"
weight: 0
page: "Dirs"
......
......@@ -2,6 +2,9 @@ modules_dir = pkgdatadir / 'modules'
plugins = [
'example_module',
'system_monitor',
]
install_subdir(plugins, install_dir: modules_dir)
foreach plugin : plugins
install_subdir(plugin, install_dir: modules_dir)
endforeach
#!/bin/bash
get_cpu_usage() {
local cpu_line="$1"
local stat1=$(grep "^${cpu_line} " /proc/stat)
sleep 0.1
local stat2=$(grep "^${cpu_line} " /proc/stat)
local idle1=$(echo "$stat1" | awk '{print $5}')
local idle2=$(echo "$stat2" | awk '{print $5}')
local total1=$(echo "$stat1" | awk '{sum=0; for(i=2;i<=NF;i++) sum+=$i; print sum}')
local total2=$(echo "$stat2" | awk '{sum=0; for(i=2;i<=NF;i++) sum+=$i; print sum}')
local idle_diff=$((idle2 - idle1))
local total_diff=$((total2 - total1))
if [ "$total_diff" -gt 0 ]; then
awk "BEGIN {printf \"%.1f\", 100 * (1 - $idle_diff / $total_diff)}"
else
echo "0.0"
fi
}
case "$1" in
cpu)
get_cpu_usage "cpu"
;;
cpu[0-9]*)
get_cpu_usage "$1"
;;
cpus_all)
cores=$(nproc)
echo -n "["
for i in $(seq 0 $((cores-1))); do
usage=$(get_cpu_usage "cpu$i")
[ $i -gt 0 ] && echo -n ", "
echo -n "{'name': 'CPU $i', 'value': $usage}"
done
echo "]"
;;
ram)
free | awk '/Mem:/ {printf "%.1f", $3/$2*100}'
;;
swap)
free | awk '/Swap:/ {if($2>0) printf "%.1f", $3/$2*100; else print "0"}'
;;
esac
- name: "System Monitor"
weight: 50
pages:
- name: "Monitor Inline"
icon: view-list-symbolic
- name: "Monitor Large"
icon: view-fullscreen-symbolic
- name: "Monitor Cards"
icon: view-grid-symbolic
sections:
- name: "CPU"
weight: 0
page: "Monitor Inline"
type: custom
settings:
- name: CPU Total
type: info_graph
update_interval: 1
get_command: "{module_path}/bin/stats.sh cpu"
map:
min: 0
max: 100
points: 60
color: "#3584e4"
suffix: "%"
- name: RAM Usage
type: info_graph
update_interval: 1
get_command: "{module_path}/bin/stats.sh ram"
map:
min: 0
max: 100
points: 60
color: "#e5a50a"
suffix: "%"
- name: Swap Usage
type: info_graph
update_interval: 2
get_command: "{module_path}/bin/stats.sh swap"
map:
min: 0
max: 100
points: 30
color: "#c061cb"
suffix: "%"
- name: "CPU"
weight: 0
page: "Monitor Large"
type: custom
settings:
- name: Total Usage
type: info_graph_large
update_interval: 1
get_command: "{module_path}/bin/stats.sh cpu"
map:
min: 0
max: 100
points: 60
color: "#3584e4"
suffix: "%"
- name: "Memory"
weight: 10
page: "Monitor Large"
type: custom
settings:
- name: RAM Usage
type: info_graph_large
update_interval: 1
get_command: "{module_path}/bin/stats.sh ram"
map:
min: 0
max: 100
points: 60
color: "#e5a50a"
suffix: "%"
show_max: true
- name: "CPU Cores"
weight: 0
page: "Monitor Cards"
type: custom
settings:
- name: CPU Cores
type: info_graph_multi
update_interval: 1
get_command: "{module_path}/bin/stats.sh cpus_all"
map:
min: 0
max: 100
points: 40
columns: 3
color: "#3584e4"
suffix: "%"
......@@ -26,6 +26,10 @@ developers = [
"Vladimir Vaskov <rirusha@altlinux.org>"
]
artists = [
"GNOME Design https://gitlab.gnome.org/Teams/Design/"
]
def build_about_dialog() -> Adw.AboutDialog:
about = Adw.AboutDialog(
application_name='tuneit',
......@@ -33,6 +37,7 @@ def build_about_dialog() -> Adw.AboutDialog:
developer_name='Etersoft',
version=tuneit_config.VERSION,
developers=developers,
artists=artists,
copyright='© 2024-2025 Etersoft',
license_type=Gtk.License.GPL_3_0
)
......
import ast
import json
import logging
import dbus
......@@ -82,10 +82,10 @@ class Daemon(dbus.service.Object):
"Permission denied"
)
try:
backend_params = ast.literal_eval(backend_params)
backend_params = json.loads(backend_params)
backend = root_backend_factory.get_backend(backend_name, backend_params)
if backend:
return str(backend.get_value(key, gtype))
return json.dumps(backend.get_value(key, gtype))
except Exception as e:
return dbus.DBusException(
"ru.ximperlinux.TuneIt.Daemon", e
......@@ -106,7 +106,7 @@ class Daemon(dbus.service.Object):
"Permission denied"
)
try:
backend_params = ast.literal_eval(backend_params)
backend_params = json.loads(backend_params)
backend = root_backend_factory.get_backend(backend_name, backend_params)
if backend:
backend.set_value(key, value, gtype)
......@@ -130,10 +130,10 @@ class Daemon(dbus.service.Object):
"Permission denied"
)
try:
backend_params = ast.literal_eval(backend_params)
backend_params = json.loads(backend_params)
backend = root_backend_factory.get_backend(backend_name, backend_params)
if backend:
return str(backend.get_range(key, gtype))
return json.dumps(backend.get_range(key, gtype))
except Exception as e:
return dbus.DBusException(
"ru.ximperlinux.TuneIt.Daemon", e
......
import json
import logging
import dbus
import ast
class DaemonClient:
......@@ -57,7 +57,7 @@ class DaemonClient:
:param backend_params: Параметры backend в формате JSON.
"""
self.backend_params = str(backend_params)
self.backend_params = json.dumps(backend_params)
def get_value(self, key, gtype):
"""
......@@ -68,7 +68,7 @@ class DaemonClient:
:return: Полученное значение.
"""
try:
return ast.literal_eval(str(self.interface.GetValue(self.backend_name, str(self.backend_params), key, gtype)))
return json.loads(str(self.interface.GetValue(self.backend_name, self.backend_params, key, gtype)))
except dbus.DBusException as e:
self.logger.error(f"Error in GetValue: {e}")
return None
......@@ -83,7 +83,7 @@ class DaemonClient:
:return: Результат операции.
"""
try:
self.interface.SetValue(self.backend_name, str(self.backend_params), key, str(value), gtype)
self.interface.SetValue(self.backend_name, self.backend_params, key, str(value), gtype)
except dbus.DBusException as e:
self.logger.error(f"Error in SetValue: {e}")
......@@ -96,7 +96,7 @@ class DaemonClient:
:return: Диапазон значений.
"""
try:
return ast.literal_eval(str(self.interface.GetRange(self.backend_name, str(self.backend_params), key, gtype)))
return json.loads(str(self.interface.GetRange(self.backend_name, self.backend_params, key, gtype)))
except dbus.DBusException as e:
self.logger.error(f"Error in GetRange: {e}")
return None
......
import threading
from concurrent.futures import ThreadPoolExecutor
from gi.repository import GLib
def _idle_call(fn, *args, **kwargs):
if fn is None:
return False
fn(*args, **kwargs)
return False
class BackendExecutor:
def __init__(self, max_workers=4):
self._pool = ThreadPoolExecutor(max_workers=max_workers)
def submit(self, func, on_success=None, on_error=None, on_done=None):
future = self._pool.submit(func)
def _complete(f):
try:
result = f.result()
except Exception as exc:
GLib.idle_add(_idle_call, on_error, exc)
else:
GLib.idle_add(_idle_call, on_success, result)
finally:
GLib.idle_add(_idle_call, on_done)
future.add_done_callback(_complete)
return future
_executor_lock = threading.Lock()
_executor = None
def get_executor():
global _executor
with _executor_lock:
if _executor is None:
_executor = BackendExecutor()
return _executor
......@@ -5,6 +5,7 @@ import traceback
from .module import Module
from .page import Page
from .sections import SectionFactory
from .executor import get_executor
from .tools.yml_tools import load_modules
from .widgets.deps_alert_dialog import TuneItDepsAlertDialog
......@@ -32,6 +33,8 @@ def init_settings_stack(stack, listbox, split_view):
modules = list(yaml_data)
window = listbox.get_root()
executor = get_executor()
def process_next_module():
nonlocal current_module_index
......@@ -42,10 +45,13 @@ def init_settings_stack(stack, listbox, split_view):
module_data = modules[current_module_index]
current_module_index += 1
try:
def _check():
deps_results = dep_manager.verify_deps(module_data.get('deps', {}))
conflicts_results = dep_manager.verify_conflicts(module_data.get('conflicts', {}))
return deps_results, conflicts_results
def _on_success(results):
deps_results, conflicts_results = results
deps_message = dep_manager.format_results(deps_results)
conflicts_message = dep_manager.format_results(conflicts_results)
......@@ -57,10 +63,13 @@ def init_settings_stack(stack, listbox, split_view):
GLib.idle_add(process_next_module)
else:
show_dialog(module_data, deps_message, conflicts_message)
except Exception as e:
handle_error(e, module_data)
def _on_error(exc):
handle_error(exc, module_data)
GLib.idle_add(process_next_module)
executor.submit(_check, on_success=_on_success, on_error=_on_error)
def show_dialog(module_data, deps_msg, conflicts_msg):
dialog = TuneItDepsAlertDialog()
dialog.set_body(module_data['name'])
......@@ -117,4 +126,4 @@ def init_settings_stack(stack, listbox, split_view):
error_msg = f"Module '{module_data['name']}' loading error\nError: {e}\nFull traceback:\n{full_traceback}"
error(error_msg)
GLib.idle_add(process_next_module)
\ No newline at end of file
GLib.idle_add(process_next_module)
......@@ -11,6 +11,10 @@ class ClassicSection(BaseSection):
self.settings = [Setting(s, module) for s in section_data.get('settings', [])]
self.settings = sorted(self.settings, key=lambda s: s.weight, reverse=True)
# Link settings to their section for page tracking
for setting in self.settings:
setting.section = self
self.module.add_section(self)
def create_preferences_group(self):
......
......@@ -6,6 +6,8 @@ import subprocess
from .base import BaseSection
from .custom import CustomSection
from ..setting.custom_setting import CustomSetting
from ..executor import get_executor
from gi.repository import Adw
class DynamicSection(CustomSection):
"""
......@@ -35,29 +37,90 @@ class DynamicSection(CustomSection):
self.generator_command = section_data.get('generator_command')
self.setting_template = section_data.get('setting_template', {})
self._generate_settings()
self.settings_dict = {s.orig_name: s for s in self.settings}
self.settings = []
self.settings_dict = {}
self._callback_buffer = []
self._generated = False
self._generation_started = False
self._group = None
self._placeholder_row = None
self.module.add_section(self)
def _generate_settings(self):
def create_preferences_group(self):
group = Adw.PreferencesGroup(title=self.name, description=self.module.name)
self._group = group
if not self._generated:
placeholder = Adw.ActionRow(title=_("Loading..."))
placeholder.set_activatable(False)
group.add(placeholder)
self._placeholder_row = placeholder
self._start_generation()
return group
return self._populate_group(group)
def _start_generation(self):
if self._generation_started:
return
self._generation_started = True
executor = get_executor()
def _on_success(settings):
self.settings = settings
self.settings_dict = {s.orig_name: s for s in self.settings}
self._generated = True
self._process_buffered_callbacks()
self._refresh_group()
def _on_error(exc):
self.logger.error(f"Dynamic section generation failed: {exc}")
executor.submit(self._generate_settings_sync, on_success=_on_success, on_error=_on_error)
def _generate_settings_sync(self):
items = self._execute_generator()
if not items:
self.logger.warning("Generator returned no items")
return
return []
settings = []
for item in items:
try:
setting_data = self._apply_template(self.setting_template, item)
setting = CustomSetting(setting_data, self.module, self)
self.settings.append(setting)
settings.append(setting)
except Exception as e:
self.logger.error(f"Error creating setting from item {item}: {e}")
self.settings = sorted(self.settings, key=lambda s: s.weight, reverse=True)
self.logger.info(f"Generated {len(self.settings)} settings")
settings = sorted(settings, key=lambda s: s.weight, reverse=True)
self.logger.info(f"Generated {len(settings)} settings")
return settings
def _populate_group(self, group):
not_empty = False
for setting in self.settings:
try:
row = setting.create_row()
if row:
self.logger.debug(f"Adding a row for setting: {setting.name}")
group.add(row)
not_empty = True
else:
self.logger.debug(f"Failed to create a row for setting: {setting.name}")
except Exception as e:
self.logger.error(f"Error creating row for {setting.orig_name}: {str(e)}")
return group if not_empty else None
def _refresh_group(self):
if not self._group:
return
if self._placeholder_row:
self._group.remove(self._placeholder_row)
self._placeholder_row = None
self._populate_group(self._group)
def _execute_generator(self):
if not self.generator_command:
......
......@@ -22,24 +22,21 @@ class CustomSetting(BaseSetting):
self.widget = WidgetFactory.create_widget(self)
if self.widget:
self.row = self.widget.create_row()
self.bind_widget(self.widget)
self.mark_widget_ready()
return self.row
except Exception as e:
self.logger.error(f"Error creating row: {str(e)}")
return None
def get_value(self):
if self._current_value is None:
self._current_value = self._execute_get_command()
return self._current_value
return self._get_backend_value()
def set_value(self, value):
success = self._execute_set_command(value)
if success:
self._current_value = value
self._update_widget()
self._set_backend_value(value)
def get_range(self):
return self._execute_get_range_command()
return self._get_backend_range()
def _execute_command(self, cmd, capture_output=True):
with subprocess.Popen(
......@@ -87,8 +84,8 @@ class CustomSetting(BaseSetting):
try: value = ast.literal_eval(output)
except Exception as e:
value = output
self.logger.info(f"GET: {output} with error {e}")
self.logger.info(f"GET VALUE {value}")
return value
except subprocess.CalledProcessError as e:
self.logger.error(f"Get command failed: {e.stderr}")
......@@ -99,6 +96,7 @@ class CustomSetting(BaseSetting):
return False
try:
self.logger.info(f"SET VALUE {value}")
cmd = self._format_command(self.set_command, value)
self._execute_command(cmd, capture_output=False)
return True
......@@ -129,7 +127,9 @@ class CustomSetting(BaseSetting):
def _handle_callback(self, line):
try:
_, action, target, value = line.split(':', 3)
self.section.handle_callback(
from gi.repository import GLib
GLib.idle_add(
self.section.handle_callback,
action.strip(),
target.strip(),
value.strip()
......@@ -145,7 +145,10 @@ class CustomSetting(BaseSetting):
_, notification, seconds = parts
from ...main import get_main_window
get_main_window().setting_notify(
from gi.repository import GLib
GLib.idle_add(
get_main_window().setting_notify,
self.module.name,
self._(notification),
int(seconds) if seconds else None
......@@ -159,11 +162,11 @@ class CustomSetting(BaseSetting):
def current_value(self):
return self.get_value()
def _get_backend_value(self):
return self.get_value()
def _get_backend_value_sync(self):
return self._execute_get_command()
def _set_backend_value(self, value):
self.set_value(value)
def _set_backend_value_sync(self, value):
self._execute_set_command(value)
def _get_backend_range(self):
return self.get_range()
def _get_backend_range_sync(self):
return self._execute_get_range_command()
......@@ -20,7 +20,12 @@ class Setting(BaseSetting):
self.logger.info("Root is true")
if dclient is not None:
self.widget = WidgetFactory.create_widget(self)
return self.widget.create_row() if self.widget else None
if not self.widget:
return None
row = self.widget.create_row()
self.bind_widget(self.widget)
self.mark_widget_ready()
return row
else:
global service_stopped
......@@ -35,35 +40,37 @@ class Setting(BaseSetting):
return None
self.widget = WidgetFactory.create_widget(self)
return self.widget.create_row() if self.widget else None
def _get_backend_value(self, force=False):
if self._current_value is None or force is True:
backend = self._get_backend()
value = self.default
if backend:
backend_value = backend.get_value(self.key, self.gtype)
if isinstance(backend_value, list):
if all(v is not None for v in backend_value):
value = backend_value
elif backend_value is not None:
if not self.widget:
return None
row = self.widget.create_row()
self.bind_widget(self.widget)
self.mark_widget_ready()
return row
def _get_backend_value_sync(self):
backend = self._get_backend()
value = self.default
if backend:
backend_value = backend.get_value(self.key, self.gtype)
if isinstance(backend_value, list):
if all(v is not None for v in backend_value):
value = backend_value
elif backend_value is not None:
value = backend_value
self._current_value = value
return self._current_value
return value
def _get_backend_range(self):
def _get_backend_range_sync(self):
backend = self._get_backend()
if backend:
return backend.get_range(self.key, self.gtype)
def _set_backend_value(self, value):
def _set_backend_value_sync(self, value):
self.logger.info(f"SET VALUE {value}")
backend = self._get_backend()
if backend:
backend.set_value(self.key, value, self.gtype)
self._current_value = value
def _get_backend(self):
if self.root is True:
......
......@@ -12,6 +12,7 @@ class AvatarWidget(BaseWidget):
activatable=False,
focusable=False
)
self.row = row
control_box = Gtk.Box(
spacing=6,
......@@ -71,7 +72,7 @@ class AvatarWidget(BaseWidget):
self.update_display()
self._update_reset_visibility()
return row
return self.row
def update_display(self):
current = self.setting._get_backend_value()
......
......@@ -23,6 +23,11 @@ class BaseWidget:
reveal_child=False,
halign=Gtk.Align.END
)
self.reset_revealer.set_visible(False)
self.reset_revealer.connect("notify::reveal-child", self._on_reveal_changed)
def _on_reveal_changed(self, revealer, pspec):
revealer.set_visible(revealer.get_reveal_child())
def update_display(self):
raise NotImplementedError("update_display method should be implemented in the subclass")
......@@ -30,8 +35,17 @@ class BaseWidget:
self.row.set_visible(visible)
def set_enabled(self, enabled: bool):
if not enabled:
root = self.row.get_root()
if root:
focus = root.get_focus()
if focus and focus.is_ancestor(self.row):
root.set_focus(None)
self.row.set_sensitive(enabled)
def set_busy(self, busy: bool):
self.set_enabled(not busy)
def create_row(self):
raise NotImplementedError("create_row method should be implemented in the subclass")
......
......@@ -19,7 +19,6 @@ class BooleanWidget(BaseWidget):
control_box.append(self.switch)
self.row.add_suffix(control_box)
self._update_initial_state()
return self.row
def _update_initial_state(self):
......
......@@ -3,11 +3,11 @@ from .BaseWidget import BaseWidget
class ChoiceWidget(BaseWidget):
def create_row(self):
items = list(self.setting.map.keys())
self.row = Adw.ActionRow(title=self.setting.name, subtitle=self.setting.help)
self.dropdown = Gtk.DropDown.new_from_strings(items)
self.model = Gtk.StringList.new([])
expression = Gtk.PropertyExpression.new(Gtk.StringObject, None, "string")
self.dropdown = Gtk.DropDown.new(self.model, expression)
self.dropdown.set_halign(Gtk.Align.CENTER)
self.dropdown.set_valign(Gtk.Align.CENTER)
......@@ -15,28 +15,39 @@ class ChoiceWidget(BaseWidget):
self.row.set_activatable_widget(self.dropdown)
self._update_dropdown_selection()
control_box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.HORIZONTAL)
control_box.append(self.reset_revealer)
control_box.append(self.dropdown)
self.row.add_suffix(control_box)
self._update_reset_visibility()
return self.row
def update_display(self):
self._update_dropdown_selection()
self._update_reset_visibility()
def on_map_updated(self):
self._rebuild_items()
self._update_dropdown_selection()
self._update_reset_visibility()
def _rebuild_items(self):
items = list(self.setting.map.keys()) if self.setting.map else []
with self.dropdown.handler_block(self.handler_id):
self.model.splice(0, self.model.get_n_items(), items)
self.dropdown.set_sensitive(bool(items))
def _update_dropdown_selection(self):
if not self.setting.map:
return
current_index = self.setting._get_selected_row_index()
with self.dropdown.handler_block(self.handler_id):
self.dropdown.set_selected(current_index)
def _on_choice_changed(self, dropdown, _):
if not self.setting.map:
return
selected = dropdown.get_selected()
if selected < 0 or selected >= len(self.setting.map):
return
......@@ -56,6 +67,9 @@ class ChoiceWidget(BaseWidget):
self._update_reset_visibility()
def _update_reset_visibility(self):
if not self.setting.map:
self.reset_revealer.set_reveal_child(False)
return
current_value = self.setting._get_selected_row_index()
default_value = self.setting._get_default_row_index()
......@@ -63,4 +77,4 @@ class ChoiceWidget(BaseWidget):
current_value != default_value
if default_value is not None
else False
)
\ No newline at end of file
)
......@@ -73,6 +73,7 @@ class DualListWidget(BaseWidget):
self.main_row = Adw.PreferencesRow(
activatable=False
)
self.row = self.main_row
content_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
margin_top=8,
......@@ -212,6 +213,7 @@ class DualListWidget(BaseWidget):
def on_move_item(self, button, row, direction):
current = self.pending_changes if self.pending_changes is not None else self.setting._get_backend_value()
current = current or []
if not current:
return
......@@ -239,6 +241,7 @@ class DualListWidget(BaseWidget):
def on_drop(self, target, value, x, y, row):
current = self.pending_changes if self.pending_changes is not None else self.setting._get_backend_value()
current = current or []
src_item = value
dst_item = row.key
......@@ -254,7 +257,10 @@ class DualListWidget(BaseWidget):
return False
def show_dialog(self, button):
if not self.setting.map:
return
current = self.setting._get_backend_value()
current = current or []
if self.pending_changes is not None:
current = self.pending_changes
dialog = AddItemsDialog(
......@@ -268,6 +274,7 @@ class DualListWidget(BaseWidget):
if response == "add":
selected = dialog.get_selected()
current = self.setting._get_backend_value() if self.pending_changes is None else self.pending_changes
current = current or []
new_pending = list(set(current + selected))
if new_pending != current:
self.pending_changes = new_pending
......@@ -276,6 +283,7 @@ class DualListWidget(BaseWidget):
def on_delete_item(self, button, row):
current = self.setting._get_backend_value() if self.pending_changes is None else self.pending_changes
current = current or []
if row.key in current:
new_pending = [k for k in current if k != row.key]
if new_pending != current:
......@@ -287,7 +295,9 @@ class DualListWidget(BaseWidget):
current = self.setting._get_backend_value()
if self.pending_changes is not None:
current = self.pending_changes
current = current or []
self.reorder_btn.set_sensitive(len(current) > 0)
self.add_btn.set_sensitive(bool(self.setting.map))
while child := self.selected_list.get_first_child():
self.selected_list.remove(child)
......@@ -300,6 +310,9 @@ class DualListWidget(BaseWidget):
self._update_reset_visibility()
def on_map_updated(self):
self.update_display()
def on_apply(self, button):
if self.pending_changes is not None:
self.logger.info(f"applying: {self.pending_changes}")
......@@ -322,5 +335,6 @@ class DualListWidget(BaseWidget):
def _update_reset_visibility(self):
current = self.setting._get_backend_value() if self.pending_changes is None else self.pending_changes
current = current or []
is_default = set(current) == set(self.setting.default or [])
self.reset_revealer.set_reveal_child(not is_default)
......@@ -4,39 +4,27 @@ from .BaseWidget import BaseWidget
class EntryWidget(BaseWidget):
def create_row(self):
self.row = Adw.ActionRow(title=self.setting.name)
self.entry = Gtk.Entry()
self.entry.set_halign(Gtk.Align.CENTER)
self.entry.set_text(str(self.setting._get_backend_value() or ""))
self.entry.connect("activate", self._on_text_changed)
control_box = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
spacing=6,
margin_start=12,
valign=Gtk.Align.CENTER,
halign=Gtk.Align.CENTER,
)
control_box.append(self.reset_revealer)
control_box.append(self.entry)
self.row.add_suffix(control_box)
self._update_reset_visibility()
self.row = Adw.EntryRow(title=self.setting.name)
if self.setting.default is not None:
self.reset_button = Gtk.Button(
icon_name="edit-undo-symbolic",
valign=Gtk.Align.CENTER,
tooltip_text=_("Restore Default")
)
self.reset_button.add_css_class("flat")
self.reset_button.connect("clicked", self._on_reset_clicked)
self.row.add_prefix(self.reset_button)
self.row.connect("entry-activated", self._on_text_changed)
return self.row
def update_display(self):
current_value = self.setting._get_backend_value()
self.entry.set_text(str(current_value) if current_value is not None else "")
self.row.set_text(str(current_value) if current_value is not None else "")
self._update_reset_visibility()
def _on_text_changed(self, entry):
new_value = entry.get_text()
def _on_text_changed(self, row):
new_value = row.get_text()
self.setting._set_backend_value(new_value)
......@@ -46,14 +34,15 @@ class EntryWidget(BaseWidget):
default_value = self.setting.default
self.setting._set_backend_value(default_value)
self.entry.set_text(str(default_value) if default_value is not None else "")
self.row.set_text(str(default_value) if default_value is not None else "")
self._update_reset_visibility()
def _update_reset_visibility(self):
current_value = self.entry.get_text()
if self.setting.default is None:
return
default_value = str(self.setting.default) if self.setting.default is not None else ""
has_default = self.setting.default is not None
current_value = self.row.get_text()
default_value = str(self.setting.default)
is_default = current_value == default_value
self.reset_revealer.set_reveal_child(not is_default and has_default)
\ No newline at end of file
self.reset_button.set_sensitive(not is_default)
\ No newline at end of file
......@@ -11,53 +11,52 @@ class FileChooser(BaseWidget):
self.multiple_mode = self.setting.map.get('multiple', False)
self.folder_mode = 'folder' in self.setting.map.get('extensions', [])
row = Adw.ActionRow(
title=self.setting.name,
subtitle=self.setting.help,
subtitle_selectable=True
)
control_box = Gtk.Box(
spacing=6,
margin_end=12,
halign=Gtk.Align.END
self.select_button = Gtk.Button.new_from_icon_name(
icon_name="folder-open-symbolic"
)
control_box.append(self.reset_revealer)
self.select_button.set_valign(Gtk.Align.CENTER)
self.select_button.add_css_class("flat")
self.select_button.connect("clicked", self._on_button_clicked)
if not self.multiple_mode and not self.folder_mode:
self.entry = Gtk.Entry(
placeholder_text="Enter path or click to browse",
hexpand=True,
if self.setting.default is not None:
self.reset_button = Gtk.Button(
icon_name="edit-undo-symbolic",
valign=Gtk.Align.CENTER,
halign=Gtk.Align.END,
tooltip_text=_("Restore Default")
)
self.entry_handler_id = self.entry.connect("activate", self._on_entry_changed)
control_box.append(self.entry)
self.reset_button.add_css_class("flat")
self.reset_button.connect("clicked", self._on_reset_clicked)
if not self.multiple_mode and not self.folder_mode:
self.row = Adw.EntryRow(title=self.setting.name)
if self.setting.default is not None:
self.row.add_prefix(self.reset_button)
self.row.add_suffix(self.select_button)
self.row.connect("entry-activated", self._on_entry_changed)
else:
self.row = Adw.ActionRow(
title=self.setting.name,
subtitle=self.setting.help,
subtitle_selectable=True
)
self.row.set_activatable_widget(self.select_button)
self.info_label = Gtk.Label(
label="No selection" if self.folder_mode else "No files selected",
valign=Gtk.Align.CENTER,
css_classes=["dim-label"]
)
control_box.append(self.info_label)
self.select_button = Gtk.Button.new_from_icon_name(
icon_name="folder-open-symbolic"
)
self.select_button.set_valign(Gtk.Align.CENTER)
row.set_activatable_widget(self.select_button)
self.select_button.connect("clicked", self._on_button_clicked)
control_box.append(self.select_button)
row.add_suffix(control_box)
if self.setting.default is not None:
self.row.add_prefix(self.reset_button)
self.row.add_suffix(self.info_label)
self.row.add_suffix(self.select_button)
self.update_display()
self._update_reset_visibility()
return row
return self.row
def _on_reset_clicked(self, button):
default_value = self.setting.default
......@@ -73,10 +72,12 @@ class FileChooser(BaseWidget):
self._update_reset_visibility()
def _update_reset_visibility(self):
if self.setting.default is None:
return
current_value = self.setting._get_backend_value()
default_value = self.setting.default
if isinstance(current_value, str) and current_value.startswith("file://"):
current_value = current_value[7:]
......@@ -88,10 +89,7 @@ class FileChooser(BaseWidget):
default_value = os.path.expanduser(default_value)
default_value = os.path.expandvars(default_value)
self.reset_revealer.set_reveal_child(
current_value != default_value if default_value is not None
else False
)
self.reset_button.set_sensitive(current_value != default_value)
def update_display(self):
current = self.setting._get_backend_value()
......@@ -210,17 +208,15 @@ class FileChooser(BaseWidget):
self.info_label.set_label(f"{count} files selected" if count else "No files selected")
def _update_single_file_display(self, current):
with self.entry.handler_block(self.entry_handler_id):
self.entry.set_text(current or "")
if current:
file = Gio.File.new_for_path(current)
self.entry.set_tooltip_text(file.get_parse_name())
else:
self.entry.set_tooltip_text(None)
self.row.set_text(current or "")
if current:
file = Gio.File.new_for_path(current)
self.row.set_tooltip_text(file.get_parse_name())
else:
self.row.set_tooltip_text(None)
def _on_entry_changed(self, entry):
if not self.folder_mode and not self.multiple_mode:
path = entry.get_text().strip()
self.setting._set_backend_value(path)
def _on_entry_changed(self, row):
path = row.get_text().strip()
self.setting._set_backend_value(path)
self._update_reset_visibility()
\ No newline at end of file
self._update_reset_visibility()
......@@ -5,6 +5,7 @@ class InfoDictWidget(BaseWidget):
def create_row(self):
main_box = Adw.PreferencesRow()
main_box.set_activatable(False)
self.row = main_box
content_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
......@@ -38,7 +39,7 @@ class InfoDictWidget(BaseWidget):
content_box.append(self.main_list)
self._update_initial_state()
return main_box
return self.row
def _create_value_label(self, value, level):
label = Gtk.Label(
......
from gi.repository import Adw, Gtk
from .BaseWidget import BaseWidget
class InfoGraphLargeWidget(BaseWidget):
def __init__(self, setting):
super().__init__(setting)
self.history = []
self.max_points = setting.map.get('points', 60) if setting.map else 60
self.min_value = setting.map.get('min', 0) if setting.map else 0
self.max_value = setting.map.get('max', 100) if setting.map else 100
self.color = setting.map.get('color', '#3584e4') if setting.map else '#3584e4'
self.suffix = setting.map.get('suffix', '%') if setting.map else '%'
self.show_max = setting.map.get('show_max', False) if setting.map else False
self.max_seen = self.min_value
def create_row(self):
self.box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=0
)
self.drawing_area = Gtk.DrawingArea()
self.drawing_area.set_size_request(-1, 120)
self.drawing_area.set_hexpand(True)
self.drawing_area.set_draw_func(self._draw_graph)
label_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=2,
margin_start=12,
margin_end=12,
margin_top=8,
margin_bottom=12
)
self.title_label = Gtk.Label(
label=self.setting.name,
xalign=0,
css_classes=["dim-label", "caption"]
)
self.value_label = Gtk.Label(
label="--",
xalign=0,
css_classes=["title-3"]
)
label_box.append(self.title_label)
label_box.append(self.value_label)
self.box.append(self.drawing_area)
self.box.append(label_box)
self.row = Gtk.ListBoxRow(
selectable=False,
activatable=False
)
self.row.set_child(self.box)
return self.row
def _parse_color(self, color_str):
if color_str.startswith('#'):
color_str = color_str[1:]
r = int(color_str[0:2], 16) / 255.0
g = int(color_str[2:4], 16) / 255.0
b = int(color_str[4:6], 16) / 255.0
return r, g, b
def _draw_graph(self, area, cr, width, height):
r, g, b = self._parse_color(self.color)
cr.set_source_rgba(r, g, b, 0.08)
cr.rectangle(0, 0, width, height)
cr.fill()
if len(self.history) < 2:
return
value_range = self.max_value - self.min_value
if value_range == 0:
value_range = 1
points = []
padding = 0
graph_height = height - padding * 2
step = width / (self.max_points - 1)
start_x = width - (len(self.history) - 1) * step
for i, value in enumerate(self.history):
x = start_x + i * step
normalized = (value - self.min_value) / value_range
normalized = max(0, min(1, normalized))
y = height - padding - (normalized * graph_height)
points.append((x, y))
cr.set_source_rgba(r, g, b, 0.25)
cr.move_to(points[0][0], height)
cr.line_to(points[0][0], points[0][1])
for x, y in points[1:]:
cr.line_to(x, y)
cr.line_to(points[-1][0], height)
cr.close_path()
cr.fill()
cr.set_source_rgba(r, g, b, 0.8)
cr.set_line_width(1.5)
cr.move_to(points[0][0], points[0][1])
for x, y in points[1:]:
cr.line_to(x, y)
cr.stroke()
def update_display(self):
value = self.setting._current_value
if value is not None:
try:
value = float(value)
self.history.append(value)
if len(self.history) > self.max_points:
self.history.pop(0)
if value > self.max_seen:
self.max_seen = value
if self.show_max:
self.value_label.set_label(
f"{value:.0f}{self.suffix} \u00b7 Max: {self.max_seen:.0f}{self.suffix}"
)
else:
self.value_label.set_label(f"{value:.0f}{self.suffix}")
self.drawing_area.queue_draw()
except (ValueError, TypeError):
pass
from gi.repository import Gtk
from .BaseWidget import BaseWidget
class InfoGraphMultiWidget(BaseWidget):
def __init__(self, setting):
super().__init__(setting)
self.graphs = {}
self.max_points = setting.map.get('points', 40) if setting.map else 40
self.min_value = setting.map.get('min', 0) if setting.map else 0
self.max_value = setting.map.get('max', 100) if setting.map else 100
self.color = setting.map.get('color', '#3584e4') if setting.map else '#3584e4'
self.suffix = setting.map.get('suffix', '%') if setting.map else '%'
self.columns = setting.map.get('columns', 3) if setting.map else 3
def create_row(self):
self.flow_box = Gtk.FlowBox(
homogeneous=True,
selection_mode=Gtk.SelectionMode.NONE,
margin_start=6,
margin_end=6,
margin_top=6,
margin_bottom=6,
row_spacing=6,
column_spacing=6
)
self.row = Gtk.ListBoxRow(
selectable=False,
activatable=False
)
self.row.set_child(self.flow_box)
return self.row
def _parse_color(self, color_str):
if color_str.startswith('#'):
color_str = color_str[1:]
r = int(color_str[0:2], 16) / 255.0
g = int(color_str[2:4], 16) / 255.0
b = int(color_str[4:6], 16) / 255.0
return r, g, b
def _create_graph_card(self, name):
box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=0,
css_classes=["card"],
width_request=150
)
drawing_area = Gtk.DrawingArea()
drawing_area.set_size_request(-1, 70)
drawing_area.set_hexpand(True)
label_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=2,
margin_start=10,
margin_end=10,
margin_top=6,
margin_bottom=10
)
title_label = Gtk.Label(
label=name,
xalign=0,
css_classes=["dim-label", "caption"]
)
value_label = Gtk.Label(
label="--",
xalign=0,
css_classes=["title-4"]
)
label_box.append(title_label)
label_box.append(value_label)
box.append(drawing_area)
box.append(label_box)
self.graphs[name] = {
'history': [],
'drawing_area': drawing_area,
'value_label': value_label,
'box': box
}
drawing_area.set_draw_func(lambda area, cr, w, h: self._draw_graph(cr, w, h, name))
return box
def _draw_graph(self, cr, width, height, name):
if name not in self.graphs:
return
history = self.graphs[name]['history']
r, g, b = self._parse_color(self.color)
cr.set_source_rgba(r, g, b, 0.1)
cr.rectangle(0, 0, width, height)
cr.fill()
if len(history) < 2:
return
value_range = self.max_value - self.min_value
if value_range == 0:
value_range = 1
points = []
padding_y = 4
graph_height = height - padding_y * 2
step = width / (self.max_points - 1)
start_x = width - (len(history) - 1) * step
for i, value in enumerate(history):
x = start_x + i * step
normalized = (value - self.min_value) / value_range
normalized = max(0, min(1, normalized))
y = height - padding_y - (normalized * graph_height)
points.append((x, y))
cr.set_source_rgba(r, g, b, 0.6)
cr.set_line_width(1.5)
cr.move_to(points[0][0], points[0][1])
for x, y in points[1:]:
cr.line_to(x, y)
cr.stroke()
def update_display(self):
data = self.setting._current_value
if not data or not isinstance(data, list):
return
for item in data:
if not isinstance(item, dict):
continue
name = item.get('name', '')
value = item.get('value')
subtitle = item.get('subtitle', '')
if not name or value is None:
continue
try:
value = float(value)
except (ValueError, TypeError):
continue
if name not in self.graphs:
card = self._create_graph_card(name)
self.flow_box.append(card)
graph = self.graphs[name]
graph['history'].append(value)
if len(graph['history']) > self.max_points:
graph['history'].pop(0)
if subtitle:
graph['value_label'].set_label(f"{value:.0f} {self.suffix} \u00b7 {subtitle}")
else:
graph['value_label'].set_label(f"{value:.0f} {self.suffix}")
graph['drawing_area'].queue_draw()
from gi.repository import Adw, Gtk
from .BaseWidget import BaseWidget
class InfoGraphWidget(BaseWidget):
def __init__(self, setting):
super().__init__(setting)
self.history = []
self.max_points = setting.map.get('points', 60) if setting.map else 60
self.min_value = setting.map.get('min', 0) if setting.map else 0
self.max_value = setting.map.get('max', 100) if setting.map else 100
self.color = setting.map.get('color', '#3584e4') if setting.map else '#3584e4'
self.suffix = setting.map.get('suffix', '%') if setting.map else '%'
def create_row(self):
self.row = Adw.ActionRow(
title=self.setting.name,
subtitle=self.setting.help
)
box = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
spacing=12,
valign=Gtk.Align.CENTER
)
self.value_label = Gtk.Label(
label="--",
width_chars=6,
xalign=1.0
)
self.value_label.add_css_class("title-4")
self.drawing_area = Gtk.DrawingArea()
self.drawing_area.set_size_request(120, 32)
self.drawing_area.set_draw_func(self._draw_graph)
box.append(self.value_label)
box.append(self.drawing_area)
self.row.add_suffix(box)
return self.row
def _parse_color(self, color_str):
if color_str.startswith('#'):
color_str = color_str[1:]
r = int(color_str[0:2], 16) / 255.0
g = int(color_str[2:4], 16) / 255.0
b = int(color_str[4:6], 16) / 255.0
return r, g, b
def _draw_graph(self, area, cr, width, height):
if not self.history:
return
r, g, b = self._parse_color(self.color)
cr.set_source_rgba(r, g, b, 0.1)
cr.rectangle(0, 0, width, height)
cr.fill()
cr.set_source_rgba(r, g, b, 0.3)
cr.set_line_width(1)
cr.rectangle(0.5, 0.5, width - 1, height - 1)
cr.stroke()
if len(self.history) < 2:
return
cr.set_source_rgba(r, g, b, 0.2)
value_range = self.max_value - self.min_value
if value_range == 0:
value_range = 1
points = []
step = width / (self.max_points - 1)
start_x = width - (len(self.history) - 1) * step
for i, value in enumerate(self.history):
x = start_x + i * step
normalized = (value - self.min_value) / value_range
y = height - (normalized * (height - 4)) - 2
points.append((x, y))
cr.move_to(points[0][0], height)
cr.line_to(points[0][0], points[0][1])
for x, y in points[1:]:
cr.line_to(x, y)
cr.line_to(points[-1][0], height)
cr.close_path()
cr.fill()
cr.set_source_rgb(r, g, b)
cr.set_line_width(1.5)
cr.move_to(points[0][0], points[0][1])
for x, y in points[1:]:
cr.line_to(x, y)
cr.stroke()
def update_display(self):
value = self.setting._current_value
if value is not None:
try:
value = float(value)
self.history.append(value)
if len(self.history) > self.max_points:
self.history.pop(0)
self.value_label.set_label(f"{value:.1f}{self.suffix}")
self.drawing_area.queue_draw()
except (ValueError, TypeError):
pass
......@@ -21,7 +21,7 @@ class InfoLabelWidget(BaseWidget):
def _update_initial_state(self):
current_value = self.setting._get_backend_value()
self.label.set_label(current_value)
self.label.set_label(str(current_value) if current_value is not None else "")
def update_display(self):
self._update_initial_state()
......@@ -4,30 +4,33 @@ from .BaseWidget import BaseWidget
class NumStepper(BaseWidget):
def create_row(self):
map = self.setting.map
map_keys = list(map.keys())
map_data = self.setting.map or {}
map_keys = list(map_data.keys())
row = Adw.ActionRow(
title=self.setting.name,
subtitle=self.setting.help,
activatable=False
)
self.row = row
self.spin = Gtk.SpinButton(
valign=Gtk.Align.CENTER,
halign=Gtk.Align.CENTER,
)
lower, upper, step, digits = self._get_range_values(map_data)
adjustment = Gtk.Adjustment(
value=self.setting._get_backend_value(),
lower=map["lower"],
upper=map["upper"],
step_increment=map["step"],
value=lower,
lower=lower,
upper=upper,
step_increment=step,
)
self.spin.set_adjustment(adjustment)
self.spin.set_sensitive(bool(map_data))
if "digits" in map_keys:
self.spin.set_digits(map["digits"])
self.spin.set_digits(map_data["digits"])
control_box = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
......@@ -39,20 +42,36 @@ class NumStepper(BaseWidget):
row.add_suffix(control_box)
self.spin_handler_id = self.spin.connect("value-changed", self._on_num_changed)
self._update_reset_visibility()
return row
return self.row
def update_display(self):
if not self.setting.map:
return
current_value = self.setting._get_backend_value()
if current_value is None:
current_value = self.spin.get_adjustment().get_lower()
with self.spin.handler_block(self.spin_handler_id):
self.spin.set_value(float(current_value))
self._update_reset_visibility()
def on_map_updated(self):
map_data = self.setting.map or {}
lower, upper, step, digits = self._get_range_values(map_data)
adjustment = self.spin.get_adjustment()
with self.spin.handler_block(self.spin_handler_id):
adjustment.set_lower(lower)
adjustment.set_upper(upper)
adjustment.set_step_increment(step)
self.spin.set_digits(digits)
self.spin.set_sensitive(bool(map_data))
self.update_display()
def _on_num_changed(self, widget):
selected_value = widget.get_value()
if self.spin.get_digits() == 0:
selected_value = int(selected_value)
self.setting._set_backend_value(selected_value)
self._update_reset_visibility()
......@@ -68,10 +87,24 @@ class NumStepper(BaseWidget):
self._update_reset_visibility()
def _update_reset_visibility(self):
current_value = float(self.setting._get_backend_value())
if not self.setting.map:
self.reset_revealer.set_reveal_child(False)
return
current_value = self.setting._get_backend_value()
if current_value is None:
self.reset_revealer.set_reveal_child(False)
return
current_value = float(current_value)
default_value = self.setting.default
self.reset_revealer.set_reveal_child(
current_value != default_value if default_value is not None
else False
)
\ No newline at end of file
)
def _get_range_values(self, map_data):
lower = map_data.get("lower", 0)
upper = map_data.get("upper", max(lower + 1, 1))
step = map_data.get("step", 1)
digits = map_data.get("digits", 0)
return lower, upper, step, digits
......@@ -5,6 +5,7 @@ from .BaseWidget import BaseWidget
class RadioChoiceWidget(BaseWidget):
def create_row(self):
main_box = Adw.PreferencesRow()
self.row = main_box
content_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
......@@ -48,24 +49,48 @@ class RadioChoiceWidget(BaseWidget):
title_box.append(subtitle_label)
radio_container = Gtk.Box(
self.radio_container = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=8
)
content_box.append(radio_container)
content_box.append(self.radio_container)
self.radio_buttons = {}
self._build_radio_buttons()
self.reset_revealer.set_halign(Gtk.Align.END)
title_horizontal_box.append(self.reset_revealer)
self._update_reset_visibility()
return self.row
def on_map_updated(self):
while True:
child = self.radio_container.get_first_child()
if child is None:
break
self.radio_container.remove(child)
self._build_radio_buttons()
self._update_reset_visibility()
def _build_radio_buttons(self):
self.radio_buttons = {}
if not self.setting.map:
placeholder = Gtk.Label(label=_("Loading..."), halign=Gtk.Align.START)
placeholder.add_css_class("dim-label")
self.radio_container.append(placeholder)
return
current_value = self.setting._get_backend_value()
group = None
# Создаем радио-кнопки
for label, value in self.setting.map.items():
radio = Gtk.CheckButton(
label=label,
halign=Gtk.Align.START,
active=(value == current_value)
)
radio.add_css_class('selection-mode')
radio.add_css_class("selection-mode")
if group:
radio.set_group(group)
......@@ -74,15 +99,11 @@ class RadioChoiceWidget(BaseWidget):
handler_id = radio.connect("toggled", self._on_toggle, value)
self.radio_buttons[value] = (radio, handler_id)
radio_container.append(radio)
self.reset_revealer.set_halign(Gtk.Align.END)
title_horizontal_box.append(self.reset_revealer)
self._update_reset_visibility()
return main_box
self.radio_container.append(radio)
def update_display(self):
if not self.setting.map:
return
current_value = self.setting._get_backend_value()
for value, (radio, handler_id) in self.radio_buttons.items():
with GObject.signal_handler_block(radio, handler_id):
......@@ -108,6 +129,9 @@ class RadioChoiceWidget(BaseWidget):
self._update_reset_visibility()
def _update_reset_visibility(self):
if not self.setting.map:
self.reset_revealer.set_reveal_child(False)
return
current_value = self.setting._get_backend_value()
default_value = self.setting.default
......
......@@ -107,22 +107,8 @@ class ThemeChooserWidget(BaseWidget):
content_box.append(self.flowbox)
self.theme_cards = {}
current_value = self.setting._get_backend_value()
previews = self.setting.previews or {}
for label, value in self.setting.map.items():
card = self._create_theme_card(label, value, previews.get(value))
flowbox_child = Gtk.FlowBoxChild()
flowbox_child.set_child(card)
flowbox_child.value = value
self.flowbox.append(flowbox_child)
self.theme_cards[value] = flowbox_child
if value == current_value:
self.flowbox.select_child(flowbox_child)
self.handler_id = self.flowbox.connect(
"child-activated", self._on_theme_selected)
self.handler_id = self.flowbox.connect("child-activated", self._on_theme_selected)
self._build_cards()
self._update_reset_visibility()
return self.row
......@@ -173,6 +159,36 @@ class ThemeChooserWidget(BaseWidget):
card.append(card_inner)
return card
def _clear_flowbox(self):
while True:
child = self.flowbox.get_first_child()
if child is None:
break
self.flowbox.remove(child)
self.theme_cards = {}
def _build_cards(self):
if not self.setting.map:
placeholder = Gtk.Label(label=_("Loading..."), halign=Gtk.Align.START)
placeholder.add_css_class("dim-label")
self.flowbox.append(placeholder)
return
current_value = self.setting._get_backend_value()
previews = self.setting.previews or {}
for label, value in self.setting.map.items():
card = self._create_theme_card(label, value, previews.get(value))
flowbox_child = Gtk.FlowBoxChild()
flowbox_child.set_child(card)
flowbox_child.value = value
self.flowbox.append(flowbox_child)
self.theme_cards[value] = flowbox_child
if value == current_value:
with GObject.signal_handler_block(self.flowbox, self.handler_id):
self.flowbox.select_child(flowbox_child)
def _create_color_preview(self, value):
"""Create a colored preview box for the theme."""
preview = Gtk.Box(
......@@ -250,6 +266,11 @@ class ThemeChooserWidget(BaseWidget):
self._update_reset_visibility()
def on_map_updated(self):
self._clear_flowbox()
self._build_cards()
self._update_reset_visibility()
def _on_theme_selected(self, flowbox, child):
value = child.value
self.setting._set_backend_value(value)
......
......@@ -9,6 +9,9 @@ from .FileChooser import FileChooser
from .ButtonWidget import ButtonWidget
from .InfoLabelWidget import InfoLabelWidget
from .InfoDictWidget import InfoDictWidget
from .InfoGraphWidget import InfoGraphWidget
from .InfoGraphLargeWidget import InfoGraphLargeWidget
from .InfoGraphMultiWidget import InfoGraphMultiWidget
from .DualListWidget import DualListWidget
from .ImageChooserWidget import ImageChooserWidget
from .DualImageChooserWidget import DualImageChooserWidget
......@@ -31,6 +34,9 @@ class WidgetFactory:
'button': ButtonWidget,
'info_label': InfoLabelWidget,
'info_dict': InfoDictWidget,
'info_graph': InfoGraphWidget,
'info_graph_large': InfoGraphLargeWidget,
'info_graph_multi': InfoGraphMultiWidget,
'list_dual': DualListWidget,
}
......
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