control-center: add optional dual-column layout

parent 089c9da7
...@@ -39,6 +39,17 @@ scrollbar { ...@@ -39,6 +39,17 @@ scrollbar {
padding: 0; padding: 0;
} }
/* Dual layout */
.dual-layout {
& > separator {
margin: 0 15px;
}
}
.notifications-placeholder {
opacity: 0.5;
}
/* Notifications widget */ /* Notifications widget */
@import "widgets/notifications"; @import "widgets/notifications";
/* Label widget */ /* Label widget */
......
...@@ -37,6 +37,28 @@ template $XimperShellNotificationCenterWidgetsNotifications: $XimperShellNotific ...@@ -37,6 +37,28 @@ template $XimperShellNotificationCenterWidgetsNotifications: $XimperShellNotific
} }
} }
Box placeholder_box {
visible: false;
halign: center;
valign: center;
hexpand: true;
vexpand: true;
orientation: vertical;
spacing: 12;
styles ["notifications-placeholder"]
Image {
pixel-size: 96;
icon-name: "preferences-system-notifications-symbolic";
use-fallback: true;
}
Label {
label: _("No Notifications");
}
}
ScrolledWindow scrolled_window { ScrolledWindow scrolled_window {
hscrollbar-policy: never; hscrollbar-policy: never;
has-frame: false; has-frame: false;
......
...@@ -156,6 +156,13 @@ config file to be able to detect config errors ...@@ -156,6 +156,13 @@ config file to be able to detect config errors
to its content, up to 'control-center-height' as ++ to its content, up to 'control-center-height' as ++
maximum. If false, height is fixed. maximum. If false, height is fixed.
*control-center-layout* ++
type: string ++
default: single ++
values: single, dual ++
description: Panel layout. "single" is a vertical column. ++
"dual" places widgets and notifications side by side.
*control-center-width* ++ *control-center-width* ++
type: integer ++ type: integer ++
default: 500 ++ default: 500 ++
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: ximper-shell-notification-center 0.1.0\n" "Project-Id-Version: ximper-shell-notification-center 0.1.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-27 20:18+0300\n" "POT-Creation-Date: 2026-03-27 23:10+0300\n"
"PO-Revision-Date: 2026-03-24 00:00+0300\n" "PO-Revision-Date: 2026-03-24 00:00+0300\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Russian\n" "Language-Team: Russian\n"
...@@ -18,24 +18,24 @@ msgstr "" ...@@ -18,24 +18,24 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#: src/notification/notification.vala:539 #: src/notification/notification.vala:536
msgid "Enter Text" msgid "Enter Text"
msgstr "Введите текст" msgstr "Введите текст"
#: src/notification/notification.vala:551 #: src/notification/notification.vala:548
msgid "Reply" msgid "Reply"
msgstr "Ответить" msgstr "Ответить"
#: src/notification/notification.vala:579 #: src/notification/notification.vala:576
#, c-format #, c-format
msgid "COPY \"%s\"" msgid "COPY \"%s\""
msgstr "КОПИРОВАТЬ \"%s\"" msgstr "КОПИРОВАТЬ \"%s\""
#: src/notification/notification.vala:621 #: src/notification/notification.vala:618
msgid "Now" msgid "Now"
msgstr "Сейчас" msgstr "Сейчас"
#: src/notification/notification.vala:625 #: src/notification/notification.vala:622
#, c-format #, c-format
msgid "%d min ago" msgid "%d min ago"
msgid_plural "%d mins ago" msgid_plural "%d mins ago"
...@@ -43,7 +43,7 @@ msgstr[0] "%d мин. назад" ...@@ -43,7 +43,7 @@ msgstr[0] "%d мин. назад"
msgstr[1] "%d мин. назад" msgstr[1] "%d мин. назад"
msgstr[2] "%d мин. назад" msgstr[2] "%d мин. назад"
#: src/notification/notification.vala:629 #: src/notification/notification.vala:626
#, c-format #, c-format
msgid "%d hour ago" msgid "%d hour ago"
msgid_plural "%d hours ago" msgid_plural "%d hours ago"
...@@ -51,7 +51,7 @@ msgstr[0] "%d ч. назад" ...@@ -51,7 +51,7 @@ msgstr[0] "%d ч. назад"
msgstr[1] "%d ч. назад" msgstr[1] "%d ч. назад"
msgstr[2] "%d ч. назад" msgstr[2] "%d ч. назад"
#: src/notification/notification.vala:633 #: src/notification/notification.vala:630
#, c-format #, c-format
msgid "%d day ago" msgid "%d day ago"
msgid_plural "%d days ago" msgid_plural "%d days ago"
...@@ -162,12 +162,13 @@ msgstr "Уведомления" ...@@ -162,12 +162,13 @@ msgstr "Уведомления"
msgid "Clear All" msgid "Clear All"
msgstr "Очистить" msgstr "Очистить"
#: data/ui/widgets/notifications.blp:58
msgid "No Notifications"
msgstr "Нет уведомлений"
#: data/ui/widgets/backlight.blp:18 #: data/ui/widgets/backlight.blp:18
msgid "Brightness" msgid "Brightness"
msgstr "Яркость" msgstr "Яркость"
#~ msgid "No Notifications"
#~ msgstr "Нет уведомлений"
#~ msgid "No active sink input" #~ msgid "No active sink input"
#~ msgstr "Нет активного источника" #~ msgstr "Нет активного источника"
...@@ -8,7 +8,7 @@ msgid "" ...@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: ximper-shell-notification-center\n" "Project-Id-Version: ximper-shell-notification-center\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-27 20:18+0300\n" "POT-Creation-Date: 2026-03-27 23:10+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -18,38 +18,38 @@ msgstr "" ...@@ -18,38 +18,38 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
#: src/notification/notification.vala:539 #: src/notification/notification.vala:536
msgid "Enter Text" msgid "Enter Text"
msgstr "" msgstr ""
#: src/notification/notification.vala:551 #: src/notification/notification.vala:548
msgid "Reply" msgid "Reply"
msgstr "" msgstr ""
#: src/notification/notification.vala:579 #: src/notification/notification.vala:576
#, c-format #, c-format
msgid "COPY \"%s\"" msgid "COPY \"%s\""
msgstr "" msgstr ""
#: src/notification/notification.vala:621 #: src/notification/notification.vala:618
msgid "Now" msgid "Now"
msgstr "" msgstr ""
#: src/notification/notification.vala:625 #: src/notification/notification.vala:622
#, c-format #, c-format
msgid "%d min ago" msgid "%d min ago"
msgid_plural "%d mins ago" msgid_plural "%d mins ago"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: src/notification/notification.vala:629 #: src/notification/notification.vala:626
#, c-format #, c-format
msgid "%d hour ago" msgid "%d hour ago"
msgid_plural "%d hours ago" msgid_plural "%d hours ago"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: src/notification/notification.vala:633 #: src/notification/notification.vala:630
#, c-format #, c-format
msgid "%d day ago" msgid "%d day ago"
msgid_plural "%d days ago" msgid_plural "%d days ago"
...@@ -159,6 +159,10 @@ msgstr "" ...@@ -159,6 +159,10 @@ msgstr ""
msgid "Clear All" msgid "Clear All"
msgstr "" msgstr ""
#: data/ui/widgets/notifications.blp:58
msgid "No Notifications"
msgstr ""
#: data/ui/widgets/backlight.blp:18 #: data/ui/widgets/backlight.blp:18
msgid "Brightness" msgid "Brightness"
msgstr "" msgstr ""
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
"control-center-width": 400, "control-center-width": 400,
"control-center-height": 740, "control-center-height": 740,
"control-center-dynamic-height": true, "control-center-dynamic-height": true,
"control-center-layout": "single",
"control-center-margin-top": 8, "control-center-margin-top": 8,
"control-center-margin-bottom": 8, "control-center-margin-bottom": 8,
"control-center-margin-right": 8, "control-center-margin-right": 8,
......
...@@ -652,6 +652,12 @@ namespace XimperShellNotificationCenter { ...@@ -652,6 +652,12 @@ namespace XimperShellNotificationCenter {
*/ */
public string control_center_preferred_output { get; set; default = ""; } public string control_center_preferred_output { get; set; default = ""; }
/**
* Panel layout: "single" (vertical) or "dual" (two columns).
*/
public string control_center_layout {
get; set; default = "single";
}
/** /**
* Notification icon size, in pixels. * Notification icon size, in pixels.
......
...@@ -146,6 +146,12 @@ ...@@ -146,6 +146,12 @@
"type": "string", "type": "string",
"description": "The preferred output to open the control center. Can either be the monitor connector name (ex: \"DP-1\"), or the full name, manufacturer model serial (ex: \"Acer Technologies XV272U V 503023B314202\"). If the output is not found, the currently focused one is picked" "description": "The preferred output to open the control center. Can either be the monitor connector name (ex: \"DP-1\"), or the full name, manufacturer model serial (ex: \"Acer Technologies XV272U V 503023B314202\"). If the output is not found, the currently focused one is picked"
}, },
"control-center-layout": {
"type": "string",
"description": "Panel layout: single (vertical column) or dual (widgets and notifications side by side)",
"default": "single",
"enum": ["single", "dual"]
},
"transition-time": { "transition-time": {
"type": "integer", "type": "integer",
"description": "The notification animation duration. 0 to disable", "description": "The notification animation duration. 0 to disable",
......
...@@ -16,6 +16,10 @@ namespace XimperShellNotificationCenter { ...@@ -16,6 +16,10 @@ namespace XimperShellNotificationCenter {
private List<unowned Widgets.BaseWidget> widgets; private List<unowned Widgets.BaseWidget> widgets;
private const string[] DEFAULT_WIDGETS = { "notifications" }; private const string[] DEFAULT_WIDGETS = { "notifications" };
private Gtk.Box ?dual_container = null;
private Gtk.Box ?left_column = null;
private Gtk.SizeGroup ?dual_size_group = null;
private string ?monitor_name = null; private string ?monitor_name = null;
public ControlCenter () { public ControlCenter () {
...@@ -177,58 +181,123 @@ namespace XimperShellNotificationCenter { ...@@ -177,58 +181,123 @@ namespace XimperShellNotificationCenter {
return true; return true;
} }
private void remove_from_parent (Gtk.Widget child) {
var parent = child.get_parent ();
if (parent != null && parent is Gtk.Box) {
((Gtk.Box) parent).remove (child);
}
}
/** Adds all custom widgets. Removes previous widgets */ /** Adds all custom widgets. Removes previous widgets */
public void add_widgets () { public void add_widgets () {
// Remove all widgets // Detach all widgets from parents first
widgets.foreach ((widget) => { foreach (unowned Widgets.BaseWidget widget
if (widget.get_parent () == box) { in widgets) {
box.remove (widget); remove_from_parent (widget);
} }
// Except for notifications. Otherwise we'd lose notifications if (notifications_widget != null) {
if (widget is Widgets.Notifications) { remove_from_parent (notifications_widget);
return; }
// Remove dual layout container
if (dual_container != null) {
box.remove (dual_container);
dual_container = null;
left_column = null;
dual_size_group = null;
}
// Reset widgets list, keep notifications
widgets = new List<unowned Widgets.BaseWidget> ();
if (notifications_widget != null) {
widgets.append (notifications_widget);
} }
widgets.remove (widget);
});
string[] w = ConfigModel.instance.widgets.data; string[] w = ConfigModel.instance.widgets.data;
if (w.length == 0) { if (w.length == 0) {
w = DEFAULT_WIDGETS; w = DEFAULT_WIDGETS;
} }
bool has_notifications = false; bool is_dual =
foreach (string key in w) { ConfigModel.instance.control_center_layout
// Add the widget if it is valid == "dual"
bool is_notifications; && notifications_widget != null;
Widgets.BaseWidget ?widget = Widgets.get_widget_from_key (
key, out is_notifications);
if (is_notifications) { // Prepare target containers
if (notifications_widget == null) { Gtk.Box widgets_target;
continue; if (is_dual) {
setup_dual_container ();
widgets_target = left_column;
} else {
widgets_target = box;
} }
if (has_notifications) {
warning ("Cannot have multiple " + bool has_notifications = false;
"\"notifications\" widgets! " +
"Skipping \"%s\"", key); foreach (string key in w) {
bool is_notif;
Widgets.BaseWidget ?widget =
Widgets.get_widget_from_key (
key, out is_notif);
if (is_notif) {
if (notifications_widget == null
|| has_notifications) {
continue; continue;
} }
has_notifications = true; has_notifications = true;
notifications_widget.reload_config ();
box.append (notifications_widget);
continue; continue;
} }
if (widget == null) { if (widget == null) {
continue; continue;
} }
// Note: Copies the value into the linked list
widgets.append (widget); widgets.append (widget);
widgets_target.append (widget);
}
// Add notifications
if (has_notifications) {
if (is_dual) {
var right = new Gtk.Box (
Gtk.Orientation.VERTICAL, 0);
right.add_css_class (
"notifications-column");
right.set_hexpand (true);
dual_size_group.add_widget (right);
right.append (notifications_widget);
dual_container.append (right);
} else {
box.append (notifications_widget);
}
notifications_widget.reload_config ();
}
unowned Widgets.BaseWidget cloned_widget = widgets.last ().data; if (is_dual) {
box.append (cloned_widget); box.append (dual_container);
}
} }
private void setup_dual_container () {
dual_container = new Gtk.Box (
Gtk.Orientation.HORIZONTAL, 0);
dual_container.add_css_class ("dual-layout");
dual_container.set_vexpand (true);
dual_size_group = new Gtk.SizeGroup (
Gtk.SizeGroupMode.HORIZONTAL);
left_column = new Gtk.Box (
Gtk.Orientation.VERTICAL, 0);
left_column.add_css_class ("widgets-column");
left_column.set_hexpand (true);
dual_size_group.add_widget (left_column);
var separator = new Gtk.Separator (
Gtk.Orientation.VERTICAL);
dual_container.append (left_column);
dual_container.append (separator);
} }
/** Resets the UI positions */ /** Resets the UI positions */
......
...@@ -18,6 +18,8 @@ namespace XimperShellNotificationCenter.Widgets { ...@@ -18,6 +18,8 @@ namespace XimperShellNotificationCenter.Widgets {
[GtkChild] [GtkChild]
unowned Gtk.Button clear_all_button; unowned Gtk.Button clear_all_button;
[GtkChild] [GtkChild]
unowned Gtk.Box placeholder_box;
[GtkChild]
unowned Gtk.ScrolledWindow scrolled_window; unowned Gtk.ScrolledWindow scrolled_window;
[GtkChild] [GtkChild]
unowned Gtk.Viewport viewport; unowned Gtk.Viewport viewport;
...@@ -99,28 +101,44 @@ namespace XimperShellNotificationCenter.Widgets { ...@@ -99,28 +101,44 @@ namespace XimperShellNotificationCenter.Widgets {
bool dynamic = bool dynamic =
ConfigModel.instance ConfigModel.instance
.control_center_dynamic_height; .control_center_dynamic_height;
set_vexpand (!dynamic); bool is_dual =
ConfigModel.instance.control_center_layout
== "dual";
set_vexpand (is_dual || !dynamic);
scrolled_window.set_propagate_natural_height ( scrolled_window.set_propagate_natural_height (
dynamic); dynamic && !is_dual);
update_header_visibility (); update_header_visibility ();
} }
private void update_header_visibility () { private void update_header_visibility () {
bool has_notis = !is_empty (); bool has_notis = !is_empty ();
scrolled_window.set_visible (has_notis); bool is_dual =
ConfigModel.instance.control_center_layout
== "dual";
// Placeholder only in dual mode when empty
bool show_placeholder = is_dual && !has_notis;
placeholder_box.set_visible (show_placeholder);
scrolled_window.set_visible (!show_placeholder
&& (is_dual || has_notis));
switch (show_title) { switch (show_title) {
case "true": case "true":
header_box.set_visible (true); header_box.set_visible (true);
title_label.set_visible (true); title_label.set_visible (true);
clear_all_button.set_sensitive (has_notis); clear_all_button.set_sensitive (
has_notis);
break; break;
case "false": case "false":
header_box.set_visible (has_notis); header_box.set_visible (has_notis);
title_label.set_visible (false); title_label.set_visible (false);
break; break;
default: // auto default: // auto
header_box.set_visible (has_notis); header_box.set_visible (
is_dual || has_notis);
title_label.set_visible (true); title_label.set_visible (true);
clear_all_button.set_sensitive (
has_notis);
break; break;
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment