Виджет 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.