mirror of
https://github.com/zoriya/ags.git
synced 2026-05-31 01:55:37 +00:00
notify::prop-name signals and bind transform method (#109)
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
})),
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -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)]],
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user