commit c16ed01fbe7daee6a6528af2201da20bb9d01c96 Author: asonix Date: Tue May 4 20:17:55 2021 -0500 oooo it workin diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7949add --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.flatpak-builder/ +build/ diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..c2777cb --- /dev/null +++ b/build.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -xe + +flatpak-builder build dog.asonix.git.asonix.owo.yml --user --install --force-clean +flatpak run dog.asonix.git.asonix.owo diff --git a/data/gschema.xml b/data/gschema.xml new file mode 100644 index 0000000..a4a1de6 --- /dev/null +++ b/data/gschema.xml @@ -0,0 +1,33 @@ + + + + + "localhost" + OBS Host + The hostname used to connect to OBS + + + 4444 + OBS Port + The port used to connect to OBS + + + + + + (-1, -1) + Window position + Most recent window position (x, y) + + + (900, 600) + Most recent window size + Most recent window size (width, height) + + + false + Open window maximized. + Whether the main window of the application should open maximized or not. + + + diff --git a/data/meson.build b/data/meson.build new file mode 100644 index 0000000..8887d7d --- /dev/null +++ b/data/meson.build @@ -0,0 +1,6 @@ +# Install our gschema.xml file so that we can write stateful information to GSettings +install_data ( + 'gschema.xml', + install_dir: join_paths (get_option ('datadir'), 'glib-2.0', 'schemas'), + rename: meson.project_name () + '.gschema.xml' +) diff --git a/data/owo.appdata.xml.in b/data/owo.appdata.xml.in new file mode 100644 index 0000000..6c98af8 --- /dev/null +++ b/data/owo.appdata.xml.in @@ -0,0 +1,15 @@ + + + dog.asonix.git.asonix.owo + AGPL3 + OwO + OwO: The Experience + +

Oh this is html isn't it, isn't it, isn't it html

+
+ + #e0005c + rgb(255, 255, 255) + 0 + +
diff --git a/data/owo.desktop.in b/data/owo.desktop.in new file mode 100644 index 0000000..c9f1dfc --- /dev/null +++ b/data/owo.desktop.in @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=OwO +GenericName=OwO UwU +Comment=Playing with GTK and elementary 6 +Categories=Utility;Education; +Exec=dog.asonix.git.asonix.owo +Terminal=false +Type=Application +Keywords=Hello;World;Example; diff --git a/dog.asonix.git.asonix.owo.yml b/dog.asonix.git.asonix.owo.yml new file mode 100644 index 0000000..a0a413c --- /dev/null +++ b/dog.asonix.git.asonix.owo.yml @@ -0,0 +1,20 @@ +app-id: dog.asonix.git.asonix.owo + +runtime: io.elementary.Platform +runtime-version: 'daily' +sdk: io.elementary.Sdk + +command: dog.asonix.git.asonix.owo + +finish-args: + - '--share=ipc' + - '--socket=fallback-x11' + - '--socket=wayland' + - '--talk-name=dog.asonix.git.asonix.Streamdeck' + +modules: + - name: owo + buildsystem: meson + sources: + - type: dir + path: . diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..c61ad78 --- /dev/null +++ b/meson.build @@ -0,0 +1,64 @@ +project('dog.asonix.git.asonix.owo', 'vala', 'c') + +i18n = import('i18n') + +add_global_arguments('-DGETTEXT_PACKAGE="@0@"'.format (meson.project_name()), language:'c') + +app_files = files( + 'src/Application.vala', + 'src/Daemon.vala', + 'src/MainWindow.vala', + 'src/Data/Command.vala', + 'src/Data/SwitchScene.vala', + 'src/Dialogs/EditCommandDialog.vala', + 'src/Dialogs/NewCommandDialog.vala', + 'src/Views/ConfigCommand.vala', + 'src/Views/DeckStack.vala', + 'src/Views/DeckView.vala', + 'src/Views/ObsView.vala', + 'src/Widgets/CommandComboBox.vala', + 'src/Widgets/CommandList.vala', + 'src/Widgets/CommandRow.vala', + 'src/Widgets/DeckList.vala', + 'src/Widgets/DeckItem.vala', + 'src/Widgets/DisconnectedPage.vala', + 'src/Widgets/EmptyConfigPane.vala' +) + +executable( + meson.project_name(), + app_files, + dependencies: [ + dependency('gdk-3.0'), + dependency('gee-0.8'), + dependency('gio-2.0'), + dependency('granite'), + dependency('gtk+-3.0'), + dependency('json-glib-1.0'), + dependency('libhandy-1'), + dependency('pango') + ], + install: true +) + +i18n.merge_file( + input: join_paths('data', 'owo.desktop.in'), + output: meson.project_name() + '.desktop', + po_dir: join_paths(meson.source_root(), 'po'), + type: 'desktop', + install: true, + install_dir: join_paths(get_option('datadir'), 'applications') +) + +i18n.merge_file( + input: join_paths('data', 'owo.appdata.xml.in'), + output: meson.project_name() + '.appdata.xml', + po_dir: join_paths(meson.source_root(), 'po'), + install: true, + install_dir: join_paths(get_option('datadir'), 'metainfo') +) + +meson.add_install_script('meson/post_install.py') + +subdir('po') +subdir('data') diff --git a/meson/post_install.py b/meson/post_install.py new file mode 100644 index 0000000..76ed255 --- /dev/null +++ b/meson/post_install.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 + +import os +import subprocess + +schemadir = os.path.join(os.environ['MESON_INSTALL_PREFIX'], 'share', 'glib-2.0', 'schemas') + +if not os.environ.get('DESTDIR'): + print('Compiling gsettings schemas...') + subprocess.call(['glib-compile-schemas', schemadir]) diff --git a/po/LINGUAS b/po/LINGUAS new file mode 100644 index 0000000..c574d07 --- /dev/null +++ b/po/LINGUAS @@ -0,0 +1 @@ +en diff --git a/po/POTFILES b/po/POTFILES new file mode 100644 index 0000000..0524857 --- /dev/null +++ b/po/POTFILES @@ -0,0 +1,3 @@ +src/Application.vala +data/owo.desktop.in +data/owo.appdata.xml.in diff --git a/po/dog.asonix.git.asonix.owo.pot b/po/dog.asonix.git.asonix.owo.pot new file mode 100644 index 0000000..9cb19ce --- /dev/null +++ b/po/dog.asonix.git.asonix.owo.pot @@ -0,0 +1,58 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the dog.asonix.git.asonix.owo package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: dog.asonix.git.asonix.owo\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-30 21:33-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/Application.vala:14 +msgid "Label 1" +msgstr "" + +#: src/Application.vala:15 +msgid "Label 2" +msgstr "" + +#: src/Application.vala:20 +msgid "Hewwo owo" +msgstr "" + +#: data/owo.desktop.in:3 data/owo.appdata.xml.in:5 +msgid "OwO" +msgstr "" + +#: data/owo.desktop.in:4 +msgid "OwO UwU" +msgstr "" + +#: data/owo.desktop.in:5 +msgid "Playing with GTK and elementary 6" +msgstr "" + +#: data/owo.desktop.in:8 +msgid "dog.asonix.git.asonix.owo" +msgstr "" + +#: data/owo.desktop.in:11 +msgid "Hello;World;Example;" +msgstr "" + +#: data/owo.appdata.xml.in:6 +msgid "OwO: The Experience" +msgstr "" + +#: data/owo.appdata.xml.in:8 +msgid "Oh this is html isn't it, isn't it, isn't it html" +msgstr "" diff --git a/po/en.po b/po/en.po new file mode 100644 index 0000000..ba7c271 --- /dev/null +++ b/po/en.po @@ -0,0 +1,58 @@ +# English translations for dog.asonix.git.asonix.owo package. +# Copyright (C) 2021 THE dog.asonix.git.asonix.owo'S COPYRIGHT HOLDER +# This file is distributed under the same license as the dog.asonix.git.asonix.owo package. +# Automatically generated, 2021. +# +msgid "" +msgstr "" +"Project-Id-Version: dog.asonix.git.asonix.owo\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-30 21:33-0500\n" +"PO-Revision-Date: 2021-04-30 21:32-0500\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/Application.vala:14 +msgid "Label 1" +msgstr "Label 1" + +#: src/Application.vala:15 +msgid "Label 2" +msgstr "Label 2" + +#: src/Application.vala:20 +msgid "Hewwo owo" +msgstr "" + +#: data/owo.desktop.in:3 data/owo.appdata.xml.in:5 +msgid "OwO" +msgstr "OwO" + +#: data/owo.desktop.in:4 +msgid "OwO UwU" +msgstr "OwO UwU" + +#: data/owo.desktop.in:5 +msgid "Playing with GTK and elementary 6" +msgstr "Playing with GTK and elementary 6" + +#: data/owo.desktop.in:8 +msgid "dog.asonix.git.asonix.owo" +msgstr "dog.asonix.git.asonix.owo" + +#: data/owo.desktop.in:11 +msgid "Hello;World;Example;" +msgstr "Hello;World;Example;" + +#: data/owo.appdata.xml.in:6 +msgid "OwO: The Experience" +msgstr "OwO: The Experience" + +#: data/owo.appdata.xml.in:8 +msgid "Oh this is html isn't it, isn't it, isn't it html" +msgstr "Oh this is html isn't it, isn't it, isn't it html" diff --git a/po/meson.build b/po/meson.build new file mode 100644 index 0000000..868f00f --- /dev/null +++ b/po/meson.build @@ -0,0 +1,4 @@ +i18n.gettext(meson.project_name(), + args: '--directory=' + meson.source_root(), + preset: 'glib' +) diff --git a/src/Application.vala b/src/Application.vala new file mode 100644 index 0000000..6bbb2ef --- /dev/null +++ b/src/Application.vala @@ -0,0 +1,38 @@ +public class Streamdeck.App : Gtk.Application { + public static GLib.Settings obs_settings { get; private set; } + public static GLib.Settings saved_state { get; private set; } + public static MainWindow main_window { get; private set; } + + static construct { + obs_settings = new GLib.Settings ("dog.asonix.git.asonix.owo.obs"); + saved_state = new GLib.Settings ("dog.asonix.git.asonix.owo.saved-state"); + } + + construct { + flags = ApplicationFlags.FLAGS_NONE; + application_id = "dog.asonix.git.asonix.owo"; + + var present_action = new SimpleAction ("app.present", null); + present_action.activate.connect (() => { + if (main_window != null) { + main_window.present_with_time ((uint32) GLib.get_monotonic_time ()); + } + }); + + add_action (present_action); + } + + protected override void activate () { + if (main_window == null) { + main_window = new MainWindow (this); + main_window.build_ui (); + add_window (main_window); + } + + main_window.present (); + } + + public static int main (string[] args) { + return new Streamdeck.App ().run (args); + } +} diff --git a/src/Daemon.vala b/src/Daemon.vala new file mode 100644 index 0000000..01acf1e --- /dev/null +++ b/src/Daemon.vala @@ -0,0 +1,284 @@ +public struct Streamdeck.ReadInput { + public uint8 key; + public string serial_number; +} + +public struct Streamdeck.DeckInfo { + public string serial_number; + public string device_name; + public string port_name; +} + +public struct Streamdeck.CommandInfo { + public uint8 key; + public string command; +} + +public class Streamdeck.Daemon : Object { + [DBus (name = "dog.asonix.git.asonix.Streamdeck")] + private interface StreamdeckBackend : Object { + public async abstract string[] get_scenes () throws GLib.Error; + public async abstract void enable_discovery () throws GLib.Error; + public async abstract void disable_discovery () throws GLib.Error; + public async abstract DeckInfo[] get_decks () throws GLib.Error; + public async abstract string connect (string host, uint16 port) throws GLib.Error; + public async abstract string disconnect () throws GLib.Error; + public async abstract string get_state () throws GLib.Error; + public async abstract string login (string password) throws GLib.Error; + public async abstract CommandInfo[] get_commands (string serial_number) throws GLib.Error; + public async abstract ReadInput[] read_input () throws GLib.Error; + public async abstract void set_input (string serial_number, uint8 key, string command) throws GLib.Error; + public async abstract void unset_input (string serial_number, uint8 key) throws GLib.Error; + } + + private static Daemon? _instance; + + public static Daemon instance { + get { + if (_instance == null) { + _instance = new Daemon (); + } + + return _instance; + } + } + + private StreamdeckBackend? backend_object; + private string? obs_state; + private DeckInfo[] decks; + private string[] scenes; + private Gee.HashMap> commands = new Gee.HashMap> (); + + public signal void dbus_connection_signal (); + public signal void obs_state_signal (string state); + public signal void decks_signal (DeckInfo[] decks); + public signal void on_decks_added (DeckInfo[] decks); + public signal void on_decks_removed (DeckInfo[] decks); + public signal void scenes_signal (string[] scenes); + public signal void commands_signal (string serial_number, CommandInfo[] commands); + public signal void key_press_signal (ReadInput key_press); + + construct { + dbus_connection_signal.connect ((_obj) => { + get_state.begin ((_obj, res) => { + try { + var state = get_state.end (res); + obs_state_signal (state); + } catch (Error e) { + print ("Get state error: %s\n", e.message); + } + }); + + load_decks.begin ((_obj, res) => { + try { + var new_decks = load_decks.end (res); + decks_signal (new_decks); + } catch (Error e) { + print ("Get decks error: %s\n", e.message); + } + }); + }); + + obs_state_signal.connect ((_obj, state) => { + on_state_change.begin (state, (_obj, res) => { + try { + on_state_change.end (res); + } catch (Error e) { + print ("State handler error: %s\n", e.message); + } + }); + }); + + decks_signal.connect ((_obj, deck_list) => { + var new_decks = diff_decks (decks, deck_list); + var removed_decks = diff_decks (deck_list, decks); + + decks = deck_list; + + on_decks_added (new_decks); + on_decks_removed (removed_decks); + + foreach (DeckInfo deck_info in new_decks) { + update_command_cache (deck_info.serial_number); + } + }); + + scenes_signal.connect ((_obj, new_scenes) => { + scenes = new_scenes; + }); + + commands_signal.connect ((_obj, serial_number, new_commands) => { + var array_list = new Gee.ArrayList ((a, b) => { + return a.key == b.key && a.command == b.command; + }); + + foreach (CommandInfo info in new_commands) { + array_list.add (info); + } + + commands.set (serial_number, array_list); + }); + + Bus.get_proxy.begin ( + BusType.SESSION, + "dog.asonix.git.asonix.Streamdeck", + "/dog/asonix/git/asonix/Streamdeck", + 0, + null, + (_obj, res) => { + try { + backend_object = Bus.get_proxy.end (res); + dbus_connection_signal (); + } catch (Error e) { + error ("Streamdeck error: %s", e.message); + } + } + ); + } + + public string get_obs_state () { + if (obs_state == null) { + return "Disconnected"; + } else { + return obs_state; + } + } + + public DeckInfo[] get_decks () { + return decks; + } + + public string[] get_scenes () { + return scenes; + } + + public Gee.ArrayList get_commands (string serial_number) { + return commands.get (serial_number); + } + + public async void add_command (string serial_number, uint8 key, string command) throws GLib.Error { + disconnected_err (); + + yield backend_object.set_input (serial_number, key, command); + } + + public async void remove_command (string serial_number, uint8 key) throws GLib.Error { + disconnected_err (); + + yield backend_object.unset_input (serial_number, key); + } + + public async void connect_obs () throws GLib.Error { + disconnected_err (); + + string host; + uint16 port; + App.obs_settings.get ("host", "s", out host); + App.obs_settings.get ("port", "q", out port); + + var state = yield backend_object.connect (host, port); + obs_state_signal (state); + } + + public async void authenticate_obs () throws GLib.Error { + disconnected_err (); + + // TODO: secure passw0rt storage + var passw0rt = "passw0rt"; + + var state = yield backend_object.login (passw0rt); + obs_state_signal (state); + } + + public async void disconnect_obs () throws GLib.Error { + disconnected_err (); + + var state = yield backend_object.disconnect (); + obs_state_signal (state); + } + + public async void key_press () throws GLib.Error { + disconnected_err (); + + var read_inputs = yield backend_object.read_input (); + foreach (ReadInput input in read_inputs) { + key_press_signal (input); + } + } + + public void update_command_cache (string serial_number) { + load_commands.begin (serial_number, (_obj, res) => { + try { + var new_commands = load_commands.end (res); + commands_signal (serial_number, new_commands); + } catch (Error e) { + print ("Command fetch error: %s\n", e.message); + } + }); + } + + private async string[] load_scenes () throws GLib.Error { + disconnected_err (); + + return yield backend_object.get_scenes (); + } + + private async DeckInfo[] load_decks () throws GLib.Error { + disconnected_err (); + + return yield backend_object.get_decks (); + } + + private async CommandInfo[] load_commands (string serial_number) throws GLib.Error { + disconnected_err (); + + return yield backend_object.get_commands (serial_number); + } + + private async void on_state_change (string state) throws GLib.Error { + if (obs_state == state) { + return; + } + + obs_state = state; + + if (state == "Disconnected") { + yield connect_obs (); + } else if (state == "Unauthenticated") { + yield authenticate_obs (); + } else if (state == "Connected") { + var new_scenes = yield load_scenes (); + scenes_signal (new_scenes); + } + } + + private async string get_state () throws GLib.Error { + disconnected_err (); + + return yield backend_object.get_state (); + } + + private void disconnected_err () throws GLib.Error { + if (backend_object == null) { + throw new GLib.IOError.FAILED ("Not connected to streamdeck daemon"); + } + } + + private DeckInfo[] diff_decks(DeckInfo[] lhs, DeckInfo[] rhs) { + DeckInfo[] new_decks = new DeckInfo[0]; + + var exists = false; + foreach (DeckInfo deck_info in rhs) { + exists = false; + foreach (DeckInfo existing_info in lhs) { + exists = exists + || deck_info.serial_number == existing_info.serial_number; + } + if (!exists) { + new_decks += deck_info; + } + } + + return new_decks; + } +} diff --git a/src/Data/Command.vala b/src/Data/Command.vala new file mode 100644 index 0000000..0174883 --- /dev/null +++ b/src/Data/Command.vala @@ -0,0 +1,71 @@ +namespace Streamdeck.Data { + public enum CommandType { + SWITCH_SCENE, + } + + public struct CommandDescription { + CommandType type; + string name; + string id; + } + + public abstract class Command : GLib.Object { + private CommandType type; + private uint8 key; + + protected Command (uint8 key, CommandType type) { + this.key = key; + this.type = type; + } + + public static CommandDescription[] available() { + CommandDescription[] available = { + CommandDescription() { + type = CommandType.SWITCH_SCENE, + name = _("Switch Scene"), + id = "SwitchScene" + } + }; + + return available; + } + + public static CommandType? type_string (string? type) { + switch (type) { + case "SwitchScene": + return CommandType.SWITCH_SCENE; + default: + return null; + } + } + + public static Command? parse (uint8 key, string command) { + var parser = new Json.Parser (); + + try { + parser.load_from_data (command); + var obj = parser.get_root ().get_object (); + + switch (type_string(obj.get_string_member ("type"))) { + case CommandType.SWITCH_SCENE: + var scene_name = obj.get_string_member ("name"); + return new SwitchScene (key, scene_name); + default: + return null; + } + } catch { + return null; + } + } + + public abstract string to_json (); + + public CommandType get_command_type () { + return type; + } + + public uint8 get_command_key () { + return key; + } + } +} diff --git a/src/Data/SwitchScene.vala b/src/Data/SwitchScene.vala new file mode 100644 index 0000000..e00986d --- /dev/null +++ b/src/Data/SwitchScene.vala @@ -0,0 +1,26 @@ +namespace Streamdeck.Data { + public class SwitchScene : Command { + public string scene_name; + + public SwitchScene (uint8 key, string scene_name) { + base (key, CommandType.SWITCH_SCENE); + this.scene_name = scene_name; + } + + public override string to_json () { + var gen = new Json.Generator (); + var root = new Json.Node (Json.NodeType.OBJECT); + var object = new Json.Object (); + root.set_object (object); + gen.set_root (root); + + object.set_string_member ("type", "SwitchScene"); + object.set_string_member ("name", scene_name); + + size_t length; + string json = gen.to_data (out length); + + return json; + } + } +} diff --git a/src/Dialogs/EditCommandDialog.vala b/src/Dialogs/EditCommandDialog.vala new file mode 100644 index 0000000..f25ab8f --- /dev/null +++ b/src/Dialogs/EditCommandDialog.vala @@ -0,0 +1,31 @@ +namespace Streamdeck.Dialogs { + public class EditCommandDialog : Granite.Dialog { + private Gtk.Stack stack; + + public EditCommandDialog (string serial_number, Data.Command initial_command) { + var disconnected_page = new Widgets.DisconnectedPage (); + + var command_page = new Views.ConfigCommand.from_existing (serial_number, initial_command); + + stack = new Gtk.Stack (); + stack.add_named (disconnected_page, "disconnected"); + stack.add_named (command_page, "command"); + + get_content_area ().add (stack); + + if (Daemon.instance.get_obs_state () != "Connected") { + add_button ("Close", Gtk.ResponseType.REJECT); + } else { + add_button ("Close", Gtk.ResponseType.CLOSE); + } + + show_all (); + + if (Daemon.instance.get_obs_state () != "Connected") { + stack.set_visible_child_name ("disconnected"); + } else { + stack.set_visible_child_name ("command"); + } + } + } +} diff --git a/src/Dialogs/NewCommandDialog.vala b/src/Dialogs/NewCommandDialog.vala new file mode 100644 index 0000000..eab5dc0 --- /dev/null +++ b/src/Dialogs/NewCommandDialog.vala @@ -0,0 +1,61 @@ +namespace Streamdeck.Dialogs { + public class NewCommandDialog : Granite.Dialog { + private string serial_number; + private Gtk.Stack stack; + private ReadInput key_info; + + public NewCommandDialog (string serial_number) { + this.serial_number = serial_number; + + var disconnected_page = new Widgets.DisconnectedPage (); + + var press_label = new Gtk.Label ( + _("Please press the button you wish to program on your stream deck") + ); + press_label.valign = Gtk.Align.CENTER; + press_label.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); + + var press_page = new Gtk.Grid (); + press_page.column_spacing = 12; + press_page.row_spacing = 12; + press_page.halign = Gtk.Align.CENTER; + press_page.margin = 24; + + press_page.attach (press_label, 0, 0); + + var command_page = new Views.ConfigCommand (serial_number); + + stack = new Gtk.Stack (); + stack.add_named (disconnected_page, "disconnected"); + stack.add_named (press_page, "keypress"); + stack.add_named (command_page, "command"); + + get_content_area ().add (stack); + + if (Daemon.instance.get_obs_state () != "Connected") { + add_button ("Close", Gtk.ResponseType.REJECT); + } else { + add_button ("Close", Gtk.ResponseType.CLOSE); + } + + show_all (); + + Daemon.instance.key_press_signal.connect ((_obj, key_info) => { + if (serial_number == key_info.serial_number) { + this.key_info = key_info; + command_page.key = key_info.key; + stack.set_visible_child_name ("command"); + } else { + Daemon.instance.key_press.begin (); + } + }); + + if (Daemon.instance.get_obs_state () != "Connected") { + stack.set_visible_child_name ("disconnected"); + } else { + stack.set_visible_child_name ("keypress"); + Daemon.instance.key_press.begin (); + } + } + } +} diff --git a/src/MainWindow.vala b/src/MainWindow.vala new file mode 100644 index 0000000..aebb42a --- /dev/null +++ b/src/MainWindow.vala @@ -0,0 +1,102 @@ +public class Streamdeck.MainWindow : Hdy.ApplicationWindow { + public const string ACTION_PREFIX = "win."; + public const string ACTION_QUIT = "action_quit"; + + public Views.DeckStack deck_stack { get; private set; } + public Gtk.Stack main_stack { get; private set; } + + private uint configure_id; + + private const ActionEntry[] ACTION_ENTRIES = { + { ACTION_QUIT, action_quit } + }; + + public MainWindow (Gtk.Application application) { + Object (application: application); + + application.set_accels_for_action ( + ACTION_PREFIX + ACTION_QUIT, + {"q", "w"} + ); + } + + construct { + Hdy.init (); + + add_action_entries (ACTION_ENTRIES, this); + } + + public void build_ui () { + height_request = 350; + width_request = 400; + title = _("Streamdeck"); + + int window_x, window_y, window_width, window_height; + App.saved_state.get ("window-position", "(ii)", out window_x, out window_y); + App.saved_state.get ("window-size", "(ii)", out window_width, out window_height); + + set_default_size (window_width, window_height); + + if (window_x != -1 || window_y != -1) { + move (window_x, window_y); + } + + if (App.saved_state.get_boolean ("window-maximized")) { + maximize (); + } + + main_stack = new Gtk.Stack (); + main_stack.add_titled (new Views.DeckView (), "deck-config", _("Streamdecks")); + main_stack.add_titled (new Views.ObsView (), "obs-config", _("OBS")); + + var stack_switcher = new Gtk.StackSwitcher (); + stack_switcher.margin = 12; + stack_switcher.halign = Gtk.Align.CENTER; + stack_switcher.homogeneous = true; + stack_switcher.stack = main_stack; + + var headerbar = new Hdy.HeaderBar (); + headerbar.show_close_button = true; + headerbar.set_title (_("Streamdeck")); + headerbar.show_all (); + + var grid = new Gtk.Grid (); + grid.attach (headerbar, 0, 0); + grid.attach (stack_switcher, 0, 1); + grid.attach (main_stack, 0, 2); + grid.show_all (); + + add (grid); + } + + private void action_quit () { + destroy (); + } + + public void to_obs () { + main_stack.set_visible_child_name ("obs-config"); + } + + public override bool configure_event (Gdk.EventConfigure event) { + if (configure_id == 0) { + configure_id = Timeout.add (200, () => { + configure_id = 0; + + App.saved_state.set_boolean ("window-maximized", is_maximized); + + if (!is_maximized) { + int width, height, root_x, root_y; + get_position (out root_x, out root_y); + get_size (out width, out height); + + App.saved_state.set ("window-position", "(ii)", root_x, root_y); + App.saved_state.set ("window-size", "(ii)", width, height); + } + + return GLib.Source.REMOVE; + }); + } + + return base.configure_event (event); + } +} diff --git a/src/Views/ConfigCommand.vala b/src/Views/ConfigCommand.vala new file mode 100644 index 0000000..ea491bf --- /dev/null +++ b/src/Views/ConfigCommand.vala @@ -0,0 +1,128 @@ +namespace Streamdeck.Views { + public class ConfigCommand : Gtk.Grid { + private Gtk.Stack command_stack; + private Gtk.ComboBoxText scenes_combobox; + private Widgets.CommandComboBox command_combobox; + private string serial_number; + + public Data.Command? command; + public uint8? key; + + public signal void changed (Data.Command command); + + public ConfigCommand (string serial_number) { + this.serial_number = serial_number; + + build (); + } + + public ConfigCommand.from_existing (string serial_number, Data.Command command) { + this.serial_number = serial_number; + this.key = command.get_command_key (); + this.command = command; + + build (); + + switch (command.get_command_type ()) { + case Data.CommandType.SWITCH_SCENE: + var scene_name = ((Data.SwitchScene) command).scene_name; + scenes_combobox.set_active_id (scene_name); + command_combobox.set_active_id ("SwitchScene"); + break; + default: + break; + } + } + + private void build () { + var command_title = new Gtk.Label ( + _("Configure the command you wish send") + ); + command_title.valign = Gtk.Align.CENTER; + command_title.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); + + var command_label = new Gtk.Label ( + _("Command:") + ); + command_label.halign = Gtk.Align.START; + command_label.valign = Gtk.Align.CENTER; + + command_combobox = new Widgets.CommandComboBox (); + + var scenes_label = new Gtk.Label (_("to")); + + scenes_combobox = new Gtk.ComboBoxText (); + scenes_combobox.id_column = 1; + foreach (var scene in Daemon.instance.get_scenes ()) { + scenes_combobox.append (scene, scene); + } + + var switch_scene_grid = new Gtk.Grid (); + switch_scene_grid.attach (scenes_label, 0, 0); + switch_scene_grid.attach (scenes_combobox, 1, 0, 2); + switch_scene_grid.column_spacing = 12; + switch_scene_grid.row_spacing = 12; + switch_scene_grid.halign = Gtk.Align.CENTER; + + command_stack = new Gtk.Stack (); + command_stack.add_named (new Gtk.Label ("..."), "Empty"); + command_stack.add_named (switch_scene_grid, "SwitchScene"); + + column_spacing = 12; + row_spacing = 12; + halign = Gtk.Align.CENTER; + margin = 24; + + attach (command_title, 0, 0, 3); + attach (command_label, 0, 1); + attach (command_combobox, 1, 1); + attach (command_stack, 2, 1); + + show_all (); + + scenes_combobox.changed.connect (() => { + var scene_name = scenes_combobox.get_active_text (); + if (command != null && command.get_command_type () == Data.CommandType.SWITCH_SCENE) { + unowned var switch_scene = (Data.SwitchScene) command; + switch_scene.scene_name = scene_name; + changed (command); + } else if (key != null) { + command = new Data.SwitchScene (key, scene_name); + changed (command); + } + }); + + changed.connect ((_obj, cmd) => { + Daemon.instance.add_command.begin ( + serial_number, + cmd.get_command_key (), + cmd.to_json (), + (obj, res) => { + try { + Daemon.instance.add_command.end (res); + Daemon.instance.update_command_cache (serial_number); + } catch (Error e) { + print ("Error saving command %s\n", e.message); + } + } + ); + }); + + command_combobox.selected.connect ((type) => { + if (key != null) { + handle_type_change (type); + } + }); + } + + private void handle_type_change (Data.CommandType type) { + switch (type) { + case Data.CommandType.SWITCH_SCENE: + command_stack.set_visible_child_name ("SwitchScene"); + break; + default: + break; + } + } + } +} diff --git a/src/Views/DeckStack.vala b/src/Views/DeckStack.vala new file mode 100644 index 0000000..2617b5d --- /dev/null +++ b/src/Views/DeckStack.vala @@ -0,0 +1,31 @@ +namespace Streamdeck.Views { + public class DeckStack : Gtk.Stack { + construct { + add_named (new Widgets.EmptyConfigPane (), "empty-state"); + + show_all(); + } + + public void select_deck (string serial_number) { + if (get_child_by_name (serial_number) != null) { + set_visible_child_name (serial_number); + } + } + + public void add_deck (DeckInfo deck_info) { + if (get_child_by_name (deck_info.serial_number) != null) { + return; + } + + var pane = new Widgets.CommandList (deck_info); + add_named (pane, deck_info.serial_number); + } + + public void remove_deck (DeckInfo deck_info) { + var child = get_child_by_name (deck_info.serial_number); + if (child != null) { + remove (child); + } + } + } +} diff --git a/src/Views/DeckView.vala b/src/Views/DeckView.vala new file mode 100644 index 0000000..f9b60f8 --- /dev/null +++ b/src/Views/DeckView.vala @@ -0,0 +1,66 @@ +namespace Streamdeck.Views { + public class DeckView : Gtk.Grid { + private DeckStack deck_stack; + + construct { + column_homogeneous = true; + column_spacing = 12; + row_spacing = 12; + + deck_stack = new Views.DeckStack (); + deck_stack.margin = 12; + deck_stack.margin_start = 0; + + var deck_list = new Widgets.DeckList (deck_stack); + + foreach (DeckInfo deck_info in Daemon.instance.get_decks()) { + deck_list.add_deck (deck_info); + } + + if (!deck_list.any_selected ()) { + deck_list.select_first_item (); + } + + deck_list.show_all (); + deck_stack.show_all (); + + var scrolled_window = new Gtk.ScrolledWindow (null, null); + scrolled_window.add (deck_list); + scrolled_window.expand = true; + + var sidebar = new Gtk.Grid (); + sidebar.orientation = Gtk.Orientation.VERTICAL; + sidebar.add(scrolled_window); + sidebar.margin = 12; + sidebar.margin_end = 0; + + attach (sidebar, 0, 0); + attach (deck_stack, 1, 0, 2, 1); + + Daemon.instance.on_decks_added.connect ((_obj, decks) => { + foreach (DeckInfo deck_info in decks) { + deck_list.add_deck (deck_info); + } + + if (!deck_list.any_selected ()) { + deck_list.select_first_item (); + } + }); + + Daemon.instance.on_decks_removed.connect ((_obj, decks) => { + foreach (DeckInfo deck_info in decks) { + deck_list.remove_deck (deck_info); + } + + if (!deck_list.any_selected ()) { + deck_list.select_first_item (); + } + + deck_list.show_all (); + }); + + show_all (); + } + } + +} diff --git a/src/Views/ObsView.vala b/src/Views/ObsView.vala new file mode 100644 index 0000000..4c454b4 --- /dev/null +++ b/src/Views/ObsView.vala @@ -0,0 +1,94 @@ +namespace Streamdeck.Views { + public class ObsView : Gtk.Grid { + construct { + column_spacing = 12; + row_spacing = 12; + halign = Gtk.Align.CENTER; + margin = 24; + margin_top = 64; + + var host_label = new Gtk.Label (_("Hostname:")); + var host_entry = new Gtk.Entry (); + host_entry.valign = Gtk.Align.CENTER; + host_entry.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); + + var host_buffer = host_entry.get_buffer (); + + var port_label = new Gtk.Label (_("Port:")); + var port_entry = new Gtk.SpinButton.with_range (1, 65535, 1); + port_entry.valign = Gtk.Align.CENTER; + port_entry.snap_to_ticks = true; + + var connect_button = new Gtk.Button (); + switch (Daemon.instance.get_obs_state ()) { + case "Disconnected": + connect_button.label = _("Connect"); + break; + default: + connect_button.label = _("Reconnect"); + break; + } + + App.obs_settings.bind ( + "host", + host_buffer, + "text", + GLib.SettingsBindFlags.DEFAULT + ); + + App.obs_settings.bind ( + "port", + port_entry, + "value", + GLib.SettingsBindFlags.DEFAULT + ); + + connect_button.clicked.connect (() => { + connect_button.sensitive = false; + switch (Daemon.instance.get_obs_state ()) { + case "Disconnected": + Daemon.instance.connect_obs.begin ((_obj, res) => { + try { + Daemon.instance.connect_obs.end (res); + } catch (Error e) { + print ("Error connecting to obs: %s\n", e.message); + } + }); + break; + default: + Daemon.instance.disconnect_obs.begin ((_obj, res) => { + try { + Daemon.instance.disconnect_obs.end (res); + } catch (Error e) { + print ("Error disconnecting from obs: %s\n", e.message); + } + }); + break; + } + }); + + Daemon.instance.obs_state_signal.connect ((_obj, state) => { + if (!connect_button.sensitive) { + connect_button.sensitive = true; + } + + switch (state) { + case "Disconnected": + connect_button.label = _("Connect"); + break; + default: + connect_button.label = _("Reconnect"); + break; + } + }); + + attach (host_label, 0, 0); + attach (host_entry, 1, 0); + attach (port_label, 0, 1); + attach (port_entry, 1, 1); + attach (connect_button, 1, 2); + + show_all (); + } + } +} diff --git a/src/Widgets/CommandComboBox.vala b/src/Widgets/CommandComboBox.vala new file mode 100644 index 0000000..8720d9a --- /dev/null +++ b/src/Widgets/CommandComboBox.vala @@ -0,0 +1,32 @@ +namespace Streamdeck.Widgets { + public class CommandComboBox : Gtk.ComboBoxText { + private Data.CommandType? _selected; + + public signal void selected (Data.CommandType type); + + construct { + id_column = 1; + + foreach (Data.CommandDescription cmd in Data.Command.available ()) { + append (cmd.id, cmd.name); + } + + changed.connect ((_obj) => { + var id = get_active_id (); + var type = Data.Command.type_string (id); + + if (type != null) { + selected (type); + } + + _selected = type; + }); + + show_all (); + } + + public Data.CommandType? get_selected_type () { + return _selected; + } + } +} diff --git a/src/Widgets/CommandList.vala b/src/Widgets/CommandList.vala new file mode 100644 index 0000000..c883688 --- /dev/null +++ b/src/Widgets/CommandList.vala @@ -0,0 +1,136 @@ +namespace Streamdeck.Widgets { + public class CommandList : Gtk.Frame { + private DeckInfo info; + private Gtk.ListBox list_box; + private Dialogs.NewCommandDialog? new_command_dialog; + private Gee.HashMap row_map = new Gee.HashMap (); + + public CommandList(DeckInfo info) { + this.info = info; + + var alert = new Granite.Widgets.AlertView ( + _("No commands registered"), + _("Try adding a new command."), + "" + ); + alert.show_all (); + + list_box = new Gtk.ListBox (); + list_box.selection_mode = Gtk.SelectionMode.SINGLE; + list_box.activate_on_single_click = true; + list_box.set_placeholder (alert); + + var add_button = new Gtk.Button.from_icon_name ( + "list-add-symbolic", + Gtk.IconSize.BUTTON + ); + add_button.tooltip_text = _("Add"); + + var remove_button = new Gtk.Button.from_icon_name ( + "list-remove-symbolic", + Gtk.IconSize.BUTTON + ); + remove_button.tooltip_text = _("Remove"); + + var actionbar = new Gtk.ActionBar (); + actionbar.get_style_context ().add_class (Gtk.STYLE_CLASS_INLINE_TOOLBAR); + actionbar.add (add_button); + actionbar.add (remove_button); + actionbar.show_all (); + + var scrolled = new Gtk.ScrolledWindow (null, null); + scrolled.expand = true; + scrolled.add (list_box); + + var commands = Daemon.instance.get_commands (info.serial_number); + if (commands != null) { + foreach (CommandInfo cmd_info in commands) { + var command = Data.Command.parse (cmd_info.key, cmd_info.command); + + if (command != null) { + var existing = row_map.get (cmd_info.key); + if (existing != null) { + list_box.remove (existing); + } + + var row = new CommandRow (info.serial_number, command); + row_map.set (cmd_info.key, row); + list_box.add (row); + } + } + } + + var grid = new Gtk.Grid (); + + grid.attach (scrolled, 0, 0); + grid.attach (actionbar, 0, 1); + + add (grid); + + remove_button.clicked.connect (() => { + var row = list_box.get_selected_row (); + if (row == null) { + return; + } + + unowned var command_row = (CommandRow) row; + var key = command_row.get_key (); + + Daemon.instance.remove_command.begin (info.serial_number, key, (_obj, res) => { + try { + Daemon.instance.remove_command.end (res); + + row_map.unset (key); + list_box.remove (row); + } catch (Error e) { + print ("Error removing command: %s\n", e.message); + } + }); + }); + + add_button.clicked.connect (() => { + if (new_command_dialog == null) { + new_command_dialog = new Dialogs.NewCommandDialog (info.serial_number); + new_command_dialog.transient_for = (Gtk.Window) get_toplevel (); + new_command_dialog.show_all (); + + new_command_dialog.response.connect ((response_id) => { + if (response_id == Gtk.ResponseType.REJECT) { + unowned var app = (MainWindow) get_toplevel (); + app.to_obs (); + } + + new_command_dialog.destroy (); + }); + + new_command_dialog.destroy.connect (() => { + new_command_dialog = null; + }); + } + + new_command_dialog.present (); + }); + + Daemon.instance.commands_signal.connect ((_obj, serial_number, commands) => { + if (serial_number == info.serial_number) { + foreach (CommandInfo cmd_info in commands) { + var command = Data.Command.parse (cmd_info.key, cmd_info.command); + + if (command != null) { + var existing = row_map.get (cmd_info.key); + if (existing != null) { + list_box.remove (existing); + } + + var row = new CommandRow (serial_number, command); + row_map.set (cmd_info.key, row); + list_box.add (row); + } + } + } + }); + + show_all (); + } + } +} diff --git a/src/Widgets/CommandRow.vala b/src/Widgets/CommandRow.vala new file mode 100644 index 0000000..a025dfa --- /dev/null +++ b/src/Widgets/CommandRow.vala @@ -0,0 +1,100 @@ +namespace Streamdeck.Widgets { + public class CommandRow : Gtk.ListBoxRow { + private string serial_number; + private Data.Command command; + private Dialogs.EditCommandDialog? edit_dialog; + + public CommandRow (string serial_number, Data.Command command) { + this.serial_number = serial_number; + this.command = command; + + var key = command.get_command_key (); + var row_key = new Gtk.Label (@"$(key)"); + row_key.halign = Gtk.Align.START; + row_key.valign = Gtk.Align.START; + row_key.show_all (); + + var row_command = new Gtk.Label (null); + row_command.halign = Gtk.Align.START; + row_command.valign = Gtk.Align.START; + row_command.show_all (); + + var row_grid = new Gtk.Grid (); + row_grid.orientation = Gtk.Orientation.HORIZONTAL; + row_grid.margin = 6; + row_grid.margin_start = 3; + row_grid.column_spacing = 3; + row_grid.add (row_key); + row_grid.add (row_command); + + switch (command.get_command_type ()) { + case Data.CommandType.SWITCH_SCENE: + row_command.label = _("Switch scene"); + switch_scene_ui (row_grid); + break; + default: + row_command.label = _("Unknown Command"); + break; + } + + var row_edit = new Gtk.Button.from_icon_name ( + "document-edit-symbolic", + Gtk.IconSize.BUTTON + ); + row_edit.tooltip_text = _("Edit"); + row_edit.halign = Gtk.Align.END; + row_edit.valign = Gtk.Align.END; + row_edit.expand = true; + row_edit.clicked.connect (() => { + if (edit_dialog == null) { + edit_dialog = new Dialogs.EditCommandDialog (serial_number, command); + edit_dialog.transient_for = (Gtk.Window) get_toplevel (); + edit_dialog.show_all (); + + edit_dialog.response.connect ((response_id) => { + if (response_id == Gtk.ResponseType.REJECT) { + unowned var app = (MainWindow) get_toplevel (); + app.to_obs (); + } + + edit_dialog.destroy (); + }); + + edit_dialog.destroy.connect (() => { + edit_dialog = null; + }); + } + + edit_dialog.present (); + }); + + row_grid.add (row_edit); + + row_grid.show_all(); + + add (row_grid); + + show_all (); + } + + public uint8 get_key () { + return command.get_command_key (); + } + + private void switch_scene_ui (Gtk.Grid row_grid) { + unowned var switch_scene = (Data.SwitchScene) command; + var to = new Gtk.Label (_("to")); + to.halign = Gtk.Align.START; + to.valign = Gtk.Align.START; + to.show_all (); + + var scene_name = new Gtk.Label (switch_scene.scene_name); + scene_name.halign = Gtk.Align.START; + scene_name.valign = Gtk.Align.START; + scene_name.show_all (); + + row_grid.add (to); + row_grid.add (scene_name); + } + } +} diff --git a/src/Widgets/DeckItem.vala b/src/Widgets/DeckItem.vala new file mode 100644 index 0000000..d1b2ee9 --- /dev/null +++ b/src/Widgets/DeckItem.vala @@ -0,0 +1,47 @@ +namespace Streamdeck.Widgets { + public class DeckItem : Gtk.ListBoxRow { + public string serial_number { get; set; } + public string device_name { get; set; } + public string port_name { get; set; } + + public DeckItem (string serial_number, string device_name, string port_name) { + Object ( + serial_number: serial_number, + device_name: device_name, + port_name: port_name + ); + } + + construct { + var row_title = new Gtk.Label (device_name); + row_title.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); + row_title.halign = Gtk.Align.START; + row_title.valign = Gtk.Align.START; + row_title.ellipsize = Pango.EllipsizeMode.END; + row_title.show_all (); + + var row_description = new Gtk.Label (port_name); + row_description.margin_top = 2; + row_description.use_markup = true; + row_description.halign = Gtk.Align.START; + row_description.valign = Gtk.Align.START; + row_description.ellipsize = Pango.EllipsizeMode.END; + row_description.show_all (); + + var row_grid = new Gtk.Grid (); + row_grid.margin = 6; + row_grid.margin_start = 3; + row_grid.column_spacing = 3; + row_grid.attach (row_title, 0, 0); + row_grid.attach (row_description, 0, 1); + row_grid.show_all (); + + add (row_grid); + + bind_property ("device-name", row_title, "label"); + bind_property ("port-name", row_description, "label"); + + show_all (); + } + } +} diff --git a/src/Widgets/DeckList.vala b/src/Widgets/DeckList.vala new file mode 100644 index 0000000..fea9512 --- /dev/null +++ b/src/Widgets/DeckList.vala @@ -0,0 +1,110 @@ +namespace Streamdeck.Widgets { + public class DeckList : Gtk.Frame { + private Gtk.Label usb_l; + private Views.DeckStack deck_stack; + private Gtk.ListBox list_box; + + public DeckList (Views.DeckStack deck_stack) { + this.deck_stack = deck_stack; + } + + construct { + var alert = new Granite.Widgets.AlertView ( + _("No streamdecks found"), + _("Try plugging one in!"), + "" + ); + alert.show_all(); + + list_box = new Gtk.ListBox (); + list_box.selection_mode = Gtk.SelectionMode.SINGLE; + list_box.activate_on_single_click = true; + list_box.set_placeholder (alert); + + usb_l = new Gtk.Label (_("Usb")); + usb_l.get_style_context ().add_class (Granite.STYLE_CLASS_H4_LABEL); + usb_l.halign = Gtk.Align.START; + + list_box.set_header_func (update_headers); + + list_box.row_selected.connect ((row) => { + row.activate (); + unowned var item = (DeckItem) row; + deck_stack.select_deck (item.serial_number); + }); + + var scroll = new Gtk.ScrolledWindow (null, null); + scroll.hscrollbar_policy = Gtk.PolicyType.NEVER; + scroll.expand = true; + scroll.add (list_box); + + add (scroll); + + show_all (); + } + + public void add_deck (DeckInfo deck_info) { + var already_present = false; + + foreach (Gtk.Widget child in list_box.get_children ()) { + var deck_item = (DeckItem) child; + + already_present = already_present + || deck_item.serial_number == deck_info.serial_number; + } + + if (!already_present) { + var item = new DeckItem ( + deck_info.serial_number, + deck_info.device_name, + deck_info.port_name + ); + + deck_stack.add_deck (deck_info); + list_box.add (item); + } + + deck_stack.show_all (); + show_all (); + } + + public void remove_deck (DeckInfo deck_info) { + foreach (Gtk.Widget child in list_box.get_children ()) { + var deck_item = (DeckItem) child; + + if (deck_item.serial_number == deck_info.serial_number) { + deck_stack.remove_deck (deck_info); + list_box.remove (deck_item); + break; + } + } + + deck_stack.show_all (); + show_all (); + } + + private void update_headers (Gtk.ListBoxRow row, Gtk.ListBoxRow? before = null) { + if (before != null) { + row.set_header (null); + } + + if (usb_l.get_parent () != null) { + usb_l.unparent (); + } + + row.set_header (usb_l); + } + + public bool any_selected () { + return list_box.get_selected_row () != null; + } + + public void select_first_item () { + var row = list_box.get_row_at_index (0); + if (row == null) { + return; + } + list_box.select_row (row); + } + } +} diff --git a/src/Widgets/DisconnectedPage.vala b/src/Widgets/DisconnectedPage.vala new file mode 100644 index 0000000..0ecd714 --- /dev/null +++ b/src/Widgets/DisconnectedPage.vala @@ -0,0 +1,20 @@ +namespace Streamdeck.Widgets { + public class DisconnectedPage : Gtk.Grid { + construct { + var disconnected_label = new Gtk.Label ( + _("OBS is currently disconnected, try connecting it before configuring commands") + ); + disconnected_label.valign = Gtk.Align.CENTER; + disconnected_label.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); + + column_spacing = 12; + row_spacing = 12; + halign = Gtk.Align.CENTER; + margin = 24; + + attach (disconnected_label, 0, 0); + + show_all (); + } + } +} diff --git a/src/Widgets/EmptyConfigPane.vala b/src/Widgets/EmptyConfigPane.vala new file mode 100644 index 0000000..3b3c910 --- /dev/null +++ b/src/Widgets/EmptyConfigPane.vala @@ -0,0 +1,21 @@ +namespace Streamdeck.Widgets { + public class EmptyConfigPane: Gtk.Grid { + construct { + column_spacing = 12; + row_spacing = 12; + halign = Gtk.Align.CENTER; + margin = 24; + margin_top = 64; + + var label = new Gtk.Label ( + _("No streamdeck selected") + ); + label.valign = Gtk.Align.CENTER; + label.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); + + attach (label, 0, 0); + + show_all (); + } + } +}