Add ControllsWidget (#203)

This commit is contained in:
Jannis 2023-02-09 19:22:29 +01:00 committed by GitHub
parent 68785053f2
commit 5779017397
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 507 additions and 1 deletions

View File

@ -49,6 +49,8 @@ These widgets can be customized, added, removed and even reordered
- Notifications (Will always be visible) - Notifications (Will always be visible)
- Label - Label
- Mpris (Media player controls for Spotify, Firefox, Chrome, etc...) - Mpris (Media player controls for Spotify, Firefox, Chrome, etc...)
- Menubar with dropdown and buttons
- Button grid
## Planned Features ## Planned Features

View File

@ -191,6 +191,10 @@ config file to be able to detect config errors
optional: true ++ optional: true ++
*mpris*++ *mpris*++
optional: true ++ optional: true ++
*menubar*++
optional: true ++
*buttons-grid*++
optional: true ++
description: ++ description: ++
Which order and which widgets to display. ++ Which order and which widgets to display. ++
If the \"notifications\" widget isn't specified, it ++ If the \"notifications\" widget isn't specified, it ++
@ -280,6 +284,89 @@ config file to be able to detect config errors
default: 12 ++ default: 12 ++
description: The border radius of the album art. ++ description: The border radius of the album art. ++
description: A widget that displays multiple music players. ++ 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#<name>: ++
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#<name>: ++
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: example:
``` ```
{ {
@ -299,6 +386,38 @@ config file to be able to detect config errors
"mpris": { "mpris": {
"image-size": 96, "image-size": 96,
"image-radius": 12 "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"
},
...
]
} }
} }
} }

View File

@ -266,6 +266,12 @@
}, },
"^mpris(#[a-zA-Z0-9_-]{1,}){0,1}?$": { "^mpris(#[a-zA-Z0-9_-]{1,}){0,1}?$": {
"$ref": "#/widgets/mpris" "$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 "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"
}
}
}
}
} }
} }
} }

View File

@ -1,3 +1,5 @@
using Posix;
namespace SwayNotificationCenter.Widgets { namespace SwayNotificationCenter.Widgets {
public abstract class BaseWidget : Gtk.Box { public abstract class BaseWidget : Gtk.Box {
public abstract string widget_name { get; } public abstract string widget_name { get; }
@ -70,5 +72,43 @@ namespace SwayNotificationCenter.Widgets {
return null; 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);
}
} }
} }

View File

@ -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 ();
}
}
}

View File

@ -20,6 +20,12 @@ namespace SwayNotificationCenter.Widgets {
case "mpris": case "mpris":
widget = new Mpris.Mpris (suffix, swaync_daemon, noti_daemon); widget = new Mpris.Mpris (suffix, swaync_daemon, noti_daemon);
break; 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: default:
warning ("Could not find widget: \"%s\"!", key); warning ("Could not find widget: \"%s\"!", key);
return null; return null;

View File

@ -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<ConfigObject ?> 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<ConfigObject ?> ();
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<string> (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<string> (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,
});
}
}
}
}

View File

@ -36,6 +36,10 @@ widget_sources = [
'controlCenter/widgets/mpris/mpris.vala', 'controlCenter/widgets/mpris/mpris.vala',
'controlCenter/widgets/mpris/interfaces.vala', 'controlCenter/widgets/mpris/interfaces.vala',
'controlCenter/widgets/mpris/mpris_player.vala', 'controlCenter/widgets/mpris/mpris_player.vala',
# Widget: Menubar
'controlCenter/widgets/menubar/menubar.vala',
# Widget: Buttons Grid
'controlCenter/widgets/buttonsGrid/buttonsGrid.vala',
] ]
app_sources = [ app_sources = [
@ -62,6 +66,7 @@ app_deps = [
dependency('libhandy-1', version: '>= 1.2.3'), dependency('libhandy-1', version: '>= 1.2.3'),
meson.get_compiler('c').find_library('gtk-layer-shell'), meson.get_compiler('c').find_library('gtk-layer-shell'),
meson.get_compiler('c').find_library('m', required : true), meson.get_compiler('c').find_library('m', required : true),
meson.get_compiler('vala').find_library('posix'),
] ]
# Checks if the user wants scripting enabled # Checks if the user wants scripting enabled

View File

@ -239,3 +239,47 @@
.widget-mpris-subtitle { .widget-mpris-subtitle {
font-size: 1.1rem; 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;
}