Git

Виджет Niti на Vala

Виджет это маленький модуль, который рисует один элемент панели Niti: часы, батарею, раскладку или что-то своё. Каждый виджет это отдельный плагин libpeas, поэтому его можно подключить, заменить или отключить, не пересобирая панель. Ниже полный пример виджета «Привет» с настройками и аккуратной очисткой ресурсов.

Как устроены виджеты#

Контракт один и очень простой. Виджет наследует Niti.Widget и возвращает корневой виджет GTK:

namespace Niti { public abstract class Widget : Peas.ExtensionBase { public abstract Gtk.Widget get_root (); } }

Всё остальное: сборка интерфейса, таймеры, подписки, чтение настроек, это ваш код внутри класса. Панель размещает корневой виджет в нужной зоне и управляет жизнью объекта.

Панель загружает виджеты из /usr/lib64/niti/widgets/ (путь libdir/niti/widgets). Для каждого модуля там лежат:

ФайлЧто это
<модуль>.pluginОписание плагина для libpeas
lib<модуль>.soСам виджет
<модуль>.default_settings.jsonНастройки по умолчанию (необязательно)

Имя модуля это то, что вы пишете в зонах panel.json (например niti-clock).

Структура проекта#

niti-hello/ meson.build data/ meson.build hello.plugin.in default_settings.json src/ meson.build plugin.vala

Файлы#

meson.build#

project('niti-hello', ['c', 'vala'], version: '0.1.0', meson_version: '>= 1.0') output = meson.project_name() subdir('data') subdir('src')

Имя проекта становится именем модуля. Берите префикс niti-, как у виджетов образа.

data/hello.plugin.in#

[Plugin] Module=@MODULE@ Name=@NAME@ Version=@VERSION@ Description=Приветствие Authors=Ваше имя

data/default_settings.json#

{ "text": "Привет", "show_time": true }

data/meson.build#

plugin_conf = configuration_data() plugin_conf.set('NAME', 'Hello') plugin_conf.set('MODULE', output) plugin_conf.set('VERSION', meson.project_version()) configure_file( input: 'hello.plugin.in', output: 'hello.plugin', configuration: plugin_conf, install: true, install_dir: get_option('libdir') / 'niti' / 'widgets', ) install_data('default_settings.json', install_dir: get_option('libdir') / 'niti' / 'widgets', rename: output + '.default_settings.json', )

src/meson.build#

deps = [ dependency('glib-2.0'), dependency('gobject-2.0'), dependency('gio-2.0'), dependency('gtk4'), dependency('libpeas-2'), dependency('json-glib-1.0'), dependency('niti-widget-1.0'), ] shared_module(output, 'plugin.vala', dependencies: deps, install: true, install_dir: get_option('libdir') / 'niti' / 'widgets', )

src/plugin.vala#

namespace Niti.Widgets { public class Hello : Niti.Widget { private Gtk.Button button = new Gtk.Button (); private Gtk.Label label = new Gtk.Label (""); private string text = "Привет"; private bool show_time = true; private uint tick_id = 0; construct { button.add_css_class ("bar-widget"); button.add_css_class ("flat"); button.set_child (label); load_settings (); update_label (); weak Hello weak_self = this; button.clicked.connect (() => { if (weak_self == null) return; Niti.WindowRegistry.get_default ().toggle_window_on_monitor ( "Calendar", Niti.AdaptiveLayout.monitor_for_widget (weak_self.button)); }); tick_id = GLib.Timeout.add_seconds (1, () => { update_label (); return GLib.Source.CONTINUE; }); } public override void dispose () { if (tick_id != 0) { GLib.Source.remove (tick_id); tick_id = 0; } base.dispose (); } private void load_settings () { var conf = Niti.Config.load_widget_settings (this.get_plugin_info ()); text = conf.has_member ("text") ? conf.get_string_member ("text") : "Привет"; show_time = !conf.has_member ("show_time") || conf.get_boolean_member ("show_time"); } private void update_label () { if (show_time) { label.set_text ("%s, %s".printf (text, new GLib.DateTime.now_local ().format ("%H:%M:%S"))); } else { label.set_text (text); } } public override Gtk.Widget get_root () { return button; } } } public void peas_register_types (GLib.TypeModule module) { var obj_module = (Peas.ObjectModule) module; obj_module.register_extension_type (typeof (Niti.Widget), typeof (Niti.Widgets.Hello)); }

Что здесь важно:

  • construct собирает интерфейс и запускает таймер. Конструктор с параметрами не нужен.
  • dispose обязан снять всё, что вы подключили: таймеры (GLib.Source.remove), подписки на сигналы, мониторы файлов. Панель удаляет виджет при отключении плагина, и всё, что осталось, станет утечкой.
  • weak Hello weak_self в замыканиях не даёт ссылке на виджет держать его в памяти. Обращайтесь через weak_self и проверяйте на null.
  • CSS-классы bar-widget и flat дают виджету обычный вид панели. Свои классы добавляйте рядом.
  • peas_register_types регистрирует расширение. Без неё libpeas виджет не найдёт.

Настройки#

Niti.Config.load_widget_settings (this.get_plugin_info ()) возвращает объект Json.Object: настройки по умолчанию из <модуль>.default_settings.json, поверх которых наложены пользовательские. Пользовательский файл лежит здесь:

~/.config/niti-vala/widgets/<модуль>/config.json

Его путь выдаёт Niti.Config.widget_config_path (имя_модуля). Всегда проверяйте has_member и берите значение по умолчанию, если ключа нет: пользователь мог удалить его или не задавать.

Чтобы виджет сразу подхватывал изменения настроек, следите за каталогом через GLib.FileMonitor. Образец с задержкой на сброс и корректной очисткой смотрите в виджете clock в NitiVala (widgets/clock/src/plugin.vala).

Важно. Каждый параметр лучше описать в default_settings.json. В будущем эти файлы станут основой для страниц настроек в Tuner, а пока они же служат справкой, что виджет умеет.

Как открыть окно Niti#

Виджет может открывать окна панели, например календарь. Для этого используется реестр окон:

Niti.WindowRegistry.get_default ().toggle_window_on_monitor ( "Calendar", Niti.AdaptiveLayout.monitor_for_widget (button));

Размещение на панели#

После установки виджета добавьте имя модуля в нужную зону ~/.config/niti-vala/panel.json:

{ "zones": { "left": ["niti-workspaces", "niti-tasks"], "center": ["niti-clock", "niti-hello"], "right": ["niti-tray", "niti-quickbutton"] } }

Или включите его через страницу «Настройки панели» в Tuner.

Сборка#

Для сборки нужен пакет niti-widget-1.0, то есть заголовки и .vapi публичного API панели. Он собирается и ставится из каталога api/ репозитория NitiVala. Остальные зависимости такие же, как у любого проекта Vala и GTK4:

# внутри контейнера сборки, в клоне NitiVala meson setup api/build api --prefix=/usr ninja -C api/build install # в каталоге вашего виджета meson setup build --prefix=/usr meson compile -C build

Контейнер сборки описан в Containerfile NitiVala: Sisyphus с vala, meson, libgtk4-devel, libpeas2-devel, libjson-glib-devel и другими. Как проверить собранный виджет на живой панели, читайте в разделе проверка без пересборки.

Правила, из-за которых ломаются виджеты#

  • Любой таймер, подписка и монитор файла снимаются в dispose.
  • weak self в замыканиях вместо сильной ссылки на this.
  • Не блокируйте главный поток: чтение файлов и запуск программ делайте асинхронно.
  • Не меняйте структуру коллекции, пока по ней идёт foreach.
  • Не берите путь от внешнего кода без проверки: тип файла и ссылки.
  • Нельзя писать пустые настройки поверх файла после ошибки разбора JSON: пользователь потеряет свои значения.

Эти шаблоны проверяет линтер scripts/lint_vala_patterns.py в NitiVala, и новые совпадения роняют CI.