notify::prop-name signals and bind transform method (#109)

This commit is contained in:
Aylur
2023-10-01 21:37:31 +02:00
committed by GitHub
parent 70041f3f9e
commit b0f5c15df4
17 changed files with 1061 additions and 606 deletions
+14 -12
View File
@@ -1,31 +1,33 @@
const { Window, Box, Label, EventBox } = ags.Widget;
import {
NotificationList, DNDSwitch, ClearButton, PopupList,
} from './widgets.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import App from 'resource:///com/github/Aylur/ags/app.js';
import { execAsync, timeout } from 'resource:///com/github/Aylur/ags/utils.js';
const Header = () => Box({
const Header = () => Widget.Box({
className: 'header',
children: [
Label('Do Not Disturb'),
Widget.Label('Do Not Disturb'),
DNDSwitch(),
Box({ hexpand: true }),
Widget.Box({ hexpand: true }),
ClearButton(),
],
});
const NotificationCenter = () => Window({
const NotificationCenter = () => Widget.Window({
name: 'notification-center',
anchor: 'right top bottom',
popup: true,
focusable: true,
child: Box({
child: Widget.Box({
children: [
EventBox({
Widget.EventBox({
hexpand: true,
connections: [['button-press-event', () =>
ags.App.closeWindow('notification-center')]]
App.closeWindow('notification-center')]]
}),
Box({
Widget.Box({
vertical: true,
children: [
Header(),
@@ -36,13 +38,13 @@ const NotificationCenter = () => Window({
}),
});
const NotificationsPopupWindow = () => Window({
const NotificationsPopupWindow = () => Widget.Window({
name: 'popup-window',
anchor: 'top',
child: PopupList(),
});
ags.Utils.timeout(1000, () => ags.Utils.execAsync([
timeout(500, () => execAsync([
'notify-send',
'Notification Center example',
'To have the panel popup run "ags toggle-window notification-center"' +
@@ -50,7 +52,7 @@ ags.Utils.timeout(1000, () => ags.Utils.execAsync([
]).catch(console.error));
export default {
style: ags.App.configDir + '/style.css',
style: App.configDir + '/style.css',
windows: [
NotificationsPopupWindow(),
NotificationCenter(),
+32 -33
View File
@@ -1,10 +1,9 @@
const { Notifications } = ags.Service;
const { lookUpIcon, timeout } = ags.Utils;
const { Box, Icon, Label, EventBox, Button } = ags.Widget;
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import { lookUpIcon, timeout } from 'resource:///com/github/Aylur/ags/utils.js';
const NotificationIcon = ({ appEntry, appIcon, image }) => {
if (image) {
return Box({
return Widget.Box({
valign: 'start',
hexpand: false,
className: 'icon img',
@@ -26,7 +25,7 @@ const NotificationIcon = ({ appEntry, appIcon, image }) => {
if (lookUpIcon(appEntry))
icon = appEntry;
return Box({
return Widget.Box({
valign: 'start',
hexpand: false,
className: 'icon',
@@ -34,7 +33,7 @@ const NotificationIcon = ({ appEntry, appIcon, image }) => {
min-width: 78px;
min-height: 78px;
`,
children: [Icon({
children: [Widget.Icon({
icon, size: 58,
halign: 'center', hexpand: true,
valign: 'center', vexpand: true,
@@ -42,40 +41,40 @@ const NotificationIcon = ({ appEntry, appIcon, image }) => {
});
};
export const Notification = ({ id, summary, body, actions, urgency, ...icon }) => EventBox({
className: `notification ${urgency}`,
onPrimaryClick: () => Notifications.dismiss(id),
export const Notification = n => Widget.EventBox({
className: `notification ${n.urgency}`,
onPrimaryClick: () => n.dismiss(),
properties: [['hovered', false]],
onHover: w => {
if (w._hovered)
onHover: self => {
if (self._hovered)
return;
// if there are action buttons and they are hovered
// EventBox onHoverLost will fire off immediately,
// so to prevent this we delay it
timeout(300, () => w._hovered = true);
timeout(300, () => self._hovered = true);
},
onHoverLost: w => {
if (!w._hovered)
onHoverLost: self => {
if (!self._hovered)
return;
w._hovered = false;
Notifications.dismiss(id);
self._hovered = false;
n.dismiss();
},
vexpand: false,
child: Box({
child: Widget.Box({
vertical: true,
children: [
Box({
Widget.Box({
children: [
NotificationIcon(icon),
Box({
NotificationIcon(n),
Widget.Box({
hexpand: true,
vertical: true,
children: [
Box({
Widget.Box({
children: [
Label({
Widget.Label({
className: 'title',
xalign: 0,
justification: 'left',
@@ -83,37 +82,37 @@ export const Notification = ({ id, summary, body, actions, urgency, ...icon }) =
maxWidthChars: 24,
truncate: 'end',
wrap: true,
label: summary,
useMarkup: summary.startsWith('<'),
label: n.summary,
useMarkup: true,
}),
Button({
Widget.Button({
className: 'close-button',
valign: 'start',
child: Icon('window-close-symbolic'),
onClicked: () => Notifications.close(id),
child: Widget.Icon('window-close-symbolic'),
onClicked: n.close.bind(n),
}),
],
}),
Label({
Widget.Label({
className: 'description',
hexpand: true,
useMarkup: true,
xalign: 0,
justification: 'left',
label: body,
label: n.body,
wrap: true,
}),
],
}),
],
}),
Box({
Widget.Box({
className: 'actions',
children: actions.map(action => Button({
children: n.actions.map(({ id, label }) => Button({
className: 'action-button',
onClicked: () => Notifications.invoke(id, action.id),
onClicked: () => n.invoke(id),
hexpand: true,
child: Label(action.label),
child: Widget.Label(label),
})),
}),
],
+32 -41
View File
@@ -1,40 +1,38 @@
import { Notification } from './notification.js';
const { Gtk } = imports.gi;
const { Notifications } = ags.Service;
const { Scrollable, Box, Icon, Label, Widget, Button, Stack } = ags.Widget;
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
import Gtk from 'gi://Gtk';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const List = () => Box({
const List = () => Widget.Box({
vertical: true,
vexpand: true,
connections: [[Notifications, box => {
box.children = Notifications.notifications
connections: [[Notifications, self => {
self.children = Notifications.notifications
.reverse()
.map(n => Notification(n));
.map(Notification);
box.visible = Notifications.notifications.length > 0;
self.visible = Notifications.notifications.length > 0;
}]],
});
const Placeholder = () => Box({
const Placeholder = () => Widget.Box({
className: 'placeholder',
vertical: true,
vexpand: true,
valign: 'center',
children: [
Icon('notifications-disabled-symbolic'),
Label('Your inbox is empty'),
Widget.Icon('notifications-disabled-symbolic'),
Widget.Label('Your inbox is empty'),
],
connections: [
[Notifications, box => {
box.visible = Notifications.notifications.length === 0;
}],
binds: [
['visible', Notifications, 'notifications', n => n.length === 0],
],
});
export const NotificationList = () => Scrollable({
export const NotificationList = () => Widget.Scrollable({
hscroll: 'never',
vscroll: 'automatic',
child: Box({
child: Widget.Box({
className: 'list',
vertical: true,
children: [
@@ -44,22 +42,19 @@ export const NotificationList = () => Scrollable({
}),
});
export const ClearButton = () => Button({
onClicked: Notifications.clear,
connections: [[Notifications, button => {
button.sensitive = Notifications.notifications.length > 0;
}]],
child: Box({
export const ClearButton = () => Widget.Button({
onClicked: () => Notifications.clear(),
binds: [
['sensitive', Notifications, 'notifications', n => n.length > 0],
],
child: Widget.Box({
children: [
Label('Clear'),
Stack({
items: [
['true', Icon('user-trash-full-symbolic')],
['false', Icon('user-trash-symbolic')],
Widget.Label('Clear'),
Widget.Icon({
binds: [
['icon', Notifications, 'notifications', n =>
`user-trash-${n.length > 0 ? 'full-' : ''}symbolic`],
],
connections: [[Notifications, stack => {
stack.shown = `${Notifications.notifications.length > 0}`;
}]],
}),
],
}),
@@ -68,19 +63,15 @@ export const ClearButton = () => Button({
export const DNDSwitch = () => Widget({
type: Gtk.Switch,
valign: 'center',
connections: [
['notify::active', ({ active }) => {
Notifications.dnd = active;
}],
],
connections: [['notify::active', ({ active }) => {
Notifications.dnd = active;
}]],
});
export const PopupList = () => Box({
export const PopupList = () => Widget.Box({
className: 'list',
style: 'padding: 1px;', // so it shows up
vertical: true,
connections: [[Notifications, box => {
box.children = Array.from(Notifications.popups.values())
.map(n => Notification(n));
}]],
binds: [['children', Notifications, 'popups',
popups => popups.map(Notification)]],
});
+54 -65
View File
@@ -8,177 +8,166 @@ import SystemTray from 'resource:///com/github/Aylur/ags/service/systemtray.js';
import App from 'resource:///com/github/Aylur/ags/app.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import { exec, execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
const { Box, Button, Stack, Label, Icon, CenterBox, Window, Slider, ProgressBar } = Widget;
// widgets can be only assigned as a child in one container
// so to make a reuseable widget, just make it a function
// then you can use it by calling simply calling it
const Workspaces = () => Box({
const Workspaces = () => Widget.Box({
className: 'workspaces',
connections: [[Hyprland, box => {
connections: [[Hyprland.active.workspace, self => {
// generate an array [1..10] then make buttons from the index
const arr = Array.from({ length: 10 }, (_, i) => i + 1);
box.children = arr.map(i => Button({
self.children = arr.map(i => Widget.Button({
onClicked: () => execAsync(`hyprctl dispatch workspace ${i}`),
child: Label({ label: `${i}` }),
child: Widget.Label(`${i}`),
className: Hyprland.active.workspace.id == i ? 'focused' : '',
}));
}]],
});
const ClientTitle = () => Label({
const ClientTitle = () => Widget.Label({
className: 'client-title',
// an initial label value can be given but its pointless
// because callbacks from connections are run on construction
// so in this case this is redundant
label: Hyprland.active.client.title || '',
connections: [[Hyprland, label => {
label.label = Hyprland.active.client.title || '';
}]],
binds: [
['label', Hyprland.active.client, 'title'],
],
});
const Clock = () => Label({
const Clock = () => Widget.Label({
className: 'clock',
connections: [
// this is bad practice, since exec() will block the main event loop
// in the case of a simple date its not really a problem
[1000, label => label.label = exec('date "+%H:%M:%S %b %e."')],
[1000, self => self.label = exec('date "+%H:%M:%S %b %e."')],
// this is what you should do
[1000, label => execAsync(['date', '+%H:%M:%S %b %e.'])
.then(date => label.label = date).catch(console.error)],
[1000, self => execAsync(['date', '+%H:%M:%S %b %e.'])
.then(date => self.label = date).catch(console.error)],
],
});
// we don't need dunst or any other notification daemon
// because ags has a notification daemon built in
const Notification = () => Box({
// because the Notifications module is a notification daemon itself
const Notification = () => Widget.Box({
className: 'notification',
children: [
Icon({
Widget.Icon({
icon: 'preferences-system-notifications-symbolic',
connections: [
[Notifications, icon => icon.visible = Notifications.popups.length > 0],
[Notifications, self => self.visible = Notifications.popups.length > 0],
],
}),
Label({
connections: [[Notifications, label => {
label.label = Notifications.popups[0]?.summary || '';
Widget.Label({
connections: [[Notifications, self => {
self.label = Notifications.popups[0]?.summary || '';
}]],
}),
],
});
const Media = () => Button({
const Media = () => Widget.Button({
className: 'media',
onPrimaryClick: () => Mpris.getPlayer('')?.playPause(),
onScrollUp: () => Mpris.getPlayer('')?.next(),
onScrollDown: () => Mpris.getPlayer('')?.previous(),
child: Label({
connections: [[Mpris, label => {
child: Widget.Label({
connections: [[Mpris, self => {
const mpris = Mpris.getPlayer('');
// mpris player can be undefined
if (mpris)
label.label = `${mpris.trackArtists.join(', ')} - ${mpris.trackTitle}`;
self.label = `${mpris.trackArtists.join(', ')} - ${mpris.trackTitle}`;
else
label.label = 'Nothing is playing';
self.label = 'Nothing is playing';
}]],
}),
});
const Volume = () => Box({
const Volume = () => Widget.Box({
className: 'volume',
style: 'min-width: 180px',
children: [
Stack({
Widget.Stack({
items: [
// tuples of [string, Widget]
['101', Icon('audio-volume-overamplified-symbolic')],
['67', Icon('audio-volume-high-symbolic')],
['34', Icon('audio-volume-medium-symbolic')],
['1', Icon('audio-volume-low-symbolic')],
['0', Icon('audio-volume-muted-symbolic')],
['101', Widget.Icon('audio-volume-overamplified-symbolic')],
['67', Widget.Icon('audio-volume-high-symbolic')],
['34', Widget.Icon('audio-volume-medium-symbolic')],
['1', Widget.Icon('audio-volume-low-symbolic')],
['0', Widget.Icon('audio-volume-muted-symbolic')],
],
connections: [[Audio, stack => {
connections: [[Audio, self => {
if (!Audio.speaker)
return;
if (Audio.speaker.isMuted) {
stack.shown = '0';
self.shown = '0';
return;
}
const show = [101, 67, 34, 1, 0].find(
threshold => threshold <= Audio.speaker.volume * 100);
stack.shown = `${show}`;
self.shown = `${show}`;
}, 'speaker-changed']],
}),
Slider({
Widget.Slider({
hexpand: true,
drawValue: false,
onChange: ({ value }) => Audio.speaker.volume = value,
connections: [[Audio, slider => {
if (!Audio.speaker)
return;
slider.value = Audio.speaker.volume;
connections: [[Audio, self => {
self.value = Audio.speaker?.volume || 0;
}, 'speaker-changed']],
}),
],
});
const BatteryLabel = () => Box({
const BatteryLabel = () => Widget.Box({
className: 'battery',
children: [
Icon({
connections: [[Battery, icon => {
icon.icon = `battery-level-${Math.floor(Battery.percent / 10) * 10}-symbolic`;
Widget.Icon({
connections: [[Battery, self => {
self.icon = `battery-level-${Math.floor(Battery.percent / 10) * 10}-symbolic`;
}]],
}),
ProgressBar({
Widget.ProgressBar({
valign: 'center',
connections: [[Battery, progress => {
connections: [[Battery, self => {
if (Battery.percent < 0)
return;
progress.fraction = Battery.percent / 100;
self.fraction = Battery.percent / 100;
}]],
}),
],
});
const SysTray = () => Box({
connections: [[SystemTray, box => {
box.children = SystemTray.items.map(item => Button({
child: Icon(),
const SysTray = () => Widget.Box({
connections: [[SystemTray, self => {
self.children = SystemTray.items.map(item => Widget.Button({
child: Widget.Icon({ binds: [['icon', item, 'icon']] }),
onPrimaryClick: (_, event) => item.activate(event),
onSecondaryClick: (_, event) => item.openMenu(event),
connections: [[item, button => {
button.child.icon = item.icon;
button.tooltipMarkup = item.tooltipMarkup;
}]],
binds: [['tooltip-markup', item, 'tooltip-markup']],
}));
}]],
});
// layout of the bar
const Left = () => Box({
const Left = () => Widget.Box({
children: [
Workspaces(),
ClientTitle(),
],
});
const Center = () => Box({
const Center = () => Widget.Box({
children: [
Media(),
Notification(),
],
});
const Right = () => Box({
const Right = () => Widget.Box({
halign: 'end',
children: [
Volume(),
@@ -188,13 +177,13 @@ const Right = () => Box({
],
});
const Bar = ({ monitor } = {}) => Window({
const Bar = ({ monitor } = {}) => Widget.Window({
name: `bar-${monitor}`, // name has to be unique
className: 'bar',
monitor,
anchor: ['top', 'left', 'right'],
exclusive: true,
child: CenterBox({
child: Widget.CenterBox({
startWidget: Left(),
centerWidget: Center(),
endWidget: Right(),