Commit ca1ae92c authored by Roman Alifanov's avatar Roman Alifanov

a new type of `custom` section with callbacks and interaction of one widget with other widgets

It has not been fully tested, it may be unstable
parent 861914f8
from .classic import ClassicSection
from .custom import CustomSection
class SectionFactory:
def __init__(self):
self.sections = {
'classic': ClassicSection,
'custom': CustomSection,
}
def create_section(self, section_data, module):
......
class BaseSection():
def __init__(self, section_data, module):
self.section_data = section_data
self.name = module.get_translation(section_data['name'])
self.weight = section_data.get('weight', 0)
self.page = section_data.get('page')
\ No newline at end of file
from gi.repository import Adw
from ..setting.custom_setting import CustomSetting
from .base import BaseSection
import logging
class CustomSection(BaseSection):
def __init__(self, section_data, module):
super().__init__(section_data, module)
self.logger = logging.getLogger(f"{self.__class__.__name__}[{self.name}]")
self.settings = [CustomSetting(s, module, self) for s in section_data.get('settings', [])]
self.settings_dict = {s.orig_name: s for s in self.settings}
self.module = module
self.module.add_section(self)
self._callback_buffer = []
def create_preferences_group(self):
group = Adw.PreferencesGroup(title=self.name, description=self.module.name)
not_empty = False
for setting in self.settings:
try:
row = setting.create_row()
if row:
print(f"Adding a row for setting: {setting.name}")
group.add(row)
not_empty = True
except Exception as e:
self.logger.error(f"Error creating row for {setting.orig_name}: {str(e)}")
self._process_buffered_callbacks()
return group if not_empty else None
def handle_callback(self, action, target, value):
self.logger.debug(f"handled callback action={action}, target={target}, value={value}")
try:
if target not in self.settings_dict:
self._callback_buffer.append((action, target, value))
self.logger.debug(f"Buffering callback for {target}")
return
self._apply_callback(action, target, value)
except Exception as e:
self.logger.error(f"Callback handling error: {str(e)}")
def _apply_callback(self, action, target, value):
setting = self.settings_dict[target]
if action == 'set':
setting.set_value(value)
elif action == 'visible':
if setting.row:
setting.row.set_visible(value.lower() == 'true')
elif action == 'enabled':
if setting.row:
setting.row.set_sensitive(value.lower() == 'true')
else:
self.logger.warning(f"Unknown callback action: {action}")
def _process_buffered_callbacks(self):
while self._callback_buffer:
action, target, value = self._callback_buffer.pop(0)
try:
if target in self.settings_dict:
self._apply_callback(action, target, value)
else:
self.logger.warning(f"Unknown target after processing buffer: {target}")
except Exception as e:
self.logger.error(f"Error processing buffered callback: {str(e)}")
def get_all_values(self):
return {
setting.orig_name: setting._current_value
for setting in self.settings
}
from ..searcher import SearcherFactory
from .widgets import WidgetFactory
import logging
import subprocess
class CustomSetting:
def __init__(self, setting_data, module, section):
self._ = module.get_translation
self.module = module
self.section = section
self.logger = logging.getLogger(f"CommandSetting[{setting_data['name']}]")
self.name = self._(setting_data['name'])
self.orig_name = setting_data['name']
self.type = setting_data['type']
self.default = setting_data.get('default', '')
self.help = self._(setting_data.get('help', ''))
self._current_value = None
self.get_command = setting_data.get('get_command')
self.set_command = setting_data.get('set_command')
self.gtype = setting_data.get('gtype', 's')
if len(self.gtype) > 2:
self.gtype = self.gtype[0]
else:
self.gtype = self.gtype
self.search_target = setting_data.get('search_target')
self.params = {
**setting_data.get('params', {}),
'module_path': module.path
}
self.widget = None
self.row = None
self.map = setting_data.get('map')
if self.map is None:
if self.search_target is not None:
self.map = SearcherFactory.create(self.search_target).search()
else:
self.map = self._default_map()
if isinstance(self.map, list) and 'choice' in self.type:
self.map = {
item.title(): item for item in self.map
}
if isinstance(self.map, dict) and 'choice' in self.type:
self.map = {
self._(key) if isinstance(key, str) else key: value
for key, value in self.map.items()
}
def _default_map(self):
if self.type == 'boolean':
# Дефолтная карта для булевых настроек
return {True: True, False: False}
if 'choice' in self.type:
# Дефолтная карта для выборов
map = {}
range = self._get_backend_range()
if range is None:
return {}
for var in range:
print(var)
map[var[0].upper() + var[1:]] = var
return map
if self.type == 'number':
map = {}
range = self._get_backend_range()
if range is None:
return {}
map["upper"] = range[1]
map["lower"] = range[0]
# Кол-во после запятой
map["digits"] = len(str(range[0]).split('.')[-1]) if '.' in str(range[0]) else 0
# Минимальное число с этим количеством
map["step"] = 10 ** -map["digits"] if map["digits"] > 0 else 0
return map
return {}
def create_row(self):
try:
self.widget = WidgetFactory.create_widget(self)
if self.widget:
self.row = self.widget.create_row()
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
def set_value(self, value):
success = self._execute_set_command(value)
if success:
self._current_value = value
self._update_widget()
def _get_selected_row_index(self):
current_value = self._get_backend_value()
return list(self.map.values()).index(current_value) if current_value in self.map.values() else 0
def _get_default_row_index(self):
return list(self.map.values()).index(self.default) if self.default in self.map.values() else None
def _execute_get_command(self):
if not self.get_command:
return self.default
try:
cmd = self._format_command(self.get_command)
result = subprocess.run(
cmd,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
shell=True
)
return self._process_output(result.stdout)
except subprocess.CalledProcessError as e:
self.logger.error(f"Get command failed: {e.stderr}")
return self.default
def _execute_set_command(self, value):
if not self.set_command:
return False
try:
cmd = self._format_command(self.set_command, value)
result = subprocess.run(
cmd,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
shell=True
)
self._process_output(result.stdout)
return True
except subprocess.CalledProcessError as e:
self.logger.error(f"Set command failed: {e.stderr}")
return False
def _format_command(self, template, value=None):
variables = {
'value': value,
**self.params,
**self.section.get_all_values()
}
return template.format(**variables)
def _process_output(self, output):
lines = []
for line in output.split('\n'):
line = line.strip()
if line.startswith('CALLBACK:'):
self._handle_callback(line)
else:
lines.append(line)
return '\n'.join(lines).strip()
def _handle_callback(self, line):
try:
_, action, target, value = line.split(':', 3)
self.section.handle_callback(
action.strip(),
target.strip(),
value.strip()
)
except ValueError:
self.logger.error(f"Invalid callback format: {line}")
def _update_widget(self):
if self.widget:
self.widget.update_display()
@property
def current_value(self):
return self.get_value()
def _get_backend_value(self):
return self.get_value()
def _set_backend_value(self, value):
self.set_value(value)
\ No newline at end of file
......@@ -33,6 +33,8 @@ class Setting:
self.default = setting_data.get('default')
self.gtype = setting_data.get('gtype', [])
self._current_value = None
self.search_target = setting_data.get('search_target', None)
self.map = setting_data.get('map')
......@@ -120,15 +122,15 @@ class Setting:
return list(self.map.values()).index(self.default) if self.default in self.map.values() else None
def _get_backend_value(self):
value = None
if self._current_value is None:
backend = self._get_backend()
value = self.default
if backend:
value = backend.get_value(self.key, self.gtype)
if value is None:
value = self.default
return value
value = backend.get_value(self.key, self.gtype) or self.default
self._current_value = value
return self._current_value
def _get_backend_range(self):
backend = self._get_backend()
......@@ -139,6 +141,7 @@ class Setting:
backend = self._get_backend()
if backend:
backend.set_value(self.key, convert_by_gvariant(value, self.gtype), self.gtype)
self._current_value = value
def _get_backend(self):
if self.root is True:
......
......@@ -21,9 +21,17 @@ class BaseWidget:
reveal_child=False,
halign=Gtk.Align.END
)
def update_display(self):
raise NotImplementedError("update_display method should be implemented in the subclass")
def set_visible(self, visible: bool):
self.row.set_visible(visible)
def set_enabled(self, enabled: bool):
self.row.set_sensitive(enabled)
def create_row(self):
raise NotImplementedError("Метод create_row должен быть реализован в подклассе")
raise NotImplementedError("create_row method should be implemented in the subclass")
def _on_reset_clicked(self, button):
raise NotImplementedError("Метод _on_reset_clicked должен быть реализован в подклассе")
raise NotImplementedError("_on_reset_clicked method should be implemented in the subclass")
......@@ -26,6 +26,9 @@ class BooleanWidget(BaseWidget):
self.switch.set_active(is_active)
self._update_reset_visibility()
def update_display(self):
self._update_initial_state()
def _on_boolean_toggled(self, switch, _):
value = self.setting.map.get(True) if switch.get_active() else self.setting.map.get(False)
self.setting._set_backend_value(value)
......
......@@ -7,14 +7,13 @@ class ChoiceWidget(BaseWidget):
self.row = Adw.ActionRow(title=self.setting.name, subtitle=self.setting.help)
self.dropdown = Gtk.DropDown.new_from_strings(items)
self.dropdown.set_halign(Gtk.Align.CENTER)
self.dropdown.set_valign(Gtk.Align.CENTER)
self._set_dropdown_width(items)
self._update_dropdown_selection()
self.dropdown.set_selected(self.setting._get_selected_row_index())
self.dropdown.connect("notify::selected", self._on_choice_changed)
control_box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.HORIZONTAL)
......@@ -25,6 +24,14 @@ class ChoiceWidget(BaseWidget):
self._update_reset_visibility()
return self.row
def update_display(self):
self._update_dropdown_selection()
self._update_reset_visibility()
def _update_dropdown_selection(self):
current_index = self.setting._get_selected_row_index()
self.dropdown.set_selected(current_index)
def _set_dropdown_width(self, items):
layout = self.dropdown.create_pango_layout("")
width = 0
......@@ -39,16 +46,18 @@ class ChoiceWidget(BaseWidget):
def _on_choice_changed(self, dropdown, _):
selected = dropdown.get_selected()
selected_value = list(self.setting.map.values())[selected]
if selected < 0 or selected >= len(self.setting.map):
return
selected_value = list(self.setting.map.values())[selected]
self.setting._set_backend_value(selected_value)
self._update_reset_visibility()
def _on_reset_clicked(self, button):
default_value = self.setting._get_default_row_index()
if default_value is not None:
with self.dropdown.handler_block_by_func(self._on_choice_changed):
self.dropdown.set_selected(default_value)
self.setting._set_backend_value(self.setting.default)
......
......@@ -9,7 +9,7 @@ class EntryWidget(BaseWidget):
self.entry = Gtk.Entry()
self.entry.set_halign(Gtk.Align.CENTER)
self.entry.set_text(self.setting._get_backend_value() or "")
self.entry.set_text(str(self.setting._get_backend_value() or ""))
self.entry.connect("activate", self._on_text_changed)
......@@ -30,6 +30,12 @@ class EntryWidget(BaseWidget):
return self.row
def update_display(self):
with self.entry.handler_block_by_func(self._on_text_changed):
current_value = self.setting._get_backend_value()
self.entry.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()
......@@ -41,14 +47,13 @@ class EntryWidget(BaseWidget):
default_value = self.setting.default
self.setting._set_backend_value(default_value)
self.entry.set_text(str(default_value))
self.entry.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() or ""
default_value = self.setting.default
current_value = self.entry.get_text()
default_value = str(self.setting.default) if self.setting.default is not None else ""
has_default = self.setting.default is not None
is_default = current_value == default_value
......
......@@ -51,7 +51,7 @@ class FileChooser(BaseWidget):
row.add_suffix(control_box)
self._update_display()
self.update_display()
self._update_reset_visibility()
......@@ -66,7 +66,7 @@ class FileChooser(BaseWidget):
self.setting._set_backend_value(default_value)
self._update_display()
self.update_display()
self._update_reset_visibility()
......@@ -91,11 +91,9 @@ class FileChooser(BaseWidget):
else False
)
def _update_display(self):
def update_display(self):
current = self.setting._get_backend_value()
self._update_reset_visibility()
if current and isinstance(current, str) and current.startswith("file://"):
current = current[7:]
self.value_separated = True
......@@ -107,6 +105,8 @@ class FileChooser(BaseWidget):
else:
self._update_single_file_display(current)
self._update_reset_visibility()
def _on_button_clicked(self, button):
dialog = Gtk.FileDialog()
......@@ -173,7 +173,7 @@ class FileChooser(BaseWidget):
if file:
self.setting._set_backend_value(file.get_path())
self._update_display()
self.update_display()
except Exception as e:
print(f"File selection error: {e}")
......@@ -183,7 +183,7 @@ class FileChooser(BaseWidget):
if file_list:
paths = [f.get_path() for f in file_list]
self.setting._set_backend_value(paths)
self._update_display()
self.update_display()
except Exception as e:
print(f"Multiple files selection error: {e}")
......@@ -192,7 +192,7 @@ class FileChooser(BaseWidget):
folder = dialog.select_folder_finish(result)
if folder:
self.setting._set_backend_value(folder.get_path())
self._update_display()
self.update_display()
except Exception as e:
print(f"Folder selection error: {e}")
......
......@@ -44,6 +44,12 @@ class NumStepper(BaseWidget):
return row
def update_display(self):
current_value = self.setting._get_backend_value()
with self.spin.handler_block_by_func(self._on_num_changed):
self.spin.set_value(float(current_value))
self._update_reset_visibility()
def _on_num_changed(self, widget):
selected_value = widget.get_value()
......
......@@ -84,6 +84,13 @@ class RadioChoiceWidget(BaseWidget):
return main_box
def update_display(self):
current_value = self.setting._get_backend_value()
for value, radio in self.radio_buttons.items():
with radio.handler_block_by_func(self._on_toggle):
radio.set_active(value == current_value)
self._update_reset_visibility()
def _on_toggle(self, button, value):
if button.get_active():
self.setting._set_backend_value(value)
......@@ -97,8 +104,8 @@ class RadioChoiceWidget(BaseWidget):
self.setting._set_backend_value(default_value)
if default_value in self.radio_buttons:
with self.radio_buttons[default_value].handler_block_by_func(self._on_toggle):
self.radio_buttons[default_value].set_active(True)
self._update_reset_visibility()
def _update_reset_visibility(self):
......
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