From 57790173970346288d8c9af3db4f795cabc260e3 Mon Sep 17 00:00:00 2001 From: Jannis <78504175+JannisPetschenka@users.noreply.github.com> Date: Thu, 9 Feb 2023 19:22:29 +0100 Subject: [PATCH] Add ControllsWidget (#203) --- README.md | 2 + man/swaync.5.scd | 119 ++++++++++++ src/configSchema.json | 76 ++++++++ src/controlCenter/widgets/baseWidget.vala | 42 ++++- .../widgets/buttonsGrid/buttonsGrid.vala | 39 ++++ src/controlCenter/widgets/factory.vala | 6 + .../widgets/menubar/menubar.vala | 175 ++++++++++++++++++ src/meson.build | 5 + src/style.css | 44 +++++ 9 files changed, 507 insertions(+), 1 deletion(-) create mode 100644 src/controlCenter/widgets/buttonsGrid/buttonsGrid.vala create mode 100644 src/controlCenter/widgets/menubar/menubar.vala diff --git a/README.md b/README.md index 4821c337..dc2d37b4 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ These widgets can be customized, added, removed and even reordered - Notifications (Will always be visible) - Label - Mpris (Media player controls for Spotify, Firefox, Chrome, etc...) +- Menubar with dropdown and buttons +- Button grid ## Planned Features diff --git a/man/swaync.5.scd b/man/swaync.5.scd index 69ffa008..f2ecb79f 100644 --- a/man/swaync.5.scd +++ b/man/swaync.5.scd @@ -191,6 +191,10 @@ config file to be able to detect config errors optional: true ++ *mpris*++ optional: true ++ + *menubar*++ + optional: true ++ + *buttons-grid*++ + optional: true ++ description: ++ Which order and which widgets to display. ++ If the \"notifications\" widget isn't specified, it ++ @@ -280,6 +284,89 @@ config file to be able to detect config errors default: 12 ++ description: The border radius of the album art. ++ description: A widget that displays multiple music players. ++ + *menubar*++ + type: object ++ + css classes: ++ + widget-menubar ++ + .widget-menubar>box>.menu-button-bar ++ + name of element given after menu or buttons with # ++ + patternProperties: ++ + menu#: ++ + type: object ++ + properties: ++ + label: ++ + type: string ++ + optional: true ++ + default: "Menu" ++ + description: Label of button to show/hide menu dropdown ++ + position: ++ + type: string ++ + optional: true ++ + default: "right" ++ + description: Horizontal position of the button in the bar ++ + enum: ["right", "left"] ++ + actions: ++ + type: array ++ + Default values: [] ++ + Valid array values: ++ + type: object ++ + properties: ++ + label: ++ + type: string ++ + default: "label" ++ + description: Text to be displayed in button ++ + command: ++ + type: string ++ + default: "" ++ + description: "Command to be executed on click" ++ + description: A list of actions containing a label and a command ++ + description: A button to reveal a dropdown with action-buttons ++ + buttons#: ++ + type: object ++ + properties: ++ + position: ++ + type: string ++ + optional: true ++ + default: "right" ++ + description: Horizontal position of the buttons in the bar ++ + enum: ["right", "left"] ++ + actions: ++ + type: array ++ + Default values: [] ++ + Valid array values: ++ + type: object ++ + properties: ++ + label: ++ + type: string ++ + default: "label" ++ + description: Text to be displayed in button ++ + command: ++ + type: string ++ + default: "" ++ + description: "Command to be executed on click" ++ + description: A list of actions containing a label and a command ++ + description: A list of buttons to be displayed in the menu-button-bar ++ + *buttons-grid*++ + type: object ++ + css class: widget-buttons (access buttons with >flowbox>flowboxchild>button) ++ + properties: ++ + actions: ++ + type: array ++ + Default values: [] ++ + Valid array values: ++ + type: object ++ + properties: ++ + label: ++ + type: string ++ + default: "label" ++ + description: Text to be displayed in button ++ + command: ++ + type: string ++ + default: "" ++ + description: "Command to be executed on click" ++ + description: A list of actions containing a label and a command ++ + description: A grid of buttons that execute shell commands ++ + example: ``` { @@ -299,6 +386,38 @@ config file to be able to detect config errors "mpris": { "image-size": 96, "image-radius": 12 + }, + "menubar": { + "menu#power": { + "label": "Power", + "position": "right", + "actions": [ + { + "label": "Shut down", + "command": "systemctl poweroff" + }, + ... + ] + }, + "buttons#screenshot": { + "position": "left", + "actions": [ + { + "label": "Screenshot", + "command": "grim" + }, + ... + ] + } + }, + "buttons": { + "actions": [ + { + "label": "wifi", + "command": "rofi-wifi-menu" + }, + ... + ] } } } diff --git a/src/configSchema.json b/src/configSchema.json index 344259c6..c9f0183f 100644 --- a/src/configSchema.json +++ b/src/configSchema.json @@ -266,6 +266,12 @@ }, "^mpris(#[a-zA-Z0-9_-]{1,}){0,1}?$": { "$ref": "#/widgets/mpris" + }, + "^buttons-grid(#[a-zA-Z0-9_-]{1,}){0,1}?$": { + "$ref": "#/widgets/buttons" + }, + "^menubar(#[a-zA-Z0-9_-]{1,}){0,1}?$": { + "$ref": "#/widgets/menubar" } } } @@ -339,6 +345,76 @@ "default": 12 } } + }, + "buttons-grid": { + "type": "object", + "description": "A widget to add a grid of buttons that execute shell commands", + "additionalProperties": false, + "properties": { + "actions": { + "type": "array", + "description": "A list of actions containing a label and a command", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Text to be displayed in button", + "default": "label" + }, + "command": { + "type": "string", + "description": "Command to be executed on click", + "default": "" + } + } + } + } + } + }, + "menubar": { + "type": "object", + "description": "A bar that contains action-buttons and buttons to open a dropdown with action-buttons", + "additionalProperties": false, + "patternProperties": { + "^menu(#[a-zA-Z0-9_-]{1,}){0,1}?$": { + "type": "object", + "description": "A button that opens a dropdown with action-buttons", + "additionalProperties": false, + "properties": { + "label": { + "type": "string", + "description": "Text to be displayed in button", + "default": "Menu" + }, + "position": { + "type": "string", + "description": "Horizontal position of the button in the bar", + "default": "right", + "enum": ["right", "left"] + }, + "actions": { + "$ref" : "#/widgets/buttons-grid/properties/actions" + } + } + }, + "^buttons(#[a-zA-Z0-9_-]{1,}){0,1}?$": { + "type": "object", + "description": "A list of action-buttons to be displayed in the topbar", + "additionalProperties": false, + "properties": { + "position": { + "type": "string", + "description": "Horizontal position of the button in the bar", + "default": "right", + "enum": ["right", "left"] + }, + "actions": { + "$ref" : "#/widgets/buttons-grid/properties/actions" + } + } + } + } } } } diff --git a/src/controlCenter/widgets/baseWidget.vala b/src/controlCenter/widgets/baseWidget.vala index 5e33e730..1eff013d 100644 --- a/src/controlCenter/widgets/baseWidget.vala +++ b/src/controlCenter/widgets/baseWidget.vala @@ -1,3 +1,5 @@ +using Posix; + namespace SwayNotificationCenter.Widgets { public abstract class BaseWidget : Gtk.Box { public abstract string widget_name { get; } @@ -39,7 +41,7 @@ namespace SwayNotificationCenter.Widgets { public virtual void on_cc_visibility_change (bool value) {} - protected T? get_prop (Json.Object config, string value_key) { + protected T ? get_prop (Json.Object config, string value_key) { if (!config.has_member (value_key)) { debug ("%s: Config doesn't have key: %s!\n", key, value_key); return null; @@ -70,5 +72,43 @@ namespace SwayNotificationCenter.Widgets { return null; } } + + protected Json.Array ? get_prop_array (Json.Object config, string value_key) { + if (!config.has_member (value_key)) { + debug ("%s: Config doesn't have key: %s!\n", key, value_key); + return null; + } + var member = config.get_member (value_key); + if (member.get_node_type () != Json.NodeType.ARRAY) { + debug ("Unable to find Json Array for member %s", value_key); + } + return config.get_array_member (value_key); + } + + protected Action[] parse_actions (Json.Array actions) { + Action[] res = new Action[actions.get_length ()]; + for (int i = 0; i < actions.get_length (); i++) { + string label = actions.get_object_element (i).get_string_member_with_default ("label", "label"); + string command = actions.get_object_element (i).get_string_member_with_default ("command", ""); + res[i] = Action () { + label = label, + command = command + }; + } + return res; + } + + protected void execute_command (string cmd) { + pid_t pid; + int status; + if ((pid = fork ()) < 0) { + perror ("fork()"); + } + if (pid == 0) { // Child process + execl ("/bin/sh", "sh", "-c", cmd); + exit (EXIT_FAILURE); // should not return from execl + } + waitpid (pid, out status, 1); + } } } diff --git a/src/controlCenter/widgets/buttonsGrid/buttonsGrid.vala b/src/controlCenter/widgets/buttonsGrid/buttonsGrid.vala new file mode 100644 index 00000000..d8f88b01 --- /dev/null +++ b/src/controlCenter/widgets/buttonsGrid/buttonsGrid.vala @@ -0,0 +1,39 @@ +using GLib; + +namespace SwayNotificationCenter.Widgets { + + public class ButtonsGrid : BaseWidget { + public override string widget_name { + get { + return "buttons-grid"; + } + } + + Action[] actions; + + public ButtonsGrid (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) { + base (suffix, swaync_daemon, noti_daemon); + + Json.Object ? config = get_config (this); + if (config != null) { + Json.Array a = get_prop_array (config, "actions"); + if (a != null) actions = parse_actions (a); + } + + Gtk.FlowBox container = new Gtk.FlowBox (); + container.set_selection_mode (Gtk.SelectionMode.NONE); + pack_start (container, true, true, 0); + + // add action to container + foreach (var act in actions) { + Gtk.Button b = new Gtk.Button.with_label (act.label); + + b.clicked.connect (() => execute_command (act.command)); + + container.insert (b, -1); + } + + show_all (); + } + } +} diff --git a/src/controlCenter/widgets/factory.vala b/src/controlCenter/widgets/factory.vala index c27b85ce..ab497906 100644 --- a/src/controlCenter/widgets/factory.vala +++ b/src/controlCenter/widgets/factory.vala @@ -20,6 +20,12 @@ namespace SwayNotificationCenter.Widgets { case "mpris": widget = new Mpris.Mpris (suffix, swaync_daemon, noti_daemon); break; + case "menubar": + widget = new Menubar (suffix, swaync_daemon, noti_daemon); + break; + case "buttons-grid": + widget = new ButtonsGrid (suffix, swaync_daemon, noti_daemon); + break; default: warning ("Could not find widget: \"%s\"!", key); return null; diff --git a/src/controlCenter/widgets/menubar/menubar.vala b/src/controlCenter/widgets/menubar/menubar.vala new file mode 100644 index 00000000..a1cfc9d3 --- /dev/null +++ b/src/controlCenter/widgets/menubar/menubar.vala @@ -0,0 +1,175 @@ +using GLib; + +namespace SwayNotificationCenter.Widgets { + + public enum MenuType { + BUTTONS, + MENU + } + + public enum Position { + LEFT, + RIGHT + } + public struct ConfigObject { + string ? name; + MenuType ? type; + string ? label; + Position ? position; + Action[] actions; + Gtk.Box ? menu; + } + + public struct Action { + string ? label; + string ? command; + } + + public class Menubar : BaseWidget { + public override string widget_name { + get { + return "menubar"; + } + } + + Gtk.Box menus_container; + Gtk.Box topbar_container; + + List menu_objects; + + public Menubar (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) { + base (suffix, swaync_daemon, noti_daemon); + + Json.Object ? config = get_config (this); + if (config != null) { + parse_config_objects (config); + } + + menus_container = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + + topbar_container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + topbar_container.get_style_context ().add_class ("menu-button-bar"); + + menus_container.add (topbar_container); + + for (int i = 0; i < menu_objects.length (); i++) { + unowned ConfigObject ? obj = menu_objects.nth_data (i); + add_menu (ref obj); + } + + pack_start (menus_container, true, true, 0); + show_all (); + + foreach (var obj in menu_objects) { + obj.menu ?.hide (); + } + } + + void add_menu (ref unowned ConfigObject ? obj) { + switch (obj.type) { + case MenuType.BUTTONS: + Gtk.Box container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + container.get_style_context ().add_class (obj.name); + + foreach (Action a in obj.actions) { + Gtk.Button b = new Gtk.Button.with_label (a.label); + + b.clicked.connect (() => execute_command (a.command)); + + container.add (b); + } + switch (obj.position) { + case Position.LEFT: + topbar_container.pack_start (container, false, false, 0); + break; + case Position.RIGHT: + topbar_container.pack_end (container, false, false, 0); + break; + } + break; + case MenuType.MENU: + Gtk.Button show_button = new Gtk.Button.with_label (obj.label); + + Gtk.Box menu = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + menu.get_style_context ().add_class (obj.name); + obj.menu = menu; + + show_button.clicked.connect (() => { + bool visible = !menu.visible; + foreach (var o in menu_objects) { + o.menu ?.set_visible (false); + } + menu.set_visible (visible); + }); + + foreach (var a in obj.actions) { + Gtk.Button b = new Gtk.Button.with_label (a.label); + b.clicked.connect (() => execute_command (a.command)); + menu.pack_start (b, true, true, 0); + } + + switch (obj.position) { + case Position.RIGHT: + topbar_container.pack_end (show_button, false, false, 0); + break; + case Position.LEFT: + topbar_container.pack_start (show_button, false, false, 0); + break; + } + + menus_container.add (menu); + break; + } + } + + protected void parse_config_objects (Json.Object config) { + var elements = config.get_members (); + + menu_objects = new List (); + for (int i = 0; i < elements.length (); i++) { + string e = elements.nth_data (i); + Json.Object ? obj = config.get_object_member (e); + + if (obj == null) continue; + + string[] key = e.split ("#"); + string t = key[0]; + MenuType type = MenuType.BUTTONS; + if (t == "buttons") type = MenuType.BUTTONS; + else if (t == "menu") type = MenuType.MENU; + else info ("Invalid type for menu-object - valid options: 'menu' || 'buttons' using default"); + + string name = key[1]; + + string ? p = get_prop (obj, "position"); + Position pos; + if (p != "left" && p != "right") { + pos = Position.RIGHT; + info ("No position for menu-object given using default"); + } else if (p == "right") pos = Position.RIGHT; + else pos = Position.LEFT; + + Json.Array ? actions = get_prop_array (obj, "actions"); + if (actions == null) { + info ("Error parsing actions for menu-object"); + } + + string ? label = get_prop (obj, "label"); + if (label == null) { + label = "Menu"; + info ("No label for menu-object given using default"); + } + + Action[] actions_list = parse_actions (actions); + menu_objects.append (ConfigObject () { + name = name, + type = type, + label = label, + position = pos, + actions = actions_list, + menu = null, + }); + } + } + } +} diff --git a/src/meson.build b/src/meson.build index 4c53ef2a..dea11f6e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -36,6 +36,10 @@ widget_sources = [ 'controlCenter/widgets/mpris/mpris.vala', 'controlCenter/widgets/mpris/interfaces.vala', 'controlCenter/widgets/mpris/mpris_player.vala', + # Widget: Menubar + 'controlCenter/widgets/menubar/menubar.vala', + # Widget: Buttons Grid + 'controlCenter/widgets/buttonsGrid/buttonsGrid.vala', ] app_sources = [ @@ -62,6 +66,7 @@ app_deps = [ dependency('libhandy-1', version: '>= 1.2.3'), meson.get_compiler('c').find_library('gtk-layer-shell'), meson.get_compiler('c').find_library('m', required : true), + meson.get_compiler('vala').find_library('posix'), ] # Checks if the user wants scripting enabled diff --git a/src/style.css b/src/style.css index cb50d1b2..0fd663ac 100644 --- a/src/style.css +++ b/src/style.css @@ -239,3 +239,47 @@ .widget-mpris-subtitle { font-size: 1.1rem; } + +/* Buttons widget */ +.widget-buttons-grid { + padding: 8px; + margin: 8px; + border-radius: 12px; + background-color: @noti-bg; +} + +.widget-buttons-grid>flowbox>flowboxchild>button{ + background: @noti-bg; + border-radius: 12px; +} + +.widget-buttons-grid>flowbox>flowboxchild>button:hover { + background: @noti-bg-hover; +} + +/* Menubar widget */ +.widget-menubar>box>.menu-button-bar>button { + border: none; + background: transparent; +} + +/* .AnyName { Name defined in config after # + background-color: @noti-bg; + padding: 8px; + margin: 8px; + border-radius: 12px; +} + +.AnyName>button { + background: transparent; + border: none; +} + +.AnyName>button:hover { + background-color: @noti-bg-hover; +} */ + +.topbar-buttons>button { /* Name defined in config after # */ + border: none; + background: transparent; +} \ No newline at end of file