5 Commits
main ... dev

54 changed files with 979 additions and 2865 deletions

View File

@@ -24,7 +24,7 @@ jobs:
- name: Install packages
run: |
pacman-key --init
pacman -Syu --noconfirm
pacman -Sy
- name: Add builduser
run: |

View File

@@ -1,6 +1,6 @@
# This is a basic workflow to help you get started with Actions
name: Check build for latest Ubuntu version.
name: Check build for latest Ubuntu LTS.
# Controls when the workflow will run
on:
@@ -14,12 +14,12 @@ on:
workflow_dispatch:
jobs:
ubuntu-build:
container: ubuntu:23.10
ubuntu-LTS-build:
container: ubuntu:22.04
runs-on: ubuntu-latest
env:
DEBIAN_FRONTEND: noninteractive
PACKAGES: meson libwayland-dev libgtk-3-dev gobject-introspection libgirepository1.0-dev valac libjson-glib-dev libhandy-1-dev libgtk-layer-shell-dev scdoc libgee-0.8-dev libpulse-dev sassc libgranite-dev
PACKAGES: meson libwayland-dev libgtk-3-dev gobject-introspection libgirepository1.0-dev valac libjson-glib-dev libhandy-1-dev libgtk-layer-shell-dev scdoc libgee-0.8-dev libpulse-dev
steps:
- name: Install packages
run: |

109
README.md
View File

@@ -15,15 +15,10 @@ support `wlr_layer_shell_unstable_v1` like Sway or anything wlroots based*
only tested with the default GTK **Adwaita** theme. Usage of any third-party
theme might require extra tweaks to the default CSS style file*
## Demo
https://github.com/ErikReider/SwayNotificationCenter/assets/35975961/93ff072f-e653-4064-8200-1c90590b83ef
![Screenshot of panel](./assets/panel.png)
Table of Contents
=================
* [Screenshots](#screenshots)
* [Want to show off your sick config?](#want-to-show-off-your-sick-config)
* [Features](#features)
* [Available Widgets](#available-widgets)
@@ -43,20 +38,24 @@ Table of Contents
* [Run](#run)
* [Control Center Shortcuts](#control-center-shortcuts)
* [Configuring](#configuring)
* [Toggle Buttons](#toggle-buttons)
* [Notification Inhibition](#notification-inhibition)
* [Scripting](#scripting)
* [Disable scripting](#disable-scripting)
* [i3status-rs Example](#i3status-rs-example)
* [Waybar Example](#waybar-example)
## Screenshots
![Screenshot of desktop notification](./assets/desktop.png)
![Screenshot of panel](./assets/panel.png)
## Want to show off your sick config?
Post your setup here: [Config flex 💪](https://github.com/ErikReider/SwayNotificationCenter/discussions/183)
## Features
- Grouped notifications
- Keyboard shortcuts
- Notification body markup with image support
- Inline replies
@@ -96,19 +95,12 @@ These widgets can be customized, added, removed and even reordered
## Install
### Alpine Linux
```zsh
apk add swaync
````
### Arch
```zsh
sudo pacman -S swaync
```
The package is available on the AUR:
Alternatively, [swaync-git](https://aur.archlinux.org/packages/swaync-git/) is available on the AUR.
- [swaync](https://aur.archlinux.org/packages/swaync/)
- [swaync-git](https://aur.archlinux.org/packages/swaync-git/)
### Fedora
@@ -134,7 +126,7 @@ An **unofficial** ebuild is available in [GURU](https://github.com/gentoo/guru)
```zsh
eselect repository enable guru
emaint sync --repo guru
emaint sync -r guru
emerge --ask gui-apps/swaync
```
@@ -148,7 +140,7 @@ sudo zypper install SwayNotificationCenter
Lunar and later:
```zsh
```
sudo apt install sway-notification-center
```
@@ -157,14 +149,14 @@ sudo apt install sway-notification-center
Bookworm and later:
```zsh
```
sudo apt install sway-notification-center
```
### Guix
The simplest way is to install it to user's profile:
```zsh
```
guix install swaynotificationcenter
```
@@ -180,31 +172,8 @@ But we recommend to use [Guix Home](https://guix.gnu.org/manual/devel/en/html_no
### Other
#### Dependencies
- `vala >= 0.56`
- `meson`
- `git`
- `scdoc`
- `sassc`
- `gtk3`
- `gtk-layer-shell`
- `dbus`
- `glib2`
- `gobject-introspection`
- `libgee`
- `json-glib`
- `libhandy`
- `gvfs`
- `granite`
##### Optional Dependencies
- `libpulse` (requires meson build options change)
- `libnotify`
```zsh
meson setup build --prefix=/usr
meson build
ninja -C build
meson install -C build
```
@@ -257,7 +226,7 @@ To reload css after changes
- Shift+D: Toggle Do Not Disturb
- Buttons 1-9: Execute alternative actions
- Left click button / actions: Activate notification action
- Middle/Right click notification: Close notification
- Right click notification: Close notification
## Configuring
@@ -267,40 +236,12 @@ See `swaync(5)` man page for more information
To reload the config, you'll need to run `swaync-client --reload-config`
The main CSS style file is located in `/etc/xdg/swaync/style.css`. Copy it over
to your `~/.config/swaync/` folder to customize without needing root access. For
more advanced/larger themes, I recommend that you use the SCSS files from source
and customize them instead. To use the SCSS files, compile with `sassc`.
The main CSS style file is located in `/etc/xdg/swaync/style.css`. Copy it over
to your `~/.config/swaync/` folder to customize without needing root access.
**Tip**: running swaync with `GTK_DEBUG=interactive swaync` will open a inspector
**Tip**: running swaync with `GTK_DEBUG=interactive swaync` will open a inspector
window that'll allow you to see all of the CSS classes + other information.
## Toggle Buttons
To add toggle buttons to your control center you can set the "type" in any acton to "toggle".
The toggle button supports different commands depending on the state of the button and
an "update-command" to update the state in case of changes from outside swaync. The update-command
is called every time the control center is opened.
The active toggle button also gains the css-class ".toggle:checked"
`config.json` example:
```jsonc
{
"buttons-grid": { // also works with actions in menubar widget
"actions": [
{
"label": "WiFi",
"type": "toggle",
"active": true,
"command": "sh -c '[[ $SWAYNC_TOGGLE_STATE == true ]] && nmcli radio wifi on || nmcli radio wifi off'",
"update-command": "sh -c '[[ $(nmcli radio wifi) == \"enabled\" ]] && echo true || echo false'"
}
]
}
}
```
## Notification Inhibition
Notifications can be inhibited through the provided `swaync-client` executable
@@ -336,7 +277,7 @@ Notification information can be printed into a terminal by running
Config properties:
```jsonc
```json
{
"scripts": {
"example-script": {
@@ -348,13 +289,13 @@ Config properties:
"category": "Notification category Regex"
}
}
// other non scripting properties...
other non scripting properties...
}
```
`config.json` example:
```jsonc
```json
{
"scripts": {
// This script will only run when Spotify sends a notification containing
@@ -366,7 +307,7 @@ Config properties:
"body": "Rick Astley - Whenever You Need Somebody"
}
}
// other non scripting properties...
other non scripting properties...
}
```
@@ -438,9 +379,9 @@ Waybar css file
Alternatively, the number of notifications can be shown by adding `{}` anywhere in the `format` field in the Waybar config
```jsonc
```json
"custom/notification": {
"format": "{} {icon}",
// ...
...
},
```

BIN
assets/desktop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -1,7 +1,7 @@
# vim: ft=sh
# vim: ft=PKGBUILD
# Maintainer: Erik Reider <erik.reider@protonmail.com>
pkgname=swaync
pkgver=0.10.1
pkgver=0.9.0
pkgrel=1
pkgdesc="A simple notification daemon with a GTK panel for checking previous notifications like other DEs"
_pkgfoldername=SwayNotificationCenter
@@ -12,12 +12,12 @@ arch=(
'armv7h' # ARM v7 hardfloat
)
license=(GPL3)
depends=("gtk3" "gtk-layer-shell" "dbus" "glib2" "gobject-introspection" "libgee" "json-glib" "libhandy" "libpulse" "gvfs" "libnotify" "granite")
depends=("gtk3" "gtk-layer-shell" "dbus" "glib2" "gobject-introspection" "libgee" "json-glib" "libhandy" "libpulse")
conflicts=("swaync" "swaync-client")
provides=("swaync" "swaync-client" "notification-daemon")
makedepends=("vala>=0.56" meson git scdoc sassc)
makedepends=(vala meson git scdoc)
source=("${_pkgfoldername}-${pkgver}.tar.gz::${url}/archive/v${pkgver}.tar.gz")
sha256sums=('5586d8a679dde5e530cb8b6f0c86abdd0d5e41362fc1c4e56e2211edea0f7a13')
sha256sums=('3f00bc858b7b3610e88ef0f6ee64d727892dd82f280f1dfc01dde863c2ea3376')
build() {
arch-meson "${_pkgfoldername}-${pkgver}" build -Dscripting=true

View File

@@ -1,8 +1,8 @@
# vim: ft=sh
# vim: ft=PKGBUILD
# Maintainer: Erik Reider <erik.reider@protonmail.com>
pkgname=swaync-git
_pkgname=swaync
pkgver=r517.4275fa3
pkgver=r448.ba4a266
pkgrel=1
pkgdesc="A simple notification daemon with a GTK panel for checking previous notifications like other DEs"
url="https://github.com/ErikReider/SwayNotificationCenter"
@@ -12,10 +12,10 @@ arch=(
'armv7h' # ARM v7 hardfloat
)
license=('GPL3')
depends=("gtk3" "gtk-layer-shell" "dbus" "glib2" "gobject-introspection" "libgee" "json-glib" "libhandy" "libpulse" "gvfs" "libnotify" "granite")
depends=("gtk3" "gtk-layer-shell" "dbus" "glib2" "gobject-introspection" "libgee" "json-glib" "libhandy" "libpulse" )
conflicts=("swaync" "swaync-client")
provides=("swaync" "swaync-client" "notification-daemon")
makedepends=("vala>=0.56" meson git scdoc sassc)
makedepends=(vala meson git scdoc)
source=("$_pkgname::git+$url")
sha256sums=('SKIP')

View File

@@ -2,7 +2,7 @@
%global alt_pkg_name swaync
Name: {{{ git_dir_name }}}
Version: 0.10.1
Version: 0.9.0
Release: 1%{?dist}
Summary: Notification daemon with GTK GUI
Provides: desktop-notification-daemon
@@ -14,7 +14,7 @@ VCS: {{{ git_dir_vcs }}}
Source: {{{ git_dir_pack }}}
BuildRequires: meson >= 0.51.0
BuildRequires: vala >= 0.56
BuildRequires: vala
BuildRequires: scdoc
BuildRequires: pkgconfig(gtk+-3.0) >= 3.22
BuildRequires: pkgconfig(gtk-layer-shell-0) >= 0.1
@@ -26,14 +26,9 @@ BuildRequires: pkgconfig(gee-0.8) >= 0.20
BuildRequires: pkgconfig(bash-completion)
BuildRequires: pkgconfig(fish)
BuildRequires: pkgconfig(libpulse)
BuildRequires: pkgconfig(granite)
BuildRequires: systemd-devel
BuildRequires: systemd
BuildRequires: sassc
BuildRequires: granite-devel
Requires: gvfs
Requires: libnotify
Requires: dbus
%{?systemd_requires}

View File

@@ -1,4 +0,0 @@
app_resources += gnome.compile_resources('icon-resources',
'swaync_icons.gresource.xml',
c_name: 'sway_notification_center_icons'
)

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 4 4 h 1 h 0.03125 c 0.253906 0.011719 0.511719 0.128906 0.6875 0.3125 l 2.28125 2.28125 l 2.3125 -2.28125 c 0.265625 -0.230469 0.445312 -0.304688 0.6875 -0.3125 h 1 v 1 c 0 0.285156 -0.035156 0.550781 -0.25 0.75 l -2.28125 2.28125 l 2.25 2.25 c 0.1875 0.1875 0.28125 0.453125 0.28125 0.71875 v 1 h -1 c -0.265625 0 -0.53125 -0.09375 -0.71875 -0.28125 l -2.28125 -2.28125 l -2.28125 2.28125 c -0.1875 0.1875 -0.453125 0.28125 -0.71875 0.28125 h -1 v -1 c 0 -0.265625 0.09375 -0.53125 0.28125 -0.71875 l 2.28125 -2.25 l -2.28125 -2.28125 c -0.210938 -0.195312 -0.304688 -0.46875 -0.28125 -0.75 z m 0 0" fill="#2e3436"/>
</svg>

Before

Width:  |  Height:  |  Size: 767 B

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="M 4 2.5 C 4 2.234 4.106 1.98 4.293 1.793 C 4.684 1.402 5.317 1.402 5.707 1.793 L 8 4.086 L 10.293 1.793 C 10.684 1.402 11.317 1.402 11.707 1.793 C 11.895 1.98 12 2.234 12 2.5 C 12 2.766 11.895 3.019 11.707 3.207 L 8.707 6.207 C 8.317 6.598 7.684 6.598 7.293 6.207 L 4.293 3.207 C 4.106 3.019 4 2.766 4 2.5 Z" fill="#2e3436"/>
<path d="M 4 13.5 C 4 13.766 4.106 14.019 4.293 14.207 C 4.684 14.598 5.317 14.598 5.707 14.207 L 8 11.914 L 10.293 14.207 C 10.684 14.598 11.317 14.598 11.707 14.207 C 11.895 14.019 12 13.766 12 13.5 C 12 13.234 11.895 12.98 11.707 12.793 L 8.707 9.793 C 8.317 9.402 7.684 9.402 7.293 9.793 L 4.293 12.793 C 4.106 12.98 4 13.234 4 13.5 Z" fill="#2e3436"/>
</svg>

Before

Width:  |  Height:  |  Size: 830 B

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/erikreider/swaync/icons/scalable/actions">
<file preprocess="xml-stripblanks">swaync-collapse-symbolic.svg</file>
<file preprocess="xml-stripblanks">swaync-close-symbolic.svg</file>
</gresource>
</gresources>

View File

@@ -2,35 +2,9 @@ install_data('org.erikreider.swaync.gschema.xml',
install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')
)
subdir('icons')
compile_schemas = find_program('glib-compile-schemas', required: false)
if compile_schemas.found()
test('Validate schema file', compile_schemas,
args: ['--strict', '--dry-run', meson.current_source_dir()]
)
endif
# SCSS Dependency
sassc = find_program('sassc')
assert(sassc.found())
# SCSS Compilation
style_css = custom_target(
'SCSS Compilation',
build_by_default: true,
build_always_stale: true,
input : 'style/style.scss',
output : 'style.css',
install: true,
install_dir: config_path,
command : [
sassc,
'-t',
'expanded',
'@INPUT@',
'@OUTPUT@'
],
)
message(style_css.full_path())

View File

@@ -1,347 +0,0 @@
@define-color cc-bg rgba(46, 46, 46, 0.7);
@define-color noti-border-color rgba(255, 255, 255, 0.15);
@define-color noti-bg rgba(48, 48, 48, 0.8);
@define-color noti-bg-opaque rgb(48, 48, 48);
@define-color noti-bg-darker rgb(38, 38, 38);
@define-color noti-bg-hover rgb(56, 56, 56);
@define-color noti-bg-hover-opaque rgb(56, 56, 56);
@define-color noti-bg-focus rgba(68, 68, 68, 0.6);
@define-color noti-close-bg rgba(255, 255, 255, 0.1);
@define-color noti-close-bg-hover rgba(255, 255, 255, 0.15);
@define-color text-color rgb(255, 255, 255);
@define-color text-color-disabled rgb(150, 150, 150);
@define-color bg-selected rgb(0, 128, 255);
$border: 1px solid #{"@noti-border-color"};
$border-radius: 12px;
$font-size-body: 15px;
$font-size-summary: 16px;
$hover-tranistion: background 0.15s ease-in-out;
$group-collapse-tranistion: opacity 400ms ease-in-out;
$notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
0 1px 3px 1px rgba(0, 0, 0, 0.7), 0 2px 6px 2px rgba(0, 0, 0, 0.3);
.notification-row {
outline: none;
&:focus,
&:hover {
background: #{"@noti-bg-focus"};
}
.notification-background {
padding: 6px 12px;
.close-button {
/* The notification Close Button */
background: #{"@noti-close-bg"};
color: #{"@text-color"};
text-shadow: none;
padding: 0;
border-radius: 100%;
$margin: 5px;
margin-top: $margin;
margin-right: $margin;
box-shadow: none;
border: none;
min-width: 24px;
min-height: 24px;
&:hover {
box-shadow: none;
background: #{"@noti-close-bg-hover"};
transition: $hover-tranistion;
border: none;
}
}
.notification {
/* The actual notification */
border-radius: $border-radius;
border: $border;
padding: 0;
transition: $hover-tranistion;
background: #{"@noti-bg"};
&.low {
/* Low Priority Notification */
}
&.normal {
/* Normal Priority Notification */
}
&.critical {
/* Critical Priority Notification */
}
%action {
padding: 4px;
margin: 0;
box-shadow: none;
background: transparent;
border: none;
color: #{"@text-color"};
transition: $hover-tranistion;
}
%action-hover {
-gtk-icon-effect: none;
background: #{"@noti-bg-hover"};
}
.notification-default-action {
/* The large action that also displays the notification summary and body */
@extend %action;
border-radius: $border-radius;
&:hover {
@extend %action-hover;
}
&:not(:only-child) {
/* When alternative actions are visible */
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
.notification-content {
background: transparent;
border-radius: $border-radius;
$margin: 4px;
padding: $margin;
.image {
/* Notification Primary Image */
-gtk-icon-effect: none;
border-radius: 100px; /* Size in px */
margin: $margin;
}
.app-icon {
/* Notification app icon (only visible when the primary image is set) */
-gtk-icon-effect: none;
-gtk-icon-shadow: 0 1px 4px black;
margin: 6px;
}
.text-box {
.summary {
/* Notification summary/title */
font-size: $font-size-summary;
font-weight: bold;
background: transparent;
color: #{"@text-color"};
text-shadow: none;
}
.time {
/* Notification time-ago */
font-size: $font-size-summary;
font-weight: bold;
background: transparent;
color: #{"@text-color"};
text-shadow: none;
margin-right: 30px;
}
.body {
/* Notification body */
font-size: $font-size-body;
font-weight: normal;
background: transparent;
color: #{"@text-color"};
text-shadow: none;
}
}
progressbar {
/* The optional notification progress bar */
margin-top: $margin;
}
.body-image {
/* The "extra" optional bottom notification image */
margin-top: $margin;
background-color: white;
border-radius: $border-radius;
-gtk-icon-effect: none;
}
.inline-reply {
/* The inline reply section */
margin-top: $margin;
.inline-reply-entry {
background: #{"@noti-bg-darker"};
color: #{"@text-color"};
caret-color: #{"@text-color"};
border: $border;
border-radius: $border-radius;
}
.inline-reply-button {
margin-left: 4px;
background: #{"@noti-bg"};
border: $border;
border-radius: $border-radius;
color: #{"@text-color"};
&:disabled {
background: initial;
color: #{"@text-color-disabled"};
border: $border;
border-color: transparent;
}
&:hover {
background: #{"@noti-bg-hover"};
}
}
}
}
}
.notification-action {
/* The alternative actions below the default action */
@extend %action;
border-top: $border;
border-radius: 0px;
border-right: $border;
&:first-child {
/* add bottom border radius to eliminate clipping */
border-bottom-left-radius: $border-radius;
}
&:last-child {
border-bottom-right-radius: $border-radius;
border-right: none;
}
&:hover {
@extend %action-hover;
}
}
}
}
}
.notification-group {
/* Styling only for Grouped Notifications */
&.low {
/* Low Priority Group */
}
&.normal {
/* Low Priority Group */
}
&.critical {
/* Low Priority Group */
}
%header {
margin: 0 16px;
color: #{"@text-color"};
}
.notification-group-headers {
/* Notification Group Headers */
@extend %header;
.notification-group-icon {
color: #{"@text-color"};
}
.notification-group-header {
color: #{"@text-color"};
}
}
.notification-group-buttons {
/* Notification Group Buttons */
@extend %header;
}
&.collapsed {
.notification-row {
.notification {
background-color: #{"@noti-bg-opaque"};
}
&:not(:last-child) {
/* Top notification in stack */
/* Set lower stacked notifications opacity to 0 */
.notification-action,
.notification-default-action {
opacity: 0;
}
}
}
&:hover {
.notification-row:not(:only-child) {
.notification {
background-color: #{"@noti-bg-hover-opaque"};
}
}
}
}
}
.control-center {
/* The Control Center which contains the old notifications + widgets */
background: #{"@cc-bg"};
color: #{"@text-color"};
border-radius: $border-radius;
.control-center-list-placeholder {
/* The placeholder when there are no notifications */
opacity: 0.5;
}
.control-center-list {
/* List of notifications */
background: transparent;
.notification {
box-shadow: $notification-shadow;
.notification-default-action,
.notification-action {
transition: $group-collapse-tranistion, $hover-tranistion;
&:hover {
background-color: #{"@noti-bg-hover"};
}
}
}
}
}
.blank-window {
/* Window behind control center and on all other monitors */
background: transparent;
}
.floating-notifications {
background: transparent;
.notification {
box-shadow: none;
}
}
/*** Widgets ***/
/* Title widget */
@import "widgets/title";
/* DND widget */
@import "widgets/dnd";
/* Label widget */
@import "widgets/label";
/* Mpris widget */
@import "widgets/mpris";
/* Buttons widget */
@import "widgets/buttons";
/* Menubar widget */
@import "widgets/menubar";
/* Volume widget */
@import "widgets/volume";
/* Backlight widget */
@import "widgets/backlight";
/* Inhibitors widget */
@import "widgets/inhibitors";

View File

@@ -1,6 +0,0 @@
.widget-backlight {
background-color: #{"@noti-bg"};
padding: 8px;
margin: 8px;
border-radius: 12px;
}

View File

@@ -1,18 +0,0 @@
.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.toggle:checked {
/* style given to the active toggle button */
}
.widget-buttons-grid > flowbox > flowboxchild > button:hover {
}

View File

@@ -1,19 +0,0 @@
.widget-dnd {
color: #{"@text-color"};
margin: 8px;
font-size: 1.1rem;
}
.widget-dnd > switch {
font-size: initial;
border-radius: 12px;
background: #{"@noti-bg"};
border: 1px solid #{"@noti-border-color"};
box-shadow: none;
}
.widget-dnd > switch:checked {
background: #{"@bg-selected"};
}
.widget-dnd > switch slider {
background: #{"@noti-bg-hover"};
border-radius: 12px;
}

View File

@@ -1,16 +0,0 @@
.widget-inhibitors {
margin: 8px;
font-size: 1.5rem;
}
.widget-inhibitors > button {
font-size: initial;
color: #{"@text-color"};
text-shadow: none;
background: #{"@noti-bg"};
border: 1px solid #{"@noti-border-color"};
box-shadow: none;
border-radius: 12px;
}
.widget-inhibitors > button:hover {
background: #{"@noti-bg-hover"};
}

View File

@@ -1,6 +0,0 @@
.widget-label {
margin: 8px;
}
.widget-label > label {
font-size: 1.1rem;
}

View File

@@ -1,26 +0,0 @@
.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;
}

View File

@@ -1,47 +0,0 @@
@define-color mpris-album-art-overlay rgba(0, 0, 0, 0.55);
@define-color mpris-button-hover rgba(0, 0, 0, 0.50);
$mpris-shadow: 0px 0px 10px rgba(0, 0, 0, 0.75);
.widget-mpris {
/* The parent to all players */
.widget-mpris-player {
padding: 8px;
padding: 16px;
margin: 16px 20px;
background-color: #{"@mpris-album-art-overlay"};
border-radius: $border-radius;
box-shadow: $mpris-shadow;
button:hover {
/* The media player buttons (play, pause, next, etc...) */
background: #{"@noti-bg-hover"};
}
.widget-mpris-album-art {
border-radius: $border-radius;
box-shadow: $mpris-shadow;
}
.widget-mpris-title {
font-weight: bold;
font-size: 1.25rem;
}
.widget-mpris-subtitle {
font-size: 1.1rem;
}
& > box > button {
/* Change player control buttons */
&:hover {
background-color: #{"@mpris-button-hover"};
}
}
}
& > box > button {
/* Change player side buttons */
}
& > box > button:disabled {
/* Change player side buttons insensitive */
}
}

View File

@@ -1,17 +0,0 @@
.widget-title {
color: #{"@text-color"};
margin: 8px;
font-size: 1.5rem;
}
.widget-title > button {
font-size: initial;
color: #{"@text-color"};
text-shadow: none;
background: #{"@noti-bg"};
border: 1px solid #{"@noti-border-color"};
box-shadow: none;
border-radius: 12px;
}
.widget-title > button:hover {
background: #{"@noti-bg-hover"};
}

View File

@@ -1,18 +0,0 @@
.widget-volume {
background-color: #{"@noti-bg"};
padding: 8px;
margin: 8px;
border-radius: 12px;
}
.widget-volume > box > button {
background: transparent;
border: none;
}
.per-app-volume {
background-color: #{"@noti-bg-alt"};
padding: 4px 8px 8px 8px;
margin: 0px 8px 8px 8px;
border-radius: 12px;
}

View File

@@ -44,7 +44,7 @@ swaync - A simple notification daemon with a GTK gui for notifications and the c
*Left click button / actions*: Activate notification action
*Middle/Right click notification*: Close notification
*Right click notification*: Close notification
# CONFIGURATION

View File

@@ -68,13 +68,6 @@ config file to be able to detect config errors
description: Layer of control center window relative to normal windows. ++
background is below all windows, overlay is above all windows.
*control-center-exclusive-zone* ++
type: bool ++
default: true ++
description: Whether or not the control center should follow the ++
compositors exclusive zones. An example would be setting it to ++
*false* to cover your panel/dock.
*notification-2fa-action* ++
type: bool ++
default: true ++
@@ -383,16 +376,10 @@ config file to be able to detect config errors
description: Type of the button. ++
Toggle buttons receive the '.active' css class ++
enum: ["normal", "toggle"] ++
update-command: ++
active: ++
type: string ++
default: "" ++
description: "Command to be executed on visibility change of ++
cc to update the active state of the toggle button (should ++
echo true or false)" ++
active: ++
type: bool ++
default: false ++
description: Wether the toggle button is active as default or not ++
description: Command to run to test if button should be active (return code 0) or not ++
description: A list of actions containing a label and a command ++
description: A button to reveal a dropdown with action-buttons ++
buttons#<name>: ++
@@ -422,20 +409,12 @@ config file to be able to detect config errors
type: string ++
default: "normal" ++
description: Type of the button ++
Toggle buttons receive the '.active' css class and an env ++
variable "SWAYNC_TOGGLE_STATE" is set. See example usage in the ++
default config.json ++
Toggle buttons receive the '.active' css class ++
enum: ["normal", "toggle"] ++
update-command: ++
active: ++
type: string ++
default: "" ++
description: "Command to be executed on visibility change of ++
cc to update the active state of the toggle button (should ++
echo true or false)" ++
active: ++
type: bool ++
default: false ++
description: Wether the toggle button is active as default or not ++
description: Command to run to test if button should be active (return code 0) or not ++
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*++
@@ -460,14 +439,12 @@ config file to be able to detect config errors
type: string ++
default: "normal" ++
description: Type of the button ++
Toggle buttons receive the '.active' css class and an env ++
variable "SWAYNC_TOGGLE_STATE" is set. See example usage in the ++
default config.json ++
Toggle buttons receive the '.active' css class ++
enum: ["normal", "toggle"] ++
active: ++
type: bool ++
default: false ++
description: Wether the toggle button is active as default or not ++
type: string ++
default: "" ++
description: Command to run to test if button should be active (return code 0) or not ++
description: A list of actions containing a label and a command ++
description: A grid of buttons that execute shell commands ++
#START pulse-audio

View File

@@ -1,5 +1,5 @@
project('sway-notificaton-center', ['c', 'vala'],
version: '0.10.1',
version: '0.9.0',
meson_version: '>= 0.59.0',
default_options: [ 'warning_level=2' ],
)
@@ -9,11 +9,6 @@ add_project_arguments(['--enable-gobject-tracing'], language: 'vala')
add_project_arguments(['--enable-checking'], language: 'vala')
i18n = import('i18n')
gnome = import('gnome')
app_resources = []
config_path = join_paths(get_option('sysconfdir'), 'xdg', 'swaync')
subdir('data')
subdir('src')

View File

@@ -1,150 +0,0 @@
namespace SwayNotificationCenter {
public class Animation : Object {
private unowned Gtk.Widget widget;
double value;
double value_from;
double value_to;
int64 duration;
int64 start_time;
uint tick_cb_id;
ulong unmap_cb_id;
unowned AnimationEasingFunc easing_func;
unowned AnimationValueCallback value_cb;
unowned AnimationDoneCallback done_cb;
bool is_done;
public delegate void AnimationValueCallback (double value);
public delegate void AnimationDoneCallback ();
public delegate double AnimationEasingFunc (double t);
public Animation (Gtk.Widget widget, int64 duration,
AnimationEasingFunc easing_func,
AnimationValueCallback value_cb,
AnimationDoneCallback done_cb) {
this.widget = widget;
this.duration = duration;
this.easing_func = easing_func;
this.value_cb = value_cb;
this.done_cb = done_cb;
this.is_done = false;
}
~Animation () {
stop ();
}
void set_value (double value) {
this.value = value;
this.value_cb (value);
}
void done () {
if (is_done) return;
is_done = true;
done_cb ();
}
bool tick_cb (Gtk.Widget widget, Gdk.FrameClock frame_clock) {
int64 frame_time = frame_clock.get_frame_time () / 1000; /* ms */
double t = (double) (frame_time - start_time) / duration;
if (t >= 1) {
tick_cb_id = 0;
set_value (value_to);
if (unmap_cb_id != 0) {
SignalHandler.disconnect (widget, unmap_cb_id);
unmap_cb_id = 0;
}
done ();
return Source.REMOVE;
}
set_value (lerp (value_from, value_to, easing_func (t)));
return Source.CONTINUE;
}
public void start (double from, double to) {
this.value_from = from;
this.value_to = to;
this.value = from;
this.is_done = false;
unowned Gtk.Settings ? gsettings = Gtk.Settings.get_default ();
bool animations_enabled =
gsettings != null ? gsettings.gtk_enable_animations : true;
if (animations_enabled != true ||
!widget.get_mapped () || duration <= 0) {
set_value (value_to);
done ();
return;
}
start_time = widget.get_frame_clock ().get_frame_time () / 1000;
if (tick_cb_id != 0) return;
unmap_cb_id =
Signal.connect_swapped (widget, "unmap", (Callback) stop, this);
tick_cb_id = widget.add_tick_callback (tick_cb);
}
public void stop () {
if (tick_cb_id != 0) {
widget.remove_tick_callback (tick_cb_id);
tick_cb_id = 0;
}
if (unmap_cb_id != 0) {
SignalHandler.disconnect (widget, unmap_cb_id);
unmap_cb_id = 0;
}
done ();
}
public double get_value () {
return value;
}
public static double lerp (double a, double b, double t) {
return a * (1.0 - t) + b * t;
}
public static double ease_out_cubic (double t) {
double p = t - 1;
return p * p * p + 1;
}
public static double ease_in_cubic (double t) {
return t * t * t;
}
public static double ease_in_out_cubic (double t) {
double p = t * 2;
if (p < 1) return 0.5 * p * p * p;
p -= 2;
return 0.5 * (p * p * p + 2);
}
}
}

View File

@@ -77,9 +77,8 @@ private void print_help (string[] args) {
private void on_subscribe (uint count, bool dnd, bool cc_open, bool inhibited) {
stdout.printf (
"{ \"count\": %u, \"dnd\": %s, \"visible\": %s, \"inhibited\": %s }\n",
count, dnd.to_string (), cc_open.to_string (), inhibited.to_string ());
stdout.flush ();
"{ \"count\": %u, \"dnd\": %s, \"visible\": %s, \"inhibited\": %s }\n"
.printf (count, dnd.to_string (), cc_open.to_string (), inhibited.to_string ()));
}
private void print_subscribe () {

View File

@@ -74,17 +74,6 @@
"mpris": {
"image-size": 96,
"image-radius": 12
},
"buttons-grid": {
"actions": [
{
"label": "直",
"type": "toggle",
"active": true,
"command": "sh -c '[[ $SWAYNC_TOGGLE_STATE == true ]] && nmcli radio wifi on || nmcli radio wifi off'",
"update_command": "sh -c '[[ $(nmcli radio wifi) == \"enabled\" ]] && echo true || echo false'"
}
]
}
}
}

View File

@@ -237,18 +237,44 @@ namespace SwayNotificationCenter {
public ScriptRunOnType run_on { get; set; default = ScriptRunOnType.RECEIVE; }
public async bool run_script (NotifyParams param, out string msg) {
string[] spawn_env = {};
spawn_env += "SWAYNC_APP_NAME=%s".printf (param.app_name);
spawn_env += "SWAYNC_SUMMARY=%s".printf (param.summary);
spawn_env += "SWAYNC_BODY=%s".printf (param.body);
spawn_env += "SWAYNC_URGENCY=%s".printf (param.urgency.to_string ());
spawn_env += "SWAYNC_CATEGORY=%s".printf (param.category);
spawn_env += "SWAYNC_ID=%s".printf (param.applied_id.to_string ());
spawn_env += "SWAYNC_REPLACES_ID=%s".printf (param.replaces_id.to_string ());
spawn_env += "SWAYNC_TIME=%s".printf (param.time.to_string ());
spawn_env += "SWAYNC_DESKTOP_ENTRY=%s".printf (param.desktop_entry ?? "");
msg = "";
try {
string[] spawn_env = Environ.get ();
// Export env variables
spawn_env += "SWAYNC_APP_NAME=%s".printf (param.app_name);
spawn_env += "SWAYNC_SUMMARY=%s".printf (param.summary);
spawn_env += "SWAYNC_BODY=%s".printf (param.body);
spawn_env += "SWAYNC_URGENCY=%s".printf (param.urgency.to_string ());
spawn_env += "SWAYNC_CATEGORY=%s".printf (param.category);
spawn_env += "SWAYNC_ID=%s".printf (param.applied_id.to_string ());
spawn_env += "SWAYNC_REPLACES_ID=%s".printf (param.replaces_id.to_string ());
spawn_env += "SWAYNC_TIME=%s".printf (param.time.to_string ());
spawn_env += "SWAYNC_DESKTOP_ENTRY=%s".printf (param.desktop_entry ?? "");
return yield Functions.execute_command (exec, spawn_env, out msg);
Pid child_pid;
Process.spawn_async (
"/",
exec.split (" "),
spawn_env,
SpawnFlags.SEARCH_PATH | SpawnFlags.DO_NOT_REAP_CHILD,
null,
out child_pid);
// Close the child when the spawned process is idling
int end_status = 0;
ChildWatch.add (child_pid, (pid, status) => {
Process.close_pid (pid);
end_status = status;
run_script.callback ();
});
// Waits until `run_script.callback()` is called above
yield;
return end_status == 0;
} catch (Error e) {
stderr.printf ("Run_Script Error: %s\n", e.message);
msg = e.message;
return false;
}
}
public override bool matches_notification (NotifyParams param) {
@@ -467,10 +493,6 @@ namespace SwayNotificationCenter {
get; set; default = Layer.TOP;
}
public bool control_center_exclusive_zone {
get; set; default = true;
}
/** Categories settings */
public OrderedHashTable<Category> categories_settings {
get;

View File

@@ -75,11 +75,6 @@
"default": "none",
"enum": ["background", "bottom", "top", "overlay", "none"]
},
"control-center-exclusive-zone": {
"type": "boolean",
"description": "Whether or not the control center should follow the compositors exclusive zones. An example would be setting it to \"false\" to cover your panel/dock.",
"default": true
},
"notification-2fa-action": {
"type": "boolean",
"description": "If each notification should display a 'COPY \"1234\"' action",
@@ -92,7 +87,7 @@
},
"notification-icon-size": {
"type": "integer",
"description": "The notification icon size (in pixels). The app icon size is 1/3",
"description": "The notification icon size (in pixels)",
"default": 64,
"minimum": 16
},
@@ -157,7 +152,7 @@
},
"image-visibility": {
"type": "string",
"description": "The notification image visibility when no icon is available.",
"description": "An explanation about the purpose of this instance.",
"default": "when-available",
"enum": ["always", "when-available", "never"]
},
@@ -402,14 +397,8 @@
},
"image-radius": {
"type": "integer",
"description": "The border radius of the album art. Will be overriden by setting the border-radius in the style.css for the \".widget-mpris-album-art\" class",
"description": "The border radius of the album art",
"default": 12
},
"blur": {
"type": "boolean",
"description": "Appy the artwork as the MPRIS background and blur it",
"default": true
}
}
},
@@ -436,19 +425,14 @@
},
"type": {
"type": "string",
"description": "Type of the button; toggle buttons receive the .active css class and an env variable 'SWAYNC_TOGGLE_STATE' is set. See example in the default config.json",
"description": "Type of the button; toggle buttons receive the .active css class",
"default": "normal",
"enum": ["normal", "toggle"]
},
"update-command": {
"type": "string",
"description": "Command to be executed on visibility change of cc to update the active state of the toggle button (should echo true or false)",
"default": ""
},
"active": {
"type": "boolean",
"description": "Wether the toggle button is active as default or not",
"default": false
"type": "string",
"description": "Command to run to test if button should be active",
"default": ""
}
}
}

View File

@@ -1,5 +1,4 @@
public class Constants {
public const string VERSION = @VERSION@;
public const string VERSIONNUM = @VERSION_NUM@;
public const uint ANIMATION_DURATION = 400;
}

View File

@@ -20,84 +20,89 @@
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="notifications_box">
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="can-focus">True</property>
<property name="hscrollbar-policy">never</property>
<child>
<object class="GtkStack" id="stack">
<object class="GtkViewport" id="viewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</property>
<property name="vexpand">True</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hscrollbar-policy">never</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">notifications-list</property>
</packing>
</child>
<child>
<object class="GtkBox">
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkImage">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">96</property>
<property name="icon-name">preferences-system-notifications-symbolic</property>
<property name="use-fallback">True</property>
<property name="icon_size">0</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">96</property>
<property name="icon-name">preferences-system-notifications-symbolic</property>
<property name="use-fallback">True</property>
<property name="icon_size">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">No Notifications</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="control-center-list-placeholder"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
<property name="name">notifications-placeholder</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<object class="GtkListBox" id="list_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">No Notifications</property>
<property name="valign">end</property>
<property name="selection-mode">none</property>
<property name="activate-on-single-click">False</property>
<style>
<class name="control-center-list"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="name">notifications-list</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="control-center-list-placeholder"/>
</style>
</object>
<packing>
<property name="name">notifications-placeholder</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>

View File

@@ -1,30 +1,18 @@
namespace SwayNotificationCenter {
[GtkTemplate (ui = "/org/erikreider/sway-notification-center/controlCenter/controlCenter.ui")]
public class ControlCenter : Gtk.ApplicationWindow {
[GtkChild]
unowned Gtk.Box notifications_box;
unowned Gtk.ScrolledWindow scrolled_window;
[GtkChild]
unowned Gtk.Viewport viewport;
[GtkChild]
unowned Gtk.Stack stack;
[GtkChild]
unowned Gtk.ScrolledWindow scrolled_window;
FadedViewport viewport = new FadedViewport (20);
Gtk.ListBox list_box = new Gtk.ListBox ();
unowned Gtk.ListBox list_box;
[GtkChild]
unowned Gtk.Box box;
unowned NotificationGroup ? expanded_group = null;
private double fade_animation_progress = 1.0;
private Animation ? notification_fade_animation;
private double scroll_animation_progress = 1.0;
private Animation ? scroll_animation;
HashTable<uint32, unowned NotificationGroup> noti_groups_id =
new HashTable<uint32, unowned NotificationGroup> (direct_hash, direct_equal);
/** NOTE: Only includes groups with ids with length of > 0 */
HashTable<string, unowned NotificationGroup> noti_groups_name =
new HashTable<string, unowned NotificationGroup> (str_hash, str_equal);
const string STACK_NOTIFICATIONS_PAGE = "notifications-list";
const string STACK_PLACEHOLDER_PAGE = "notifications-placeholder";
@@ -35,8 +23,9 @@ namespace SwayNotificationCenter {
private SwayncDaemon swaync_daemon;
private NotiDaemon noti_daemon;
private int list_position = 0;
private uint list_position = 0;
private double last_upper = 0;
private bool list_reverse = false;
private Gtk.Align list_align = Gtk.Align.START;
@@ -49,18 +38,6 @@ namespace SwayNotificationCenter {
this.swaync_daemon.reloading_css.connect (reload_notifications_style);
viewport.set_visible (true);
viewport.set_vexpand (true);
viewport.set_shadow_type (Gtk.ShadowType.NONE);
scrolled_window.add (viewport);
list_box.set_visible (true);
list_box.set_valign (Gtk.Align.END);
list_box.set_selection_mode (Gtk.SelectionMode.NONE);
list_box.set_activate_on_single_click (false);
list_box.get_style_context ().add_class ("control-center-list");
viewport.add (list_box);
if (swaync_daemon.use_layer_shell) {
if (!GtkLayerShell.is_supported ()) {
stderr.printf ("GTKLAYERSHELL IS NOT SUPPORTED!\n");
@@ -77,6 +54,8 @@ namespace SwayNotificationCenter {
GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.BOTTOM, true);
}
viewport.size_allocate.connect (size_alloc);
this.map.connect (() => {
set_anchor ();
// Wait until the layer has attached
@@ -173,10 +152,72 @@ namespace SwayNotificationCenter {
return true;
});
key_press_event.connect (key_press_event_cb);
this.key_press_event.connect ((w, event_key) => {
if (this.get_focus () is Gtk.Entry) return false;
if (event_key.type == Gdk.EventType.KEY_PRESS) {
var children = list_box.get_children ();
Notification noti = (Notification)
list_box.get_focus_child ();
switch (Gdk.keyval_name (event_key.keyval)) {
case "Return":
if (noti != null) noti.click_default_action ();
break;
case "Delete":
case "BackSpace":
if (noti != null) {
if (children.length () == 0) break;
if (list_reverse &&
children.first ().data != noti) {
list_position--;
} else if (children.last ().data == noti) {
if (list_position > 0) list_position--;
}
close_notification (noti.param.applied_id);
}
break;
case "C":
close_all_notifications ();
break;
case "D":
try {
swaync_daemon.toggle_dnd ();
} catch (Error e) {
error ("Error: %s\n", e.message);
}
break;
case "Down":
if (list_position + 1 < children.length ()) {
++list_position;
}
break;
case "Up":
if (list_position > 0) --list_position;
break;
case "Home":
list_position = 0;
break;
case "End":
list_position = children.length () - 1;
if (list_position == uint.MAX) list_position = 0;
break;
default:
// Pressing 1-9 to activate a notification action
for (int i = 0; i < 9; i++) {
uint keyval = Gdk.keyval_from_name (
(i + 1).to_string ());
if (event_key.keyval == keyval) {
if (noti != null) noti.click_alt_action (i);
break;
}
}
break;
}
navigate_list (list_position);
}
return false;
});
stack.set_visible_child_name (STACK_PLACEHOLDER_PAGE);
// Switches the stack page depending on the amount of notifications
// Switches the stack page depending on the
list_box.add.connect (() => {
stack.set_visible_child_name (STACK_NOTIFICATIONS_PAGE);
});
@@ -187,171 +228,6 @@ namespace SwayNotificationCenter {
});
add_widgets ();
notification_fade_animation = new Animation (this, Constants.ANIMATION_DURATION,
Animation.ease_in_out_cubic,
fade_animation_value_cb,
fade_animation_done_cb);
scroll_animation = new Animation (this, Constants.ANIMATION_DURATION,
Animation.ease_in_out_cubic,
scroll_animation_value_cb,
scroll_animation_done_cb);
list_box.draw.connect (list_box_draw_cb);
}
void fade_animation_value_cb (double progress) {
this.fade_animation_progress = progress;
this.queue_draw ();
}
void fade_animation_done_cb () {}
void fade_animate (double to) {
notification_fade_animation.stop ();
notification_fade_animation.start (fade_animation_progress, to);
}
void scroll_animation_value_cb (double progress) {
this.scroll_animation_progress = progress;
// Scroll to the top of the group
if (scroll_animation_progress > 0) {
scrolled_window.vadjustment.set_value (scroll_animation_progress);
}
}
void scroll_animation_done_cb () {
int y = expanded_group.get_relative_y (list_box);
if (y > 0) {
scrolled_window.vadjustment.set_value (y);
}
}
void scroll_animate (double to) {
scroll_animation.stop ();
scroll_animation.start (scroll_animation_progress, to);
}
/// Fade non-expanded groups when one group is expanded
private bool list_box_draw_cb (Cairo.Context cr) {
Cairo.Pattern fade_gradient = new Cairo.Pattern.linear (0, 0, 0, 1);
fade_gradient.add_color_stop_rgba (0, 1, 1, 1, 1 - fade_animation_progress - 0.5);
foreach (unowned Gtk.Widget widget in list_box.get_children ()) {
Gtk.Allocation alloc;
widget.get_allocated_size (out alloc, null);
cr.save ();
cr.translate (0, alloc.y);
cr.push_group ();
widget.draw (cr);
cr.scale (alloc.width, alloc.height);
if (widget != expanded_group) {
cr.set_source (fade_gradient);
cr.rectangle (0, 0, alloc.width, alloc.height);
cr.set_operator (Cairo.Operator.DEST_OUT);
cr.fill ();
}
cr.pop_group_to_source ();
cr.paint ();
cr.restore ();
}
return true;
}
private bool key_press_event_cb (Gdk.EventKey event_key) {
if (this.get_focus () is Gtk.Entry) return false;
if (event_key.type == Gdk.EventType.KEY_PRESS) {
var children = list_box.get_children ();
var group = (NotificationGroup) list_box.get_focus_child ();
switch (Gdk.keyval_name (event_key.keyval)) {
case "Return":
if (group != null) {
var noti = group.get_latest_notification ();
if (group.only_single_notification () && noti != null) {
noti.click_default_action ();
break;
}
group.on_expand_change (group.toggle_expanded ());
}
break;
case "Delete":
case "BackSpace":
if (group != null) {
int len = (int) children.length ();
if (len == 0) break;
// Add a delta so that we select the next notification
// due to it not being gone from the list yet due to
// the fade transition
int delta = 2;
if (list_reverse) {
if (children.first ().data != group) {
delta = 0;
}
list_position--;
} else {
if (list_position > 0) list_position--;
if (children.last ().data == group) {
delta = 0;
}
}
var noti = group.get_latest_notification ();
if (group.only_single_notification () && noti != null) {
close_notification (noti.param.applied_id, true);
break;
}
group.close_all_notifications ();
navigate_list (list_position + delta);
return true;
}
break;
case "C":
close_all_notifications ();
break;
case "D":
try {
swaync_daemon.toggle_dnd ();
} catch (Error e) {
error ("Error: %s\n", e.message);
}
break;
case "Down":
if (list_position + 1 < children.length ()) {
++list_position;
}
break;
case "Up":
if (list_position > 0) --list_position;
break;
case "Home":
list_position = 0;
break;
case "End":
list_position = ((int) children.length ()) - 1;
if (list_position == uint.MAX) list_position = 0;
break;
default:
// Pressing 1-9 to activate a notification action
for (int i = 0; i < 9; i++) {
uint keyval = Gdk.keyval_from_name (
(i + 1).to_string ());
if (event_key.keyval == keyval && group != null) {
var noti = group.get_latest_notification ();
noti.click_alt_action (i);
break;
}
}
break;
}
navigate_list (list_position);
}
// Override the builtin list navigation
return true;
}
/** Adds all custom widgets. Removes previous widgets */
@@ -366,11 +242,11 @@ namespace SwayNotificationCenter {
if (w.length == 0) w = DEFAULT_WIDGETS;
bool has_notification = false;
foreach (string key in w) {
// Reposition the notifications_box
// Reposition the scrolled_window
if (key == "notifications") {
has_notification = true;
uint pos = box.get_children ().length ();
box.reorder_child (notifications_box, (int) (pos > 0 ? --pos : 0));
box.reorder_child (scrolled_window, (int) (pos > 0 ? --pos : 0));
continue;
}
// Add the widget if it is valid
@@ -384,22 +260,23 @@ namespace SwayNotificationCenter {
if (!has_notification) {
warning ("Notification widget not included in \"widgets\" config. Using default bottom position");
uint pos = box.get_children ().length ();
box.reorder_child (notifications_box, (int) (pos > 0 ? --pos : 0));
box.reorder_child (scrolled_window, (int) (pos > 0 ? --pos : 0));
}
}
/** Resets the UI positions */
private void set_anchor () {
if (swaync_daemon.use_layer_shell) {
// Set the exlusive zone
int exclusive_zone = ConfigModel.instance.control_center_exclusive_zone ? 0 : 100;
GtkLayerShell.set_exclusive_zone (this, exclusive_zone);
// Grabs the keyboard input until closed
bool keyboard_shortcuts = ConfigModel.instance.keyboard_shortcuts;
#if HAVE_LATEST_GTK_LAYER_SHELL
var mode = keyboard_shortcuts ?
GtkLayerShell.KeyboardMode.EXCLUSIVE :
GtkLayerShell.KeyboardMode.NONE;
GtkLayerShell.set_keyboard_mode (this, mode);
#else
GtkLayerShell.set_keyboard_interactivity (this, keyboard_shortcuts);
#endif
// Set layer
GtkLayerShell.Layer layer;
@@ -474,37 +351,28 @@ namespace SwayNotificationCenter {
box.set_valign (align_y);
list_box.set_valign (list_align);
list_box.set_sort_func (list_box_sort_func);
list_box.set_sort_func ((w1, w2) => {
var a = (Notification) w1;
var b = (Notification) w2;
if (a == null || b == null) return 0;
// Sort the list in reverse if needed
if (a.param.time == b.param.time) return 0;
int val = list_reverse ? 1 : -1;
return a.param.time > b.param.time ? val : val * -1;
});
// Always set the size request in all events.
box.set_size_request (ConfigModel.instance.control_center_width,
ConfigModel.instance.control_center_height);
}
/**
* Returns < 0 if row1 should be before row2, 0 if they are equal
* and > 0 otherwise
*/
private int list_box_sort_func (Gtk.ListBoxRow row1, Gtk.ListBoxRow row2) {
int val = list_reverse ? 1 : -1;
var a_group = (NotificationGroup) row1;
var b_group = (NotificationGroup) row2;
// Check urgency before time
var a_urgency = a_group.get_is_urgent ();
var b_urgency = b_group.get_is_urgent ();
if (a_urgency != b_urgency) {
return a_urgency ? val : val * -1;
private void size_alloc () {
var adj = viewport.vadjustment;
double upper = adj.get_upper ();
if (last_upper < upper) {
scroll_to_start (list_reverse);
}
// Check time
var a_time = a_group.get_time ();
var b_time = b_group.get_time ();
if (a_time < 0 || b_time < 0) return 0;
// Sort the list in reverse if needed
if (a_time == b_time) return 0;
return a_time > b_time ? val : val * -1;
last_upper = upper;
}
private void scroll_to_start (bool reverse) {
@@ -516,26 +384,20 @@ namespace SwayNotificationCenter {
}
public uint notification_count () {
uint count = 0;
foreach (unowned Gtk.Widget widget in list_box.get_children ()) {
if (widget is NotificationGroup) {
count += ((NotificationGroup) widget).get_num_notifications ();
}
}
return count;
return list_box.get_children ().length ();
}
public void close_all_notifications () {
foreach (var w in list_box.get_children ()) {
NotificationGroup group = (NotificationGroup) w;
if (group != null) group.close_all_notifications ();
Notification noti = (Notification) w;
if (noti != null) noti.close_notification (false);
}
try {
swaync_daemon.subscribe_v2 (notification_count (),
swaync_daemon.get_dnd (),
get_visibility (),
swaync_daemon.inhibited);
swaync_daemon.get_dnd (),
get_visibility (),
swaync_daemon.inhibited);
} catch (Error e) {
stderr.printf (e.message + "\n");
}
@@ -545,20 +407,11 @@ namespace SwayNotificationCenter {
}
}
private void navigate_list (int i) {
unowned Gtk.Widget ? widget = list_box.get_children ().nth_data (i);
if (widget == null) {
// Try getting the last widget
if (list_reverse) {
widget = list_box.get_children ().nth_data (0);
} else {
int len = ((int) list_box.get_children ().length ()) - 1;
widget = list_box.get_children ().nth_data (len);
}
}
private void navigate_list (uint i) {
var widget = list_box.get_children ().nth_data (i);
if (widget != null) {
widget.grab_focus ();
list_box.set_focus_child (widget);
widget.grab_focus ();
}
}
@@ -571,24 +424,20 @@ namespace SwayNotificationCenter {
if (this.visible) {
// Focus the first notification
list_position = list_reverse ?
(((int) list_box.get_children ().length ()) - 1) : 0;
(list_box.get_children ().length () - 1) : 0;
if (list_position == uint.MAX) list_position = 0;
list_box.grab_focus ();
navigate_list (list_position);
foreach (var w in list_box.get_children ()) {
var group = (NotificationGroup) w;
if (group != null) group.update ();
var noti = (Notification) w;
if (noti != null) noti.set_time ();
}
this.get_style_context ().add_class ("open");
}
else {
this.get_style_context ().remove_class ("open");
}
swaync_daemon.subscribe_v2 (notification_count (),
noti_daemon.dnd,
this.visible,
swaync_daemon.inhibited);
noti_daemon.dnd,
this.visible,
swaync_daemon.inhibited);
}
public bool toggle_visibility () {
@@ -606,114 +455,41 @@ namespace SwayNotificationCenter {
on_visibility_change ();
}
public void close_notification (uint32 id, bool dismiss) {
unowned NotificationGroup group = null;
if (!noti_groups_id.lookup_extended (id, null, out group))return;
foreach (var w in group.get_notifications ()) {
public void close_notification (uint32 id, bool replaces = false) {
foreach (var w in list_box.get_children ()) {
var noti = (Notification) w;
if (noti != null && noti.param.applied_id == id) {
if (!dismiss) {
if (replaces) {
noti.remove_noti_timeout ();
noti.destroy ();
} else {
noti.close_notification (false);
group.remove_notification (noti);
list_box.remove (w);
}
noti_groups_id.remove (id);
break;
}
}
if (group.is_empty ()) {
if (group.name_id.length > 0) {
noti_groups_name.remove (group.name_id);
}
if (expanded_group == group) {
expanded_group = null;
fade_animate (1);
}
group.destroy ();
}
}
public void replace_notification (uint32 id, NotifyParams new_params) {
unowned NotificationGroup group = null;
if (noti_groups_id.lookup_extended (id, null, out group)) {
foreach (var w in group.get_notifications ()) {
var noti = (Notification) w;
if (noti != null && noti.param.applied_id == id) {
noti_groups_id.remove (id);
noti_groups_id.set (new_params.applied_id, group);
noti.replace_notification (new_params);
// Position the notification in the beginning of the list
list_box.invalidate_sort ();
return;
}
}
}
// Add a new notification if the old one isn't visible
add_notification (new_params);
}
public void add_notification (NotifyParams param) {
public void add_notification (NotifyParams param,
NotiDaemon noti_daemon) {
var noti = new Notification.regular (param,
noti_daemon,
NotificationType.CONTROL_CENTER);
noti.grab_focus.connect ((w) => {
int i = list_box.get_children ().index (w);
uint i = list_box.get_children ().index (w);
if (list_position != uint.MAX && list_position != i) {
list_position = i;
}
});
noti.set_time ();
NotificationGroup ? group = null;
if (param.name_id.length > 0) {
noti_groups_name.lookup_extended (param.name_id, null, out group);
}
if (group == null) {
group = new NotificationGroup (param.name_id, param.display_name);
// Collapse other groups on expand
group.on_expand_change.connect ((expanded) => {
if (!expanded) {
fade_animate (1);
foreach (unowned Gtk.Widget child in list_box.get_children ()) {
child.set_sensitive (true);
}
return;
}
expanded_group = group;
expanded_group.set_sensitive (true);
fade_animate (0);
int y = expanded_group.get_relative_y (list_box);
if (y > 0) {
scroll_animate (y);
}
foreach (unowned Gtk.Widget child in list_box.get_children ()) {
NotificationGroup g = (NotificationGroup) child;
if (g != null && g != group) {
g.set_expanded (false);
if (g.only_single_notification ()) {
g.set_sensitive (false);
}
}
}
});
if (param.name_id.length > 0) {
noti_groups_name.set (param.name_id, group);
}
list_box.add (group);
}
noti_groups_id.set (param.applied_id, group);
group.add_notification (noti);
list_box.invalidate_sort ();
list_box.add (noti);
scroll_to_start (list_reverse);
try {
swaync_daemon.subscribe_v2 (notification_count (),
swaync_daemon.get_dnd (),
get_visibility (),
swaync_daemon.inhibited);
swaync_daemon.get_dnd (),
get_visibility (),
swaync_daemon.inhibited);
} catch (Error e) {
stderr.printf (e.message + "\n");
}
@@ -730,13 +506,8 @@ namespace SwayNotificationCenter {
/** Forces each notification EventBox to reload its style_context #27 */
private void reload_notifications_style () {
foreach (var c in list_box.get_children ()) {
NotificationGroup group = (NotificationGroup) c;
if (group != null) {
foreach (unowned var widget in group.get_notifications ()) {
Notification noti = (Notification) widget;
noti.reload_style_context ();
}
}
Notification noti = (Notification) c;
if (noti != null) noti.reload_style_context ();
}
}
}

View File

@@ -52,7 +52,7 @@ namespace SwayNotificationCenter.Widgets {
slider.set_draw_value (false);
slider.set_round_digits (0);
slider.value_changed.connect (() => {
this.client.set_brightness.begin ((float) slider.get_value ());
this.client.set_brightness ((float) slider.get_value ());
slider.tooltip_text = ((int) slider.get_value ()).to_string ();
});

View File

@@ -3,7 +3,7 @@ namespace SwayNotificationCenter.Widgets {
[DBus (name = "org.freedesktop.login1.Session")]
interface Login1 : Object {
public abstract async void set_brightness (string subsystem,
public abstract void set_brightness (string subsystem,
string name, uint32 brightness) throws GLib.Error;
}
@@ -77,14 +77,14 @@ namespace SwayNotificationCenter.Widgets {
if (monitor != null) monitor.cancel ();
}
public async void set_brightness (float percent) {
public void set_brightness (float percent) {
this.close ();
try {
if (subsystem == "backlight") {
int actual = calc_actual (percent);
login1.set_brightness.begin (subsystem, device, actual);
login1.set_brightness (subsystem, device, actual);
} else {
login1.set_brightness.begin (subsystem, device, (uint32) percent);
login1.set_brightness (subsystem, device, (uint32) percent);
}
} catch (Error e) {
error ("Error %s\n", e.message);

View File

@@ -1,3 +1,5 @@
using Posix;
namespace SwayNotificationCenter.Widgets {
public abstract class BaseWidget : Gtk.Box {
public abstract string widget_name { get; }
@@ -19,12 +21,8 @@ namespace SwayNotificationCenter.Widgets {
NORMAL;
public static ButtonType parse (string value) {
switch (value) {
case "toggle":
return ButtonType.TOGGLE;
default:
return ButtonType.NORMAL;
}
if (value == "toggle") return ButtonType.TOGGLE;
return ButtonType.NORMAL;
}
}
@@ -107,24 +105,28 @@ namespace SwayNotificationCenter.Widgets {
string command = actions.get_object_element (i).get_string_member_with_default ("command", "");
string t = actions.get_object_element (i).get_string_member_with_default ("type", "normal");
ButtonType type = ButtonType.parse (t);
string update_command =
actions.get_object_element (i).get_string_member_with_default ("update-command", "");
bool active = actions.get_object_element (i).get_boolean_member_with_default ("active", false);
string active = actions.get_object_element (i).get_string_member_with_default ("active", "");
res[i] = Action () {
label = label,
command = command,
type = type,
update_command = update_command,
active = active
};
}
return res;
}
protected async void execute_command (string cmd, string[] env_additions = {}) {
string msg = "";
yield Functions.execute_command (cmd, env_additions, out msg);
public static 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

@@ -10,7 +10,7 @@ namespace SwayNotificationCenter.Widgets {
}
Action[] actions;
List<ToggleButton> toggle_buttons;
ToggleButton[] toggle_buttons;
public ButtonsGrid (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) {
base (suffix, swaync_daemon, noti_daemon);
@@ -27,27 +27,26 @@ namespace SwayNotificationCenter.Widgets {
// add action to container
foreach (var act in actions) {
switch (act.type) {
case ButtonType.TOGGLE:
ToggleButton tb = new ToggleButton (act.label, act.command, act.update_command, act.active);
container.insert (tb, -1);
toggle_buttons.append (tb);
break;
default:
Gtk.Button b = new Gtk.Button.with_label (act.label);
b.clicked.connect (() => execute_command.begin (act.command));
container.insert (b, -1);
break;
if (act.type == ButtonType.TOGGLE) {
ToggleButton toggle_button = new ToggleButton (act.label, act.command, act.active);
toggle_buttons += toggle_button;
Gtk.ToggleButton tb = toggle_button;
container.insert (tb, -1);
} else {
Gtk.Button b = new Gtk.Button.with_label (act.label);
b.clicked.connect (() => execute_command (act.command));
container.insert (b, -1);
}
}
show_all ();
}
public override void on_cc_visibility_change (bool value) {
if (value) {
public override void on_cc_visibility_change (bool val) {
debug("buttonsGrid:on_cc_visibility_change %i", (int)val);
if (val) {
foreach (var tb in toggle_buttons) {
tb.on_update.begin ();
tb.compute_state();
}
}
}

View File

@@ -26,8 +26,7 @@ namespace SwayNotificationCenter.Widgets {
string ? label;
string ? command;
BaseWidget.ButtonType ? type;
string ? update_command;
bool ? active;
string ? active;
}
public class Menubar : BaseWidget {
@@ -41,7 +40,6 @@ namespace SwayNotificationCenter.Widgets {
Gtk.Box topbar_container;
List<ConfigObject ?> menu_objects;
List<ToggleButton> toggle_buttons;
public Menubar (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) {
base (suffix, swaync_daemon, noti_daemon);
@@ -73,22 +71,18 @@ namespace SwayNotificationCenter.Widgets {
void add_menu (ref unowned ConfigObject ? obj) {
switch (obj.type) {
case MenuType.BUTTONS:
case MenuType.BUTTONS :
Gtk.Box container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
if (obj.name != null) container.get_style_context ().add_class (obj.name);
foreach (Action a in obj.actions) {
switch (a.type) {
case ButtonType.TOGGLE:
ToggleButton tb = new ToggleButton (a.label, a.command, a.update_command, a.active);
container.add (tb);
toggle_buttons.append (tb);
break;
default:
Gtk.Button b = new Gtk.Button.with_label (a.label);
b.clicked.connect (() => execute_command.begin (a.command));
container.add (b);
break;
if (a.type == ButtonType.TOGGLE) {
ToggleButton tb = new ToggleButton (a.label, a.command, a.active);
container.add (tb);
} else {
Gtk.Button b = new Gtk.Button.with_label (a.label);
b.clicked.connect (() => execute_command (a.command));
container.add (b);
}
}
switch (obj.position) {
@@ -121,18 +115,9 @@ namespace SwayNotificationCenter.Widgets {
});
foreach (var a in obj.actions) {
switch (a.type) {
case ButtonType.TOGGLE:
ToggleButton tb = new ToggleButton (a.label, a.command, a.update_command, a.active);
menu.pack_start (tb, true, true, 0);
toggle_buttons.append (tb);
break;
default:
Gtk.Button b = new Gtk.Button.with_label (a.label);
b.clicked.connect (() => execute_command.begin (a.command));
menu.pack_start (b, true, true, 0);
break;
}
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) {
@@ -230,10 +215,6 @@ namespace SwayNotificationCenter.Widgets {
foreach (var obj in menu_objects) {
obj.revealer ?.set_reveal_child (false);
}
} else {
foreach (var tb in toggle_buttons) {
tb.on_update.begin ();
}
}
}
}

View File

@@ -2,7 +2,6 @@ namespace SwayNotificationCenter.Widgets.Mpris {
public struct Config {
int image_size;
int image_radius;
bool blur;
}
public class Mpris : BaseWidget {
@@ -12,8 +11,6 @@ namespace SwayNotificationCenter.Widgets.Mpris {
}
}
private const int FADE_WIDTH = 20;
const string MPRIS_PREFIX = "org.mpris.MediaPlayer2.";
HashTable<string, MprisPlayer> players = new HashTable<string, MprisPlayer> (str_hash, str_equal);
@@ -29,7 +26,6 @@ namespace SwayNotificationCenter.Widgets.Mpris {
Config mpris_config = Config () {
image_size = 96,
image_radius = 12,
blur = true,
};
public Mpris (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) {
@@ -57,8 +53,9 @@ namespace SwayNotificationCenter.Widgets.Mpris {
carousel = new Hdy.Carousel () {
visible = true,
};
#if HAVE_LATEST_LIBHANDY
carousel.allow_scroll_wheel = true;
carousel.draw.connect (carousel_draw_cb);
#endif
carousel.page_changed.connect ((index) => {
GLib.List<weak Gtk.Widget> children = carousel.get_children ();
int children_length = (int) children.length ();
@@ -94,11 +91,6 @@ namespace SwayNotificationCenter.Widgets.Mpris {
// Clamp the radius
mpris_config.image_radius = mpris_config.image_radius.clamp (
0, (int) (mpris_config.image_size * 0.5));
// Get blur
bool blur_found;
bool? blur = get_prop<bool> (config, "blur", out blur_found);
if (blur_found) mpris_config.blur = blur;
}
hide ();
@@ -109,51 +101,6 @@ namespace SwayNotificationCenter.Widgets.Mpris {
}
}
private bool carousel_draw_cb (Cairo.Context cr) {
Gtk.Allocation alloc;
carousel.get_allocated_size (out alloc, null);
Cairo.Pattern left_fade_gradient = new Cairo.Pattern.linear (0, 0, 1, 0);
left_fade_gradient.add_color_stop_rgba (0, 1, 1, 1, 1);
left_fade_gradient.add_color_stop_rgba (1, 1, 1, 1, 0);
Cairo.Pattern right_fade_gradient = new Cairo.Pattern.linear (0, 0, 1, 0);
right_fade_gradient.add_color_stop_rgba (0, 1, 1, 1, 0);
right_fade_gradient.add_color_stop_rgba (1, 1, 1, 1, 1);
cr.save ();
cr.push_group ();
// Draw widgets
carousel.draw.disconnect (carousel_draw_cb);
carousel.draw (cr);
carousel.draw.connect (carousel_draw_cb);
/// Draw vertical fade
// Top fade
cr.save ();
cr.scale (FADE_WIDTH, alloc.height);
cr.rectangle (0, 0, FADE_WIDTH, alloc.height);
cr.set_source (left_fade_gradient);
cr.set_operator (Cairo.Operator.DEST_OUT);
cr.fill ();
cr.restore ();
// Bottom fade
cr.save ();
cr.translate (alloc.width - FADE_WIDTH, 0);
cr.scale (FADE_WIDTH, alloc.height);
cr.rectangle (0, 0, FADE_WIDTH, alloc.height);
cr.set_source (right_fade_gradient);
cr.set_operator (Cairo.Operator.DEST_OUT);
cr.fill ();
cr.restore ();
cr.pop_group_to_source ();
cr.paint ();
cr.restore ();
return true;
}
/**
* Forces the carousel to reload its style_context.
* Fixes carousel items not redrawing when window isn't visible.

View File

@@ -19,12 +19,8 @@
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">96</property>
<property name="icon-name">audio-x-generic-symbolic</property>
<property name="use-fallback">True</property>
<property name="use-fallback">False</property>
<property name="icon_size">0</property>
<style>
<class name="widget-mpris-album-art"/>
</style>
</object>
<packing>
<property name="expand">False</property>

View File

@@ -38,9 +38,6 @@ namespace SwayNotificationCenter.Widgets.Mpris {
private string prev_art_url;
private DesktopAppInfo ? desktop_entry = null;
private Gdk.Pixbuf ? album_art_pixbuf = null;
private Granite.Drawing.BufferSurface ? blurred_cover_surface = null;
private unowned Config mpris_config;
public MprisPlayer (MprisSource source, Config mpris_config) {
@@ -96,84 +93,12 @@ namespace SwayNotificationCenter.Widgets.Mpris {
update_buttons (source.media_player.metadata);
});
});
album_art.set_pixel_size (mpris_config.image_size);
}
public void before_destroy () {
source.properties_changed.disconnect (properties_changed);
}
public override bool draw (Cairo.Context cr) {
unowned Gdk.Window ? window = get_window ();
if (!mpris_config.blur || window == null ||
!(album_art_pixbuf is Gdk.Pixbuf)) {
return base.draw (cr);
}
const double DEGREES = Math.PI / 180.0;
unowned Gtk.StyleContext style_ctx = this.get_style_context ();
unowned Gtk.StateFlags state = style_ctx.get_state ();
Gtk.Border border = style_ctx.get_border (state);
Gtk.Border margin = style_ctx.get_margin (state);
Value radius_value = style_ctx.get_property (
Gtk.STYLE_PROPERTY_BORDER_RADIUS, state);
int radius = 0;
if (!radius_value.holds (Type.INT) ||
(radius = radius_value.get_int ()) == 0) {
radius = mpris_config.image_radius;
}
int scale = style_ctx.get_scale ();
int width = get_allocated_width ()
- margin.left - margin.right
- border.left - border.right;
int height = get_allocated_height ()
- margin.top - margin.bottom
- border.top - border.bottom;
cr.save ();
cr.new_sub_path ();
// Top Right
cr.arc (width - radius + margin.right,
radius + margin.top,
radius, -90 * DEGREES, 0 * DEGREES);
// Bottom Right
cr.arc (width - radius + margin.right,
height - radius + margin.bottom,
radius, 0 * DEGREES, 90 * DEGREES);
// Bottom Left
cr.arc (radius + margin.left,
height - radius + margin.bottom,
radius, 90 * DEGREES, 180 * DEGREES);
// Top Left
cr.arc (radius + margin.left,
radius + margin.top,
radius, 180 * DEGREES, 270 * DEGREES);
cr.close_path ();
cr.set_source_rgba (0, 0, 0, 0);
cr.clip ();
// Draw blurred player background
if (blurred_cover_surface == null
|| blurred_cover_surface.width != width
|| blurred_cover_surface.height != height) {
var buffer = new Granite.Drawing.BufferSurface (width, height);
Cairo.Surface surface = Functions.scale_pixbuf (
album_art_pixbuf, width, height, scale);
buffer.context.set_source_surface (surface, 0, 0);
buffer.context.paint ();
buffer.fast_blur (8, 2);
blurred_cover_surface = buffer;
}
cr.set_source_surface (blurred_cover_surface.surface, margin.left, margin.top);
cr.paint ();
cr.restore ();
return base.draw (cr);
}
private void properties_changed (string iface,
HashTable<string, Variant> changed,
string[] invalid) {
@@ -321,6 +246,7 @@ namespace SwayNotificationCenter.Widgets.Mpris {
int scale = get_style_context ().get_scale ();
Gdk.Pixbuf ? pixbuf = null;
// Cancel previous download, reset the state and download again
album_art_cancellable.cancel ();
album_art_cancellable.reset ();
@@ -329,80 +255,23 @@ namespace SwayNotificationCenter.Widgets.Mpris {
InputStream stream = yield file.read_async (Priority.DEFAULT,
album_art_cancellable);
this.album_art_pixbuf = yield new Gdk.Pixbuf.from_stream_async (
pixbuf = yield new Gdk.Pixbuf.from_stream_async (
stream, album_art_cancellable);
} catch (Error e) {
debug ("Could not download album art for %s. Using fallback...",
source.media_player.identity);
this.album_art_pixbuf = null;
}
if (this.album_art_pixbuf != null) {
unowned Gtk.StyleContext style_ctx = album_art.get_style_context ();
Value br_value =
style_ctx.get_property (Gtk.STYLE_PROPERTY_BORDER_RADIUS,
style_ctx.get_state ());
int radius = 0;
if (!br_value.holds (Type.INT) ||
(radius = br_value.get_int ()) == 0) {
radius = mpris_config.image_radius;
}
var surface = Functions.scale_pixbuf (this.album_art_pixbuf,
mpris_config.image_size,
mpris_config.image_size,
scale);
this.album_art_pixbuf = Gdk.pixbuf_get_from_surface (surface,
0, 0,
mpris_config.image_size,
mpris_config.image_size);
this.blurred_cover_surface = null;
surface = Functions.round_surface (surface,
mpris_config.image_size,
mpris_config.image_size,
scale,
radius);
var pix_buf = Gdk.pixbuf_get_from_surface (surface,
0, 0,
mpris_config.image_size,
mpris_config.image_size);
surface.finish ();
album_art.set_from_pixbuf (pix_buf);
if (pixbuf != null) {
pixbuf = Functions.scale_round_pixbuf (pixbuf,
mpris_config.image_size,
mpris_config.image_size,
scale,
mpris_config.image_radius);
album_art.set_from_pixbuf (pixbuf);
album_art.get_style_context ().set_scale (1);
this.queue_draw ();
return;
}
}
this.album_art_pixbuf = null;
this.blurred_cover_surface = null;
// Get the app icon
Icon ? icon = null;
if (desktop_entry is DesktopAppInfo) {
icon = desktop_entry.get_icon ();
}
Gtk.IconInfo ? icon_info = null;
if (icon != null) {
album_art.set_from_gicon (icon, Gtk.IconSize.INVALID);
icon_info = Gtk.IconTheme.get_default ().lookup_by_gicon (icon,
mpris_config.image_size,
Gtk.IconLookupFlags.USE_BUILTIN);
} else {
// Default icon
string icon_name = "audio-x-generic-symbolic";
album_art.set_from_icon_name (icon_name,
Gtk.IconSize.INVALID);
icon_info = Gtk.IconTheme.get_default ().lookup_icon (icon_name,
mpris_config.image_size,
Gtk.IconLookupFlags.USE_BUILTIN);
}
if (icon_info != null) {
try {
this.album_art_pixbuf = icon_info.load_icon ();
} catch (Error e) {
warning ("Could not load icon: %s", e.message);
}
}
}
private void update_button_shuffle (HashTable<string, Variant> metadata) {

View File

@@ -1,47 +1,56 @@
namespace SwayNotificationCenter.Widgets {
class ToggleButton : Gtk.ToggleButton {
private bool _active;
private string command;
private string update_command;
private ulong handler_id;
public ToggleButton (string label, string command, string update_command, bool active) {
private string active_test;
public ToggleButton (string label, string command, string active) {
this.command = command;
this.update_command = update_command;
this.active_test = active;
this.label = label;
this._active = false;
debug("ToggleButton created with test: %s", active);
this.toggled.connect (on_toggle);
}
public void compute_state() {
debug("compute_state");
this.set_active(this.check_should_be_active());
}
private void set_active(bool active) {
debug("set_active: %i", (int)active);
if (active) {
this.active = true;
this.get_style_context ().add_class ("active");
this._active = true;
} else {
this.get_style_context ().remove_class ("active");
this._active = false;
}
this.handler_id = this.toggled.connect (on_toggle);
}
private async void on_toggle () {
string msg = "";
string[] env_additions = { "SWAYNC_TOGGLE_STATE=" + this.active.to_string () };
yield Functions.execute_command (this.command, env_additions, out msg);
private void on_toggle (Gtk.ToggleButton tb) {
debug("on_toggle");
this.set_active(!this._active);
BaseWidget.execute_command (command);
}
public async void on_update () {
if (update_command == "") return;
string msg = "";
string[] env_additions = { "SWAYNC_TOGGLE_STATE=" + this.active.to_string () };
yield Functions.execute_command (this.update_command, env_additions, out msg);
try {
// remove trailing whitespaces
Regex regex = new Regex ("\\s+$");
string res = regex.replace (msg, msg.length, 0, "");
GLib.SignalHandler.block (this, this.handler_id);
if (res.up () == "TRUE") {
this.active = true;
} else {
this.active = false;
}
GLib.SignalHandler.unblock (this, this.handler_id);
} catch (RegexError e) {
stderr.printf ("RegexError: %s\n", e.message);
}
private bool check_should_be_active () {
string stdout;
string stderr;
int end_status;
Process.spawn_sync (
"/",
this.active_test.split (" "),
Environ.get(),
SpawnFlags.SEARCH_PATH,
null,
out stdout,
out stderr,
out end_status);
return end_status == 0;
}
}
}

View File

@@ -1,162 +0,0 @@
namespace SwayNotificationCenter {
public class FadedViewport : Gtk.Viewport {
private int fade_height = 30;
private FadedViewportChild container;
public FadedViewport (int fade_height) {
if (fade_height > 0) this.fade_height = fade_height;
this.container = new FadedViewportChild (this.fade_height);
base.add (container);
}
public override void add (Gtk.Widget widget) {
container.add (widget);
}
public override void remove (Gtk.Widget widget) {
container.remove (widget);
}
public override bool draw (Cairo.Context cr) {
Gtk.Allocation alloc;
get_allocated_size (out alloc, null);
Cairo.Pattern top_fade_gradient = new Cairo.Pattern.linear (0, 0, 0, 1);
top_fade_gradient.add_color_stop_rgba (0, 1, 1, 1, 1);
top_fade_gradient.add_color_stop_rgba (1, 1, 1, 1, 0);
Cairo.Pattern bottom_fade_gradient = new Cairo.Pattern.linear (0, 0, 0, 1);
bottom_fade_gradient.add_color_stop_rgba (0, 1, 1, 1, 0);
bottom_fade_gradient.add_color_stop_rgba (1, 1, 1, 1, 1);
cr.save ();
cr.push_group ();
// Draw widgets
base.draw (cr);
/// Draw vertical fade
// Top fade
cr.save ();
cr.scale (alloc.width, fade_height);
cr.rectangle (0, 0, alloc.width, fade_height);
cr.set_source (top_fade_gradient);
cr.set_operator (Cairo.Operator.DEST_OUT);
cr.fill ();
cr.restore ();
// Bottom fade
cr.save ();
cr.translate (0, alloc.height - fade_height);
cr.scale (alloc.width, fade_height);
cr.rectangle (0, 0, alloc.width, fade_height);
cr.set_source (bottom_fade_gradient);
cr.set_operator (Cairo.Operator.DEST_OUT);
cr.fill ();
cr.restore ();
cr.pop_group_to_source ();
cr.paint ();
cr.restore ();
return true;
}
}
}
private class FadedViewportChild : Gtk.Container {
private int y_padding;
private unowned Gtk.Widget _child;
public FadedViewportChild (int y_padding) {
base.set_has_window (false);
base.set_can_focus (true);
base.set_redraw_on_allocate (false);
// Half due to the fade basically stopping at 50% of the height
this.y_padding = y_padding / 2;
this._child = null;
this.show ();
}
public override void add (Gtk.Widget widget) {
if (this._child == null) {
widget.set_parent (this);
this._child = widget;
}
}
public override void remove (Gtk.Widget widget) {
if (this._child == widget) {
widget.unparent ();
this._child = null;
if (this.get_visible () && widget.get_visible ()) {
this.queue_resize_no_redraw ();
}
}
}
public override void forall_internal (bool include_internals, Gtk.Callback callback) {
if (this._child != null) {
callback (this._child);
}
}
public override Gtk.SizeRequestMode get_request_mode () {
if (this._child != null) {
return this._child.get_request_mode ();
} else {
return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
}
}
public override void size_allocate (Gtk.Allocation allocation) {
Gtk.Allocation child_allocation = Gtk.Allocation ();
uint border_width = this.get_border_width ();
if (this._child != null && this._child.get_visible ()) {
child_allocation.x = allocation.x + (int) border_width;
child_allocation.y = allocation.y + (int) border_width;
Gtk.Align align_y = _child.get_valign ();
if (align_y == Gtk.Align.END) {
child_allocation.y -= y_padding;
} else {
child_allocation.y += y_padding;
}
child_allocation.width = allocation.width - 2 * (int) border_width;
child_allocation.height = allocation.height - 2 * (int) border_width;
this._child.size_allocate (child_allocation);
if (this.get_realized ()) {
this._child.show ();
}
}
if (this.get_realized ()) {
if (this._child != null) {
this._child.set_child_visible (true);
}
}
base.size_allocate (allocation);
}
public override void get_preferred_height_for_width (int width,
out int minimum_height,
out int natural_height) {
minimum_height = 0;
natural_height = 0;
if (_child != null && _child.get_visible ()) {
_child.get_preferred_height_for_width (width,
out minimum_height,
out natural_height);
minimum_height += y_padding * 2;
natural_height += y_padding * 2;
}
}
public override bool draw (Cairo.Context cr) {
base.draw (cr);
return false;
}
}

View File

@@ -8,53 +8,42 @@ namespace SwayNotificationCenter {
public static void init () {
system_css_provider = new Gtk.CssProvider ();
user_css_provider = new Gtk.CssProvider ();
// Init resources
var theme = Gtk.IconTheme.get_default ();
theme.add_resource_path ("/org/erikreider/swaync/icons");
}
public static void set_image_uri (owned string uri,
Gtk.Image img,
int icon_size,
int radius,
bool file_exists) {
const string URI_PREFIX = "file://";
const uint PREFIX_SIZE = 7;
bool is_uri = (uri.length >= PREFIX_SIZE
&& uri.slice (0, PREFIX_SIZE) == URI_PREFIX);
if (is_uri || file_exists) {
public static void set_image_path (owned string path,
Gtk.Image img,
int icon_size,
bool file_exists) {
if ((path.length > 6 && path.slice (0, 7) == "file://") || file_exists) {
// Try as a URI (file:// is the only URI schema supported right now)
try {
if (is_uri) uri = uri.slice (PREFIX_SIZE, uri.length);
if (!file_exists) path = path.slice (7, path.length);
var pixbuf = new Gdk.Pixbuf.from_file_at_scale (
uri,
path,
icon_size * img.scale_factor,
icon_size * img.scale_factor,
true);
// Scale and round the image. Scales to fit the size
var surface = scale_round_pixbuf (pixbuf,
icon_size,
icon_size,
img.scale_factor,
radius);
var surface = Gdk.cairo_surface_create_from_pixbuf (
pixbuf,
img.scale_factor,
img.get_window ());
img.set_from_surface (surface);
return;
} catch (Error e) {
stderr.printf (e.message + "\n");
}
}
// Try as icon name
if (img.storage_type == Gtk.ImageType.EMPTY) {
img.set_from_icon_name (uri, Gtk.IconSize.INVALID);
} else if (Gtk.IconTheme.get_default ().has_icon (path)) {
// Try as a freedesktop.org-compliant icon theme
img.set_from_icon_name (path, Notification.icon_size);
} else {
img.set_from_icon_name (
"image-missing",
Notification.icon_size);
}
}
public static void set_image_data (ImageData data,
Gtk.Image img,
int icon_size,
int radius) {
public static void set_image_data (ImageData data, Gtk.Image img, int icon_size) {
// Rebuild and scale the image
var pixbuf = new Gdk.Pixbuf.with_unowned_data (data.data,
Gdk.Colorspace.RGB,
@@ -65,11 +54,14 @@ namespace SwayNotificationCenter {
data.rowstride,
null);
var surface = scale_round_pixbuf (pixbuf,
icon_size,
icon_size,
img.scale_factor,
radius);
pixbuf = pixbuf.scale_simple (
icon_size * img.scale_factor,
icon_size * img.scale_factor,
Gdk.InterpType.BILINEAR);
var surface = Gdk.cairo_surface_create_from_pixbuf (
pixbuf,
img.scale_factor,
img.get_window ());
img.set_from_surface (surface);
}
@@ -219,15 +211,12 @@ namespace SwayNotificationCenter {
return type;
}
/** Roundes the Cairo Surface to the given radii */
public static Cairo.Surface round_surface (Cairo.Surface base_surf,
int buffer_width,
int buffer_height,
int img_scale,
int radius) {
// Limit radii size
radius = int.min (radius, int.min (buffer_width / 2, buffer_height / 2));
/** Scales the pixbuf to fit the given dimensions */
public static Gdk.Pixbuf scale_round_pixbuf (Gdk.Pixbuf pixbuf,
int buffer_width,
int buffer_height,
int img_scale,
int radius) {
Cairo.Surface surface = new Cairo.ImageSurface (Cairo.Format.ARGB32,
buffer_width,
buffer_height);
@@ -241,31 +230,12 @@ namespace SwayNotificationCenter {
cr.arc (radius, buffer_height - radius, radius, 90 * DEGREES, 180 * DEGREES);
cr.arc (radius, radius, radius, 180 * DEGREES, 270 * DEGREES);
cr.close_path ();
cr.set_source_rgba (0, 0, 0, 0);
cr.set_source_rgb (0, 0, 0);
cr.clip ();
cr.paint ();
cr.save ();
cr.set_source_surface (base_surf, 0, 0);
cr.paint ();
cr.restore ();
return surface;
}
/** Scales the pixbuf to fit the given dimensions */
public static Cairo.Surface scale_pixbuf (Gdk.Pixbuf pixbuf,
int buffer_width,
int buffer_height,
int img_scale) {
Cairo.Surface surface = new Cairo.ImageSurface (Cairo.Format.ARGB32,
buffer_width,
buffer_height);
var cr = new Cairo.Context (surface);
cr.save ();
Cairo.Surface base_surf = Gdk.cairo_surface_create_from_pixbuf (pixbuf,
Cairo.Surface scale_surf = Gdk.cairo_surface_create_from_pixbuf (pixbuf,
img_scale,
null);
int width = pixbuf.width / img_scale;
@@ -275,41 +245,23 @@ namespace SwayNotificationCenter {
if (window_ratio > bg_ratio) { // Taller wallpaper than monitor
double scale = (double) buffer_width / width;
if (scale * height < buffer_height) {
draw_scale_wide (buffer_width, width, buffer_height, height, cr, base_surf);
draw_scale_wide (buffer_width, width, buffer_height, height, cr, scale_surf);
} else {
draw_scale_tall (buffer_width, width, buffer_height, height, cr, base_surf);
draw_scale_tall (buffer_width, width, buffer_height, height, cr, scale_surf);
}
} else { // Wider wallpaper than monitor
double scale = (double) buffer_height / height;
if (scale * width < buffer_width) {
draw_scale_tall (buffer_width, width, buffer_height, height, cr, base_surf);
draw_scale_tall (buffer_width, width, buffer_height, height, cr, scale_surf);
} else {
draw_scale_wide (buffer_width, width, buffer_height, height, cr, base_surf);
draw_scale_wide (buffer_width, width, buffer_height, height, cr, scale_surf);
}
}
cr.paint ();
cr.restore ();
base_surf.finish ();
return surface;
}
/** Scales the pixbuf to fit the given dimensions */
public static Cairo.Surface scale_round_pixbuf (Gdk.Pixbuf pixbuf,
int buffer_width,
int buffer_height,
int img_scale,
int radius) {
var surface = Functions.scale_pixbuf (pixbuf,
buffer_width,
buffer_height,
img_scale);
surface = Functions.round_surface (surface,
buffer_width,
buffer_height,
img_scale,
radius);
return surface;
scale_surf.finish ();
return Gdk.pixbuf_get_from_surface (surface, 0, 0, buffer_width, buffer_height);
}
private static void draw_scale_tall (int buffer_width,
@@ -347,68 +299,5 @@ namespace SwayNotificationCenter {
}
return result;
}
public static async bool execute_command (string cmd, string[] env_additions = {}, out string msg) {
msg = "";
try {
string[] spawn_env = Environ.get ();
// Export env variables
foreach (string additions in env_additions) {
spawn_env += additions;
}
string[] argvp = {};
Shell.parse_argv (cmd, out argvp);
Pid child_pid;
int std_output;
Process.spawn_async_with_pipes (
"/",
argvp,
spawn_env,
SpawnFlags.SEARCH_PATH | SpawnFlags.DO_NOT_REAP_CHILD,
null,
out child_pid,
null,
out std_output,
null);
// stdout:
string res = "";
IOChannel output = new IOChannel.unix_new (std_output);
output.add_watch (IOCondition.IN | IOCondition.HUP, (channel, condition) => {
if (condition == IOCondition.HUP) {
return false;
}
try {
channel.read_line (out res, null, null);
return true;
} catch (IOChannelError e) {
stderr.printf ("stdout: IOChannelError: %s\n", e.message);
return false;
} catch (ConvertError e) {
stderr.printf ("stdout: ConvertError: %s\n", e.message);
return false;
}
});
// Close the child when the spawned process is idling
int end_status = 0;
ChildWatch.add (child_pid, (pid, status) => {
Process.close_pid (pid);
GLib.FileUtils.close (std_output);
end_status = status;
execute_command.callback ();
});
// Waits until `run_script.callback()` is called above
yield;
msg = res;
return end_status == 0;
} catch (Error e) {
stderr.printf ("Run_Script Error: %s\n", e.message);
msg = e.message;
return false;
}
}
}
}

View File

@@ -50,16 +50,13 @@ widget_sources = [
app_sources = [
'main.vala',
'animation/animation.vala',
'orderedHashTable/orderedHashTable.vala',
'configModel/configModel.vala',
'swayncDaemon/swayncDaemon.vala',
'notiDaemon/notiDaemon.vala',
'notiModel/notiModel.vala',
'fadedViewport/fadedViewport.vala',
'notificationWindow/notificationWindow.vala',
'notification/notification.vala',
'notificationGroup/notificationGroup.vala',
'controlCenter/controlCenter.vala',
widget_sources,
'blankWindow/blankWindow.vala',
@@ -67,19 +64,13 @@ app_sources = [
constants,
]
assert(meson.get_compiler('vala').version() >= '0.56')
app_deps = [
dependency('gio-2.0', version: '>= 2.50'),
dependency('gio-unix-2.0', version: '>= 2.50'),
dependency('gtk+-3.0', version: '>= 3.22'),
dependency('json-glib-1.0', version: '>= 1.0'),
dependency('libhandy-1', version: '>= 1.8.0'),
dependency('granite', version: '>= 6.2.0'),
dependency('gtk-layer-shell-0',
fallback: ['gtk-layer-shell-0', 'gtk-layer-shell'],
version: '>= 0.8.0'
),
dependency('libhandy-1', version: '>= 1.2.3'),
meson.get_compiler('c').find_library('gtk-layer-shell'),
meson.get_compiler('c').find_library('m', required : true),
meson.get_compiler('vala').find_library('posix'),
dependency('gee-0.8'),
@@ -107,18 +98,36 @@ if get_option('pulse-audio')
]
endif
# Detect libhandy version
libhandy = dependency('libhandy-1')
if libhandy.version() >= '1.3.9'
add_project_arguments('-D', 'HAVE_LATEST_LIBHANDY', language: 'vala')
endif
# Detect gtk-layer-shell version
gtk_layer_shell = dependency(
'gtk-layer-shell-0',
fallback: ['gtk-layer-shell-0', 'gtk-layer-shell'],
)
if gtk_layer_shell.version() >= '0.6.0'
add_project_arguments('-D', 'HAVE_LATEST_GTK_LAYER_SHELL', language: 'vala')
endif
args = [
'--target-glib=2.50',
'--pkg=GtkLayerShell-0.1',
]
app_resources += gnome.compile_resources('sway_notification_center-resources',
sysconfdir = get_option('sysconfdir')
gnome = import('gnome')
app_sources += gnome.compile_resources('sway_notification_center-resources',
'sway_notification_center.gresource.xml',
c_name: 'sway_notification_center'
)
executable('swaync',
[ app_sources, app_resources ],
app_sources,
vala_args: args,
dependencies: app_deps,
install: true,
@@ -131,6 +140,8 @@ executable('swaync-client',
install: true,
)
config_path = join_paths(sysconfdir, 'xdg/swaync')
config_data = configuration_data()
config_data.set_quoted('JSONPATH', join_paths('/', config_path, 'configSchema.json'))
config_json = configure_file(
@@ -139,5 +150,6 @@ config_json = configure_file(
configuration : config_data
)
install_data('style.css', install_dir : config_path)
install_data(config_json, install_dir : config_path)
install_data('configSchema.json', install_dir : config_path)

View File

@@ -62,9 +62,9 @@ namespace SwayNotificationCenter {
/** Method to close notification and send DISMISSED signal */
public void manually_close_notification (uint32 id, bool timeout)
throws DBusError, IOError {
NotificationWindow.instance.close_notification (id, true);
NotificationWindow.instance.close_notification (id, false);
if (!timeout) {
control_center.close_notification (id, true);
control_center.close_notification (id);
NotificationClosed (id, ClosedReasons.DISMISSED);
swaync_daemon.subscribe_v2 (control_center.notification_count (),
@@ -170,10 +170,11 @@ namespace SwayNotificationCenter {
debug ("Notification: %s\n", param.to_string ());
// Get the notification id to replace
uint32 replace_notification = 0;
// Replace notification logic
if (id == replaces_id) {
replace_notification = id;
param.replaces = true;
NotificationWindow.instance.close_notification (id, true);
control_center.close_notification (id, true);
} else if (param.synchronous != null
&& param.synchronous.length > 0) {
// Tries replacing without replaces_id instead
@@ -181,47 +182,29 @@ namespace SwayNotificationCenter {
// if there is any notification to replace
if (synchronous_ids.lookup_extended (
param.synchronous, null, out r_id)) {
replace_notification = r_id;
param.replaces = true;
// Close the notification
NotificationWindow.instance.close_notification (r_id, true);
control_center.close_notification (r_id, true);
}
synchronous_ids.set (param.synchronous, id);
}
bool show_notification = state == NotificationStatusEnum.ENABLED
|| state == NotificationStatusEnum.TRANSIENT;
// Don't show the notification window if the control center is open
if (control_center.get_visibility ()) {
show_notification = false;
}
bool bypass_dnd = param.urgency == UrgencyLevels.CRITICAL || param.swaync_bypass_dnd;
// Don't show the notification window if dnd or inhibited
if (!bypass_dnd && (dnd || swaync_daemon.inhibited)) {
show_notification = false;
}
if (show_notification) {
if (replace_notification > 0) {
NotificationWindow.instance.replace_notification (replace_notification, param);
} else {
NotificationWindow.instance.add_notification (param);
// Only show popup notification if it is ENABLED or TRANSIENT
if ((state == NotificationStatusEnum.ENABLED || state == NotificationStatusEnum.TRANSIENT)
&& !control_center.get_visibility ()) {
// Also check if urgency is Critical and not inhibited and dnd
if (param.urgency == UrgencyLevels.CRITICAL ||
(!dnd && !swaync_daemon.inhibited
&& param.urgency != UrgencyLevels.CRITICAL)) {
NotificationWindow.instance.add_notification (param, this);
}
} else if (replace_notification > 0) {
// Remove the old notification due to it not being replaced
NotificationWindow.instance.close_notification (replace_notification, false);
}
// Only add notification to CC if it isn't IGNORED and not transient/TRANSIENT
if (state != NotificationStatusEnum.IGNORED
&& state != NotificationStatusEnum.TRANSIENT
&& !param.transient) {
if (replace_notification > 0) {
control_center.replace_notification (replace_notification, param);
} else {
control_center.add_notification (param);
}
} else if (replace_notification > 0) {
// Remove the old notification due to it not being replaced
control_center.close_notification (replace_notification, false);
control_center.add_notification (param, this);
}
#if WANT_SCRIPTING
@@ -309,8 +292,8 @@ namespace SwayNotificationCenter {
*/
[DBus (name = "CloseNotification")]
public void close_notification (uint32 id) throws DBusError, IOError {
NotificationWindow.instance.close_notification (id, true);
control_center.close_notification (id, true);
NotificationWindow.instance.close_notification (id, false);
control_center.close_notification (id);
NotificationClosed (id, ClosedReasons.CLOSED_BY_CLOSENOTIFICATION);
}

View File

@@ -110,16 +110,10 @@ namespace SwayNotificationCenter {
/** Disables scripting for notification */
public bool swaync_no_script { get; set; }
/** Always show the notification, regardless of dnd/inhibit state */
public bool swaync_bypass_dnd { get; set; }
public Array<Action> actions { get; set; }
public DesktopAppInfo ? desktop_app_info = null;
public string name_id;
public string display_name;
/** If the notification replaces another */
public bool replaces { get; set; }
public NotifyParams (uint32 applied_id,
string app_name,
@@ -140,37 +134,12 @@ namespace SwayNotificationCenter {
this.expire_timeout = expire_timeout;
this.time = (int64) (get_real_time () * 0.000001);
this.replaces = false;
this.has_synch = false;
parse_hints ();
parse_actions (actions);
// Try to get the desktop file
string[] entries = {};
if (desktop_entry != null) entries += desktop_entry.replace (".desktop", "");
if (app_name != null) entries += app_name.replace (".desktop", "");
foreach (string entry in entries) {
var app_info = new DesktopAppInfo ("%s.desktop".printf (entry));
// Checks if the .desktop file actually exists or not
if (app_info is DesktopAppInfo) {
desktop_app_info = app_info;
break;
}
}
// Set name_id
this.name_id = this.desktop_entry ?? this.app_name ?? "";
// Set display_name and make the first letter upper case
string ? display_name = this.desktop_entry ?? this.app_name;
if (desktop_app_info != null) {
display_name = desktop_app_info.get_display_name ();
}
if (display_name == null || display_name.length == 0) {
display_name = "Unknown";
}
this.display_name = display_name.splice (0, 1, display_name.up (1));
}
private void parse_hints () {
@@ -182,11 +151,6 @@ namespace SwayNotificationCenter {
swaync_no_script = hint_value.get_boolean ();
}
break;
case "SWAYNC_BYPASS_DND":
if (hint_value.is_of_type (VariantType.BOOLEAN)) {
swaync_bypass_dnd = hint_value.get_boolean ();
}
break;
case "value":
if (hint_value.is_of_type (VariantType.INT32)) {
this.has_synch = true;

View File

@@ -60,37 +60,15 @@
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkOverlay">
<object class="GtkImage" id="img">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<child>
<object class="GtkImage" id="img">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">center</property>
<property name="icon_size">6</property>
<style>
<class name="image"/>
</style>
</object>
<packing>
<property name="index">-1</property>
</packing>
</child>
<child type="overlay">
<object class="GtkImage" id="img_app_icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="valign">end</property>
<property name="icon_size">6</property>
<style>
<class name="app-icon"/>
</style>
</object>
</child>
<property name="margin-end">12</property>
<property name="icon_size">6</property>
<style>
<class name="image"/>
</style>
</object>
<packing>
<property name="expand">False</property>
@@ -103,6 +81,7 @@
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">center</property>
<property name="margin-end">14</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
@@ -174,9 +153,6 @@
<property name="position">1</property>
</packing>
</child>
<style>
<class name="text-box"/>
</style>
</object>
<packing>
<property name="expand">True</property>
@@ -305,7 +281,7 @@
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">swaync-close-symbolic</property>
<property name="icon-name">window-close-symbolic</property>
</object>
</child>
<style>

View File

@@ -30,7 +30,7 @@ namespace SwayNotificationCenter {
[GtkChild]
unowned Gtk.Button close_button;
private Gtk.ButtonBox ? alt_actions_box = null;
private Gtk.ButtonBox alt_actions_box = new Gtk.ButtonBox (Gtk.Orientation.HORIZONTAL);
[GtkChild]
unowned Gtk.Label summary;
@@ -41,8 +41,6 @@ namespace SwayNotificationCenter {
[GtkChild]
unowned Gtk.Image img;
[GtkChild]
unowned Gtk.Image img_app_icon;
[GtkChild]
unowned Gtk.Image body_image;
// Inline Reply
@@ -72,8 +70,8 @@ namespace SwayNotificationCenter {
public bool is_timed { get; construct; default = false; }
public NotifyParams param { get; private set; }
public unowned NotiDaemon noti_daemon { get; construct; }
public NotifyParams param { get; construct; }
public NotiDaemon noti_daemon { get; construct; }
public NotificationType notification_type {
get;
@@ -114,9 +112,8 @@ namespace SwayNotificationCenter {
NotiDaemon noti_daemon,
NotificationType notification_type) {
Object (noti_daemon: noti_daemon,
param: param,
notification_type: notification_type);
this.param = param;
build_noti ();
}
/** Show a timed notification */
@@ -127,6 +124,7 @@ namespace SwayNotificationCenter {
uint timeout_low,
uint timeout_critical) {
Object (noti_daemon: noti_daemon,
param: param,
notification_type: notification_type,
is_timed: true,
timeout_delay: timeout,
@@ -134,13 +132,11 @@ namespace SwayNotificationCenter {
timeout_critical_delay: timeout_critical,
number_of_body_lines: 5
);
this.param = param;
build_noti ();
}
construct {
try {
code_regex = new Regex ("(?<= |^)(\\d{3}(-| )\\d{3}|\\d{4,8})(?= |$|\\.|,)",
code_regex = new Regex ("(?<= |^)(\\d{3}(-| )\\d{3}|\\d{4,7})(?= |$|\\.|,)",
RegexCompileFlags.MULTILINE);
string joined_tags = string.joinv ("|", TAGS);
tag_regex = new Regex ("&lt;(/?(?:%s))&gt;".printf (joined_tags));
@@ -200,85 +196,12 @@ namespace SwayNotificationCenter {
});
this.transition_time = ConfigModel.instance.transition_time;
build_noti ();
///
/// Signals
///
this.button_press_event.connect ((event) => {
// Close notification on middle and right button click
switch (event.button) {
case Gdk.BUTTON_MIDDLE:
case Gdk.BUTTON_SECONDARY:
this.close_notification ();
return true;
default:
return false;
}
});
// Adds CSS :hover selector to EventBox
default_action.enter_notify_event.connect ((event) => {
if (event.detail != Gdk.NotifyType.INFERIOR
&& event.window == default_action.get_window ()) {
default_action_in = true;
default_action_update_state ();
}
return true;
});
default_action.leave_notify_event.connect ((event) => {
if (event.detail != Gdk.NotifyType.INFERIOR
&& event.window == default_action.get_window ()) {
default_action_in = false;
default_action_update_state ();
}
return true;
});
default_action.unmap.connect (() => default_action_in = false);
close_button.clicked.connect (() => close_notification ());
this.event_box.enter_notify_event.connect ((event) => {
close_revealer.set_reveal_child (true);
remove_noti_timeout ();
return false;
});
this.event_box.leave_notify_event.connect ((event) => {
if (event.detail == Gdk.NotifyType.INFERIOR) return true;
close_revealer.set_reveal_child (false);
if (is_timed) {
add_notification_timeout ();
return false;
});
this.carousel.page_changed.connect ((_, i) => {
if (i != this.carousel_empty_widget_index) return;
remove_noti_timeout ();
try {
noti_daemon.manually_close_notification (
param.applied_id, false);
} catch (Error e) {
print ("Error: %s\n", e.message);
this.destroy ();
}
});
inline_reply_entry.key_release_event.connect ((w, event_key) => {
switch (Gdk.keyval_name (event_key.keyval)) {
case "Return":
inline_reply_button.clicked ();
return true;
default:
return false;
}
});
inline_reply_button.clicked.connect (() => {
string text = inline_reply_entry.get_text ().strip ();
if (text.length == 0) return;
noti_daemon.NotificationReplied (param.applied_id, text);
// Dismiss notification without activating Action
action_clicked (null);
});
this.size_allocate.connect (on_size_allocation);
}
}
private void default_action_update_state () {
@@ -312,8 +235,50 @@ namespace SwayNotificationCenter {
this.summary.set_text (param.summary ?? param.app_name);
this.summary.set_ellipsize (Pango.EllipsizeMode.END);
this.button_press_event.connect ((event) => {
if (event.button != Gdk.BUTTON_SECONDARY) return false;
// Right click
this.close_notification ();
return true;
});
// Adds CSS :hover selector to EventBox
default_action.enter_notify_event.connect ((event) => {
if (event.detail != Gdk.NotifyType.INFERIOR
&& event.window == default_action.get_window ()) {
default_action_in = true;
default_action_update_state ();
}
return true;
});
default_action.leave_notify_event.connect ((event) => {
if (event.detail != Gdk.NotifyType.INFERIOR
&& event.window == default_action.get_window ()) {
default_action_in = false;
default_action_update_state ();
}
return true;
});
default_action.unmap.connect (() => default_action_in = false);
close_revealer.set_transition_duration (this.transition_time);
close_button.clicked.connect (() => close_notification ());
this.event_box.enter_notify_event.connect ((event) => {
close_revealer.set_reveal_child (true);
remove_noti_timeout ();
return false;
});
this.event_box.leave_notify_event.connect ((event) => {
if (event.detail == Gdk.NotifyType.INFERIOR) return true;
close_revealer.set_reveal_child (false);
add_notification_timeout ();
return false;
});
this.revealer.set_transition_duration (this.transition_time);
this.carousel.set_animation_duration (this.transition_time);
@@ -330,9 +295,20 @@ namespace SwayNotificationCenter {
this.carousel_empty_widget_index = 0;
break;
}
// Reset state
this.carousel.scroll_to (event_box);
this.carousel.page_changed.connect ((_, i) => {
if (i != this.carousel_empty_widget_index) return;
remove_noti_timeout ();
try {
noti_daemon.manually_close_notification (
param.applied_id, false);
} catch (Error e) {
print ("Error: %s\n", e.message);
this.destroy ();
}
});
#if HAVE_LATEST_LIBHANDY
this.carousel.allow_scroll_wheel = false;
#endif
if (this.progress_bar.visible = param.has_synch) {
this.progress_bar.set_fraction (param.value * 0.01);
@@ -346,16 +322,13 @@ namespace SwayNotificationCenter {
this.show ();
Timeout.add (0, () => {
if (param.replaces) {
this.revealer.set_reveal_child (true);
return Source.REMOVE;
});
remove_noti_timeout ();
this.size_allocate.disconnect (on_size_allocation);
if (is_timed) {
add_notification_timeout ();
this.size_allocate.connect (on_size_allocation);
} else {
Timeout.add (0, () => {
this.revealer.set_reveal_child (true);
return Source.REMOVE;
});
}
}
@@ -364,9 +337,6 @@ namespace SwayNotificationCenter {
this.body.set_lines (this.number_of_body_lines);
// Reset state
this.body_image.hide ();
// Removes all image tags and adds them to an array
if (text.length > 0) {
try {
@@ -404,29 +374,22 @@ namespace SwayNotificationCenter {
// Markup
try {
// Escapes all characters
string escaped = Markup.escape_text (text);
// Replace all valid tags brackets with <,</,> so that the
// markup parser only parses valid tags
// Ex: &lt;b&gt;BOLD&lt;/b&gt; -> <b>BOLD</b>
escaped = tag_regex.replace (escaped, escaped.length, 0, "<\\1>");
// Unescape a few characters that may have been double escaped
// Sending "<" in Discord would result in "&amp;lt;" without this
// &amp;lt; -> &lt;
escaped = tag_unescape_regex.replace_literal (escaped, escaped.length, 0, "&");
// Turns it back to markdown, defaults to original if not valid
Pango.AttrList ? attr = null;
string ? buf = null;
try {
// Try parsing without any hacks
Pango.parse_markup (text, -1, 0, out attr, out buf, null);
} catch (Error e) {
// Default to hack if the initial markup couldn't be parsed
// Escapes all characters
string escaped = Markup.escape_text (text);
// Replace all valid tags brackets with <,</,> so that the
// markup parser only parses valid tags
// Ex: &lt;b&gt;BOLD&lt;/b&gt; -> <b>BOLD</b>
escaped = tag_regex.replace (escaped, escaped.length, 0, "<\\1>");
// Unescape a few characters that may have been double escaped
// Sending "<" in Discord would result in "&amp;lt;" without this
// &amp;lt; -> &lt;
escaped = tag_unescape_regex.replace_literal (escaped, escaped.length, 0, "&");
// Turns it back to markup, defaults to original if not valid
Pango.parse_markup (escaped, -1, 0, out attr, out buf, null);
}
Pango.parse_markup (escaped, -1, 0, out attr, out buf, null);
this.body.set_text (buf);
if (attr != null) this.body.set_attributes (attr);
@@ -458,7 +421,6 @@ namespace SwayNotificationCenter {
}
public void click_alt_action (uint index) {
if (alt_actions_box == null) return;
List<weak Gtk.Widget> ? children = alt_actions_box.get_children ();
uint length = children.length ();
if (length == 0 || index >= length) return;
@@ -490,11 +452,6 @@ namespace SwayNotificationCenter {
}
private void set_style_urgency () {
// Reset state
base_box.get_style_context ().remove_class ("low");
base_box.get_style_context ().remove_class ("normal");
base_box.get_style_context ().remove_class ("critical");
switch (param.urgency) {
case UrgencyLevels.LOW:
base_box.get_style_context ().add_class ("low");
@@ -510,8 +467,6 @@ namespace SwayNotificationCenter {
}
private void set_inline_reply () {
// Reset state
inline_reply_box.hide ();
// Only show inline replies in popup notifications if the compositor
// supports ON_DEMAND layer shell keyboard interactivity
if (!ConfigModel.instance.notification_inline_replies
@@ -539,23 +494,32 @@ namespace SwayNotificationCenter {
},
null);
inline_reply_entry.key_release_event.connect ((w, event_key) => {
switch (Gdk.keyval_name (event_key.keyval)) {
case "Return":
inline_reply_button.clicked ();
return true;
default:
return false;
}
});
inline_reply_button.set_label (param.inline_reply.name ?? "Reply");
inline_reply_button.clicked.connect (() => {
string text = inline_reply_entry.get_text ().strip ();
if (text.length == 0) return;
noti_daemon.NotificationReplied (param.applied_id, text);
// Dismiss notification without activating Action
action_clicked (null);
});
}
private void set_actions () {
// Reset state
foreach (Gtk.Widget child in base_box.get_children ()) {
if (child is Gtk.ScrolledWindow) {
child.destroy ();
}
}
// Check for security codes
string ? code = parse_body_codes ();
if (param.actions.length > 0 || code != null) {
var viewport = new Gtk.Viewport (null, null);
var scroll = new Gtk.ScrolledWindow (null, null);
alt_actions_box = new Gtk.ButtonBox (Gtk.Orientation.HORIZONTAL);
alt_actions_box.set_homogeneous (true);
alt_actions_box.set_layout (Gtk.ButtonBoxStyle.EXPAND);
@@ -650,104 +614,59 @@ namespace SwayNotificationCenter {
});
}
public void replace_notification (NotifyParams new_params) {
this.param = new_params;
build_noti ();
}
private void set_icon () {
img.clear ();
img.set_visible (true);
img_app_icon.clear ();
img_app_icon.set_visible (true);
Icon ? app_icon_name = null;
string ? app_icon_uri = null;
if (param.desktop_app_info != null) {
app_icon_name = param.desktop_app_info.get_icon ();
}
if (param.app_icon != null && param.app_icon != "") {
app_icon_uri = param.app_icon;
}
var image_visibility = ConfigModel.instance.image_visibility;
if (image_visibility == ImageVisibility.NEVER) {
img.set_visible (false);
img_app_icon.set_visible (false);
return;
}
img.set_pixel_size (notification_icon_size);
img.height_request = notification_icon_size;
img.width_request = notification_icon_size;
int app_icon_size = notification_icon_size / 3;
img_app_icon.set_pixel_size (app_icon_size);
var img_path_exists = File.new_for_uri (
var img_path_exists = File.new_for_path (
param.image_path ?? "").query_exists ();
if (param.image_path != null && !img_path_exists) {
// Check if it's not a URI
img_path_exists = File.new_for_path (
param.image_path ?? "").query_exists ();
}
var app_icon_exists = File.new_for_uri (
app_icon_uri ?? "").query_exists ();
if (app_icon_uri != null && !img_path_exists) {
// Check if it's not a URI
app_icon_exists = File.new_for_path (
app_icon_uri ?? "").query_exists ();
}
var app_icon_exists = File.new_for_path (
param.app_icon ?? "").query_exists ();
// Get the image CSS corner radius in pixels
int radius = 0;
unowned var ctx = img.get_style_context ();
var value = ctx.get_property (Gtk.STYLE_PROPERTY_BORDER_RADIUS,
ctx.get_state ());
if (value.type () == Type.INT) {
radius = value.get_int ();
}
// Set the main image to the provided image
if (param.image_data.is_initialized) {
Functions.set_image_data (param.image_data, img,
notification_icon_size, radius);
notification_icon_size);
} else if (param.image_path != null &&
param.image_path != "" &&
img_path_exists) {
Functions.set_image_uri (param.image_path, img,
Functions.set_image_path (param.image_path, img,
notification_icon_size,
radius,
img_path_exists);
} else if (param.app_icon != null && param.app_icon != "") {
Functions.set_image_path (param.app_icon, img,
notification_icon_size,
app_icon_exists);
} else if (param.icon_data.is_initialized) {
Functions.set_image_data (param.icon_data, img,
notification_icon_size, radius);
}
if (img.storage_type == Gtk.ImageType.EMPTY) {
notification_icon_size);
} else {
// Get the app icon
if (app_icon_uri != null) {
Functions.set_image_uri (app_icon_uri, img,
notification_icon_size,
radius,
app_icon_exists);
} else if (app_icon_name != null) {
img.set_from_gicon (app_icon_name, icon_size);
Icon ? icon = null;
if (param.desktop_entry != null) {
string entry = param.desktop_entry;
entry = entry.replace (".desktop", "");
DesktopAppInfo entry_info = new DesktopAppInfo (
"%s.desktop".printf (entry));
// Checks if the .desktop file actually exists or not
if (entry_info is DesktopAppInfo) {
icon = entry_info.get_icon ();
}
}
if (icon != null) {
img.set_from_gicon (icon, icon_size);
} else if (image_visibility == ImageVisibility.ALWAYS) {
// Default icon
img.set_from_icon_name ("image-missing", icon_size);
} else {
img.set_visible (false);
}
} else {
// We only set the app icon if the main image is set
if (app_icon_uri != null) {
Functions.set_image_uri (app_icon_uri, img_app_icon,
app_icon_size,
0,
app_icon_exists);
} else if (app_icon_name != null) {
img_app_icon.set_from_gicon (app_icon_name, Gtk.IconSize.INVALID);
}
}
}

View File

@@ -1,586 +0,0 @@
namespace SwayNotificationCenter {
public class NotificationGroup : Gtk.ListBoxRow {
const string STYLE_CLASS_URGENT = "critical";
const string STYLE_CLASS_COLLAPSED = "collapsed";
public string name_id;
private ExpandableGroup group;
private Gtk.Revealer revealer = new Gtk.Revealer ();
private Gtk.Image app_icon;
private Gtk.Label app_label;
private Gtk.GestureMultiPress gesture;
private bool gesture_down = false;
private bool gesture_in = false;
private HashTable<uint32, bool> urgent_notifications
= new HashTable<uint32, bool> (direct_hash, direct_equal);
public signal void on_expand_change (bool state);
public NotificationGroup (string name_id, string display_name) {
this.name_id = name_id;
get_style_context ().add_class ("notification-group");
Gtk.Box box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
revealer.set_transition_type (Gtk.RevealerTransitionType.SLIDE_UP);
revealer.set_reveal_child (false);
revealer.set_transition_duration (Constants.ANIMATION_DURATION);
// Add top controls
Gtk.Box controls_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 4);
Gtk.Box end_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 4);
end_box.set_halign (Gtk.Align.END);
end_box.get_style_context ().add_class ("notification-group-buttons");
// Collapse button
Gtk.Button collapse_button = new Gtk.Button.from_icon_name (
"swaync-collapse-symbolic", Gtk.IconSize.BUTTON);
collapse_button.get_style_context ().add_class ("flat");
collapse_button.get_style_context ().add_class ("circular");
collapse_button.get_style_context ().add_class ("notification-group-collapse-button");
collapse_button.set_relief (Gtk.ReliefStyle.NORMAL);
collapse_button.set_halign (Gtk.Align.END);
collapse_button.set_valign (Gtk.Align.CENTER);
collapse_button.clicked.connect (() => {
set_expanded (false);
on_expand_change (false);
});
end_box.add (collapse_button);
// Close all button
Gtk.Button close_all_button = new Gtk.Button.from_icon_name (
"swaync-close-symbolic", Gtk.IconSize.BUTTON);
close_all_button.get_style_context ().add_class ("flat");
close_all_button.get_style_context ().add_class ("circular");
close_all_button.get_style_context ().add_class ("notification-group-close-all-button");
close_all_button.set_relief (Gtk.ReliefStyle.NORMAL);
close_all_button.set_halign (Gtk.Align.END);
close_all_button.set_valign (Gtk.Align.CENTER);
close_all_button.clicked.connect (() => {
close_all_notifications ();
on_expand_change (false);
});
end_box.add (close_all_button);
// Group name label
Gtk.Box start_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 4);
start_box.set_halign (Gtk.Align.START);
start_box.get_style_context ().add_class ("notification-group-headers");
// App Icon
app_icon = new Gtk.Image ();
app_icon.set_pixel_size (32);
app_icon.set_valign (Gtk.Align.CENTER);
app_icon.get_style_context ().add_class ("notification-group-icon");
start_box.add (app_icon);
// App Label
app_label = new Gtk.Label (display_name);
app_label.xalign = 0;
app_label.get_style_context ().add_class ("title-1");
app_label.get_style_context ().add_class ("notification-group-header");
start_box.add (app_label);
controls_box.pack_start (start_box);
controls_box.pack_end (end_box);
revealer.add (controls_box);
box.add (revealer);
set_activatable (false);
group = new ExpandableGroup (Constants.ANIMATION_DURATION, (state) => {
revealer.set_reveal_child (state);
// Change CSS Class
if (parent != null) {
set_classes ();
}
});
set_classes ();
box.add (group);
add (box);
show_all ();
/*
* Handling of group presses
*/
gesture = new Gtk.GestureMultiPress (this);
gesture.set_touch_only (false);
gesture.set_exclusive (true);
gesture.set_button (Gdk.BUTTON_PRIMARY);
gesture.set_propagation_phase (Gtk.PropagationPhase.CAPTURE);
gesture.pressed.connect ((_gesture, _n_press, x, y) => {
gesture_in = true;
gesture_down = true;
});
gesture.released.connect ((gesture, _n_press, _x, _y) => {
// Emit released
if (!gesture_down) return;
gesture_down = false;
if (gesture_in) {
bool single_noti = only_single_notification ();
if (!group.is_expanded && !single_noti) {
group.set_expanded (true);
on_expand_change (true);
}
group.set_sensitive (single_noti || group.is_expanded);
}
Gdk.EventSequence ? sequence = gesture.get_current_sequence ();
if (sequence == null) {
gesture_in = false;
}
});
gesture.update.connect ((gesture, sequence) => {
Gtk.GestureSingle gesture_single = (Gtk.GestureSingle) gesture;
if (sequence != gesture_single.get_current_sequence ()) return;
Gtk.Allocation allocation;
double x, y;
get_allocation (out allocation);
gesture.get_point (sequence, out x, out y);
bool intersects = (x >= 0 && y >= 0 && x < allocation.width && y < allocation.height);
if (gesture_in != intersects) {
gesture_in = intersects;
}
});
gesture.cancel.connect ((gesture, sequence) => {
if (gesture_down) {
gesture_down = false;
}
});
}
private void set_classes () {
unowned Gtk.StyleContext ctx = get_style_context ();
ctx.remove_class (STYLE_CLASS_COLLAPSED);
if (!group.is_expanded) {
if (!ctx.has_class (STYLE_CLASS_COLLAPSED)) {
ctx.add_class (STYLE_CLASS_COLLAPSED);
}
}
}
private void set_icon () {
if (is_empty ()) return;
unowned Notification first = (Notification) group.widgets.first ().data;
unowned NotifyParams param = first.param;
// Get the app icon
Icon ? icon = null;
if (param.desktop_app_info != null
&& (icon = param.desktop_app_info.get_icon ()) != null) {
app_icon.set_from_gicon (icon, Gtk.IconSize.INVALID);
app_icon.show ();
} else {
app_icon.set_from_icon_name ("application-x-executable-symbolic",
Gtk.IconSize.INVALID);
}
}
/// Returns if there's more than one notification
public bool only_single_notification () {
unowned Gtk.Widget ? widget = group.widgets.nth_data (1);
return widget == null;
}
public void set_expanded (bool state) {
group.set_expanded (state);
group.set_sensitive (only_single_notification () || group.is_expanded);
}
public bool toggle_expanded () {
bool state = !group.is_expanded;
set_expanded (state);
return state;
}
public void add_notification (Notification noti) {
if (noti.param.urgency == UrgencyLevels.CRITICAL) {
urgent_notifications.insert (noti.param.applied_id, true);
unowned Gtk.StyleContext ctx = get_style_context ();
if (!ctx.has_class (STYLE_CLASS_URGENT)) {
ctx.add_class (STYLE_CLASS_URGENT);
}
}
group.add (noti);
if (!only_single_notification ()) {
if (!group.is_expanded) {
group.set_sensitive (false);
}
} else {
set_icon ();
}
}
public void remove_notification (Notification noti) {
urgent_notifications.remove (noti.param.applied_id);
if (urgent_notifications.length == 0) {
get_style_context ().remove_class (STYLE_CLASS_URGENT);
}
group.remove (noti);
if (only_single_notification ()) {
set_expanded (false);
on_expand_change (false);
}
}
public List<weak Gtk.Widget> get_notifications () {
return group.widgets.copy ();
}
public unowned Notification ? get_latest_notification () {
return (Notification ?) group.widgets.last ().data;
}
public int64 get_time () {
if (group.widgets.is_empty ()) return -1;
return ((Notification) group.widgets.last ().data).param.time;
}
public bool get_is_urgent () {
return urgent_notifications.length > 0;
}
public uint get_num_notifications () {
return group.widgets.length ();
}
public bool is_empty () {
return group.widgets.is_empty ();
}
public void close_all_notifications () {
urgent_notifications.remove_all ();
foreach (unowned Gtk.Widget widget in group.widgets) {
var noti = (Notification) widget;
if (noti != null) noti.close_notification (false);
}
}
public void update () {
set_icon ();
foreach (unowned Gtk.Widget widget in group.widgets) {
var noti = (Notification) widget;
if (noti != null) noti.set_time ();
}
}
public int get_relative_y (Gtk.Widget parent) {
int dest_y;
translate_coordinates (parent, 0, 0, null, out dest_y);
return dest_y;
}
}
private class ExpandableGroup : Gtk.Container {
const int NUM_STACKED_NOTIFICATIONS = 3;
const int COLLAPSED_NOTIFICATION_OFFSET = 8;
public bool is_expanded { get; private set; default = true; }
private double animation_progress = 1.0;
private double animation_progress_inv {
get {
return (1 - animation_progress);
}
}
private Animation ? animation;
private unowned on_expand_change change_cb;
public List<unowned Gtk.Widget> widgets = new List<unowned Gtk.Widget> ();
public delegate void on_expand_change (bool state);
public ExpandableGroup (uint animation_duration, on_expand_change change_cb) {
base.set_has_window (false);
base.set_can_focus (true);
base.set_redraw_on_allocate (false);
this.change_cb = change_cb;
animation = new Animation (this, animation_duration,
Animation.ease_in_out_cubic,
animation_value_cb,
animation_done_cb);
this.show ();
set_expanded (false);
}
public void set_expanded (bool value) {
if (is_expanded == value) return;
is_expanded = value;
animate (is_expanded ? 1 : 0);
this.queue_resize ();
change_cb (is_expanded);
}
public override void add (Gtk.Widget widget) {
widget.set_parent (this);
widgets.append (widget);
}
public override void remove (Gtk.Widget widget) {
widget.unparent ();
widgets.remove (widget);
if (this.get_visible () && widget.get_visible ()) {
this.queue_resize_no_redraw ();
}
}
public override void forall_internal (bool include_internals, Gtk.Callback callback) {
foreach (unowned Gtk.Widget widget in widgets) {
callback (widget);
}
}
public override Gtk.SizeRequestMode get_request_mode () {
return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
}
public override void size_allocate (Gtk.Allocation allocation) {
base.size_allocate (allocation);
int length = (int) widgets.length ();
if (length == 0) return;
uint border_width = get_border_width ();
Gtk.Allocation prev_allocation = Gtk.Allocation ();
prev_allocation.y = allocation.y;
// The height of the most recent notification
unowned Gtk.Widget last = widgets.last ().data;
int target_height = 0;
last.get_preferred_height_for_width (allocation.width,
out target_height, null);
for (int i = length - 1; i >= 0; i--) {
unowned Gtk.Widget widget = widgets.nth_data (i);
if (widget != null && widget.get_visible ()) {
int height;
widget.get_preferred_height_for_width (allocation.width,
out height, null);
Gtk.Allocation alloc = Gtk.Allocation ();
alloc.x = allocation.x + (int) border_width;
alloc.y = (int) (prev_allocation.y +
animation_progress * prev_allocation.height +
border_width);
alloc.width = allocation.width - 2 * (int) border_width;
alloc.height = height;
// Expand smaller stacked notifications to the expected height
// But only when the animation has finished
if (target_height > height && !is_expanded && animation_progress == 0) {
alloc.height = target_height;
}
alloc.height -= 2 * (int) border_width;
// Add the collapsed offset to only stacked notifications.
// Excludes notifications index > NUM_STACKED_NOTIFICATIONS
if (i < length - 1 && length - 1 - i < NUM_STACKED_NOTIFICATIONS) {
alloc.y += (int) (animation_progress_inv * COLLAPSED_NOTIFICATION_OFFSET);
}
prev_allocation = alloc;
widget.size_allocate (alloc);
if (get_realized ()) {
widget.show ();
}
}
if (get_realized ()) {
widget.set_child_visible (true);
}
}
}
public override void get_preferred_height_for_width (int width,
out int minimum_height,
out int natural_height) {
minimum_height = 0;
natural_height = 0;
foreach (unowned Gtk.Widget widget in widgets) {
if (widget != null && widget.get_visible ()) {
int widget_minimum_height = 0;
int widget_natural_height = 0;
widget.get_preferred_height_for_width (width,
out widget_minimum_height,
out widget_natural_height);
minimum_height += widget_minimum_height;
natural_height += widget_natural_height;
}
}
int target_minimum_height;
int target_natural_height;
get_height_for_latest_notifications (width,
out target_minimum_height,
out target_natural_height);
minimum_height = (int) Animation.lerp (minimum_height,
target_minimum_height,
animation_progress_inv);
natural_height = (int) Animation.lerp (natural_height,
target_natural_height,
animation_progress_inv);
}
public override bool draw (Cairo.Context cr) {
int length = (int) widgets.length ();
if (length == 0) return true;
Gtk.Allocation alloc;
get_allocated_size (out alloc, null);
unowned Gtk.Widget latest = widgets.nth_data (length - 1);
Gtk.Allocation latest_alloc;
latest.get_allocated_size (out latest_alloc, null);
Cairo.Pattern hover_gradient = new Cairo.Pattern.linear (0, 0, 0, 1);
hover_gradient.add_color_stop_rgba (0, 1, 1, 1, 1);
hover_gradient.add_color_stop_rgba (1, 1, 1, 1, 1);
// Fades from the bottom at 0.5 -> top at 0.0 opacity
Cairo.Pattern fade_gradient = new Cairo.Pattern.linear (0, 0, 0, 1);
fade_gradient.add_color_stop_rgba (0, 1, 1, 1, animation_progress_inv);
fade_gradient.add_color_stop_rgba (1, 1, 1, 1, 0);
// Cross-fades in the non visible stacked notifications when expanded
Cairo.Pattern cross_fade_pattern =
new Cairo.Pattern.rgba (1, 1, 1, 1.5 * animation_progress_inv);
int width = alloc.width;
for (int i = 0; i < length; i++) {
// Skip drawing excess notifications
if (!is_expanded &&
animation_progress == 0 &&
i < length - NUM_STACKED_NOTIFICATIONS) {
continue;
}
unowned Gtk.Widget widget = widgets.nth_data (i);
int preferred_height;
widget.get_preferred_height_for_width (width,
out preferred_height, null);
Gtk.Allocation widget_alloc;
widget.get_allocated_size (out widget_alloc, null);
int height_diff = latest_alloc.height - widget_alloc.height;
cr.save ();
// Translate to the widgets allocated y
double translate_y = widget_alloc.y - alloc.y;
// Move down even more if the height is larger than the latest
// in the stack (helps with only rendering the bottom portion)
translate_y += height_diff * animation_progress_inv;
cr.translate (0, translate_y);
// Scale down lower notifications in the stack
if (i + 1 != length) {
double scale = double.min (
animation_progress + Math.pow (0.95, length - 1 - i), 1);
// Moves the scaled notification to the center of X and bottom y
cr.translate ((widget_alloc.width - width * scale) * 0.5,
widget_alloc.height * (1 - scale));
cr.scale (scale, scale);
}
int lerped_y = (int) Animation.lerp (-height_diff, 0, animation_progress);
int lerped_height = (int) Animation.lerp (latest_alloc.height,
widget_alloc.height,
animation_progress);
// Clip to the size of the latest notification
// (fixes issue where a larger bottom notification would
// be visible above)
cr.rectangle (0, lerped_y, width, lerped_height);
cr.clip ();
// Draw patterns on the notification
cr.push_group ();
widget.draw (cr);
if (i + 1 != length) {
// Draw Fade Gradient
cr.save ();
cr.translate (0, lerped_y);
cr.scale (1, lerped_height * 0.5);
cr.set_source (fade_gradient);
cr.rectangle (0, 0, width, lerped_height * 0.5);
cr.set_operator (Cairo.Operator.DEST_OUT);
cr.fill ();
cr.restore ();
}
// Draw notification cross-fade
if (i < length - NUM_STACKED_NOTIFICATIONS) {
cr.save ();
cr.translate (0, lerped_y);
cr.scale (1, lerped_height);
cr.set_source (cross_fade_pattern);
cr.rectangle (0, 0, width, lerped_height);
cr.set_operator (Cairo.Operator.DEST_OUT);
cr.fill ();
cr.restore ();
}
cr.pop_group_to_source ();
cr.paint ();
cr.restore ();
}
return true;
}
/** Gets the collapsed height (first notification + stacked) */
private void get_height_for_latest_notifications (int width,
out int minimum_height,
out int natural_height) {
minimum_height = 0;
natural_height = 0;
uint length = widgets.length ();
if (length == 0) return;
int offset = 0;
for (uint i = 1;
i < length && i < NUM_STACKED_NOTIFICATIONS;
i++) {
offset += COLLAPSED_NOTIFICATION_OFFSET;
}
unowned Gtk.Widget last = widgets.last ().data;
last.get_preferred_height_for_width (width,
out minimum_height,
out natural_height);
minimum_height += offset;
natural_height += offset;
}
void animation_value_cb (double progress) {
this.animation_progress = progress;
this.queue_resize ();
}
void animation_done_cb () {
animation.dispose ();
this.queue_allocate ();
}
void animate (double to) {
animation.stop ();
animation.start (animation_progress, to);
}
}
}

View File

@@ -151,7 +151,6 @@ namespace SwayNotificationCenter {
public void change_visibility (bool value) {
if (!value) {
close_all_notifications ();
close ();
} else {
this.set_anchor ();
}
@@ -171,9 +170,10 @@ namespace SwayNotificationCenter {
}
}
private void remove_notification (Notification ? noti, bool dismiss) {
private void remove_notification (Notification ? noti, bool replaces) {
// Remove notification and its destruction timeout
if (noti != null) {
#if HAVE_LATEST_GTK_LAYER_SHELL
if (noti.has_inline_reply) {
inline_reply_notifications.remove (noti.param.applied_id);
if (inline_reply_notifications.size == 0
@@ -183,11 +183,12 @@ namespace SwayNotificationCenter {
this, GtkLayerShell.KeyboardMode.NONE);
}
}
#endif
noti.remove_noti_timeout ();
noti.destroy ();
}
if (dismiss
if (!replaces
&& (!get_realized ()
|| !get_mapped ()
|| !(get_child () is Gtk.Widget)
@@ -197,13 +198,15 @@ namespace SwayNotificationCenter {
}
}
public void add_notification (NotifyParams param) {
public void add_notification (NotifyParams param,
NotiDaemon noti_daemon) {
var noti = new Notification.timed (param,
swaync_daemon.noti_daemon,
noti_daemon,
NotificationType.POPUP,
ConfigModel.instance.timeout,
ConfigModel.instance.timeout_low,
ConfigModel.instance.timeout_critical);
#if HAVE_LATEST_GTK_LAYER_SHELL
if (noti.has_inline_reply) {
inline_reply_notifications.add (param.applied_id);
@@ -213,6 +216,7 @@ namespace SwayNotificationCenter {
this, GtkLayerShell.KeyboardMode.ON_DEMAND);
}
}
#endif
if (list_reverse) {
box.pack_start (noti);
@@ -230,31 +234,16 @@ namespace SwayNotificationCenter {
scroll_to_start (list_reverse);
}
public void close_notification (uint32 id, bool dismiss) {
public void close_notification (uint32 id, bool replaces) {
foreach (var w in box.get_children ()) {
var noti = (Notification) w;
if (noti != null && noti.param.applied_id == id) {
remove_notification (noti, dismiss);
remove_notification (noti, replaces);
break;
}
}
}
public void replace_notification (uint32 id, NotifyParams new_params) {
foreach (var w in box.get_children ()) {
var noti = (Notification) w;
if (noti != null && noti.param.applied_id == id) {
noti.replace_notification (new_params);
// Position the notification in the beginning of the list
box.reorder_child (noti, (int) box.get_children ().length ());
return;
}
}
// Display a new notification if the old one isn't visible
add_notification (new_params);
}
public uint32 ? get_latest_notification () {
List<weak Gtk.Widget> children = box.get_children ();
if (children.is_empty ()) return null;

366
src/style.css Normal file
View File

@@ -0,0 +1,366 @@
/*
* vim: ft=less
*/
@define-color cc-bg rgba(0, 0, 0, 0.7);
@define-color noti-border-color rgba(255, 255, 255, 0.15);
@define-color noti-bg rgb(48, 48, 48);
@define-color noti-bg-darker rgb(38, 38, 38);
@define-color noti-bg-hover rgb(56, 56, 56);
@define-color noti-bg-focus rgba(68, 68, 68, 0.6);
@define-color noti-close-bg rgba(255, 255, 255, 0.1);
@define-color noti-close-bg-hover rgba(255, 255, 255, 0.15);
@define-color text-color rgb(255, 255, 255);
@define-color text-color-disabled rgb(150, 150, 150);
@define-color bg-selected rgb(0, 128, 255);
.notification-row {
outline: none;
}
.notification-row:focus,
.notification-row:hover {
background: @noti-bg-focus;
}
.notification {
border-radius: 12px;
margin: 6px 12px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3), 0 1px 3px 1px rgba(0, 0, 0, 0.7),
0 2px 6px 2px rgba(0, 0, 0, 0.3);
padding: 0;
}
/* Uncomment to enable specific urgency colors
.low {
background: yellow;
padding: 6px;
border-radius: 12px;
}
.normal {
background: green;
padding: 6px;
border-radius: 12px;
}
.critical {
background: red;
padding: 6px;
border-radius: 12px;
}
*/
.notification-content {
background: transparent;
padding: 6px;
border-radius: 12px;
}
.close-button {
background: @noti-close-bg;
color: @text-color;
text-shadow: none;
padding: 0;
border-radius: 100%;
margin-top: 10px;
margin-right: 16px;
box-shadow: none;
border: none;
min-width: 24px;
min-height: 24px;
}
.close-button:hover {
box-shadow: none;
background: @noti-close-bg-hover;
transition: all 0.15s ease-in-out;
border: none;
}
.notification-default-action,
.notification-action {
padding: 4px;
margin: 0;
box-shadow: none;
background: @noti-bg;
border: 1px solid @noti-border-color;
color: @text-color;
transition: all 0.15s ease-in-out;
}
.notification-default-action:hover,
.notification-action:hover {
-gtk-icon-effect: none;
background: @noti-bg-hover;
}
.notification-default-action {
border-radius: 12px;
}
/* When alternative actions are visible */
.notification-default-action:not(:only-child) {
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
.notification-action {
border-radius: 0px;
border-top: none;
border-right: none;
}
/* add bottom border radius to eliminate clipping */
.notification-action:first-child {
border-bottom-left-radius: 10px;
}
.notification-action:last-child {
border-bottom-right-radius: 10px;
border-right: 1px solid @noti-border-color;
}
.inline-reply {
margin-top: 8px;
}
.inline-reply-entry {
background: @noti-bg-darker;
color: @text-color;
caret-color: @text-color;
border: 1px solid @noti-border-color;
border-radius: 12px;
}
.inline-reply-button {
margin-left: 4px;
background: @noti-bg;
border: 1px solid @noti-border-color;
border-radius: 12px;
color: @text-color;
}
.inline-reply-button:disabled {
background: initial;
color: @text-color-disabled;
border: 1px solid transparent;
}
.inline-reply-button:hover {
background: @noti-bg-hover;
}
.image {
}
.body-image {
margin-top: 6px;
background-color: white;
border-radius: 12px;
}
.summary {
font-size: 16px;
font-weight: bold;
background: transparent;
color: @text-color;
text-shadow: none;
}
.time {
font-size: 16px;
font-weight: bold;
background: transparent;
color: @text-color;
text-shadow: none;
margin-right: 18px;
}
.body {
font-size: 15px;
font-weight: normal;
background: transparent;
color: @text-color;
text-shadow: none;
}
.control-center {
background: @cc-bg;
}
.control-center-list {
background: transparent;
}
.control-center-list-placeholder {
opacity: 0.5;
}
.floating-notifications {
background: transparent;
}
/* Window behind control center and on all other monitors */
.blank-window {
background: alpha(black, 0.25);
}
/*** Widgets ***/
/* Title widget */
.widget-title {
margin: 8px;
font-size: 1.5rem;
}
.widget-title > button {
font-size: initial;
color: @text-color;
text-shadow: none;
background: @noti-bg;
border: 1px solid @noti-border-color;
box-shadow: none;
border-radius: 12px;
}
.widget-title > button:hover {
background: @noti-bg-hover;
}
/* DND widget */
.widget-dnd {
margin: 8px;
font-size: 1.1rem;
}
.widget-dnd > switch {
font-size: initial;
border-radius: 12px;
background: @noti-bg;
border: 1px solid @noti-border-color;
box-shadow: none;
}
.widget-dnd > switch:checked {
background: @bg-selected;
}
.widget-dnd > switch slider {
background: @noti-bg-hover;
border-radius: 12px;
}
/* Label widget */
.widget-label {
margin: 8px;
}
.widget-label > label {
font-size: 1.1rem;
}
/* Mpris widget */
.widget-mpris {
/* The parent to all players */
}
.widget-mpris-player {
padding: 8px;
margin: 8px;
}
.widget-mpris-title {
font-weight: bold;
font-size: 1.25rem;
}
.widget-mpris-subtitle {
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;
}
/* style given to the active toggle button */
.widget-buttons-grid>flowbox>flowboxchild>.active {
background: @noti-bg-focus;
}
.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;
}
/* Volume widget */
.widget-volume {
background-color: @noti-bg;
padding: 8px;
margin: 8px;
border-radius: 12px;
}
.widget-volume>box>button {
background: transparent;
border: none;
}
.per-app-volume {
background-color: @noti-bg-alt;
padding: 4px 8px 8px 8px;
margin: 0px 8px 8px 8px;
border-radius: 12px;
}
/* Backlight widget */
.widget-backlight {
background-color: @noti-bg;
padding: 8px;
margin: 8px;
border-radius: 12px;
}
/* Title widget */
.widget-inhibitors {
margin: 8px;
font-size: 1.5rem;
}
.widget-inhibitors > button {
font-size: initial;
color: @text-color;
text-shadow: none;
background: @noti-bg;
border: 1px solid @noti-border-color;
box-shadow: none;
border-radius: 12px;
}
.widget-inhibitors > button:hover {
background: @noti-bg-hover;
}

View File

@@ -251,7 +251,7 @@ namespace SwayNotificationCenter {
/** Closes a specific notification with the `id` */
public void close_notification (uint32 id) throws DBusError, IOError {
noti_daemon.control_center.close_notification (id, true);
noti_daemon.control_center.close_notification (id);
}
/**