Commit 17c7f71d authored by Anton Palgunov's avatar Anton Palgunov

Merge remote-tracking branch 'origin/master' into shop

parents 853879a8 3c783c54
option('systemd_unitdir', type: 'string', value: '/usr/lib/systemd/system', description: 'Path to systemd unit directory') option('systemd_unitdir',
\ No newline at end of file type: 'string',
value: '/usr/lib/systemd/system',
description: 'Path to systemd unit directory'
)
option('is_devel',
type: 'boolean',
value: false,
description: 'devel?'
)
...@@ -5,8 +5,17 @@ data/ru.ximperlinux.TuneIt.metainfo.xml.in ...@@ -5,8 +5,17 @@ data/ru.ximperlinux.TuneIt.metainfo.xml.in
data/ru.ximperlinux.TuneIt.gschema.xml data/ru.ximperlinux.TuneIt.gschema.xml
data/ru.ximperlinux.TuneIt.Daemon.policy data/ru.ximperlinux.TuneIt.Daemon.policy
src/about.py
src/main.py src/main.py
src/window.py src/window.py
src/window.blp src/window.blp
src/settings/main.py src/settings/main.py
src/settings/widgets/service_dialog.py src/settings/deps/__init__.py
\ No newline at end of file src/settings/widgets/deps_alert_dialog.blp
src/settings/widgets/deps_alert_dialog.py
src/settings/widgets/service_dialog.py
src/settings/widgets/error_dialog.py
src/settings/widgets/error_dialog.blp
src/settings/setting/widgets/DualListWidget.py
\ No newline at end of file
...@@ -8,15 +8,15 @@ msgid "" ...@@ -8,15 +8,15 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: tuneit\n" "Project-Id-Version: tuneit\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-21 13:16+0300\n" "POT-Creation-Date: 2025-05-05 18:50+0300\n"
"PO-Revision-Date: 2025-01-21 13:18+0300\n" "PO-Revision-Date: 2025-05-05 18:55+0300\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"Language: ru_RU\n" "Language: ru_RU\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.5\n" "X-Generator: Poedit 3.6\n"
#: data/ru.ximperlinux.TuneIt.desktop.in:3 #: data/ru.ximperlinux.TuneIt.desktop.in:3
msgid "tuneit" msgid "tuneit"
...@@ -26,7 +26,7 @@ msgstr "" ...@@ -26,7 +26,7 @@ msgstr ""
msgid "GTK;" msgid "GTK;"
msgstr "" msgstr ""
#: data/ru.ximperlinux.TuneIt.metainfo.xml.in:7 src/window.blp:92 #: data/ru.ximperlinux.TuneIt.metainfo.xml.in:7 src/window.blp:9
msgid "TuneIt" msgid "TuneIt"
msgstr "" msgstr ""
...@@ -48,39 +48,56 @@ msgid "Show modules that require root permissions" ...@@ -48,39 +48,56 @@ msgid "Show modules that require root permissions"
msgstr "Показывать модули которым нужны root права" msgstr "Показывать модули которым нужны root права"
#. Translators: Replace "translator-credits" with your name/username, and optionally an email or URL. #. Translators: Replace "translator-credits" with your name/username, and optionally an email or URL.
#: src/main.py:65 #: src/about.py:40
msgid "translator-credits" msgid "translator-credits"
msgstr "ximper@etersoft.ru" msgstr "ximper@etersoft.ru"
#: src/window.blp:26 #: src/window.blp:40 src/window.blp:66 src/window.blp:87
msgid "Main Menu" msgid "Main Menu"
msgstr "" msgstr ""
#: src/window.blp:72 #: src/window.blp:48
msgid "Settings" msgid "Settings"
msgstr "Настройки" msgstr "Настройки"
#: src/window.blp:79 #: src/window.blp:75
msgid "Sections"
msgstr ""
#: src/window.blp:113
msgid "Shop" msgid "Shop"
msgstr "Магазин" msgstr "Магазин"
#: src/window.blp:99 #: src/window.blp:131
msgid "_Preferences" msgid "_Preferences"
msgstr "" msgstr ""
#: src/window.blp:104 #: src/window.blp:136
msgid "_Keyboard Shortcuts" msgid "_Keyboard Shortcuts"
msgstr "" msgstr ""
#: src/window.blp:109 #: src/window.blp:141
msgid "_About TuneIt" msgid "_About TuneIt"
msgstr "О программе" msgstr "О программе"
#: src/settings/widgets/service_dialog.py:13 #: src/settings/widgets/deps_alert_dialog.blp:5
msgid "The module has unmet dependencies"
msgstr "Модуль имеет неудовлетворенные зависимости"
#: src/settings/widgets/deps_alert_dialog.blp:8
#: src/settings/widgets/error_dialog.blp:9
msgid "Ignore"
msgstr "Игнорировать"
#: src/settings/widgets/deps_alert_dialog.blp:9
msgid "Skip module"
msgstr "Пропустить модуль"
#: src/settings/widgets/service_dialog.py:20
msgid "Dbus service is disabled or unresponsive." msgid "Dbus service is disabled or unresponsive."
msgstr "Dbus сервис отключен или не отвечает." msgstr "Dbus сервис отключен или не отвечает."
#: src/settings/widgets/service_dialog.py:14 #: src/settings/widgets/service_dialog.py:21
msgid "" msgid ""
"It is needed for modules that require root permissions.\n" "It is needed for modules that require root permissions.\n"
"Do you want to try to turn on the service?\n" "Do you want to try to turn on the service?\n"
...@@ -90,13 +107,65 @@ msgstr "" ...@@ -90,13 +107,65 @@ msgstr ""
"Вы хотите попробовать включить сервис?\n" "Вы хотите попробовать включить сервис?\n"
"После включения сервиса Tune It перезапустится." "После включения сервиса Tune It перезапустится."
#: src/settings/widgets/service_dialog.py:16 #: src/settings/widgets/service_dialog.py:23
msgid "Yes" msgid "Yes"
msgstr "Да" msgstr "Да"
#: src/settings/widgets/service_dialog.py:17 #: src/settings/widgets/service_dialog.py:24
msgid "No" msgid "No"
msgstr "Нет" msgstr "Нет"
#: src/settings/widgets/error_dialog.blp:5
msgid "Error in Tune it!"
msgstr "Ошибка в Tune It!"
#: src/settings/widgets/error_dialog.blp:6
msgid "You can write in an official telegram chat by applying this log."
msgstr "Вы можете написать в официальный телеграм чат с приложенным логом."
#: src/settings/widgets/error_dialog.blp:10
msgid "Copy the log and go to telegram chat"
msgstr "Скопировать в лог и перейти в телеграм чат"
#: src/settings/setting/widgets/DualListWidget.py:18
msgid "Search..."
msgstr "Поиск..."
#: src/settings/setting/widgets/DualListWidget.py:34
msgid "_Cancel"
msgstr "Отменить"
#: src/settings/setting/widgets/DualListWidget.py:35
msgid "_Add"
msgstr "Добавить"
#: src/settings/setting/widgets/DualListWidget.py:109
msgid "Add"
msgstr "Добавить"
#: src/settings/setting/widgets/DualListWidget.py:110
msgid "Add items"
msgstr "Добавить элементы"
#: src/settings/setting/widgets/DualListWidget.py:122
msgid "Apply"
msgstr "Применть"
#: src/settings/setting/widgets/DualListWidget.py:130
msgid "Edit"
msgstr "Изменить"
#: src/settings/setting/widgets/DualListWidget.py:131
msgid "Toggle reorder mode"
msgstr "Переключение режима изменения порядка"
#: src/settings/setting/widgets/DualListWidget.py:175
msgid "Move up"
msgstr "Передвинуть вверх"
#: src/settings/setting/widgets/DualListWidget.py:183
msgid "Move down"
msgstr "Передвинуть вниз"
#~ msgid "You need root rights to read and modify system configs." #~ msgid "You need root rights to read and modify system configs."
#~ msgstr "Вам нужны права суперпользователя (root), чтобы читать и изменять конфигм системы." #~ msgstr "Вам нужны права суперпользователя (root), чтобы читать и изменять конфигм системы."
# about.py
#
# Copyright 2025 Etersoft
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
from gi.repository import Gtk, Adw
developers = [
"Roman Alifanov https://gitlab.eterfund.ru/ximper",
"Anton Palgunov https://gitlab.eterfund.ru/Toxblh",
"Kirill Unitsaev https://gitlab.eterfund.ru/fiersik",
"Vladimir Vaskov <rirusha@altlinux.org>"
]
def build_about_dialog() -> Adw.AboutDialog:
about = Adw.AboutDialog(
application_name='tuneit',
application_icon='ru.ximperlinux.TuneIt',
developer_name='Etersoft',
version=tuneit_config.VERSION,
developers=developers,
copyright='© 2024-2025 Etersoft',
license_type=Gtk.License.GPL_3_0
)
# Translators: Replace "translator-credits" with your name/username, and optionally an email or URL.
about.set_translator_credits(_('translator-credits'))
return about
class Config:
VERSION = '@VERSION@'
pkgdatadir = '@pkgdatadir@'
localedir = '@localedir@'
IS_DEVEL = @IS_DEVEL@
\ No newline at end of file
...@@ -25,7 +25,6 @@ import signal ...@@ -25,7 +25,6 @@ import signal
import locale import locale
import gettext import gettext
VERSION = '@VERSION@'
pkgdatadir = '@pkgdatadir@' pkgdatadir = '@pkgdatadir@'
localedir = '@localedir@' localedir = '@localedir@'
......
...@@ -25,10 +25,14 @@ gi.require_version('Adw', '1') ...@@ -25,10 +25,14 @@ gi.require_version('Adw', '1')
from gi.repository import Gtk, Gio, Adw from gi.repository import Gtk, Gio, Adw
from .window import TuneitWindow from .window import TuneitWindow
from .about import build_about_dialog
def get_main_window(): def get_main_window():
return _application.props.active_window return _application.props.active_window
def get_error():
return _application.props.active_window.error
class TuneitApplication(Adw.Application): class TuneitApplication(Adw.Application):
"""The main application singleton class.""" """The main application singleton class."""
...@@ -55,14 +59,7 @@ class TuneitApplication(Adw.Application): ...@@ -55,14 +59,7 @@ class TuneitApplication(Adw.Application):
def on_about_action(self, *args): def on_about_action(self, *args):
"""Callback for the app.about action.""" """Callback for the app.about action."""
about = Adw.AboutDialog(application_name='tuneit', about = build_about_dialog()
application_icon='ru.ximperlinux.TuneIt',
developer_name='Etersoft',
version='0.1.0',
developers=['Ximper'],
copyright='© 2024 Etersoft')
# Translators: Replace "translator-credits" with your name/username, and optionally an email or URL.
about.set_translator_credits(_('translator-credits'))
about.present(self.props.active_window) about.present(self.props.active_window)
def on_preferences_action(self, widget, _): def on_preferences_action(self, widget, _):
...@@ -85,7 +82,7 @@ class TuneitApplication(Adw.Application): ...@@ -85,7 +82,7 @@ class TuneitApplication(Adw.Application):
self.set_accels_for_action(f"app.{name}", shortcuts) self.set_accels_for_action(f"app.{name}", shortcuts)
def main(version): def main():
"""The application's entry point.""" """The application's entry point."""
app = TuneitApplication() app = TuneitApplication()
return app.run(sys.argv) return app.run(sys.argv)
...@@ -31,6 +31,8 @@ conf.set('VERSION', meson.project_version()) ...@@ -31,6 +31,8 @@ conf.set('VERSION', meson.project_version())
conf.set('localedir', get_option('prefix') / get_option('localedir')) conf.set('localedir', get_option('prefix') / get_option('localedir'))
conf.set('pkgdatadir', pkgdatadir) conf.set('pkgdatadir', pkgdatadir)
conf.set('IS_DEVEL', get_option('is_devel'))
configure_file( configure_file(
input: 'tuneit.in', input: 'tuneit.in',
output: 'tuneit', output: 'tuneit',
...@@ -49,8 +51,18 @@ configure_file( ...@@ -49,8 +51,18 @@ configure_file(
install_mode: 'r-xr-xr-x' install_mode: 'r-xr-xr-x'
) )
configure_file(
input: 'config.py',
output: 'config.py',
configuration: conf,
install: true,
install_dir: moduledir
)
tuneit_sources = [ tuneit_sources = [
'__init__.py', '__init__.py',
'about.py',
'main.py', 'main.py',
'window.py', 'window.py',
'daemon.py', 'daemon.py',
......
import logging
class Backend: class Backend:
def __init__(self, params=None): def __init__(self, params=None):
# Параметры, передаваемые при инициализации # Параметры, передаваемые при инициализации
self.params = params or {} self.params = params or {}
self.logger = logging.getLogger(f"{self.__class__.__name__}")
def get_value(self, key, gtype): def get_value(self, key, gtype):
raise NotImplementedError("Метод get_value должен быть реализован") raise NotImplementedError("Метод get_value должен быть реализован")
......
...@@ -26,11 +26,11 @@ class BinaryBackend(Backend): ...@@ -26,11 +26,11 @@ class BinaryBackend(Backend):
return result.stdout.strip() return result.stdout.strip()
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"[ERROR] Ошибка при выполнении команды {command}: {e}") self.logger.error(f"Ошибка при выполнении команды {command}: {e}")
return None return None
def get_value(self, key, gtype): def get_value(self, key, gtype):
print(f"[DEBUG] Получение значения: key={key}, gtype={gtype}") self.logger.debug(f"Получение значения: key={key}, gtype={gtype}")
result = self._run_binary('get_value', key) result = self._run_binary('get_value', key)
...@@ -38,28 +38,28 @@ class BinaryBackend(Backend): ...@@ -38,28 +38,28 @@ class BinaryBackend(Backend):
try: try:
return ast.literal_eval(result) return ast.literal_eval(result)
except (ValueError, SyntaxError) as e: except (ValueError, SyntaxError) as e:
print(f"[ERROR] Ошибка при преобразовании результата {result}: {e}") self.logger.warning(f"Ошибка при преобразовании результата {result}: {e}")
return result return result
return None return None
def get_range(self, key, gtype): def get_range(self, key, gtype):
print(f"[DEBUG] Получение диапазона: key={key}, gtype={gtype}") self.logger.debug(f"Получение диапазона: key={key}, gtype={gtype}")
result = self._run_binary('get_range', key) result = self._run_binary('get_range', key)
if not result: if not result:
print(f"[ERROR] Пустой результат или ошибка при выполнении команды get_range для ключа {key}") self.logger.error(f"Пустой результат или ошибка при выполнении команды get_range для ключа {key}")
return None return None
try: try:
parsed_result = ast.literal_eval(result) parsed_result = ast.literal_eval(result)
return parsed_result return parsed_result
except (ValueError, SyntaxError) as e: except (ValueError, SyntaxError) as e:
print(f"[ERROR] Ошибка при преобразовании результата {result} для ключа {key}: {e}") self.logger.error(f"Ошибка при преобразовании результата {result} для ключа {key}: {e}")
return None return None
def set_value(self, key, value, gtype): def set_value(self, key, value, gtype):
print(f"[DEBUG] Установка значения: key={key}, value={value}, gtype={gtype}") self.logger.debug(f"Установка значения: key={key}, value={value}, gtype={gtype}")
result = self._run_binary('set_value', key, str(value)) result = self._run_binary('set_value', key, str(value))
...@@ -67,5 +67,5 @@ class BinaryBackend(Backend): ...@@ -67,5 +67,5 @@ class BinaryBackend(Backend):
try: try:
return ast.literal_eval(result) return ast.literal_eval(result)
except (ValueError, SyntaxError) as e: except (ValueError, SyntaxError) as e:
print(f"[ERROR] Ошибка при преобразовании результата {result}: {e}") self.logger.error(f"Ошибка при преобразовании результата {result}: {e}")
return None return None
...@@ -4,36 +4,51 @@ from .base import Backend ...@@ -4,36 +4,51 @@ from .base import Backend
class GSettingsBackend(Backend): class GSettingsBackend(Backend):
def _get_schema(self, schema_name):
source = Gio.SettingsSchemaSource.get_default()
if source.lookup(schema_name, True) is None:
self.logger.error(f"Scheme {schema_name} is not installed")
return None
return Gio.Settings.new(schema_name)
def get_value(self, key, gtype): def get_value(self, key, gtype):
schema_name, key_name = key.rsplit('.', 1) schema_name, key_name = key.rsplit('.', 1)
schema = Gio.Settings.new(schema_name) schema = self._get_schema(schema_name)
if not schema:
return None
print(f"[DEBUG] Получение значения: schema={schema_name}, key={key_name}, gtype={gtype}")
try: try:
value = schema.get_value(key_name) value = schema.get_value(key_name)
return value.unpack() return value.unpack()
except Exception as e: except Exception as e:
print(f"[ERROR] Ошибка при получении значения {key}: {e}") self.logger.error(f"Error when getting the value {key}: {e}")
return None return None
def get_range(self, key, gtype): def get_range(self, key, gtype):
schema_name, key_name = key.rsplit('.', 1) schema_name, key_name = key.rsplit('.', 1)
schema = Gio.Settings.new(schema_name) schema = self._get_schema(schema_name)
if not schema:
return None
print(f"[DEBUG] Получение значения: schema={schema_name}, key={key_name}, gtype={gtype}")
try: try:
value = schema.get_range(key_name) value = schema.get_range(key_name)
return value.unpack()[1] return value.unpack()[1]
except Exception as e: except Exception as e:
print(f"[ERROR] Ошибка при получении значения {key}: {e}") self.logger.error(f"Error when getting the range {key}: {e}")
return None return None
def set_value(self, schema_key, value, gtype): def set_value(self, schema_key, value, gtype):
schema_name, key_name = schema_key.rsplit('.', 1) schema_name, key_name = schema_key.rsplit('.', 1)
schema = Gio.Settings.new(schema_name) schema = self._get_schema(schema_name)
if not schema:
return
print(f"[DEBUG] Установка значения: schema={schema_name}, key={key_name}, value={value}, gtype={gtype}")
try: try:
schema.set_value(key_name, GLib.Variant(gtype, value)) schema.set_value(key_name, GLib.Variant(gtype, value))
except Exception as e: except Exception as e:
print(f"[ERROR] Ошибка при установке значения {schema_key}: {e}") self.logger.error(f"Error when setting the value {schema_key}: {e}")
import logging
import dbus import dbus
import ast import ast
...@@ -11,12 +12,14 @@ class DaemonClient: ...@@ -11,12 +12,14 @@ class DaemonClient:
:param object_path: Путь объекта в D-Bus. :param object_path: Путь объекта в D-Bus.
:return: Экземпляр DaemonClient или None, если сервис недоступен. :return: Экземпляр DaemonClient или None, если сервис недоступен.
""" """
logger = logging.getLogger(f"DaemonClient")
try: try:
bus = dbus.SystemBus() bus = dbus.SystemBus()
bus.get_object(bus_name, object_path) # Проверка доступности объекта bus.get_object(bus_name, object_path) # Проверка доступности объекта
return super(DaemonClient, cls).__new__(cls) return super(DaemonClient, cls).__new__(cls)
except dbus.DBusException: except dbus.DBusException:
print(f"Service '{bus_name}' is not running.") logger.debug(f"Service '{bus_name}' is not running.")
return None return None
def __init__(self, bus_name="ru.ximperlinux.TuneIt.Daemon", object_path="/Daemon"): def __init__(self, bus_name="ru.ximperlinux.TuneIt.Daemon", object_path="/Daemon"):
...@@ -26,6 +29,8 @@ class DaemonClient: ...@@ -26,6 +29,8 @@ class DaemonClient:
:param bus_name: Имя D-Bus сервиса. :param bus_name: Имя D-Bus сервиса.
:param object_path: Путь объекта в D-Bus. :param object_path: Путь объекта в D-Bus.
""" """
self.logger = logging.getLogger(f"{self.__class__.__name__}")
self.bus_name = bus_name self.bus_name = bus_name
self.object_path = object_path self.object_path = object_path
self.bus = dbus.SystemBus() self.bus = dbus.SystemBus()
...@@ -33,7 +38,7 @@ class DaemonClient: ...@@ -33,7 +38,7 @@ class DaemonClient:
self.interface = dbus.Interface( self.interface = dbus.Interface(
self.proxy, dbus_interface="ru.ximperlinux.TuneIt.DaemonInterface" self.proxy, dbus_interface="ru.ximperlinux.TuneIt.DaemonInterface"
) )
print("dbus client connected") self.logger.debug("dbus client connected")
self.backend_name = None self.backend_name = None
self.backend_params = None self.backend_params = None
...@@ -65,7 +70,7 @@ class DaemonClient: ...@@ -65,7 +70,7 @@ class DaemonClient:
try: try:
return ast.literal_eval(str(self.interface.GetValue(self.backend_name, str(self.backend_params), key, gtype))) return ast.literal_eval(str(self.interface.GetValue(self.backend_name, str(self.backend_params), key, gtype)))
except dbus.DBusException as e: except dbus.DBusException as e:
print(f"Error in GetValue: {e}") self.logger.error(f"Error in GetValue: {e}")
return None return None
def set_value(self, key, value, gtype): def set_value(self, key, value, gtype):
...@@ -80,7 +85,7 @@ class DaemonClient: ...@@ -80,7 +85,7 @@ class DaemonClient:
try: try:
self.interface.SetValue(self.backend_name, str(self.backend_params), key, str(value), gtype) self.interface.SetValue(self.backend_name, str(self.backend_params), key, str(value), gtype)
except dbus.DBusException as e: except dbus.DBusException as e:
print(f"Error in SetValue: {e}") self.logger.error(f"Error in SetValue: {e}")
def get_range(self, key, gtype): def get_range(self, key, gtype):
""" """
...@@ -93,7 +98,7 @@ class DaemonClient: ...@@ -93,7 +98,7 @@ class DaemonClient:
try: try:
return ast.literal_eval(str(self.interface.GetRange(self.backend_name, str(self.backend_params), key, gtype))) return ast.literal_eval(str(self.interface.GetRange(self.backend_name, str(self.backend_params), key, gtype)))
except dbus.DBusException as e: except dbus.DBusException as e:
print(f"Error in GetRange: {e}") self.logger.error(f"Error in GetRange: {e}")
return None return None
dclient = DaemonClient() dclient = DaemonClient()
import logging
from .os import OSReleaseChecker
from .path import PathChecker
from .binary import BinaryChecker
class DependencyCheckerFactory:
def __init__(self):
self._checkers = {
'os': OSReleaseChecker,
'path': PathChecker,
'binary': BinaryChecker,
}
def create_checker(self, dependency_type):
checker_class = self._checkers.get(dependency_type)
if not checker_class:
raise ValueError(f"Unfinished type of dependence: {dependency_type}")
return checker_class()
class DependencyManager:
def __init__(self):
self.factory = DependencyCheckerFactory()
self.logger = logging.getLogger(f"{self.__class__.__name__}")
def _verify(self, items, check_type):
results = []
for item_type, expected_value in items.items():
try:
checker = self.factory.create_checker(item_type)
result = checker.check(expected_value, is_conflict=(check_type == "conflict"))
results.append({
'type': check_type,
'name': item_type,
'success': result['success'],
'actual': result['actual'],
'expected': result['expected'],
'error': result.get('error', '')
})
except Exception as e:
self.logger.error(f"Error when checking {item_type}: {str(e)}")
results.append({
'type': check_type,
'name': item_type,
'success': False,
'error': str(e)
})
return results
def verify_deps(self, dependencies):
return self._verify(dependencies, "dependency")
def verify_conflicts(self, conflicts):
return self._verify(conflicts, "conflict")
def format_results(self, results):
message = ""
for result in results:
label = {
'dependency': f"{result['name']} {_("dependency")}",
'conflict': f"{result['name']} {_("conflict")}"
}[result['type']]
status = "✓" if result['success'] else "✕"
message += f"{label} {status}\n"
if 'actual' in result:
message += f" {_("Actual")} {result['actual']}\n"
message += f" {_("Expected")}: {result['expected']}\n"
if result['error']:
message += f" {_("Error")} {result['error']}\n"
return message
class DependencyChecker:
def check(self, expected_value, is_conflict=False):
raise NotImplementedError("check() method must be implemented in the subclass!")
from .base import DependencyChecker
class BinaryChecker(DependencyChecker):
def check(self, expected_value, is_conflict=False):
from shutil import which
binary_path = which(expected_value)
if isinstance(expected_value, (str, bytes)):
expected_values = (expected_value,)
else:
expected_values = tuple(expected_value)
if is_conflict:
success = binary_path is None
else:
success = binary_path is not None
return {
'success': success,
'actual': binary_path,
'expected': expected_values
}
from .base import DependencyChecker
class OSReleaseChecker(DependencyChecker):
def check(self, expected_value='Etersoft Ximper', is_conflict=False):
from ..backends.file import FileBackend
actual_name = FileBackend({'file_path': '/etc/os-release'}).get_value('NAME', 's')
if isinstance(expected_value, (str, bytes)):
expected_values = (expected_value,)
else:
expected_values = tuple(expected_value)
if is_conflict:
success = actual_name not in expected_values
else:
success = actual_name in expected_values
return {
'success': success,
'actual': actual_name,
'expected': expected_values
}
from .base import DependencyChecker
class PathChecker(DependencyChecker):
def check(self, expected_value, is_conflict=False):
from os import path
file_path = path.expanduser(expected_value)
file_path = path.expandvars(file_path)
file_exists = path.exists(file_path)
if isinstance(expected_value, (str, bytes)):
expected_values = (expected_value,)
else:
expected_values = tuple(expected_value)
if is_conflict:
success = not file_exists
else:
success = file_exists
return {
'success': success,
'actual': file_path,
'expected': expected_values,
'exists': file_exists
}
import logging
from gi.repository import GLib
import traceback
from .module import Module from .module import Module
from .page import Page from .page import Page
from .sections import SectionFactory from .sections import SectionFactory
from .tools.yml_tools import load_modules from .tools.yml_tools import load_modules
from .widgets.deps_alert_dialog import TuneItDepsAlertDialog
from .deps import DependencyManager
logger = logging.getLogger("init_settings_stack")
def init_settings_stack(stack, listbox, split_view): def init_settings_stack(stack, listbox, split_view):
yaml_data = load_modules() yaml_data = load_modules()
...@@ -11,59 +19,102 @@ def init_settings_stack(stack, listbox, split_view): ...@@ -11,59 +19,102 @@ def init_settings_stack(stack, listbox, split_view):
modules_dict = {} modules_dict = {}
pages_dict = {} pages_dict = {}
dep_manager = DependencyManager()
if stack.get_pages(): if stack.get_pages():
print("Clear pages...") logger.info("Clear pages...")
listbox.remove_all() listbox.remove_all()
for page in stack.get_pages(): for page in stack.get_pages(): stack.remove(page)
stack.remove(page)
else: else:
print("First init...") logger.info("First init...")
current_module_index = 0
modules = list(yaml_data)
window = listbox.get_root()
def process_next_module():
nonlocal current_module_index
if current_module_index >= len(modules):
finalize_processing()
return
module_data = modules[current_module_index]
current_module_index += 1
try:
deps_results = dep_manager.verify_deps(module_data.get('deps', {}))
conflicts_results = dep_manager.verify_conflicts(module_data.get('conflicts', {}))
deps_message = dep_manager.format_results(deps_results)
conflicts_message = dep_manager.format_results(conflicts_results)
all_deps_ok = all(r['success'] for r in deps_results)
all_conflicts_ok = all(r['success'] for r in conflicts_results)
if all_deps_ok and all_conflicts_ok:
process_module(module_data)
GLib.idle_add(process_next_module)
else:
show_dialog(module_data, deps_message, conflicts_message)
except Exception as e:
handle_error(e, module_data)
GLib.idle_add(process_next_module)
for module_data in yaml_data: def show_dialog(module_data, deps_msg, conflicts_msg):
dialog = TuneItDepsAlertDialog()
dialog.set_body(module_data['name'])
dialog.deps_message_textbuffer.set_text(f"{deps_msg}\n{conflicts_msg}")
def handle_response(response):
if response == "skip":
GLib.idle_add(process_next_module)
else:
process_module(module_data)
GLib.idle_add(process_next_module)
dialog.ask_user(window, handle_response)
def process_module(module_data):
module = Module(module_data) module = Module(module_data)
modules_dict[module.name] = module modules_dict[module.name] = module
for section_data in module_data.get('sections', []): for section_data in module_data.get('sections', []):
page_name = module.get_translation(section_data.get('page', 'Default')) page_name = module.get_translation(section_data.get('page', 'Default'))
module_page_name = section_data.get('page', 'Default') module_page_name = section_data.get('page', 'Default')
print(module_page_name)
if page_name not in pages_dict: if page_name not in pages_dict:
page_info = module.pages.get(f"_{module_page_name}", {}) or module.pages.get(module_page_name, {})
page_info = ( page = Page(name=page_name, icon=page_info.get('icon'))
module.pages.get(f"_{module_page_name}", {})
or module.pages.get(module_page_name, {})
)
page = Page(
name=page_name,
icon=page_info.get('icon'),
)
pages_dict[page_name] = page pages_dict[page_name] = page
section = section_factory.create_section(section_data, module) section = section_factory.create_section(section_data, module)
pages_dict[page_name].add_section(section) pages_dict[page_name].add_section(section)
pages = list(pages_dict.values()) def finalize_processing():
for page in pages: pages = sorted(pages_dict.values(), key=lambda p: p.name)
page.sort_sections() for page in pages:
page.sort_sections()
pages = sorted(pages, key=lambda p: p.name) page.create_stack_page(stack, listbox)
for page in pages: if not stack:
page.create_stack_page(stack, listbox) logger.error("settings_pagestack не найден.")
if not stack: def on_row_selected(listbox, row):
print("Ошибка: settings_pagestack не найден.") if row:
page_id = row.props.name
def on_row_selected(listbox, row): visible_child = stack.get_child_by_name(page_id)
if row: if visible_child:
page_id = row.props.name stack.set_visible_child(visible_child)
print(f"Selected page: {page_id}") split_view.set_show_content(True)
visible_child = stack.get_child_by_name(page_id) listbox.connect("row-selected", on_row_selected)
if visible_child:
stack.set_visible_child(visible_child) def handle_error(e, module_data):
split_view.set_show_content(True) from ..main import get_error
error = get_error()
listbox.connect("row-selected", on_row_selected) full_traceback = traceback.format_exc()
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
import gettext import gettext
import locale import locale
import logging
import os import os
class Module: class Module:
def __init__(self, module_data): def __init__(self, module_data):
self.name = module_data['name'] self.name = module_data['name']
self.logger = logging.getLogger(f"{self.__class__.__name__}[{self.name}]")
self.weight = module_data.get('weight', 0) self.weight = module_data.get('weight', 0)
self.path = module_data.get("module_path") self.path = module_data.get("module_path")
print(self.path) self.logger.debug(self.path)
self.pages = { self.pages = {
page['name']: page for page in module_data.get('pages', []) page['name']: page for page in module_data.get('pages', [])
} }
......
from gi.repository import Adw import logging
from gi.repository import GLib, Adw
from .widgets.panel_row import TuneItPanelRow from .widgets.panel_row import TuneItPanelRow
...@@ -7,6 +8,9 @@ class Page: ...@@ -7,6 +8,9 @@ class Page:
def __init__(self, name, icon=None): def __init__(self, name, icon=None):
self.name = name self.name = name
self.icon = icon or "preferences-system" # Значение по умолчанию self.icon = icon or "preferences-system" # Значение по умолчанию
self.logger = logging.getLogger(f"{self.__class__.__name__}[{self.name}]")
self.sections = [] self.sections = []
def add_section(self, section): def add_section(self, section):
...@@ -26,17 +30,18 @@ class Page: ...@@ -26,17 +30,18 @@ class Page:
pref_page.add(preferences_group) pref_page.add(preferences_group)
not_empty = True not_empty = True
else: else:
print(f"Секция {section.name} не создала виджетов.") self.logger.warn(f"Секция {section.name} не создала виджетов.")
if not_empty: if not_empty:
stack_page = stack.add_child(pref_page) self.update_ui(stack, listbox, pref_page)
stack_page.set_title(self.name)
stack_page.set_name(self.name)
row = TuneItPanelRow()
row.props.name = self.name
row.props.title = self.name
row.props.icon_name = self.icon
listbox.append(row)
else: else:
print(f"the page {self.name} is empty, ignored") self.logger.warn(f"the page {self.name} is empty, ignored")
def update_ui(self, stack, listbox, pref_page):
stack.add_titled(pref_page, self.name, self.name)
row = TuneItPanelRow()
row.props.name = self.name
row.props.title = self.name
row.props.icon_name = self.icon
listbox.append(row)
import logging
import os import os
import fnmatch
from .backends import FileBackend from .backends import FileBackend
...@@ -82,13 +84,13 @@ class FileSearcher(Searcher): ...@@ -82,13 +84,13 @@ class FileSearcher(Searcher):
result.extend([ result.extend([
os.path.join(root, f) os.path.join(root, f)
for f in files for f in files
if f.endswith(self.pattern) if fnmatch.fnmatch(f, self.pattern)
and not self.is_excluded(os.path.join(root, f)) and not self.is_excluded(os.path.join(root, f))
]) ])
else: else:
if os.path.isfile(base_path) and base_path.endswith(self.pattern): if os.path.isfile(base_path):
filename = os.path.basename(base_path)
if not self.is_excluded(base_path): if fnmatch.fnmatch(filename, self.pattern) and not self.is_excluded(base_path):
result.append(base_path) result.append(base_path)
else: else:
try: try:
...@@ -96,7 +98,7 @@ class FileSearcher(Searcher): ...@@ -96,7 +98,7 @@ class FileSearcher(Searcher):
os.path.join(base_path, f) os.path.join(base_path, f)
for f in os.listdir(base_path) for f in os.listdir(base_path)
if os.path.isfile(os.path.join(base_path, f)) if os.path.isfile(os.path.join(base_path, f))
and f.endswith(self.pattern) and fnmatch.fnmatch(f, self.pattern)
and not self.is_excluded(os.path.join(base_path, f)) and not self.is_excluded(os.path.join(base_path, f))
]) ])
except PermissionError: except PermissionError:
...@@ -109,6 +111,8 @@ class ValueInFileSearcher(Searcher): ...@@ -109,6 +111,8 @@ class ValueInFileSearcher(Searcher):
file_pattern, key, exclude_neighbor_files=None): file_pattern, key, exclude_neighbor_files=None):
super().__init__(search_paths, exclude_paths, exclude_names, recursive) super().__init__(search_paths, exclude_paths, exclude_names, recursive)
self.logger = logging.getLogger(f"{self.__class__.__name__}")
self.file_pattern = file_pattern self.file_pattern = file_pattern
self.key = key self.key = key
self.exclude_neighbor_files = exclude_neighbor_files or [] self.exclude_neighbor_files = exclude_neighbor_files or []
...@@ -140,7 +144,7 @@ class ValueInFileSearcher(Searcher): ...@@ -140,7 +144,7 @@ class ValueInFileSearcher(Searcher):
result.append(FileBackend(params={'file_path': file_path}).get_value(self.key, 's')) result.append(FileBackend(params={'file_path': file_path}).get_value(self.key, 's'))
except Exception as e: except Exception as e:
print(f"Error processing {file_path}: {str(e)}") self.logger.error(f"Error processing {file_path}: {str(e)}")
continue continue
return result return result
......
from ..setting.setting import Setting from .classic import ClassicSection
from .custom import CustomSection
from .base_strategy import SectionStrategy
from .classic import ClassicSectionStrategy
class Section:
def __init__(self, section_data, strategy, module):
self.name = module.get_translation(section_data['name'])
self.weight = section_data.get('weight', 0)
self.page = section_data.get('page')
self.settings = [Setting(s, module) for s in section_data.get('settings', [])]
self.strategy = strategy
self.module = module
self.module.add_section(self)
def create_preferences_group(self):
return self.strategy.create_preferences_group(self)
class SectionFactory: class SectionFactory:
def __init__(self): def __init__(self):
self.strategies = { self.sections = {
'classic': ClassicSectionStrategy(), 'classic': ClassicSection,
'custom': CustomSection,
} }
def create_section(self, section_data, module): def create_section(self, section_data, module):
section_type = section_data.get('type', 'classic') section_type = section_data.get('type', 'classic')
strategy = self.strategies.get(section_type) section = self.sections.get(section_type)
if not strategy: if not section:
raise ValueError(f"Неизвестный тип секции: {section_type}") raise ValueError(f"Unknown type of section: {section_type}")
return Section(section_data, strategy, module) return section(section_data, module)
\ No newline at end of file
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')
class SectionStrategy:
def create_preferences_group(self, section):
raise NotImplementedError("Метод create_preferences_group должен быть реализован")
import logging
from gi.repository import Adw from gi.repository import Adw
from ..setting.setting import Setting
from .base_strategy import SectionStrategy from .base import BaseSection
class ClassicSection(BaseSection):
def __init__(self, section_data, module):
super().__init__(section_data, module)
self.logger = logging.getLogger(f"{self.__class__.__name__}[{self.name}]")
self.settings = [Setting(s, module) for s in section_data.get('settings', [])]
self.settings = sorted(self.settings, key=lambda s: s.weight, reverse=True)
self.module = module
class ClassicSectionStrategy(SectionStrategy): self.module.add_section(self)
def create_preferences_group(self, section):
group = Adw.PreferencesGroup(title=section.name, description=section.module.name) def create_preferences_group(self):
group = Adw.PreferencesGroup(title=self.name, description=self.module.name)
not_empty = False not_empty = False
for setting in section.settings: for setting in self.settings:
row = setting.create_row() row = setting.create_row()
if row: if row:
print(f"Добавление строки для настройки: {setting.name}") self.logger.debug(f"Adding a row for setting: {setting.name}")
group.add(row) group.add(row)
not_empty = True not_empty = True
else: else:
print(f"Не удалось создать строку для настройки: {setting.name}") self.logger.debug(f"Failed to create a row for setting: {setting.name}")
if not_empty: if not_empty:
return group return group
else: else:
......
from gi.repository import Adw
from ..setting.custom_setting import CustomSetting
from .base import BaseSection
import logging
class CustomSection(BaseSection):
settings = []
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:
self.logger.debug(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.create_row = value
setting._update_widget()
elif action == 'set_apply':
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
}
import logging
import threading
import time
from ..searcher import SearcherFactory
from gi.repository import GLib
class BaseSetting:
def __init__(self, setting_data, module):
self._ = module.get_translation
self.module = module
self.name = self._(setting_data['name'])
self.orig_name = setting_data['name']
self.logger = logging.getLogger(f"{self.__class__.__name__}[{self.name}]")
self.weight = setting_data.get('weight', 1)
self.widget = None
self.root = setting_data.get('root', False)
self.params = {
**setting_data.get('params', {}),
'module_path': module.path
}
self.type = setting_data['type']
self.help = setting_data.get('help', None)
if self.help is not None:
self.help = self._(self.help)
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')
self.prepare_map()
self.update_interval = setting_data.get('update_interval', None)
if self.update_interval:
self._start_update_thread()
def prepare_map(self):
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 or 'list_dual' in self.type):
self.map = {
item.title(): item for item in self.map
if item is not None
}
if not self.map:
self.logger.warning(f"Warning: 'map' is empty for setting {self.name}. Check data source.")
if isinstance(self.map, dict) and ('choice' in self.type or 'list_dual' in self.type):
self.map = {
self._(key) if isinstance(key, str) else key: value
for key, value in self.map.items()
}
if not self.map:
self.logger.warning(f"Warning: 'map' is empty for setting {self.name}. Check data source.")
if len(self.gtype) > 2:
self.gtype = self.gtype[0]
else:
self.gtype = self.gtype
def _default_map(self):
if self.type == 'boolean':
# Дефолтная карта для булевых настроек
return {True: True, False: False}
if 'choice' in self.type or 'list_dual' in self.type:
map = {}
range = self._get_backend_range()
if range is None:
return {}
for var in range:
key = var
display_name = var[0].upper() + var[1:] if var else ''
map[key] = display_name
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 _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 _start_update_thread(self):
def update_loop():
while True:
time.sleep(self.update_interval)
prev_value = self._current_value
current_value = self._get_backend_value(force=True)
if current_value != prev_value:
GLib.idle_add(self._update_widget)
thread = threading.Thread(target=update_loop, daemon=True)
thread.start()
def _update_widget(self):
if self.widget:
self.widget.update_display()
return False
from .base import BaseSetting
from .widgets import WidgetFactory
import logging
import subprocess
import ast
class CustomSetting(BaseSetting):
def __init__(self, setting_data, module, section):
self.section = section
self.get_command = setting_data.get('get_command')
self.get_range_command = setting_data.get('get_range_command')
self.set_command = setting_data.get('set_command')
super().__init__(setting_data, module)
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_range(self):
return self._execute_get_range_command()
def _execute_command(self, cmd, capture_output=True):
with subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
shell=True,
bufsize=1
) as p:
output = []
def process_line(line):
line = line.strip()
if not line:
return
if line.startswith('CALLBACK:'):
self._handle_callback(line)
elif line.startswith('NOTIFY:'):
self._handle_notify(line)
elif capture_output:
output.append(line)
while p.poll() is None:
line = p.stdout.readline()
process_line(line)
for line in p.stdout.read().splitlines():
process_line(line)
stderr = p.stderr.read().strip()
if p.returncode != 0:
raise subprocess.CalledProcessError(p.returncode, cmd, stderr=stderr)
return '\n'.join(output) if capture_output else ''
def _execute_get_command(self):
if not self.get_command:
return self.default
try:
cmd = self._format_command(self.get_command)
output = self._execute_command(cmd)
try: value = ast.literal_eval(output)
except Exception as e:
value = output
self.logger.info(f"GET: {output} with error {e}")
return value
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)
self._execute_command(cmd, capture_output=False)
return True
except subprocess.CalledProcessError as e:
self.logger.error(f"Set command failed: {e.stderr}")
return False
def _execute_get_range_command(self):
if not self.get_range_command:
return None
try:
cmd = self._format_command(self.get_range_command)
output = self._execute_command(cmd)
return ast.literal_eval(output)
except subprocess.CalledProcessError as e:
self.logger.error(f"Get range command failed: {e.stderr}")
return None
def _format_command(self, template, value=None):
variables = {
'value': value,
**self.params,
**self.section.get_all_values()
}
return template.format(**variables)
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 _handle_notify(self, line):
self.logger.debug("handled notify")
try:
parts = line.split(':', 2)
parts += [None] * (3 - len(parts))
_, notification, seconds = parts
from ...main import get_main_window
get_main_window().setting_notify(
self.module.name,
self._(notification),
int(seconds) if seconds else None
)
except ValueError:
self.logger.error(f"Invalid notify format: {line}")
@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)
def _get_backend_range(self):
return self.get_range()
from ..searcher import SearcherFactory from .base import BaseSetting
from .widgets import WidgetFactory from .widgets import WidgetFactory
from ..backends import backend_factory from ..backends import backend_factory
from ..daemon_client import dclient from ..daemon_client import dclient
from ..tools.gvariant import convert_by_gvariant from ..tools.gvariant import convert_by_gvariant
from ..widgets.service_dialog import ServiceNotStartedDialog from ..widgets.service_dialog import ServiceNotStartedDialog
dialog_presented = False service_stopped = False
class Setting: class Setting(BaseSetting):
def __init__(self, setting_data, module): def __init__(self, setting_data, module):
self._ = module.get_translation
self.name = self._(setting_data['name'])
self.root = setting_data.get('root', False)
self.backend = setting_data.get('backend') self.backend = setting_data.get('backend')
self.params = {
**setting_data.get('params', {}),
'module_path': module.path
}
self.type = setting_data['type']
self.help = setting_data.get('help', None)
if self.help is not None:
self.help = self._(self.help)
self.key = setting_data.get('key') self.key = setting_data.get('key')
self.default = setting_data.get('default')
self.gtype = setting_data.get('gtype', [])
self.search_target = setting_data.get('search_target', 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
}
super().__init__(setting_data, module)
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()
}
if len(self.gtype) > 2:
self.gtype = self.gtype[0]
else:
self.gtype = self.gtype
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): def create_row(self):
if self.root is True: if self.root is True:
print("Root is true") self.logger.info("Root is true")
if dclient is not None: if dclient is not None:
widget = WidgetFactory.create_widget(self) self.widget = WidgetFactory.create_widget(self)
return widget.create_row() if widget else None return self.widget.create_row() if self.widget else None
else: else:
global dialog_presented global service_stopped
if dialog_presented is False:
from ..main import get_main_window
if service_stopped is False:
from ...main import get_main_window
dialog = ServiceNotStartedDialog() dialog = ServiceNotStartedDialog()
dialog.present(get_main_window())
dialog_presented = True
return None
widget = WidgetFactory.create_widget(self) dialog.present(get_main_window())
return widget.create_row() if widget else None
def _get_selected_row_index(self): service_stopped = True
current_value = self._get_backend_value() return None
return list(self.map.values()).index(current_value) if current_value in self.map.values() else 0
def _get_default_row_index(self): self.widget = WidgetFactory.create_widget(self)
return list(self.map.values()).index(self.default) if self.default in self.map.values() else None return self.widget.create_row() if self.widget else None
def _get_backend_value(self): def _get_backend_value(self, force=False):
value = None if self._current_value is None or force is True:
backend = self._get_backend()
value = self.default
backend = self._get_backend() if backend:
value = backend.get_value(self.key, self.gtype) or self.default
if backend: self._current_value = value
value = backend.get_value(self.key, self.gtype) return self._current_value
if value is None:
value = self.default
return value
def _get_backend_range(self): def _get_backend_range(self):
backend = self._get_backend() backend = self._get_backend()
...@@ -136,9 +54,11 @@ class Setting: ...@@ -136,9 +54,11 @@ class Setting:
return backend.get_range(self.key, self.gtype) return backend.get_range(self.key, self.gtype)
def _set_backend_value(self, value): def _set_backend_value(self, value):
self.logger.info(f"SET VALUE {value}")
backend = self._get_backend() backend = self._get_backend()
if backend: if backend:
backend.set_value(self.key, convert_by_gvariant(value, self.gtype), self.gtype) backend.set_value(self.key, value, self.gtype)
self._current_value = value
def _get_backend(self): def _get_backend(self):
if self.root is True: if self.root is True:
...@@ -149,5 +69,5 @@ class Setting: ...@@ -149,5 +69,5 @@ class Setting:
backend = backend_factory.get_backend(self.backend, self.params) backend = backend_factory.get_backend(self.backend, self.params)
if not backend: if not backend:
print(f"Бекенд {self.backend} не зарегистрирован.") self.logger.error(f"Бекенд {self.backend} не зарегистрирован.")
return backend return backend
from gi.repository import Adw, Gtk, Gdk
from gi.repository import Gio
import os
from .BaseWidget import BaseWidget
class AvatarWidget(BaseWidget):
def create_row(self):
row = Adw.PreferencesRow(
activatable=False,
focusable=False
)
control_box = Gtk.Box(
spacing=6,
orientation=Gtk.Orientation.HORIZONTAL,
halign=Gtk.Align.CENTER,
hexpand=True,
margin_top=12,
margin_bottom=12,
)
self.avatar = Adw.Avatar(
size=128,
text=None
)
self.change_button = Gtk.Button(
icon_name="edit-symbolic",
valign=Gtk.Align.END,
halign=Gtk.Align.END,
margin_bottom=4,
margin_end=4,
tooltip_text="Change your avatar"
)
self.change_button.get_style_context().add_class("circular")
self.change_button.get_style_context().add_class("secondary")
self.change_button.connect("clicked", self._on_change_clicked)
self.reset_button = Gtk.Button(
icon_name="user-trash-symbolic",
valign=Gtk.Align.START,
halign=Gtk.Align.END,
margin_top=4,
margin_end=4,
tooltip_text="Reset the avatar"
)
self.reset_button.get_style_context().add_class("circular")
self.reset_button.get_style_context().add_class("destructive-action")
self.reset_button.connect("clicked", self._on_reset_clicked)
self.reset_revealer = Gtk.Revealer(
transition_type=Gtk.RevealerTransitionType.CROSSFADE,
transition_duration=150,
child=self.reset_button,
reveal_child=False,
valign=Gtk.Align.START,
halign=Gtk.Align.END
)
overlay = Gtk.Overlay()
overlay.set_child(self.avatar)
overlay.add_overlay(self.reset_revealer)
overlay.add_overlay(self.change_button)
control_box.append(overlay)
row.set_child(control_box)
self.update_display()
self._update_reset_visibility()
return row
def update_display(self):
current = self.setting._get_backend_value()
if current and isinstance(current, str) and current.startswith("file://"):
current = current[7:]
self.value_separated = True
if current and os.path.exists(current):
file = Gio.File.new_for_path(current)
try:
texture = Gdk.Texture.new_from_file(file)
self.avatar.set_custom_image(texture)
except Exception as e:
self.avatar.set_custom_image(None)
else:
self.avatar.set_custom_image(None)
self._update_reset_visibility()
def _on_reset_clicked(self, button):
default_value = self.setting.default
if default_value is not None:
if isinstance(default_value, str) and default_value.startswith("file://"):
default_value = default_value[7:]
self.setting._set_backend_value(default_value)
self.update_display()
self._update_reset_visibility()
def _update_reset_visibility(self):
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:]
if current_value:
current_value = os.path.expanduser(current_value)
current_value = os.path.expandvars(current_value)
if default_value:
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
)
def _on_change_clicked(self, button):
dialog = Gtk.FileDialog()
# Настройка фильтров
if '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))
try:
dialog.open(
parent=button.get_root(),
callback=self._on_file_selected
)
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("Supported 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):
try:
file = dialog.open_finish(result)
if file:
self.setting._set_backend_value(file.get_path())
self.update_display()
except Exception as e:
self.logger.error(f"File selection error: {e}")
import logging
from gi.repository import Gtk from gi.repository import Gtk
class BaseWidget: class BaseWidget:
def __init__(self, setting): def __init__(self, setting):
self.setting = setting self.setting = setting
self.logger = logging.getLogger(f"{self.__class__.__name__}")
self.reset_button = Gtk.Button( self.reset_button = Gtk.Button(
icon_name="edit-undo-symbolic", icon_name="edit-undo-symbolic",
...@@ -21,9 +23,17 @@ class BaseWidget: ...@@ -21,9 +23,17 @@ class BaseWidget:
reveal_child=False, reveal_child=False,
halign=Gtk.Align.END 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): 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): def _on_reset_clicked(self, button):
raise NotImplementedError("Метод _on_reset_clicked должен быть реализован в подклассе") raise NotImplementedError("_on_reset_clicked method should be implemented in the subclass")
...@@ -10,7 +10,9 @@ class BooleanWidget(BaseWidget): ...@@ -10,7 +10,9 @@ class BooleanWidget(BaseWidget):
valign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER,
halign=Gtk.Align.CENTER, halign=Gtk.Align.CENTER,
) )
self.switch.connect("notify::active", self._on_boolean_toggled) self.handler_id = self.switch.connect("notify::active", self._on_boolean_toggled)
self.row.set_activatable_widget(self.switch)
control_box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.HORIZONTAL) control_box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.HORIZONTAL)
control_box.append(self.reset_revealer) control_box.append(self.reset_revealer)
...@@ -23,9 +25,15 @@ class BooleanWidget(BaseWidget): ...@@ -23,9 +25,15 @@ class BooleanWidget(BaseWidget):
def _update_initial_state(self): def _update_initial_state(self):
current_value = self.setting._get_backend_value() current_value = self.setting._get_backend_value()
is_active = current_value == self.setting.map.get(True) is_active = current_value == self.setting.map.get(True)
self.switch.set_active(is_active)
with self.switch.handler_block(self.handler_id):
self.switch.set_active(is_active)
self._update_reset_visibility() self._update_reset_visibility()
def update_display(self):
self._update_initial_state()
def _on_boolean_toggled(self, switch, _): def _on_boolean_toggled(self, switch, _):
value = self.setting.map.get(True) if switch.get_active() else self.setting.map.get(False) value = self.setting.map.get(True) if switch.get_active() else self.setting.map.get(False)
self.setting._set_backend_value(value) self.setting._set_backend_value(value)
...@@ -36,8 +44,10 @@ class BooleanWidget(BaseWidget): ...@@ -36,8 +44,10 @@ class BooleanWidget(BaseWidget):
default_value = self.setting.map.get(self.setting.default) default_value = self.setting.map.get(self.setting.default)
self.setting._set_backend_value(default_value) self.setting._set_backend_value(default_value)
self.switch.set_active(self.setting.default)
with self.switch.handler_block(self.handler_id):
self.switch.set_active(self.setting.default)
self._update_reset_visibility() self._update_reset_visibility()
def _update_reset_visibility(self): def _update_reset_visibility(self):
......
from gi.repository import Adw, Gtk
from .BaseWidget import BaseWidget
class ButtonWidget(BaseWidget):
def create_row(self):
self.row = Adw.ButtonRow(
title=self.setting.name,
)
self.row.connect("activated", self._on_button_clicked)
return self.row
def _on_button_clicked(self, button):
self.setting._set_backend_value(True)
def update_display(self):
pass
...@@ -7,15 +7,16 @@ class ChoiceWidget(BaseWidget): ...@@ -7,15 +7,16 @@ class ChoiceWidget(BaseWidget):
self.row = Adw.ActionRow(title=self.setting.name, subtitle=self.setting.help) self.row = Adw.ActionRow(title=self.setting.name, subtitle=self.setting.help)
self.dropdown = Gtk.DropDown.new_from_strings(items) self.dropdown = Gtk.DropDown.new_from_strings(items)
self.dropdown.set_halign(Gtk.Align.CENTER) self.dropdown.set_halign(Gtk.Align.CENTER)
self.dropdown.set_valign(Gtk.Align.CENTER) self.dropdown.set_valign(Gtk.Align.CENTER)
self._set_dropdown_width(items) self.handler_id = self.dropdown.connect("notify::selected", self._on_choice_changed)
self.row.set_activatable_widget(self.dropdown)
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) control_box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.HORIZONTAL)
control_box.append(self.reset_revealer) control_box.append(self.reset_revealer)
...@@ -25,31 +26,31 @@ class ChoiceWidget(BaseWidget): ...@@ -25,31 +26,31 @@ class ChoiceWidget(BaseWidget):
self._update_reset_visibility() self._update_reset_visibility()
return self.row return self.row
def _set_dropdown_width(self, items): def update_display(self):
layout = self.dropdown.create_pango_layout("") self._update_dropdown_selection()
width = 0 self._update_reset_visibility()
for item in items: def _update_dropdown_selection(self):
layout.set_text(item) current_index = self.setting._get_selected_row_index()
text_width = layout.get_pixel_size()[0]
if text_width > width:
width = text_width
self.dropdown.set_size_request(width + 50, -1) with self.dropdown.handler_block(self.handler_id):
self.dropdown.set_selected(current_index)
def _on_choice_changed(self, dropdown, _): def _on_choice_changed(self, dropdown, _):
selected = dropdown.get_selected() 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.setting._set_backend_value(selected_value)
self._update_reset_visibility() self._update_reset_visibility()
def _on_reset_clicked(self, button): def _on_reset_clicked(self, button):
default_value = self.setting._get_default_row_index() default_value = self.setting._get_default_row_index()
if default_value is not None: if default_value is not None:
self.dropdown.set_selected(default_value) with self.dropdown.handler_block(self.handler_id):
self.dropdown.set_selected(default_value)
self.setting._set_backend_value(self.setting.default) self.setting._set_backend_value(self.setting.default)
self._update_reset_visibility() self._update_reset_visibility()
......
from gi.repository import Gtk, Adw, GObject, Gdk
from .BaseWidget import BaseWidget
class AddItemsDialog(Adw.AlertDialog):
__gtype_name__ = 'AddItemsDialog'
def __init__(self, available_items, selected_items, **kwargs):
super().__init__(**kwargs)
self.set_presentation_mode(Adw.DialogPresentationMode.AUTO)
self.available_items = available_items
self.selected_items = selected_items
self.set_heading("Select Items")
self.set_size_request(360, 500)
self.search_entry = Gtk.SearchEntry(placeholder_text=_("Search..."))
self.search_entry.connect("search-changed", self.on_search_changed)
self.scrolled_window = Gtk.ScrolledWindow(vexpand=True)
self.list_box = Gtk.ListBox(
selection_mode=Gtk.SelectionMode.SINGLE,
css_classes=["boxed-list"]
)
self.list_box.connect("row-activated", self.on_row_activated)
self.scrolled_window.set_child(self.list_box)
self.content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
self.content_box.append(self.search_entry)
self.content_box.append(self.scrolled_window)
self.set_extra_child(self.content_box)
self.add_response("cancel", _("_Cancel"))
self.add_response("add", _("_Add"))
self.set_response_appearance("add", Adw.ResponseAppearance.SUGGESTED)
self.populate_list()
def on_row_activated(self, list_box, row):
if row.is_selected():
list_box.unselect_row(row)
else:
list_box.select_row(row)
def populate_list(self):
for key in self.available_items:
if key not in self.selected_items:
row = Adw.ActionRow(title=self.available_items[key])
row.key = key
self.list_box.append(row)
self.list_box.unselect_all()
def on_search_changed(self, entry):
search_text = entry.get_text().lower()
row = self.list_box.get_first_child()
while row is not None:
title = row.get_title().lower()
row.set_visible(search_text in title)
row = row.get_next_sibling()
def get_selected(self):
return [row.key for row in self.list_box.get_selected_rows()]
class DualListWidget(BaseWidget):
def __init__(self, setting):
super().__init__(setting)
self.pending_changes = None
self.is_reorder_mode = False
def create_row(self):
self.main_row = Adw.PreferencesRow(
activatable=False
)
content_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
margin_top=8,
margin_bottom=8,
margin_start=12,
margin_end=12
)
self.main_row.set_child(content_box)
header_box = Gtk.Box(spacing=12, margin_bottom=12)
content_box.append(header_box)
title_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, hexpand=True)
title_label = Gtk.Label(
label=self.setting.name,
halign=Gtk.Align.START,
css_classes=["title-4"]
)
title_box.append(title_label)
if self.setting.help:
subtitle = Gtk.Label(
label=self.setting.help,
halign=Gtk.Align.START,
wrap=True,
css_classes=["caption", "dim-label"]
)
title_box.append(subtitle)
header_box.append(title_box)
header_box.append(self.reset_revealer)
self.add_btn = Gtk.Button(
label=_("Add"),
tooltip_text=_("Add items"),
hexpand=True,
)
self.add_btn.connect("clicked", self.show_dialog)
self.selected_list = Gtk.ListBox(
css_classes=["boxed-list"],
selection_mode=Gtk.SelectionMode.NONE
)
content_box.append(self.selected_list)
self.apply_btn = Gtk.Button(
label=_("Apply"),
css_classes=["suggested-action"],
hexpand=True,
sensitive=False,
)
self.apply_btn.connect("clicked", self.on_apply)
self.reorder_btn = Gtk.ToggleButton(
label=_("Edit"),
tooltip_text=_("Toggle reorder mode"),
hexpand=True
)
self.reorder_btn.connect("toggled", self.toggle_reorder_mode)
button_box = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
spacing=12,
margin_top=12,
hexpand=True
)
button_box.append(self.add_btn)
button_box.append(self.apply_btn)
button_box.append(self.reorder_btn)
content_box.append(button_box)
self.update_display()
return self.main_row
def create_item_row(self, key, value):
if key not in self.setting.map:
return None
row = Adw.ActionRow(title=value)
row.key = key
suffix_box = Gtk.Box(spacing=6, valign=Gtk.Align.CENTER)
row.add_suffix(suffix_box)
delete_btn = Gtk.Button(
icon_name="user-trash-symbolic",
valign=Gtk.Align.CENTER,
css_classes=["flat", "circular", "destructive-action"],
visible=not self.is_reorder_mode
)
delete_btn.connect("clicked", self.on_delete_item, row)
suffix_box.append(delete_btn)
order_box = Gtk.Box(spacing=4, visible=self.is_reorder_mode)
suffix_box.append(order_box)
up_btn = Gtk.Button(
icon_name="go-up-symbolic",
css_classes=["flat", "circular"],
tooltip_text=_("Move up")
)
up_btn.connect("clicked", self.on_move_item, row, -1)
order_box.append(up_btn)
down_btn = Gtk.Button(
icon_name="go-down-symbolic",
css_classes=["flat", "circular"],
tooltip_text=_("Move down")
)
down_btn.connect("clicked", self.on_move_item, row, 1)
order_box.append(down_btn)
if self.is_reorder_mode:
drag_handle = Gtk.Image(
icon_name="list-drag-handle-symbolic",
)
row.add_prefix(drag_handle)
self.setup_drag_and_drop(row)
return row
def setup_drag_and_drop(self, row):
drag_source = Gtk.DragSource()
drag_source.set_actions(Gdk.DragAction.MOVE)
drag_source.connect("prepare", self.on_drag_prepare, row)
drag_source.connect("drag-begin", self.on_drag_begin, row)
drag_source.connect("drag-end", self.on_drag_end, row)
row.add_controller(drag_source)
drop_target = Gtk.DropTarget.new(GObject.TYPE_STRING, Gdk.DragAction.MOVE)
drop_target.connect("drop", self.on_drop, row)
row.add_controller(drop_target)
def toggle_reorder_mode(self, button):
self.is_reorder_mode = button.get_active()
self.update_display()
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()
if not current:
return
try:
index = current.index(row.key)
new_index = index + direction
if 0 <= new_index < len(current):
current.insert(new_index, current.pop(index))
self.pending_changes = current
self.apply_btn.set_sensitive(True)
self.update_display()
except ValueError:
pass
def on_drag_prepare(self, source, x, y, row):
value = row.key
return Gdk.ContentProvider.new_for_value(value)
def on_drag_begin(self, source, drag, row):
row.add_css_class("dragging")
def on_drag_end(self, source, drag, delete_data, row):
row.remove_css_class("dragging")
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()
src_item = value
dst_item = row.key
if src_item in current and dst_item in current:
src_idx = current.index(src_item)
dst_idx = current.index(dst_item)
current.insert(dst_idx, current.pop(src_idx))
self.pending_changes = current
self.apply_btn.set_sensitive(True)
self.update_display()
return True
return False
def show_dialog(self, button):
current = self.setting._get_backend_value()
if self.pending_changes is not None:
current = self.pending_changes
dialog = AddItemsDialog(
self.setting.map,
current
)
dialog.connect("response", self.on_dialog_response)
dialog.present(self.main_row.get_root())
def on_dialog_response(self, dialog, response):
if response == "add":
selected = dialog.get_selected()
current = self.setting._get_backend_value() if self.pending_changes is None else self.pending_changes
new_pending = list(set(current + selected))
if new_pending != current:
self.pending_changes = new_pending
self.update_display()
self.apply_btn.set_sensitive(True)
def on_delete_item(self, button, row):
current = self.setting._get_backend_value() if self.pending_changes is None else self.pending_changes
if row.key in current:
new_pending = [k for k in current if k != row.key]
if new_pending != current:
self.pending_changes = new_pending
self.update_display()
self.apply_btn.set_sensitive(True)
def update_display(self):
current = self.setting._get_backend_value()
if self.pending_changes is not None:
current = self.pending_changes
self.reorder_btn.set_sensitive(len(current) > 0)
while child := self.selected_list.get_first_child():
self.selected_list.remove(child)
for key in current:
if key in self.setting.map:
row = self.create_item_row(key, self.setting.map[key])
if row:
self.selected_list.append(row)
self._update_reset_visibility()
def on_apply(self, button):
if self.pending_changes is not None:
self.logger.info(f"applying: {self.pending_changes}")
self.setting._set_backend_value(self.pending_changes)
self.pending_changes = None
self.update_display()
self.apply_btn.set_sensitive(False)
self.is_reorder_mode = False
self.reorder_btn.set_active(False)
def _on_reset_clicked(self, button):
self.setting._set_backend_value(self.setting.default or [])
self.pending_changes = None
self.update_display()
self.apply_btn.set_sensitive(False)
self.is_reorder_mode = False
self.reorder_btn.set_active(False)
def _update_reset_visibility(self):
current = self.setting._get_backend_value() if self.pending_changes is None else self.pending_changes
is_default = set(current) == set(self.setting.default or [])
self.reset_revealer.set_reveal_child(not is_default)
...@@ -9,7 +9,7 @@ class EntryWidget(BaseWidget): ...@@ -9,7 +9,7 @@ class EntryWidget(BaseWidget):
self.entry = Gtk.Entry() self.entry = Gtk.Entry()
self.entry.set_halign(Gtk.Align.CENTER) 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) self.entry.connect("activate", self._on_text_changed)
...@@ -30,6 +30,11 @@ class EntryWidget(BaseWidget): ...@@ -30,6 +30,11 @@ class EntryWidget(BaseWidget):
return self.row 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._update_reset_visibility()
def _on_text_changed(self, entry): def _on_text_changed(self, entry):
new_value = entry.get_text() new_value = entry.get_text()
...@@ -41,14 +46,13 @@ class EntryWidget(BaseWidget): ...@@ -41,14 +46,13 @@ class EntryWidget(BaseWidget):
default_value = self.setting.default default_value = self.setting.default
self.setting._set_backend_value(default_value) 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() self._update_reset_visibility()
def _update_reset_visibility(self): def _update_reset_visibility(self):
current_value = self.entry.get_text() or "" current_value = self.entry.get_text()
default_value = self.setting.default
default_value = str(self.setting.default) if self.setting.default is not None else ""
has_default = self.setting.default is not None has_default = self.setting.default is not None
is_default = current_value == default_value is_default = current_value == default_value
......
...@@ -31,7 +31,7 @@ class FileChooser(BaseWidget): ...@@ -31,7 +31,7 @@ class FileChooser(BaseWidget):
valign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER,
halign=Gtk.Align.END, halign=Gtk.Align.END,
) )
self.entry.connect("changed", self._on_entry_changed) self.entry_handler_id = self.entry.connect("activate", self._on_entry_changed)
control_box.append(self.entry) control_box.append(self.entry)
else: else:
self.info_label = Gtk.Label( self.info_label = Gtk.Label(
...@@ -46,12 +46,14 @@ class FileChooser(BaseWidget): ...@@ -46,12 +46,14 @@ class FileChooser(BaseWidget):
) )
self.select_button.set_valign(Gtk.Align.CENTER) self.select_button.set_valign(Gtk.Align.CENTER)
row.set_activatable_widget(self.select_button)
self.select_button.connect("clicked", self._on_button_clicked) self.select_button.connect("clicked", self._on_button_clicked)
control_box.append(self.select_button) control_box.append(self.select_button)
row.add_suffix(control_box) row.add_suffix(control_box)
self._update_display() self.update_display()
self._update_reset_visibility() self._update_reset_visibility()
...@@ -66,7 +68,7 @@ class FileChooser(BaseWidget): ...@@ -66,7 +68,7 @@ class FileChooser(BaseWidget):
self.setting._set_backend_value(default_value) self.setting._set_backend_value(default_value)
self._update_display() self.update_display()
self._update_reset_visibility() self._update_reset_visibility()
...@@ -91,11 +93,9 @@ class FileChooser(BaseWidget): ...@@ -91,11 +93,9 @@ class FileChooser(BaseWidget):
else False else False
) )
def _update_display(self): def update_display(self):
current = self.setting._get_backend_value() current = self.setting._get_backend_value()
self._update_reset_visibility()
if current and isinstance(current, str) and current.startswith("file://"): if current and isinstance(current, str) and current.startswith("file://"):
current = current[7:] current = current[7:]
self.value_separated = True self.value_separated = True
...@@ -107,6 +107,8 @@ class FileChooser(BaseWidget): ...@@ -107,6 +107,8 @@ class FileChooser(BaseWidget):
else: else:
self._update_single_file_display(current) self._update_single_file_display(current)
self._update_reset_visibility()
def _on_button_clicked(self, button): def _on_button_clicked(self, button):
dialog = Gtk.FileDialog() dialog = Gtk.FileDialog()
...@@ -129,7 +131,7 @@ class FileChooser(BaseWidget): ...@@ -129,7 +131,7 @@ class FileChooser(BaseWidget):
parent = current_file.get_parent() if not self.folder_mode else current_file parent = current_file.get_parent() if not self.folder_mode else current_file
dialog.set_initial_folder(parent) dialog.set_initial_folder(parent)
except Exception as e: except Exception as e:
print(f"Error setting initial folder: {e}") self.logger.error(f"Error setting initial folder: {e}")
# Выбор метода открытия # Выбор метода открытия
try: try:
...@@ -149,7 +151,7 @@ class FileChooser(BaseWidget): ...@@ -149,7 +151,7 @@ class FileChooser(BaseWidget):
callback=self._on_file_selected callback=self._on_file_selected
) )
except Exception as e: except Exception as e:
print(f"File dialog error: {e}") self.logger.error(f"File dialog error: {e}")
def _create_file_filters(self): def _create_file_filters(self):
filters = Gio.ListStore.new(Gtk.FileFilter) filters = Gio.ListStore.new(Gtk.FileFilter)
...@@ -173,9 +175,9 @@ class FileChooser(BaseWidget): ...@@ -173,9 +175,9 @@ class FileChooser(BaseWidget):
if file: if file:
self.setting._set_backend_value(file.get_path()) self.setting._set_backend_value(file.get_path())
self._update_display() self.update_display()
except Exception as e: except Exception as e:
print(f"File selection error: {e}") self.logger.error(f"File selection error: {e}")
def _on_files_selected(self, dialog, result): def _on_files_selected(self, dialog, result):
try: try:
...@@ -183,18 +185,18 @@ class FileChooser(BaseWidget): ...@@ -183,18 +185,18 @@ class FileChooser(BaseWidget):
if file_list: if file_list:
paths = [f.get_path() for f in file_list] paths = [f.get_path() for f in file_list]
self.setting._set_backend_value(paths) self.setting._set_backend_value(paths)
self._update_display() self.update_display()
except Exception as e: except Exception as e:
print(f"Multiple files selection error: {e}") self.logger.error(f"Multiple files selection error: {e}")
def _on_folder_selected(self, dialog, result): def _on_folder_selected(self, dialog, result):
try: try:
folder = dialog.select_folder_finish(result) folder = dialog.select_folder_finish(result)
if folder: if folder:
self.setting._set_backend_value(folder.get_path()) self.setting._set_backend_value(folder.get_path())
self._update_display() self.update_display()
except Exception as e: except Exception as e:
print(f"Folder selection error: {e}") self.logger.error(f"Folder selection error: {e}")
def _update_folder_display(self, current): def _update_folder_display(self, current):
if current: if current:
...@@ -208,12 +210,13 @@ class FileChooser(BaseWidget): ...@@ -208,12 +210,13 @@ class FileChooser(BaseWidget):
self.info_label.set_label(f"{count} files selected" if count else "No files selected") self.info_label.set_label(f"{count} files selected" if count else "No files selected")
def _update_single_file_display(self, current): def _update_single_file_display(self, current):
self.entry.set_text(current or "") with self.entry.handler_block(self.entry_handler_id):
if current: self.entry.set_text(current or "")
file = Gio.File.new_for_path(current) if current:
self.entry.set_tooltip_text(file.get_parse_name()) file = Gio.File.new_for_path(current)
else: self.entry.set_tooltip_text(file.get_parse_name())
self.entry.set_tooltip_text(None) else:
self.entry.set_tooltip_text(None)
def _on_entry_changed(self, entry): def _on_entry_changed(self, entry):
if not self.folder_mode and not self.multiple_mode: if not self.folder_mode and not self.multiple_mode:
......
from gi.repository import Adw, Gtk
from .BaseWidget import BaseWidget
class InfoDictWidget(BaseWidget):
def create_row(self):
main_box = Adw.PreferencesRow()
main_box.set_activatable(False)
content_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
margin_top=8,
margin_bottom=8,
margin_start=12,
margin_end=12
)
main_box.set_child(content_box)
title_label = Gtk.Label(
label=self.setting.name,
halign=Gtk.Align.START,
margin_bottom=6
)
content_box.append(title_label)
if self.setting.help:
subtitle_label = Gtk.Label(
label=self.setting.help,
halign=Gtk.Align.START,
wrap=True,
margin_bottom=8
)
subtitle_label.add_css_class("dim-label")
content_box.append(subtitle_label)
self.main_list = Gtk.ListBox()
self.main_list.set_selection_mode(Gtk.SelectionMode.NONE)
self.main_list.add_css_class('boxed-list')
content_box.append(self.main_list)
self._update_initial_state()
return main_box
def _create_value_label(self, value, level):
label = Gtk.Label(
label=str(value),
halign=Gtk.Align.END,
hexpand=True,
margin_start=12,
margin_end=12 * level
)
label.add_css_class('dim-label')
return label
def _render_value(self, value, container, level=0):
if isinstance(value, dict):
self._render_dict(value, container, level)
elif isinstance(value, (list, tuple)):
self._render_list(value, container, level)
else:
container.append(self._create_value_label(value, level))
def _render_dict(self, data, parent_container, level):
for key, value in data.items():
row = Adw.ActionRow()
row.set_title(key)
row.set_activatable(False)
if isinstance(value, (dict, list, tuple)):
value_widget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self._render_value(value, value_widget, level + 1)
else:
value_widget = self._create_value_label(value, level)
row.add_suffix(value_widget)
parent_container.append(row)
def _render_list(self, data, parent_container, level):
for item in data:
row = Adw.ActionRow()
row.set_title("•")
row.set_activatable(False)
if isinstance(item, (dict, list, tuple)):
value_widget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self._render_value(item, value_widget, level + 1)
else:
value_widget = self._create_value_label(item, level)
row.add_suffix(value_widget)
parent_container.append(row)
def _update_initial_state(self):
current_value = self.setting._get_backend_value() or {}
while child := self.main_list.get_first_child():
self.main_list.remove(child)
self._render_value(current_value, self.main_list, 0)
def update_display(self):
self._update_initial_state()
from gi.repository import Adw, Gtk, GLib
from .BaseWidget import BaseWidget
class InfoLabelWidget(BaseWidget):
def create_row(self):
self.row = Adw.ActionRow(title=self.setting.name, subtitle=self.setting.help)
self.label = Gtk.Label(
valign=Gtk.Align.CENTER,
halign=Gtk.Align.CENTER,
)
self.label.add_css_class("dim-label")
control_box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.HORIZONTAL)
control_box.append(self.label)
self.row.add_suffix(control_box)
self._update_initial_state()
return self.row
def _update_initial_state(self):
current_value = self.setting._get_backend_value()
self.label.set_label(current_value)
def update_display(self):
self._update_initial_state()
...@@ -38,12 +38,18 @@ class NumStepper(BaseWidget): ...@@ -38,12 +38,18 @@ class NumStepper(BaseWidget):
control_box.append(self.spin) control_box.append(self.spin)
row.add_suffix(control_box) row.add_suffix(control_box)
self.spin.connect("value-changed", self._on_num_changed) self.spin_handler_id = self.spin.connect("value-changed", self._on_num_changed)
self._update_reset_visibility() self._update_reset_visibility()
return row return row
def update_display(self):
current_value = self.setting._get_backend_value()
with self.spin.handler_block(self.spin_handler_id):
self.spin.set_value(float(current_value))
self._update_reset_visibility()
def _on_num_changed(self, widget): def _on_num_changed(self, widget):
selected_value = widget.get_value() selected_value = widget.get_value()
...@@ -55,8 +61,9 @@ class NumStepper(BaseWidget): ...@@ -55,8 +61,9 @@ class NumStepper(BaseWidget):
default_value = self.setting.default default_value = self.setting.default
if default_value is not None: if default_value is not None:
self.setting._set_backend_value(default_value) with self.spin.handler_block(self.spin_handler_id):
self.spin.set_value(float(default_value)) self.setting._set_backend_value(default_value)
self.spin.set_value(float(default_value))
self._update_reset_visibility() self._update_reset_visibility()
......
from gi.repository import Gtk, Adw from gi.repository import Gtk, Adw, GObject
from .BaseWidget import BaseWidget from .BaseWidget import BaseWidget
...@@ -72,18 +72,23 @@ class RadioChoiceWidget(BaseWidget): ...@@ -72,18 +72,23 @@ class RadioChoiceWidget(BaseWidget):
else: else:
group = radio group = radio
radio.connect("toggled", self._on_toggle, value) handler_id = radio.connect("toggled", self._on_toggle, value)
self.radio_buttons[value] = (radio, handler_id)
radio_container.append(radio) radio_container.append(radio)
self.radio_buttons[value] = radio
self.reset_revealer.set_halign(Gtk.Align.END) self.reset_revealer.set_halign(Gtk.Align.END)
title_horizontal_box.append(self.reset_revealer) title_horizontal_box.append(self.reset_revealer)
self._update_reset_visibility() self._update_reset_visibility()
return main_box return main_box
def update_display(self):
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):
radio.set_active(value == current_value)
self._update_reset_visibility()
def _on_toggle(self, button, value): def _on_toggle(self, button, value):
if button.get_active(): if button.get_active():
self.setting._set_backend_value(value) self.setting._set_backend_value(value)
...@@ -97,8 +102,9 @@ class RadioChoiceWidget(BaseWidget): ...@@ -97,8 +102,9 @@ class RadioChoiceWidget(BaseWidget):
self.setting._set_backend_value(default_value) self.setting._set_backend_value(default_value)
if default_value in self.radio_buttons: if default_value in self.radio_buttons:
self.radio_buttons[default_value].set_active(True) radio, handler_id = self.radio_buttons[default_value]
with GObject.signal_handler_block(radio, handler_id):
radio.set_active(True)
self._update_reset_visibility() self._update_reset_visibility()
def _update_reset_visibility(self): def _update_reset_visibility(self):
...@@ -106,6 +112,5 @@ class RadioChoiceWidget(BaseWidget): ...@@ -106,6 +112,5 @@ class RadioChoiceWidget(BaseWidget):
default_value = self.setting.default default_value = self.setting.default
self.reset_revealer.set_reveal_child( self.reset_revealer.set_reveal_child(
current_value != default_value if default_value is not None current_value != default_value if default_value is not None else False
else False )
)
\ No newline at end of file
from .AvatarWidget import AvatarWidget
from .BooleanWidget import BooleanWidget from .BooleanWidget import BooleanWidget
from .ChoiceWidget import ChoiceWidget from .ChoiceWidget import ChoiceWidget
from .RadioChoiceWidget import RadioChoiceWidget from .RadioChoiceWidget import RadioChoiceWidget
from .EntryWidget import EntryWidget from .EntryWidget import EntryWidget
from .NumStepper import NumStepper from .NumStepper import NumStepper
from .FileChooser import FileChooser from .FileChooser import FileChooser
from .ButtonWidget import ButtonWidget
from .InfoLabelWidget import InfoLabelWidget
from .InfoDictWidget import InfoDictWidget
from .DualListWidget import DualListWidget
import logging
logger = logging.getLogger(f"{__name__}")
class WidgetFactory: class WidgetFactory:
widget_map = { widget_map = {
'avatar': AvatarWidget,
'file': FileChooser, 'file': FileChooser,
'choice': ChoiceWidget, 'choice': ChoiceWidget,
'choice_radio': RadioChoiceWidget, 'choice_radio': RadioChoiceWidget,
'boolean': BooleanWidget, 'boolean': BooleanWidget,
'entry': EntryWidget, 'entry': EntryWidget,
'number': NumStepper, 'number': NumStepper,
'button': ButtonWidget,
'info_label': InfoLabelWidget,
'info_dict': InfoDictWidget,
'list_dual': DualListWidget,
} }
@staticmethod @staticmethod
...@@ -21,5 +34,5 @@ class WidgetFactory: ...@@ -21,5 +34,5 @@ class WidgetFactory:
if widget_class: if widget_class:
return widget_class(setting) return widget_class(setting)
else: else:
print(f"Неизвестный тип виджета: {setting.type}") logger.error(f"Неизвестный тип виджета: {setting.type}")
return None return None
import logging
logger = logging.getLogger(f"{__name__}")
def convert_by_gvariant(value, gtype): def convert_by_gvariant(value, gtype):
""" """
Приводит значение к нужному типу в зависимости от GVariant gtype. Приводит значение к нужному типу в зависимости от GVariant gtype.
...@@ -30,5 +35,5 @@ def convert_by_gvariant(value, gtype): ...@@ -30,5 +35,5 @@ def convert_by_gvariant(value, gtype):
else: else:
raise ValueError(f"Неизвестный GVariant тип: {gtype}") raise ValueError(f"Неизвестный GVariant тип: {gtype}")
except (ValueError, TypeError) as e: except (ValueError, TypeError) as e:
print(f"Ошибка приведения типа: {e}") logger.error(f"Ошибка приведения типа: {e}")
return None return None
...@@ -2,12 +2,16 @@ import os ...@@ -2,12 +2,16 @@ import os
import yaml import yaml
import logging
logger = logging.getLogger(f"{__name__}")
def get_local_module_directory(): def get_local_module_directory():
home_directory = os.path.expanduser("~") home_directory = os.path.expanduser("~")
return os.path.join(home_directory, ".local", "share", "tuneit", "modules") return os.path.join(home_directory, ".local", "share", "tuneit", "modules")
def get_module_directory(): def get_module_directory():
return "/usr/share/tuneit/modules" return os.path.join(tuneit_config.pkgdatadir, "modules")
def load_modules(): def load_modules():
modules = [] modules = []
...@@ -18,11 +22,12 @@ def load_modules(): ...@@ -18,11 +22,12 @@ def load_modules():
if os.path.isdir(global_modules_directory): if os.path.isdir(global_modules_directory):
all_modules = set(os.listdir(global_modules_directory)) all_modules = set(os.listdir(global_modules_directory))
for module_name in os.listdir(local_modules_directory): if os.path.exists(local_modules_directory) and os.path.isdir(local_modules_directory):
module_path = os.path.join(local_modules_directory, module_name) for module_name in os.listdir(local_modules_directory):
if os.path.isdir(module_path): module_path = os.path.join(local_modules_directory, module_name)
modules += load_yaml_files_from_directory(module_path) if os.path.isdir(module_path):
all_modules.discard(module_name) modules += load_yaml_files_from_directory(module_path)
all_modules.discard(module_name)
if all_modules: if all_modules:
for module_name in all_modules: for module_name in all_modules:
...@@ -46,7 +51,7 @@ def load_yaml_files_from_directory(directory): ...@@ -46,7 +51,7 @@ def load_yaml_files_from_directory(directory):
item['module_path'] = directory item['module_path'] = directory
yaml_data.extend(data) yaml_data.extend(data)
except yaml.YAMLError as e: except yaml.YAMLError as e:
print(f"Ошибка при чтении файла {file_path}: {e}") logger.error(f"Ошибка при чтении файла {file_path}: {e}")
sections_data = [] sections_data = []
sections_directory = os.path.join(directory, 'sections') sections_directory = os.path.join(directory, 'sections')
...@@ -60,7 +65,7 @@ def load_yaml_files_from_directory(directory): ...@@ -60,7 +65,7 @@ def load_yaml_files_from_directory(directory):
if data: if data:
sections_data.extend(data) sections_data.extend(data)
except yaml.YAMLError as e: except yaml.YAMLError as e:
print(f"Ошибка при чтении файла {file_path}: {e}") logger.error(f"Ошибка при чтении файла {file_path}: {e}")
for module in yaml_data: for module in yaml_data:
if 'sections' in module: if 'sections' in module:
......
using Gtk 4.0;
using Adw 1;
template $TuneItDepsAlertDialog: Adw.AlertDialog {
heading: _("The module has unmet dependencies");
responses [
close: _("Ignore") destructive,
skip: _("Skip module") suggested,
]
close-response: "close";
extra-child: Gtk.TextView{
wrap-mode: word_char;
buffer: Gtk.TextBuffer deps_message_textbuffer {};
};
}
from gi.repository import GLib, Adw, Gtk
@Gtk.Template(resource_path='/ru.ximperlinux.TuneIt/settings/widgets/deps_alert_dialog.ui')
class TuneItDepsAlertDialog(Adw.AlertDialog):
__gtype_name__ = "TuneItDepsAlertDialog"
deps_message_textbuffer = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.callback = None
def ask_user(self, window, callback):
self.callback = callback
self.present(window)
self.connect('response', self.on_response)
def on_response(self, dialog, response):
if self.callback:
self.callback(response)
using Gtk 4.0;
using Adw 1;
template $TuneItErrorDialog: Adw.AlertDialog {
heading: _("Error in Tune it!");
body: _("You can write in an official telegram chat by applying this log.");
responses [
close: _("Ignore") destructive,
copy: _("Copy the log and go to telegram chat") suggested,
]
close-response: "close";
extra-child: Gtk.ScrolledWindow {
hscrollbar-policy: never;
vscrollbar-policy: automatic;
height-request: 185;
Gtk.TextView{
wrap-mode: word_char;
buffer: Gtk.TextBuffer textbuffer {};
}
};
}
import webbrowser
from gi.repository import Adw, Gtk, Gdk, Gio
@Gtk.Template(resource_path='/ru.ximperlinux.TuneIt/settings/widgets/error_dialog.ui')
class TuneItErrorDialog(Adw.AlertDialog):
__gtype_name__ = "TuneItErrorDialog"
textbuffer = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.connect('response', self.on_response)
def on_response(self, _, response):
if response == 'copy':
self.on_copy()
def on_copy(self):
app_info = Gio.AppInfo.get_default_for_uri_scheme('tg')
if app_info:
Gio.AppInfo.launch_default_for_uri('tg://resolve?domain=tuneit', None)
else:
webbrowser.open("https://t.me/tuneit")
def copy_error(self):
display = Gdk.Display.get_default()
clipboard = display.get_clipboard()
clipboard.set(self.textbuffer.get_text(
self.textbuffer.get_start_iter(),
self.textbuffer.get_end_iter(),
False
))
import os import os
import subprocess import subprocess
import sys import sys
from time import sleep
from gi.repository import Adw from gi.repository import GLib, Adw
import logging
logger = logging.getLogger(f"{__name__}")
class ServiceNotStartedDialog(Adw.AlertDialog): class ServiceNotStartedDialog(Adw.AlertDialog):
response = ""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
...@@ -16,13 +23,11 @@ class ServiceNotStartedDialog(Adw.AlertDialog): ...@@ -16,13 +23,11 @@ class ServiceNotStartedDialog(Adw.AlertDialog):
self.add_response("yes", _("Yes")) self.add_response("yes", _("Yes"))
self.add_response("no", _("No")) self.add_response("no", _("No"))
self.connect("response", self.on_response) self.connect('response', self.on_response)
def on_response(self, dialog, response): def on_response(self, dialog, response):
if response == "yes": if response == "yes":
self.service_enable() self.service_enable_with_restart()
dialog.close()
os.execv(sys.argv[0], sys.argv)
elif response in ("no", "close"): elif response in ("no", "close"):
dialog.close() dialog.close()
...@@ -40,7 +45,7 @@ class ServiceNotStartedDialog(Adw.AlertDialog): ...@@ -40,7 +45,7 @@ class ServiceNotStartedDialog(Adw.AlertDialog):
else: else:
return False return False
except Exception as e: except Exception as e:
print(f"An error occurred: {e}") logger.error(f"An error occurred: {e}")
return False return False
def service_enable(self): def service_enable(self):
...@@ -50,5 +55,10 @@ class ServiceNotStartedDialog(Adw.AlertDialog): ...@@ -50,5 +55,10 @@ class ServiceNotStartedDialog(Adw.AlertDialog):
stdout=subprocess.PIPE, stderr=subprocess.PIPE stdout=subprocess.PIPE, stderr=subprocess.PIPE
) )
except Exception as e: except Exception as e:
print(f"An error occurred: {e}") logger.error(f"An error occurred: {e}")
def service_enable_with_restart(self):
self.service_enable()
self.close()
os.execv(sys.argv[0], sys.argv)
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
<gresource prefix="/ru.ximperlinux.TuneIt"> <gresource prefix="/ru.ximperlinux.TuneIt">
<file preprocess="xml-stripblanks">window.ui</file> <file preprocess="xml-stripblanks">window.ui</file>
<file preprocess="xml-stripblanks">settings/widgets/panel_row.ui</file> <file preprocess="xml-stripblanks">settings/widgets/panel_row.ui</file>
<file preprocess="xml-stripblanks">settings/widgets/deps_alert_dialog.ui</file>
<file preprocess="xml-stripblanks">settings/widgets/error_dialog.ui</file>
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file> <file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
</gresource> </gresource>
</gresources> </gresources>
...@@ -25,7 +25,34 @@ import signal ...@@ -25,7 +25,34 @@ import signal
import locale import locale
import gettext import gettext
VERSION = '@VERSION@' import logging
class ColorFormatter(logging.Formatter):
COLORS = {
'DEBUG': '\033[36m',
'INFO': '\033[32m',
'WARNING': '\033[33m',
'ERROR': '\033[31m',
'CRITICAL': '\033[37;41m'
}
RESET = '\033[0m'
def format(self, record):
color = self.COLORS.get(record.levelname, '')
message = super().format(record)
return f"{color}{message}{self.RESET}"
handler = logging.StreamHandler()
handler.setFormatter(ColorFormatter(
fmt="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
))
logging.basicConfig(
level=logging.DEBUG,
handlers=[handler]
)
pkgdatadir = '@pkgdatadir@' pkgdatadir = '@pkgdatadir@'
localedir = '@localedir@' localedir = '@localedir@'
...@@ -35,6 +62,12 @@ locale.bindtextdomain('tuneit', localedir) ...@@ -35,6 +62,12 @@ locale.bindtextdomain('tuneit', localedir)
locale.textdomain('tuneit') locale.textdomain('tuneit')
gettext.install('tuneit', localedir) gettext.install('tuneit', localedir)
import builtins
from tuneit.config import Config
builtins.tuneit_config = Config
if __name__ == '__main__': if __name__ == '__main__':
import gi import gi
...@@ -43,4 +76,4 @@ if __name__ == '__main__': ...@@ -43,4 +76,4 @@ if __name__ == '__main__':
resource._register() resource._register()
from tuneit import main from tuneit import main
sys.exit(main.main(VERSION)) sys.exit(main.main())
...@@ -47,7 +47,7 @@ template $TuneitWindow: Adw.ApplicationWindow { ...@@ -47,7 +47,7 @@ template $TuneitWindow: Adw.ApplicationWindow {
name: "settings"; name: "settings";
title: _("Settings"); title: _("Settings");
child: Adw.NavigationSplitView settings_split_view { child: Adw.ToastOverlay settings_toast_overlay { child: Adw.NavigationSplitView settings_split_view {
hexpand: true; hexpand: true;
content: Adw.NavigationPage { content: Adw.NavigationPage {
...@@ -104,7 +104,7 @@ template $TuneitWindow: Adw.ApplicationWindow { ...@@ -104,7 +104,7 @@ template $TuneitWindow: Adw.ApplicationWindow {
} }
} }
}; };
}; }; };
} }
Adw.ViewStackPage { Adw.ViewStackPage {
......
...@@ -17,16 +17,21 @@ ...@@ -17,16 +17,21 @@
# #
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import threading import threading, traceback
from gi.repository import GObject, Adw, Gtk, GLib from gi.repository import GObject, Adw, Gtk, GLib
from .settings.main import init_settings_stack from .settings.main import init_settings_stack
from .shop.main import init_shop_stack from .shop.main import init_shop_stack
from .settings.widgets.error_dialog import TuneItErrorDialog
@Gtk.Template(resource_path='/ru.ximperlinux.TuneIt/window.ui') @Gtk.Template(resource_path='/ru.ximperlinux.TuneIt/window.ui')
class TuneitWindow(Adw.ApplicationWindow): class TuneitWindow(Adw.ApplicationWindow):
__gtype_name__ = 'TuneitWindow' __gtype_name__ = 'TuneitWindow'
settings_toast_overlay = Gtk.Template.Child()
settings_pagestack = Gtk.Template.Child() settings_pagestack = Gtk.Template.Child()
settings_listbox = Gtk.Template.Child() settings_listbox = Gtk.Template.Child()
settings_split_view = Gtk.Template.Child() settings_split_view = Gtk.Template.Child()
...@@ -41,23 +46,50 @@ class TuneitWindow(Adw.ApplicationWindow): ...@@ -41,23 +46,50 @@ class TuneitWindow(Adw.ApplicationWindow):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.error_dialog = TuneItErrorDialog()
self.connect('settings_page_update', self.update_settings_page) self.connect('settings_page_update', self.update_settings_page)
if tuneit_config.IS_DEVEL:
print("!!! DEVEL BUILD")
self.add_css_class("devel")
self.update_settings_page() self.update_settings_page()
def update_settings_page(self): def update_settings_page(self):
thread = threading.Thread(target=self._update_settings_page) self._update_settings_page()
thread.daemon = True
thread.start()
def _update_settings_page(self, *args): def _update_settings_page(self, *args):
""" """
Можно вызвать вот так, благодаря сигналу: Можно вызвать вот так, благодаря сигналу:
self.settings_pagestack.get_root().emit("settings_page_update") self.settings_pagestack.get_root().emit("settings_page_update")
""" """
init_settings_stack( self.setting_notify("Tune It", "Init settings...")
self.settings_pagestack, try:
self.settings_listbox, init_settings_stack(
self.settings_split_view, self.settings_pagestack,
self.settings_listbox,
self.settings_split_view,
)
self.setting_notify("Tune It", "Settings are initialized!")
except Exception as e:
self.error(traceback.format_exc())
def error(self, error):
print(error)
self.error_dialog.textbuffer.set_text(str(error))
self.error_dialog.present(self)
def setting_notify(self, module_name: str, notify: str, seconds: int = 5) -> None:
seconds = 5 if seconds is None else seconds
self.settings_toast_overlay.dismiss_all()
toast = Adw.Toast(
title=f"{module_name}: {notify}",
timeout=seconds,
) )
init_shop_stack( init_shop_stack(
...@@ -65,3 +97,5 @@ class TuneitWindow(Adw.ApplicationWindow): ...@@ -65,3 +97,5 @@ class TuneitWindow(Adw.ApplicationWindow):
self.shop_listbox, self.shop_listbox,
self.shop_split_view, self.shop_split_view,
) )
self.settings_toast_overlay.add_toast(toast)
...@@ -23,7 +23,7 @@ BuildRequires: libadwaita-gir-devel ...@@ -23,7 +23,7 @@ BuildRequires: libadwaita-gir-devel
BuildRequires: blueprint-compiler BuildRequires: blueprint-compiler
BuildRequires: meson BuildRequires: meson
%add_python3_path %_datadir/%name %add_python3_path %_datadir/tuneit
%description %description
A modular configuration tool A modular configuration tool
......
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