Compare commits

59 Commits
dev ... main

Author SHA1 Message Date
Stian Onarheim
653058beac Close notification with middle mouse button (#413)
* Close notification with middle mouse button

Signed-off-by: Stian Onarheim <stian.onarheim@protonmail.com>

* Fix linting issue

---------

Signed-off-by: Stian Onarheim <stian.onarheim@protonmail.com>
Co-authored-by: Erik Reider <35975961+ErikReider@users.noreply.github.com>
2024-05-03 21:26:37 +02:00
Jakub Jirutka
931bea2759 Add Alpine Linux package to readme (#421) 2024-04-21 20:04:44 +02:00
Robin Candau
30cd80b2eb Update README.md with the swaync Arch package (#423) 2024-04-21 19:46:25 +02:00
Abílio Costa
af8ac2f7d7 Add MPRIS background blur for app icons (#369)
* Fix default mpris icon size

The widget was was using the .ui size instead of the user config
size in the following situations:

- there was no media player app
- the media player app icon was used
- the audio-x-generic-symbolic icon was used

This fix sets the default size for the album art icon according to the
user's config.

set_from_gicon and set_from_icon_name receive a Gtk.IconSize enum value,
not an int, so we need to call set_pixel_size to set the proper size.

* Add MPRIS background blur for app icons

---------

Co-authored-by: Erik Reider <35975961+ErikReider@users.noreply.github.com>
2024-04-21 10:40:57 +02:00
Apostroll Hardtrollias
8cb9be5970 Fix invalid data types in configSchema.json (#420)
Fix two instances inside `configSchema.json` where the invalid data
type `bool` is used, instead of `boolean`.
2024-04-15 23:31:37 +02:00
Gabriel Carneiro
be1533ad7f block signal handlers in toggle button when updating state (#406)
* blockg signal handlers in toggle button when updating state

* fix linting

---------

Co-authored-by: Erik Reider <35975961+ErikReider@users.noreply.github.com>
2024-03-25 23:16:13 +01:00
o0nd7ots
7e503e6885 Fixed sources annotation (#407)
* Fixed sources annotation

github supports jsonc

Also unified shell commands to use zsh

* Replaced `json` with `jsonc` where needed
2024-03-25 23:09:03 +01:00
Erik Reider
1b6380b2a8 Fixed Arch build Github Action 2024-03-25 23:07:26 +01:00
o0nd7ots
9a3d5b3fe4 Changed gentoo emaint flag (#403)
It's more verbose, as with the emerge command
2024-03-25 16:02:40 +01:00
Boh_132_Boh
d1eb9bfe28 Updated meson build instructions (#402)
Just switched from "meson build --prefix=/usr" to "meson setup build --prefix=/usr " as while compiling I received this warning:

WARNING: Running the setup command as `meson [options]` instead of `meson setup [options]` is ambiguous and deprecated.
2024-03-14 12:36:28 +01:00
Erik Reider
d2b797406a Bumped PKGBUILD versions 2024-03-09 19:26:23 +01:00
Erik Reider
4275fa3915 Bumped version to 0.10.1 2024-03-09 19:19:48 +01:00
Abílio Costa
ed1b721a15 Fix default mpris icon size (#370)
* Fix default mpris icon size

The widget was was using the .ui size instead of the user config
size in the following situations:

- there was no media player app
- the media player app icon was used
- the audio-x-generic-symbolic icon was used

This fix sets the default size for the album art icon according to the
user's config.

set_from_gicon and set_from_icon_name receive a Gtk.IconSize enum value,
not an int, so we need to call set_pixel_size to set the proper size.

* Use INVALID for IconSize

* Fix lint
2024-03-09 19:13:03 +01:00
Erik Reider
54a4c5d0d2 Fixed bottom aligned notifications being clipped in CC (#398) 2024-03-08 18:58:33 +01:00
System64
ee046a027e Fix brightness slider choppiness (#372)
* Fix brightness slider choppiness

Make setting the brightness function async to prevent stutter on some devices.

* Fix implicit .begin deprecation warning

---------

Co-authored-by: Amir Dahan <amirdahan24@gmail.com>
Co-authored-by: Erik Reider <35975961+ErikReider@users.noreply.github.com>
2024-03-07 19:44:15 +01:00
Erik Reider
7e4a8a1c5e Close stdout script fd. Fixes #388 2024-02-21 19:51:55 +01:00
Erik Reider
d1f4a0a8eb Updated PKGBUILD checksum 2024-02-10 17:27:11 +01:00
Erik Reider
dd04d9a258 Bumped version to v0.10.0 2024-02-10 17:19:01 +01:00
Erik Reider
8613cdc868 Added demo video to README (#382) 2024-02-10 17:12:27 +01:00
Erik Reider
6a5869244d Adjusted default notification content margins/paddings 2024-02-10 16:18:12 +01:00
Erik Reider
0cd2e8eac9 Fixed group icon sometimes not following the defined icon size 2024-02-10 15:23:01 +01:00
System64
ae0728e2f1 Add control center visibility class (#371)
This addresses https://github.com/ErikReider/SwayNotificationCenter/issues/359
2024-02-10 15:13:10 +01:00
Erik Reider
42b9050950 Replaced README control center picture 2024-01-08 19:01:24 +01:00
1over137
ed1f2dcd53 Fix text color on light theme (#362)
* Apply color on widget title

* Update dnd.scss

* Update style.scss
2024-01-02 11:14:30 +01:00
Erik Reider
bcedc20c36 Fix MPRIS art not resetting when there's no art URI 2023-12-20 21:22:07 +01:00
Erik Reider
831a89d29d Updated README with list of dependencies 2023-12-20 19:31:39 +01:00
Erik Reider
74ba2f375d Fix small SVGs being blurry in notifications 2023-12-20 19:18:18 +01:00
Erik Reider
e498068ef5 Added back support for non-URI image-paths for notifications. Fixes #356 2023-12-20 19:11:09 +01:00
Erik Reider
605f195585 Bumped version requirements for libhandy and gtk-layer-shell (#357)
* Bumped version requirements for libhandy and gtk-layer-shell

* Bumped Ubuntu Action version to 23.10
2023-12-19 20:04:32 +01:00
Erik Reider
bf3baba21a Bumped PKGBUILD-git version 2023-12-19 19:22:58 +01:00
Erik Reider
71e43ecc80 Adjusted default CSS with new default CC background color 2023-12-19 19:00:55 +01:00
Erik Reider
76b3441bb3 Fixed notifications not checking if app-icon is a icon-name. Fixes: #356 2023-12-19 19:00:12 +01:00
Erik Reider
ff04ca3924 Add MPRIS player background blur (#355)
* Add MPRIS player background blur

* Added edge fade

* Only blur cover if the size or source changes
2023-12-19 18:40:23 +01:00
Erik Reider
1fcef5c966 Add app icon above the regular icon (#354)
* Add app icon above the regular icon

* Fixed linting issues

* Tweaked the CSS
2023-12-15 16:21:12 +01:00
Erik Reider
b07a296bf8 Updated default style of notifications and control center 2023-12-15 15:45:49 +01:00
Erik Reider
f68dfa4254 Require vala >= 0.56 2023-12-15 12:16:46 +01:00
Erik Reider
e592a53b0b Tweaked grouped notifications look 2023-12-15 01:10:27 +01:00
Erik Reider
929d0d54b7 Add border-radius property to notification images (#353) 2023-12-15 01:04:49 +01:00
Erik Reider
2d87e22906 Fixed notification border radius being broken with alt actions 2023-12-15 00:10:37 +01:00
Erik Reider
e5de83645c Added gvfs and libnotify 2023-12-14 19:03:56 +01:00
Erik Reider
624e467947 Bumped PKGBUILD pkgrel to 2 2023-12-14 17:21:52 +01:00
Erik Reider
8882b225fd [New Dependency: sassc] Move to SCSS from regular CSS (#352)
* Moved to SCSS from regular CSS

* Fixed Ubuntu build
2023-12-14 17:12:30 +01:00
Erik Reider
a6cf977e90 Minor grouped notifications fixes 2023-12-14 16:50:45 +01:00
Erik Reider
86166a46b7 Grouped notifications (#345)
* Initial animation work

* Added custom icon

* Fixed collapse icon not being symbolic

* Centered collapse button

* Fixed group of 2 notifications being invisible

* Added back notification logic

Notifications are now separated into their own group depending on their provided name

* Added close all button

Also changed the notification close button icon to the new provided icon

* Fixed replacing notifications not working as expected

* Fixed group sensitivity not being set when auto collapsed

* Don't group notis with no provided app-name/desktop-file

Also adds parsing of desktop file in NotiModel which helps with getting and using the display name as the group name

* Remove testing notifications

* General fixes

* Added fade to cc viewport

* Added padding to cc viewport fade

* Call on_expand_change on close all button click

* Updated README

* Sort critical notification groups before regular groups

* Added group title icon

* Fixed not being able to navigate through single notifications

* Scroll to top of group on expand

* Fix non expanded single noti groups being clickable

* Fixed linting issues

* Added styling

* Fixed invalid style reload cast

* Set lower ordered notifications content opacity to 0

* Added hover effect to groups
2023-12-12 21:02:46 +01:00
Erik Reider
305ebf8734 Fixed CC placeholder not showing on startup. Fixes #351 2023-12-11 13:08:24 +01:00
Erik Reider
09c14e4ea9 Don't use missing image icon when noti provides invalid icon name. Fixes #350 2023-12-07 20:52:45 +01:00
peelz
5a23d5eeb5 Add notification hint to bypass dnd/inhibition (#334) 2023-11-28 13:53:07 +01:00
Jannis
ab46163f1e Fix togglebutton in menu dropdown (#339)
* Fix togglebutton in menu dropdown

* Minor bugfixes and fix typo in readme
2023-11-28 06:50:26 +01:00
Shaikh Shafeen
2f5253b72b Update README.md (#343)
"buttons-gird" -> "buttons-grid"
2023-11-28 06:38:29 +01:00
Erik Reider
70eaa5b291 Fixed NotificationWindow sometimes not closing, blocking input 2023-11-22 10:37:25 +01:00
Jannis
b7631c7de5 Add toggle button (#304)
* Add toggle button

* Toggle button documentation

* Fix issues

* Add env variable for toggle button

* Add state update command for toggle buttons

* Fix Uncrustify

* Update README
2023-11-13 00:52:26 +01:00
Carlos &
69e92da659 flush stdout after subscription events (#329) 2023-11-08 19:58:56 +01:00
Daniel Morgan
3ed3ab8fec allow 8 digits in 2fa code regex (#332)
change 2fa regex to allow for 8 digits in a row
2023-11-08 19:56:44 +01:00
Erik Reider
90688d0fe9 Added config option to set cc exclusive zone (#321) 2023-09-27 00:43:54 +02:00
Erik Reider
0d368c2f07 Remove notification replace flicker (#320) 2023-09-26 23:09:51 +02:00
Erik Reider
7de2c71b98 Test parsing markup without any hacks before using said hacks (#319) 2023-09-26 21:17:32 +02:00
Erik Reider
ea66e68783 Simplified script and base_widget command execution (#318) 2023-09-26 18:17:34 +02:00
Erik Reider
237310c627 Fixed notifications list not scrolling when changing focus 2023-09-26 17:04:56 +02:00
Erik Reider
3069f7120d Fixed CC list navigation skipping a notification (#305) 2023-09-26 17:04:13 +02:00
54 changed files with 2920 additions and 909 deletions

View File

@@ -24,7 +24,7 @@ jobs:
- name: Install packages
run: |
pacman-key --init
pacman -Sy
pacman -Syu --noconfirm
- 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 LTS.
name: Check build for latest Ubuntu version.
# Controls when the workflow will run
on:
@@ -14,12 +14,12 @@ on:
workflow_dispatch:
jobs:
ubuntu-LTS-build:
container: ubuntu:22.04
ubuntu-build:
container: ubuntu:23.10
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
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
steps:
- name: Install packages
run: |

109
README.md
View File

@@ -15,10 +15,15 @@ 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)
@@ -38,24 +43,20 @@ 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
@@ -95,12 +96,19 @@ These widgets can be customized, added, removed and even reordered
## Install
### Alpine Linux
```zsh
apk add swaync
````
### Arch
The package is available on the AUR:
```zsh
sudo pacman -S swaync
```
- [swaync](https://aur.archlinux.org/packages/swaync/)
- [swaync-git](https://aur.archlinux.org/packages/swaync-git/)
Alternatively, [swaync-git](https://aur.archlinux.org/packages/swaync-git/) is available on the AUR.
### Fedora
@@ -126,7 +134,7 @@ An **unofficial** ebuild is available in [GURU](https://github.com/gentoo/guru)
```zsh
eselect repository enable guru
emaint sync -r guru
emaint sync --repo guru
emerge --ask gui-apps/swaync
```
@@ -140,7 +148,7 @@ sudo zypper install SwayNotificationCenter
Lunar and later:
```
```zsh
sudo apt install sway-notification-center
```
@@ -149,14 +157,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
```
@@ -172,8 +180,31 @@ 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 build
meson setup build --prefix=/usr
ninja -C build
meson install -C build
```
@@ -226,7 +257,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
- Right click notification: Close notification
- Middle/Right click notification: Close notification
## Configuring
@@ -236,12 +267,40 @@ 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.
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`.
**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
@@ -277,7 +336,7 @@ Notification information can be printed into a terminal by running
Config properties:
```json
```jsonc
{
"scripts": {
"example-script": {
@@ -289,13 +348,13 @@ Config properties:
"category": "Notification category Regex"
}
}
other non scripting properties...
// other non scripting properties...
}
```
`config.json` example:
```json
```jsonc
{
"scripts": {
// This script will only run when Spotify sends a notification containing
@@ -307,7 +366,7 @@ Config properties:
"body": "Rick Astley - Whenever You Need Somebody"
}
}
other non scripting properties...
// other non scripting properties...
}
```
@@ -379,9 +438,9 @@ Waybar css file
Alternatively, the number of notifications can be shown by adding `{}` anywhere in the `format` field in the Waybar config
```json
```jsonc
"custom/notification": {
"format": "{} {icon}",
...
// ...
},
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -1,7 +1,7 @@
# vim: ft=PKGBUILD
# vim: ft=sh
# Maintainer: Erik Reider <erik.reider@protonmail.com>
pkgname=swaync
pkgver=0.9.0
pkgver=0.10.1
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")
depends=("gtk3" "gtk-layer-shell" "dbus" "glib2" "gobject-introspection" "libgee" "json-glib" "libhandy" "libpulse" "gvfs" "libnotify" "granite")
conflicts=("swaync" "swaync-client")
provides=("swaync" "swaync-client" "notification-daemon")
makedepends=(vala meson git scdoc)
makedepends=("vala>=0.56" meson git scdoc sassc)
source=("${_pkgfoldername}-${pkgver}.tar.gz::${url}/archive/v${pkgver}.tar.gz")
sha256sums=('3f00bc858b7b3610e88ef0f6ee64d727892dd82f280f1dfc01dde863c2ea3376')
sha256sums=('5586d8a679dde5e530cb8b6f0c86abdd0d5e41362fc1c4e56e2211edea0f7a13')
build() {
arch-meson "${_pkgfoldername}-${pkgver}" build -Dscripting=true

View File

@@ -1,8 +1,8 @@
# vim: ft=PKGBUILD
# vim: ft=sh
# Maintainer: Erik Reider <erik.reider@protonmail.com>
pkgname=swaync-git
_pkgname=swaync
pkgver=r448.ba4a266
pkgver=r517.4275fa3
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" )
depends=("gtk3" "gtk-layer-shell" "dbus" "glib2" "gobject-introspection" "libgee" "json-glib" "libhandy" "libpulse" "gvfs" "libnotify" "granite")
conflicts=("swaync" "swaync-client")
provides=("swaync" "swaync-client" "notification-daemon")
makedepends=(vala meson git scdoc)
makedepends=("vala>=0.56" meson git scdoc sassc)
source=("$_pkgname::git+$url")
sha256sums=('SKIP')

View File

@@ -2,7 +2,7 @@
%global alt_pkg_name swaync
Name: {{{ git_dir_name }}}
Version: 0.9.0
Version: 0.10.1
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
BuildRequires: vala >= 0.56
BuildRequires: scdoc
BuildRequires: pkgconfig(gtk+-3.0) >= 3.22
BuildRequires: pkgconfig(gtk-layer-shell-0) >= 0.1
@@ -26,9 +26,14 @@ 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}

4
data/icons/meson.build Normal file
View File

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

View File

@@ -0,0 +1,4 @@
<?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>

After

Width:  |  Height:  |  Size: 767 B

View File

@@ -0,0 +1,5 @@
<?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>

After

Width:  |  Height:  |  Size: 830 B

View File

@@ -0,0 +1,7 @@
<?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,9 +2,35 @@ 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())

347
data/style/style.scss Normal file
View File

@@ -0,0 +1,347 @@
@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

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

View File

@@ -0,0 +1,18 @@
.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

@@ -0,0 +1,19 @@
.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

@@ -0,0 +1,16 @@
.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

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

View File

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

@@ -0,0 +1,47 @@
@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

@@ -0,0 +1,17 @@
.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

@@ -0,0 +1,18 @@
.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
*Right click notification*: Close notification
*Middle/Right click notification*: Close notification
# CONFIGURATION

View File

@@ -68,6 +68,13 @@ 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 ++
@@ -370,6 +377,22 @@ config file to be able to detect config errors
type: string ++
default: "" ++
description: "Command to be executed on click" ++
type: ++
type: string ++
default: "normal" ++
description: Type of the button. ++
Toggle buttons receive the '.active' css class ++
enum: ["normal", "toggle"] ++
update-command: ++
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: A list of actions containing a label and a command ++
description: A button to reveal a dropdown with action-buttons ++
buttons#<name>: ++
@@ -395,6 +418,24 @@ config file to be able to detect config errors
type: string ++
default: "" ++
description: "Command to be executed on click" ++
type: ++
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 ++
enum: ["normal", "toggle"] ++
update-command: ++
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: 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*++
@@ -415,6 +456,18 @@ config file to be able to detect config errors
type: string ++
default: "" ++
description: "Command to be executed on click" ++
type: ++
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 ++
enum: ["normal", "toggle"] ++
active: ++
type: bool ++
default: false ++
description: Wether the toggle button is active as default 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.9.0',
version: '0.10.1',
meson_version: '>= 0.59.0',
default_options: [ 'warning_level=2' ],
)
@@ -9,6 +9,11 @@ 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

@@ -0,0 +1,150 @@
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,8 +77,9 @@ 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"
.printf (count, dnd.to_string (), cc_open.to_string (), inhibited.to_string ()));
"{ \"count\": %u, \"dnd\": %s, \"visible\": %s, \"inhibited\": %s }\n",
count, dnd.to_string (), cc_open.to_string (), inhibited.to_string ());
stdout.flush ();
}
private void print_subscribe () {

View File

@@ -74,6 +74,17 @@
"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,44 +237,18 @@ namespace SwayNotificationCenter {
public ScriptRunOnType run_on { get; set; default = ScriptRunOnType.RECEIVE; }
public async bool run_script (NotifyParams param, out string msg) {
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 ?? "");
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 ?? "");
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;
}
return yield Functions.execute_command (exec, spawn_env, out msg);
}
public override bool matches_notification (NotifyParams param) {
@@ -493,6 +467,10 @@ 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,6 +75,11 @@
"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",
@@ -87,7 +92,7 @@
},
"notification-icon-size": {
"type": "integer",
"description": "The notification icon size (in pixels)",
"description": "The notification icon size (in pixels). The app icon size is 1/3",
"default": 64,
"minimum": 16
},
@@ -152,7 +157,7 @@
},
"image-visibility": {
"type": "string",
"description": "An explanation about the purpose of this instance.",
"description": "The notification image visibility when no icon is available.",
"default": "when-available",
"enum": ["always", "when-available", "never"]
},
@@ -397,8 +402,14 @@
},
"image-radius": {
"type": "integer",
"description": "The border radius of the album art",
"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",
"default": 12
},
"blur": {
"type": "boolean",
"description": "Appy the artwork as the MPRIS background and blur it",
"default": true
}
}
},
@@ -422,6 +433,22 @@
"type": "string",
"description": "Command to be executed on click",
"default": ""
},
"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",
"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
}
}
}

View File

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

View File

@@ -20,89 +20,84 @@
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<object class="GtkBox" id="notifications_box">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hscrollbar-policy">never</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkViewport" id="viewport">
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="vexpand">True</property>
<property name="shadow-type">none</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkStack" id="stack">
<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">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</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="GtkBox">
<object class="GtkImage">
<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>
<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>
<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="name">notifications-placeholder</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkListBox" id="list_box">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</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>
<property name="label" translatable="yes">No Notifications</property>
</object>
<packing>
<property name="name">notifications-list</property>
<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="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">True</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>

View File

@@ -1,18 +1,30 @@
namespace SwayNotificationCenter {
[GtkTemplate (ui = "/org/erikreider/sway-notification-center/controlCenter/controlCenter.ui")]
public class ControlCenter : Gtk.ApplicationWindow {
[GtkChild]
unowned Gtk.ScrolledWindow scrolled_window;
[GtkChild]
unowned Gtk.Viewport viewport;
unowned Gtk.Box notifications_box;
[GtkChild]
unowned Gtk.Stack stack;
[GtkChild]
unowned Gtk.ListBox list_box;
unowned Gtk.ScrolledWindow scrolled_window;
FadedViewport viewport = new FadedViewport (20);
Gtk.ListBox list_box = new Gtk.ListBox ();
[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";
@@ -23,9 +35,8 @@ namespace SwayNotificationCenter {
private SwayncDaemon swaync_daemon;
private NotiDaemon noti_daemon;
private uint list_position = 0;
private int list_position = 0;
private double last_upper = 0;
private bool list_reverse = false;
private Gtk.Align list_align = Gtk.Align.START;
@@ -38,6 +49,18 @@ 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");
@@ -54,8 +77,6 @@ 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
@@ -152,72 +173,10 @@ namespace SwayNotificationCenter {
return true;
});
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;
});
key_press_event.connect (key_press_event_cb);
// Switches the stack page depending on the
stack.set_visible_child_name (STACK_PLACEHOLDER_PAGE);
// Switches the stack page depending on the amount of notifications
list_box.add.connect (() => {
stack.set_visible_child_name (STACK_NOTIFICATIONS_PAGE);
});
@@ -228,6 +187,171 @@ 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 */
@@ -242,11 +366,11 @@ namespace SwayNotificationCenter {
if (w.length == 0) w = DEFAULT_WIDGETS;
bool has_notification = false;
foreach (string key in w) {
// Reposition the scrolled_window
// Reposition the notifications_box
if (key == "notifications") {
has_notification = true;
uint pos = box.get_children ().length ();
box.reorder_child (scrolled_window, (int) (pos > 0 ? --pos : 0));
box.reorder_child (notifications_box, (int) (pos > 0 ? --pos : 0));
continue;
}
// Add the widget if it is valid
@@ -260,23 +384,22 @@ 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 (scrolled_window, (int) (pos > 0 ? --pos : 0));
box.reorder_child (notifications_box, (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;
@@ -351,28 +474,37 @@ namespace SwayNotificationCenter {
box.set_valign (align_y);
list_box.set_valign (list_align);
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;
});
list_box.set_sort_func (list_box_sort_func);
// Always set the size request in all events.
box.set_size_request (ConfigModel.instance.control_center_width,
ConfigModel.instance.control_center_height);
}
private void size_alloc () {
var adj = viewport.vadjustment;
double upper = adj.get_upper ();
if (last_upper < upper) {
scroll_to_start (list_reverse);
/**
* 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;
}
last_upper = upper;
// 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;
}
private void scroll_to_start (bool reverse) {
@@ -384,20 +516,26 @@ namespace SwayNotificationCenter {
}
public uint notification_count () {
return list_box.get_children ().length ();
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;
}
public void close_all_notifications () {
foreach (var w in list_box.get_children ()) {
Notification noti = (Notification) w;
if (noti != null) noti.close_notification (false);
NotificationGroup group = (NotificationGroup) w;
if (group != null) group.close_all_notifications ();
}
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");
}
@@ -407,11 +545,20 @@ namespace SwayNotificationCenter {
}
}
private void navigate_list (uint i) {
var widget = list_box.get_children ().nth_data (i);
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);
}
}
if (widget != null) {
list_box.set_focus_child (widget);
widget.grab_focus ();
list_box.set_focus_child (widget);
}
}
@@ -424,20 +571,24 @@ namespace SwayNotificationCenter {
if (this.visible) {
// Focus the first notification
list_position = list_reverse ?
(list_box.get_children ().length () - 1) : 0;
(((int) 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 noti = (Notification) w;
if (noti != null) noti.set_time ();
var group = (NotificationGroup) w;
if (group != null) group.update ();
}
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 () {
@@ -455,41 +606,114 @@ namespace SwayNotificationCenter {
on_visibility_change ();
}
public void close_notification (uint32 id, bool replaces = false) {
foreach (var w in list_box.get_children ()) {
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 ()) {
var noti = (Notification) w;
if (noti != null && noti.param.applied_id == id) {
if (replaces) {
if (!dismiss) {
noti.remove_noti_timeout ();
noti.destroy ();
} else {
noti.close_notification (false);
list_box.remove (w);
group.remove_notification (noti);
}
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 add_notification (NotifyParams param,
NotiDaemon noti_daemon) {
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) {
var noti = new Notification.regular (param,
noti_daemon,
NotificationType.CONTROL_CENTER);
noti.grab_focus.connect ((w) => {
uint i = list_box.get_children ().index (w);
int i = list_box.get_children ().index (w);
if (list_position != uint.MAX && list_position != i) {
list_position = i;
}
});
noti.set_time ();
list_box.add (noti);
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 ();
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");
}
@@ -506,8 +730,13 @@ 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 ()) {
Notification noti = (Notification) c;
if (noti != null) noti.reload_style_context ();
NotificationGroup group = (NotificationGroup) c;
if (group != null) {
foreach (unowned var widget in group.get_notifications ()) {
Notification noti = (Notification) widget;
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 ((float) slider.get_value ());
this.client.set_brightness.begin ((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 void set_brightness (string subsystem,
public abstract async 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 void set_brightness (float percent) {
public async void set_brightness (float percent) {
this.close ();
try {
if (subsystem == "backlight") {
int actual = calc_actual (percent);
login1.set_brightness (subsystem, device, actual);
login1.set_brightness.begin (subsystem, device, actual);
} else {
login1.set_brightness (subsystem, device, (uint32) percent);
login1.set_brightness.begin (subsystem, device, (uint32) percent);
}
} catch (Error e) {
error ("Error %s\n", e.message);

View File

@@ -1,5 +1,3 @@
using Posix;
namespace SwayNotificationCenter.Widgets {
public abstract class BaseWidget : Gtk.Box {
public abstract string widget_name { get; }
@@ -16,6 +14,20 @@ namespace SwayNotificationCenter.Widgets {
public unowned SwayncDaemon swaync_daemon;
public unowned NotiDaemon noti_daemon;
public enum ButtonType {
TOGGLE,
NORMAL;
public static ButtonType parse (string value) {
switch (value) {
case "toggle":
return ButtonType.TOGGLE;
default:
return ButtonType.NORMAL;
}
}
}
protected BaseWidget (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) {
this.suffix = suffix;
this.key = widget_name + (suffix.length > 0 ? "#%s".printf (suffix) : "");
@@ -93,25 +105,26 @@ namespace SwayNotificationCenter.Widgets {
for (int i = 0; i < actions.get_length (); i++) {
string label = actions.get_object_element (i).get_string_member_with_default ("label", "label");
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);
res[i] = Action () {
label = label,
command = command
command = command,
type = type,
update_command = update_command,
active = active
};
}
return res;
}
protected 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);
protected async void execute_command (string cmd, string[] env_additions = {}) {
string msg = "";
yield Functions.execute_command (cmd, env_additions, out msg);
}
}
}

View File

@@ -10,6 +10,7 @@ namespace SwayNotificationCenter.Widgets {
}
Action[] actions;
List<ToggleButton> toggle_buttons;
public ButtonsGrid (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) {
base (suffix, swaync_daemon, noti_daemon);
@@ -26,14 +27,29 @@ namespace SwayNotificationCenter.Widgets {
// add action to container
foreach (var act in actions) {
Gtk.Button b = new Gtk.Button.with_label (act.label);
b.clicked.connect (() => execute_command (act.command));
container.insert (b, -1);
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;
}
}
show_all ();
}
public override void on_cc_visibility_change (bool value) {
if (value) {
foreach (var tb in toggle_buttons) {
tb.on_update.begin ();
}
}
}
}
}

View File

@@ -25,6 +25,9 @@ namespace SwayNotificationCenter.Widgets {
public struct Action {
string ? label;
string ? command;
BaseWidget.ButtonType ? type;
string ? update_command;
bool ? active;
}
public class Menubar : BaseWidget {
@@ -38,6 +41,7 @@ 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);
@@ -74,11 +78,18 @@ namespace SwayNotificationCenter.Widgets {
if (obj.name != null) container.get_style_context ().add_class (obj.name);
foreach (Action a in obj.actions) {
Gtk.Button b = new Gtk.Button.with_label (a.label);
b.clicked.connect (() => execute_command (a.command));
container.add (b);
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;
}
}
switch (obj.position) {
case Position.LEFT:
@@ -110,9 +121,18 @@ namespace SwayNotificationCenter.Widgets {
});
foreach (var a in obj.actions) {
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 (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;
}
}
switch (obj.position) {
@@ -210,6 +230,10 @@ 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,6 +2,7 @@ namespace SwayNotificationCenter.Widgets.Mpris {
public struct Config {
int image_size;
int image_radius;
bool blur;
}
public class Mpris : BaseWidget {
@@ -11,6 +12,8 @@ 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);
@@ -26,6 +29,7 @@ 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) {
@@ -53,9 +57,8 @@ namespace SwayNotificationCenter.Widgets.Mpris {
carousel = new Hdy.Carousel () {
visible = true,
};
#if HAVE_LATEST_LIBHANDY
carousel.allow_scroll_wheel = true;
#endif
carousel.draw.connect (carousel_draw_cb);
carousel.page_changed.connect ((index) => {
GLib.List<weak Gtk.Widget> children = carousel.get_children ();
int children_length = (int) children.length ();
@@ -91,6 +94,11 @@ 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 ();
@@ -101,6 +109,51 @@ 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

@@ -22,6 +22,9 @@
<property name="icon-name">audio-x-generic-symbolic</property>
<property name="use-fallback">True</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,6 +38,9 @@ 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) {
@@ -93,12 +96,84 @@ 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) {
@@ -246,7 +321,6 @@ 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 ();
@@ -255,34 +329,79 @@ namespace SwayNotificationCenter.Widgets.Mpris {
InputStream stream = yield file.read_async (Priority.DEFAULT,
album_art_cancellable);
pixbuf = yield new Gdk.Pixbuf.from_stream_async (
this.album_art_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 (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);
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);
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, mpris_config.image_size);
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
album_art.set_from_icon_name ("audio-x-generic-symbolic",
mpris_config.image_size);
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);
}
}
}

View File

@@ -0,0 +1,47 @@
namespace SwayNotificationCenter.Widgets {
class ToggleButton : Gtk.ToggleButton {
private string command;
private string update_command;
private ulong handler_id;
public ToggleButton (string label, string command, string update_command, bool active) {
this.command = command;
this.update_command = update_command;
this.label = label;
if (active) {
this.active = true;
}
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);
}
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);
}
}
}
}

View File

@@ -0,0 +1,162 @@
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,42 +8,53 @@ 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_path (owned string path,
Gtk.Image img,
int icon_size,
bool file_exists) {
if ((path.length > 6 && path.slice (0, 7) == "file://") || file_exists) {
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) {
// Try as a URI (file:// is the only URI schema supported right now)
try {
if (!file_exists) path = path.slice (7, path.length);
if (is_uri) uri = uri.slice (PREFIX_SIZE, uri.length);
var pixbuf = new Gdk.Pixbuf.from_file_at_scale (
path,
uri,
icon_size * img.scale_factor,
icon_size * img.scale_factor,
true);
var surface = Gdk.cairo_surface_create_from_pixbuf (
pixbuf,
img.scale_factor,
img.get_window ());
// Scale and round the image. Scales to fit the size
var surface = scale_round_pixbuf (pixbuf,
icon_size,
icon_size,
img.scale_factor,
radius);
img.set_from_surface (surface);
return;
} catch (Error e) {
stderr.printf (e.message + "\n");
}
} 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);
}
// Try as icon name
if (img.storage_type == Gtk.ImageType.EMPTY) {
img.set_from_icon_name (uri, Gtk.IconSize.INVALID);
}
}
public static void set_image_data (ImageData data, Gtk.Image img, int icon_size) {
public static void set_image_data (ImageData data,
Gtk.Image img,
int icon_size,
int radius) {
// Rebuild and scale the image
var pixbuf = new Gdk.Pixbuf.with_unowned_data (data.data,
Gdk.Colorspace.RGB,
@@ -54,14 +65,11 @@ namespace SwayNotificationCenter {
data.rowstride,
null);
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 ());
var surface = scale_round_pixbuf (pixbuf,
icon_size,
icon_size,
img.scale_factor,
radius);
img.set_from_surface (surface);
}
@@ -211,12 +219,15 @@ namespace SwayNotificationCenter {
return type;
}
/** 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) {
/** 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));
Cairo.Surface surface = new Cairo.ImageSurface (Cairo.Format.ARGB32,
buffer_width,
buffer_height);
@@ -230,12 +241,31 @@ 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_rgb (0, 0, 0);
cr.set_source_rgba (0, 0, 0, 0);
cr.clip ();
cr.paint ();
cr.save ();
Cairo.Surface scale_surf = Gdk.cairo_surface_create_from_pixbuf (pixbuf,
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,
img_scale,
null);
int width = pixbuf.width / img_scale;
@@ -245,23 +275,41 @@ 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, scale_surf);
draw_scale_wide (buffer_width, width, buffer_height, height, cr, base_surf);
} else {
draw_scale_tall (buffer_width, width, buffer_height, height, cr, scale_surf);
draw_scale_tall (buffer_width, width, buffer_height, height, cr, base_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, scale_surf);
draw_scale_tall (buffer_width, width, buffer_height, height, cr, base_surf);
} else {
draw_scale_wide (buffer_width, width, buffer_height, height, cr, scale_surf);
draw_scale_wide (buffer_width, width, buffer_height, height, cr, base_surf);
}
}
cr.paint ();
cr.restore ();
scale_surf.finish ();
return Gdk.pixbuf_get_from_surface (surface, 0, 0, buffer_width, buffer_height);
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;
}
private static void draw_scale_tall (int buffer_width,
@@ -299,5 +347,68 @@ 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

@@ -26,6 +26,7 @@ widget_sources = [
# Helpers
'controlCenter/widgets/baseWidget.vala',
'controlCenter/widgets/factory.vala',
'controlCenter/widgets/shared/toggleButton.vala',
# Widget: Title
'controlCenter/widgets/title/title.vala',
# Widget: Dnd
@@ -49,13 +50,16 @@ 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',
@@ -63,13 +67,19 @@ 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.2.3'),
meson.get_compiler('c').find_library('gtk-layer-shell'),
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'
),
meson.get_compiler('c').find_library('m', required : true),
meson.get_compiler('vala').find_library('posix'),
dependency('gee-0.8'),
@@ -97,36 +107,18 @@ 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',
]
sysconfdir = get_option('sysconfdir')
gnome = import('gnome')
app_sources += gnome.compile_resources('sway_notification_center-resources',
app_resources += gnome.compile_resources('sway_notification_center-resources',
'sway_notification_center.gresource.xml',
c_name: 'sway_notification_center'
)
executable('swaync',
app_sources,
[ app_sources, app_resources ],
vala_args: args,
dependencies: app_deps,
install: true,
@@ -139,8 +131,6 @@ 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(
@@ -149,6 +139,5 @@ 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, false);
NotificationWindow.instance.close_notification (id, true);
if (!timeout) {
control_center.close_notification (id);
control_center.close_notification (id, true);
NotificationClosed (id, ClosedReasons.DISMISSED);
swaync_daemon.subscribe_v2 (control_center.notification_count (),
@@ -170,11 +170,10 @@ namespace SwayNotificationCenter {
debug ("Notification: %s\n", param.to_string ());
// Replace notification logic
// Get the notification id to replace
uint32 replace_notification = 0;
if (id == replaces_id) {
param.replaces = true;
NotificationWindow.instance.close_notification (id, true);
control_center.close_notification (id, true);
replace_notification = id;
} else if (param.synchronous != null
&& param.synchronous.length > 0) {
// Tries replacing without replaces_id instead
@@ -182,29 +181,47 @@ namespace SwayNotificationCenter {
// if there is any notification to replace
if (synchronous_ids.lookup_extended (
param.synchronous, null, out r_id)) {
param.replaces = true;
// Close the notification
NotificationWindow.instance.close_notification (r_id, true);
control_center.close_notification (r_id, true);
replace_notification = r_id;
}
synchronous_ids.set (param.synchronous, id);
}
// 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);
}
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);
}
} 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) {
control_center.add_notification (param, this);
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);
}
#if WANT_SCRIPTING
@@ -292,8 +309,8 @@ namespace SwayNotificationCenter {
*/
[DBus (name = "CloseNotification")]
public void close_notification (uint32 id) throws DBusError, IOError {
NotificationWindow.instance.close_notification (id, false);
control_center.close_notification (id);
NotificationWindow.instance.close_notification (id, true);
control_center.close_notification (id, true);
NotificationClosed (id, ClosedReasons.CLOSED_BY_CLOSENOTIFICATION);
}

View File

@@ -110,10 +110,16 @@ 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; }
/** If the notification replaces another */
public bool replaces { get; set; }
public DesktopAppInfo ? desktop_app_info = null;
public string name_id;
public string display_name;
public NotifyParams (uint32 applied_id,
string app_name,
@@ -134,12 +140,37 @@ 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 () {
@@ -151,6 +182,11 @@ 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,15 +60,37 @@
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkImage" id="img">
<object class="GtkOverlay">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin-end">12</property>
<property name="icon_size">6</property>
<style>
<class name="image"/>
</style>
<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>
</object>
<packing>
<property name="expand">False</property>
@@ -81,7 +103,6 @@
<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>
@@ -153,6 +174,9 @@
<property name="position">1</property>
</packing>
</child>
<style>
<class name="text-box"/>
</style>
</object>
<packing>
<property name="expand">True</property>
@@ -281,7 +305,7 @@
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">window-close-symbolic</property>
<property name="icon-name">swaync-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 = new Gtk.ButtonBox (Gtk.Orientation.HORIZONTAL);
private Gtk.ButtonBox ? alt_actions_box = null;
[GtkChild]
unowned Gtk.Label summary;
@@ -41,6 +41,8 @@ namespace SwayNotificationCenter {
[GtkChild]
unowned Gtk.Image img;
[GtkChild]
unowned Gtk.Image img_app_icon;
[GtkChild]
unowned Gtk.Image body_image;
// Inline Reply
@@ -70,8 +72,8 @@ namespace SwayNotificationCenter {
public bool is_timed { get; construct; default = false; }
public NotifyParams param { get; construct; }
public NotiDaemon noti_daemon { get; construct; }
public NotifyParams param { get; private set; }
public unowned NotiDaemon noti_daemon { get; construct; }
public NotificationType notification_type {
get;
@@ -112,8 +114,9 @@ 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 */
@@ -124,7 +127,6 @@ 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,
@@ -132,11 +134,13 @@ 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,7})(?= |$|\\.|,)",
code_regex = new Regex ("(?<= |^)(\\d{3}(-| )\\d{3}|\\d{4,8})(?= |$|\\.|,)",
RegexCompileFlags.MULTILINE);
string joined_tags = string.joinv ("|", TAGS);
tag_regex = new Regex ("&lt;(/?(?:%s))&gt;".printf (joined_tags));
@@ -196,12 +200,85 @@ namespace SwayNotificationCenter {
});
this.transition_time = ConfigModel.instance.transition_time;
build_noti ();
if (is_timed) {
///
/// 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);
add_notification_timeout ();
this.size_allocate.connect (on_size_allocation);
}
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);
});
}
private void default_action_update_state () {
@@ -235,50 +312,8 @@ 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);
@@ -295,20 +330,9 @@ namespace SwayNotificationCenter {
this.carousel_empty_widget_index = 0;
break;
}
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
// Reset state
this.carousel.scroll_to (event_box);
this.carousel.allow_scroll_wheel = false;
#endif
if (this.progress_bar.visible = param.has_synch) {
this.progress_bar.set_fraction (param.value * 0.01);
@@ -322,13 +346,16 @@ namespace SwayNotificationCenter {
this.show ();
if (param.replaces) {
Timeout.add (0, () => {
this.revealer.set_reveal_child (true);
} else {
Timeout.add (0, () => {
this.revealer.set_reveal_child (true);
return Source.REMOVE;
});
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);
}
}
@@ -337,6 +364,9 @@ 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 {
@@ -374,22 +404,29 @@ 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;
Pango.parse_markup (escaped, -1, 0, out attr, out 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);
}
this.body.set_text (buf);
if (attr != null) this.body.set_attributes (attr);
@@ -421,6 +458,7 @@ 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;
@@ -452,6 +490,11 @@ 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");
@@ -467,6 +510,8 @@ 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
@@ -494,32 +539,23 @@ 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);
@@ -614,59 +650,104 @@ 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_path (
var img_path_exists = File.new_for_uri (
param.image_path ?? "").query_exists ();
var app_icon_exists = File.new_for_path (
param.app_icon ?? "").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 ();
}
// 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);
notification_icon_size, radius);
} else if (param.image_path != null &&
param.image_path != "" &&
img_path_exists) {
Functions.set_image_path (param.image_path, img,
Functions.set_image_uri (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);
} else {
notification_icon_size, radius);
}
if (img.storage_type == Gtk.ImageType.EMPTY) {
// Get the app icon
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);
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);
} 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

@@ -0,0 +1,586 @@
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,6 +151,7 @@ namespace SwayNotificationCenter {
public void change_visibility (bool value) {
if (!value) {
close_all_notifications ();
close ();
} else {
this.set_anchor ();
}
@@ -170,10 +171,9 @@ namespace SwayNotificationCenter {
}
}
private void remove_notification (Notification ? noti, bool replaces) {
private void remove_notification (Notification ? noti, bool dismiss) {
// 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,12 +183,11 @@ namespace SwayNotificationCenter {
this, GtkLayerShell.KeyboardMode.NONE);
}
}
#endif
noti.remove_noti_timeout ();
noti.destroy ();
}
if (!replaces
if (dismiss
&& (!get_realized ()
|| !get_mapped ()
|| !(get_child () is Gtk.Widget)
@@ -198,15 +197,13 @@ namespace SwayNotificationCenter {
}
}
public void add_notification (NotifyParams param,
NotiDaemon noti_daemon) {
public void add_notification (NotifyParams param) {
var noti = new Notification.timed (param,
noti_daemon,
swaync_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);
@@ -216,7 +213,6 @@ namespace SwayNotificationCenter {
this, GtkLayerShell.KeyboardMode.ON_DEMAND);
}
}
#endif
if (list_reverse) {
box.pack_start (noti);
@@ -234,16 +230,31 @@ namespace SwayNotificationCenter {
scroll_to_start (list_reverse);
}
public void close_notification (uint32 id, bool replaces) {
public void close_notification (uint32 id, bool dismiss) {
foreach (var w in box.get_children ()) {
var noti = (Notification) w;
if (noti != null && noti.param.applied_id == id) {
remove_notification (noti, replaces);
remove_notification (noti, dismiss);
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;

View File

@@ -1,361 +0,0 @@
/*
* 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;
}
.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);
noti_daemon.control_center.close_notification (id, true);
}
/**