diff --git a/example/notification-center/config.js b/example/notification-center/config.js new file mode 100644 index 0000000..0aa694a --- /dev/null +++ b/example/notification-center/config.js @@ -0,0 +1,49 @@ +const { Window, Box, Label } = ags.Widget; +import { + NotificationList, DNDSwitch, ClearButton, PopupList, +} from './widgets.js'; + +const Header = () => Box({ + className: 'header', + children: [ + Label('Do Not Disturb'), + DNDSwitch(), + Box({ hexpand: true }), + ClearButton(), + ], +}); + +const NotificationCenter = () => Window({ + name: 'notification-center', + anchor: 'right top bottom', + popup: true, + focusable: true, + child: Box({ + vertical: true, + children: [ + Header(), + NotificationList(), + ], + }), +}); + +const NotificationsPopupWindow = () => Window({ + name: 'popup-window', + anchor: 'top', + child: PopupList(), +}); + +ags.Utils.timeout(1000, () => ags.Utils.execAsync([ + 'notify-send', + 'Notification Center example', + 'To have the panel popup run "ags toggle-window notification-center"' + + '\nPress ESC to close it.', +]).catch(console.error)); + +export default { + style: ags.App.configDir + '/style.css', + windows: [ + NotificationsPopupWindow(), + NotificationCenter(), + ] +} diff --git a/example/notification-center/notification.js b/example/notification-center/notification.js new file mode 100644 index 0000000..2a0a5bf --- /dev/null +++ b/example/notification-center/notification.js @@ -0,0 +1,121 @@ +const { Notifications } = ags.Service; +const { lookUpIcon, timeout } = ags.Utils; +const { Box, Icon, Label, EventBox, Button } = ags.Widget; + +const NotificationIcon = ({ appEntry, appIcon, image }) => { + if (image) { + return Box({ + valign: 'start', + hexpand: false, + className: 'icon img', + style: ` + background-image: url("${image}"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + min-width: 78px; + min-height: 78px; + `, + }); + } + + let icon = 'dialog-information-symbolic'; + if (lookUpIcon(appIcon)) + icon = appIcon; + + if (lookUpIcon(appEntry)) + icon = appEntry; + + return Box({ + valign: 'start', + hexpand: false, + className: 'icon', + style: ` + min-width: 78px; + min-height: 78px; + `, + children: [Icon({ + icon, size: 58, + halign: 'center', hexpand: true, + valign: 'center', vexpand: true, + })], + }); +}; + +export const Notification = ({ id, summary, body, actions, urgency, ...icon }) => EventBox({ + className: `notification ${urgency}`, + onPrimaryClick: () => Notifications.dismiss(id), + properties: [['hovered', false]], + onHover: w => { + if (w._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); + }, + onHoverLost: w => { + if (!w._hovered) + return; + + w._hovered = false; + Notifications.dismiss(id); + }, + vexpand: false, + child: Box({ + vertical: true, + children: [ + Box({ + children: [ + NotificationIcon(icon), + Box({ + hexpand: true, + vertical: true, + children: [ + Box({ + children: [ + Label({ + className: 'title', + xalign: 0, + justification: 'left', + hexpand: true, + maxWidthChars: 24, + truncate: 'end', + wrap: true, + label: summary, + useMarkup: summary.startsWith('<'), + }), + Button({ + className: 'close-button', + valign: 'start', + child: Icon('window-close-symbolic'), + onClicked: () => Notifications.close(id), + }), + ], + }), + Label({ + className: 'description', + hexpand: true, + useMarkup: true, + xalign: 0, + justification: 'left', + label: body, + wrap: true, + }), + ], + }), + ], + }), + Box({ + className: 'actions', + children: actions.map(action => Button({ + className: 'action-button', + onClicked: () => Notifications.invoke(id, action.id), + hexpand: true, + child: Label(action.label), + })), + }), + ], + }), +}); diff --git a/example/notification-center/style.css b/example/notification-center/style.css new file mode 100644 index 0000000..8e463cd --- /dev/null +++ b/example/notification-center/style.css @@ -0,0 +1,97 @@ +/* Assuming Adwaita-dark as Gtk theme */ + +#popup-window { + background-color: transparent; +} + +#notification-center { + background-color: #232323; +} + +#popup-window .notification > * { + box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.5); +} + +#popup-window *, +#notification-center *{ + outline: none; + color: white; +} + +button { + border: none; + box-shadow: none; + border-radius: 7px; + background-color: rgba(255,255,255, 0.1); +} + +button:hover { + background-color: rgba(255,255,255, 0.2); +} + +button:active { + background-color: rgba(255,255,255, 0.3); +} + +.notification > * { + margin: 8px; + padding: 8px; + border-radius: 15px; + background-color: #141414; +} + +.notification .icon { + border-radius: 7px; + margin-right: 8px; +} + +.notification .close-button { + padding: 0; + margin: 0; + min-height: 1.4em; + min-width: 1.4em; + border-radius: 1em; +} + +.notification .title { + font-size: 1.1em; + color: white; +} + +.notification .description { + color: rgba(255, 255, 255, 0.7); +} + +.list { + margin: 8px; + min-width: 380px; +} + +.action-button { + margin: 0 4px; +} + +.action-button:first-child { + margin-left: 0; +} + +.action-button:last-child { + margin-right: 0; +} + +.action-button:last-child:first-child { + margin: 0; +} + +.header { + padding: 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); +} + +.placeholder image { + font-size: 8em; +} + +.placeholder label { + font-size: 1.4em; +} diff --git a/example/notification-center/widgets.js b/example/notification-center/widgets.js new file mode 100644 index 0000000..bb40828 --- /dev/null +++ b/example/notification-center/widgets.js @@ -0,0 +1,85 @@ +import { Notification } from './notification.js'; +const { Gtk } = imports.gi; +const { Notifications } = ags.Service; +const { Scrollable, Box, Icon, Label, Widget, Button, Stack } = ags.Widget; + +const List = () => Box({ + vertical: true, + vexpand: true, + connections: [[Notifications, box => { + box.children = Array.from(Notifications.notifications.values()) + .map(n => Notification(n)); + + box.visible = Notifications.notifications.size > 0; + }]], +}); + +const Placeholder = () => Box({ + className: 'placeholder', + vertical: true, + vexpand: true, + valign: 'center', + children: [ + Icon('notifications-disabled-symbolic'), + Label('Your inbox is empty'), + ], + connections: [ + [Notifications, box => { + box.visible = Notifications.notifications.size === 0; + }], + ], +}); + +export const NotificationList = () => Scrollable({ + hscroll: 'never', + vscroll: 'automatic', + child: Box({ + className: 'list', + vertical: true, + children: [ + List(), + Placeholder(), + ], + }), +}); + +export const ClearButton = () => Button({ + onClicked: Notifications.clear, + connections: [[Notifications, button => { + button.sensitive = Notifications.notifications.size > 0; + }]], + child: Box({ + children: [ + Label('Clear'), + Stack({ + items: [ + ['true', Icon('user-trash-full-symbolic')], + ['false', Icon('user-trash-symbolic')], + ], + connections: [[Notifications, stack => { + stack.shown = `${Notifications.notifications.size > 0}`; + }]], + }), + ], + }), +}); + +export const DNDSwitch = () => Widget({ + type: Gtk.Switch, + valign: 'center', + connections: [ + ['notify::active', ({ active }) => { + Notifications.dnd = active; + }], + ], +}); + +export const PopupList = () => 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)); + }]], +}); diff --git a/example/config.js b/example/simple-bar/config.js similarity index 100% rename from example/config.js rename to example/simple-bar/config.js diff --git a/example/style.css b/example/simple-bar/style.css similarity index 100% rename from example/style.css rename to example/simple-bar/style.css