5 Commits
main ... dev

54 changed files with 979 additions and 2865 deletions

View File

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

View File

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

109
README.md
View File

@@ -15,15 +15,10 @@ support `wlr_layer_shell_unstable_v1` like Sway or anything wlroots based*
only tested with the default GTK **Adwaita** theme. Usage of any third-party only tested with the default GTK **Adwaita** theme. Usage of any third-party
theme might require extra tweaks to the default CSS style file* 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 Table of Contents
================= =================
* [Screenshots](#screenshots)
* [Want to show off your sick config?](#want-to-show-off-your-sick-config) * [Want to show off your sick config?](#want-to-show-off-your-sick-config)
* [Features](#features) * [Features](#features)
* [Available Widgets](#available-widgets) * [Available Widgets](#available-widgets)
@@ -43,20 +38,24 @@ Table of Contents
* [Run](#run) * [Run](#run)
* [Control Center Shortcuts](#control-center-shortcuts) * [Control Center Shortcuts](#control-center-shortcuts)
* [Configuring](#configuring) * [Configuring](#configuring)
* [Toggle Buttons](#toggle-buttons)
* [Notification Inhibition](#notification-inhibition) * [Notification Inhibition](#notification-inhibition)
* [Scripting](#scripting) * [Scripting](#scripting)
* [Disable scripting](#disable-scripting) * [Disable scripting](#disable-scripting)
* [i3status-rs Example](#i3status-rs-example) * [i3status-rs Example](#i3status-rs-example)
* [Waybar Example](#waybar-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? ## Want to show off your sick config?
Post your setup here: [Config flex 💪](https://github.com/ErikReider/SwayNotificationCenter/discussions/183) Post your setup here: [Config flex 💪](https://github.com/ErikReider/SwayNotificationCenter/discussions/183)
## Features ## Features
- Grouped notifications
- Keyboard shortcuts - Keyboard shortcuts
- Notification body markup with image support - Notification body markup with image support
- Inline replies - Inline replies
@@ -96,19 +95,12 @@ These widgets can be customized, added, removed and even reordered
## Install ## Install
### Alpine Linux
```zsh
apk add swaync
````
### Arch ### Arch
```zsh The package is available on the AUR:
sudo pacman -S swaync
```
Alternatively, [swaync-git](https://aur.archlinux.org/packages/swaync-git/) is available on the AUR. - [swaync](https://aur.archlinux.org/packages/swaync/)
- [swaync-git](https://aur.archlinux.org/packages/swaync-git/)
### Fedora ### Fedora
@@ -134,7 +126,7 @@ An **unofficial** ebuild is available in [GURU](https://github.com/gentoo/guru)
```zsh ```zsh
eselect repository enable guru eselect repository enable guru
emaint sync --repo guru emaint sync -r guru
emerge --ask gui-apps/swaync emerge --ask gui-apps/swaync
``` ```
@@ -148,7 +140,7 @@ sudo zypper install SwayNotificationCenter
Lunar and later: Lunar and later:
```zsh ```
sudo apt install sway-notification-center sudo apt install sway-notification-center
``` ```
@@ -157,14 +149,14 @@ sudo apt install sway-notification-center
Bookworm and later: Bookworm and later:
```zsh ```
sudo apt install sway-notification-center sudo apt install sway-notification-center
``` ```
### Guix ### Guix
The simplest way is to install it to user's profile: The simplest way is to install it to user's profile:
```zsh ```
guix install swaynotificationcenter guix install swaynotificationcenter
``` ```
@@ -180,31 +172,8 @@ But we recommend to use [Guix Home](https://guix.gnu.org/manual/devel/en/html_no
### Other ### 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 ```zsh
meson setup build --prefix=/usr meson build
ninja -C build ninja -C build
meson install -C build meson install -C build
``` ```
@@ -257,7 +226,7 @@ To reload css after changes
- Shift+D: Toggle Do Not Disturb - Shift+D: Toggle Do Not Disturb
- Buttons 1-9: Execute alternative actions - Buttons 1-9: Execute alternative actions
- Left click button / actions: Activate notification action - Left click button / actions: Activate notification action
- Middle/Right click notification: Close notification - Right click notification: Close notification
## Configuring ## Configuring
@@ -267,40 +236,12 @@ See `swaync(5)` man page for more information
To reload the config, you'll need to run `swaync-client --reload-config` 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 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 to your `~/.config/swaync/` folder to customize without needing root access.
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. 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 ## Notification Inhibition
Notifications can be inhibited through the provided `swaync-client` executable Notifications can be inhibited through the provided `swaync-client` executable
@@ -336,7 +277,7 @@ Notification information can be printed into a terminal by running
Config properties: Config properties:
```jsonc ```json
{ {
"scripts": { "scripts": {
"example-script": { "example-script": {
@@ -348,13 +289,13 @@ Config properties:
"category": "Notification category Regex" "category": "Notification category Regex"
} }
} }
// other non scripting properties... other non scripting properties...
} }
``` ```
`config.json` example: `config.json` example:
```jsonc ```json
{ {
"scripts": { "scripts": {
// This script will only run when Spotify sends a notification containing // This script will only run when Spotify sends a notification containing
@@ -366,7 +307,7 @@ Config properties:
"body": "Rick Astley - Whenever You Need Somebody" "body": "Rick Astley - Whenever You Need Somebody"
} }
} }
// other non scripting properties... other non scripting properties...
} }
``` ```
@@ -438,9 +379,9 @@ Waybar css file
Alternatively, the number of notifications can be shown by adding `{}` anywhere in the `format` field in the Waybar config Alternatively, the number of notifications can be shown by adding `{}` anywhere in the `format` field in the Waybar config
```jsonc ```json
"custom/notification": { "custom/notification": {
"format": "{} {icon}", "format": "{} {icon}",
// ... ...
}, },
``` ```

BIN
assets/desktop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

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

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 767 B

View File

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

Before

Width:  |  Height:  |  Size: 830 B

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +0,0 @@
.widget-buttons-grid {
padding: 8px;
margin: 8px;
border-radius: 12px;
background-color: #{"@noti-bg"};
}
.widget-buttons-grid > flowbox > flowboxchild > button {
background: #{"@noti-bg"};
border-radius: 12px;
}
.widget-buttons-grid > flowbox > flowboxchild > button.toggle:checked {
/* style given to the active toggle button */
}
.widget-buttons-grid > flowbox > flowboxchild > button:hover {
}

View File

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

View File

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

View File

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

View File

@@ -1,26 +0,0 @@
.widget-menubar > box > .menu-button-bar > button {
border: none;
background: transparent;
}
/* .AnyName { Name defined in config after #
background-color: @noti-bg;
padding: 8px;
margin: 8px;
border-radius: 12px;
}
.AnyName>button {
background: transparent;
border: none;
}
.AnyName>button:hover {
background-color: @noti-bg-hover;
} */
.topbar-buttons > button {
/* Name defined in config after # */
border: none;
background: transparent;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,30 +1,18 @@
namespace SwayNotificationCenter { namespace SwayNotificationCenter {
[GtkTemplate (ui = "/org/erikreider/sway-notification-center/controlCenter/controlCenter.ui")] [GtkTemplate (ui = "/org/erikreider/sway-notification-center/controlCenter/controlCenter.ui")]
public class ControlCenter : Gtk.ApplicationWindow { public class ControlCenter : Gtk.ApplicationWindow {
[GtkChild] [GtkChild]
unowned Gtk.Box notifications_box; unowned Gtk.ScrolledWindow scrolled_window;
[GtkChild]
unowned Gtk.Viewport viewport;
[GtkChild] [GtkChild]
unowned Gtk.Stack stack; unowned Gtk.Stack stack;
[GtkChild] [GtkChild]
unowned Gtk.ScrolledWindow scrolled_window; unowned Gtk.ListBox list_box;
FadedViewport viewport = new FadedViewport (20);
Gtk.ListBox list_box = new Gtk.ListBox ();
[GtkChild] [GtkChild]
unowned Gtk.Box box; 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_NOTIFICATIONS_PAGE = "notifications-list";
const string STACK_PLACEHOLDER_PAGE = "notifications-placeholder"; const string STACK_PLACEHOLDER_PAGE = "notifications-placeholder";
@@ -35,8 +23,9 @@ namespace SwayNotificationCenter {
private SwayncDaemon swaync_daemon; private SwayncDaemon swaync_daemon;
private NotiDaemon noti_daemon; private NotiDaemon noti_daemon;
private int list_position = 0; private uint list_position = 0;
private double last_upper = 0;
private bool list_reverse = false; private bool list_reverse = false;
private Gtk.Align list_align = Gtk.Align.START; private Gtk.Align list_align = Gtk.Align.START;
@@ -49,18 +38,6 @@ namespace SwayNotificationCenter {
this.swaync_daemon.reloading_css.connect (reload_notifications_style); 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 (swaync_daemon.use_layer_shell) {
if (!GtkLayerShell.is_supported ()) { if (!GtkLayerShell.is_supported ()) {
stderr.printf ("GTKLAYERSHELL IS NOT SUPPORTED!\n"); stderr.printf ("GTKLAYERSHELL IS NOT SUPPORTED!\n");
@@ -77,6 +54,8 @@ namespace SwayNotificationCenter {
GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.BOTTOM, true); GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.BOTTOM, true);
} }
viewport.size_allocate.connect (size_alloc);
this.map.connect (() => { this.map.connect (() => {
set_anchor (); set_anchor ();
// Wait until the layer has attached // Wait until the layer has attached
@@ -173,10 +152,72 @@ namespace SwayNotificationCenter {
return true; return true;
}); });
key_press_event.connect (key_press_event_cb); this.key_press_event.connect ((w, event_key) => {
if (this.get_focus () is Gtk.Entry) return false;
if (event_key.type == Gdk.EventType.KEY_PRESS) {
var children = list_box.get_children ();
Notification noti = (Notification)
list_box.get_focus_child ();
switch (Gdk.keyval_name (event_key.keyval)) {
case "Return":
if (noti != null) noti.click_default_action ();
break;
case "Delete":
case "BackSpace":
if (noti != null) {
if (children.length () == 0) break;
if (list_reverse &&
children.first ().data != noti) {
list_position--;
} else if (children.last ().data == noti) {
if (list_position > 0) list_position--;
}
close_notification (noti.param.applied_id);
}
break;
case "C":
close_all_notifications ();
break;
case "D":
try {
swaync_daemon.toggle_dnd ();
} catch (Error e) {
error ("Error: %s\n", e.message);
}
break;
case "Down":
if (list_position + 1 < children.length ()) {
++list_position;
}
break;
case "Up":
if (list_position > 0) --list_position;
break;
case "Home":
list_position = 0;
break;
case "End":
list_position = children.length () - 1;
if (list_position == uint.MAX) list_position = 0;
break;
default:
// Pressing 1-9 to activate a notification action
for (int i = 0; i < 9; i++) {
uint keyval = Gdk.keyval_from_name (
(i + 1).to_string ());
if (event_key.keyval == keyval) {
if (noti != null) noti.click_alt_action (i);
break;
}
}
break;
}
navigate_list (list_position);
}
return false;
});
stack.set_visible_child_name (STACK_PLACEHOLDER_PAGE); // Switches the stack page depending on the
// Switches the stack page depending on the amount of notifications
list_box.add.connect (() => { list_box.add.connect (() => {
stack.set_visible_child_name (STACK_NOTIFICATIONS_PAGE); stack.set_visible_child_name (STACK_NOTIFICATIONS_PAGE);
}); });
@@ -187,171 +228,6 @@ namespace SwayNotificationCenter {
}); });
add_widgets (); 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 */ /** Adds all custom widgets. Removes previous widgets */
@@ -366,11 +242,11 @@ namespace SwayNotificationCenter {
if (w.length == 0) w = DEFAULT_WIDGETS; if (w.length == 0) w = DEFAULT_WIDGETS;
bool has_notification = false; bool has_notification = false;
foreach (string key in w) { foreach (string key in w) {
// Reposition the notifications_box // Reposition the scrolled_window
if (key == "notifications") { if (key == "notifications") {
has_notification = true; has_notification = true;
uint pos = box.get_children ().length (); uint pos = box.get_children ().length ();
box.reorder_child (notifications_box, (int) (pos > 0 ? --pos : 0)); box.reorder_child (scrolled_window, (int) (pos > 0 ? --pos : 0));
continue; continue;
} }
// Add the widget if it is valid // Add the widget if it is valid
@@ -384,22 +260,23 @@ namespace SwayNotificationCenter {
if (!has_notification) { if (!has_notification) {
warning ("Notification widget not included in \"widgets\" config. Using default bottom position"); warning ("Notification widget not included in \"widgets\" config. Using default bottom position");
uint pos = box.get_children ().length (); uint pos = box.get_children ().length ();
box.reorder_child (notifications_box, (int) (pos > 0 ? --pos : 0)); box.reorder_child (scrolled_window, (int) (pos > 0 ? --pos : 0));
} }
} }
/** Resets the UI positions */ /** Resets the UI positions */
private void set_anchor () { private void set_anchor () {
if (swaync_daemon.use_layer_shell) { 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 // Grabs the keyboard input until closed
bool keyboard_shortcuts = ConfigModel.instance.keyboard_shortcuts; bool keyboard_shortcuts = ConfigModel.instance.keyboard_shortcuts;
#if HAVE_LATEST_GTK_LAYER_SHELL
var mode = keyboard_shortcuts ? var mode = keyboard_shortcuts ?
GtkLayerShell.KeyboardMode.EXCLUSIVE : GtkLayerShell.KeyboardMode.EXCLUSIVE :
GtkLayerShell.KeyboardMode.NONE; GtkLayerShell.KeyboardMode.NONE;
GtkLayerShell.set_keyboard_mode (this, mode); GtkLayerShell.set_keyboard_mode (this, mode);
#else
GtkLayerShell.set_keyboard_interactivity (this, keyboard_shortcuts);
#endif
// Set layer // Set layer
GtkLayerShell.Layer layer; GtkLayerShell.Layer layer;
@@ -474,37 +351,28 @@ namespace SwayNotificationCenter {
box.set_valign (align_y); box.set_valign (align_y);
list_box.set_valign (list_align); list_box.set_valign (list_align);
list_box.set_sort_func (list_box_sort_func); list_box.set_sort_func ((w1, w2) => {
var a = (Notification) w1;
var b = (Notification) w2;
if (a == null || b == null) return 0;
// Sort the list in reverse if needed
if (a.param.time == b.param.time) return 0;
int val = list_reverse ? 1 : -1;
return a.param.time > b.param.time ? val : val * -1;
});
// Always set the size request in all events. // Always set the size request in all events.
box.set_size_request (ConfigModel.instance.control_center_width, box.set_size_request (ConfigModel.instance.control_center_width,
ConfigModel.instance.control_center_height); ConfigModel.instance.control_center_height);
} }
/** private void size_alloc () {
* Returns < 0 if row1 should be before row2, 0 if they are equal var adj = viewport.vadjustment;
* and > 0 otherwise double upper = adj.get_upper ();
*/ if (last_upper < upper) {
private int list_box_sort_func (Gtk.ListBoxRow row1, Gtk.ListBoxRow row2) { scroll_to_start (list_reverse);
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) { private void scroll_to_start (bool reverse) {
@@ -516,26 +384,20 @@ namespace SwayNotificationCenter {
} }
public uint notification_count () { public uint notification_count () {
uint count = 0; return list_box.get_children ().length ();
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 () { public void close_all_notifications () {
foreach (var w in list_box.get_children ()) { foreach (var w in list_box.get_children ()) {
NotificationGroup group = (NotificationGroup) w; Notification noti = (Notification) w;
if (group != null) group.close_all_notifications (); if (noti != null) noti.close_notification (false);
} }
try { try {
swaync_daemon.subscribe_v2 (notification_count (), swaync_daemon.subscribe_v2 (notification_count (),
swaync_daemon.get_dnd (), swaync_daemon.get_dnd (),
get_visibility (), get_visibility (),
swaync_daemon.inhibited); swaync_daemon.inhibited);
} catch (Error e) { } catch (Error e) {
stderr.printf (e.message + "\n"); stderr.printf (e.message + "\n");
} }
@@ -545,20 +407,11 @@ namespace SwayNotificationCenter {
} }
} }
private void navigate_list (int i) { private void navigate_list (uint i) {
unowned Gtk.Widget ? widget = list_box.get_children ().nth_data (i); var 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) { if (widget != null) {
widget.grab_focus ();
list_box.set_focus_child (widget); list_box.set_focus_child (widget);
widget.grab_focus ();
} }
} }
@@ -571,24 +424,20 @@ namespace SwayNotificationCenter {
if (this.visible) { if (this.visible) {
// Focus the first notification // Focus the first notification
list_position = list_reverse ? list_position = list_reverse ?
(((int) list_box.get_children ().length ()) - 1) : 0; (list_box.get_children ().length () - 1) : 0;
if (list_position == uint.MAX) list_position = 0; if (list_position == uint.MAX) list_position = 0;
list_box.grab_focus (); list_box.grab_focus ();
navigate_list (list_position); navigate_list (list_position);
foreach (var w in list_box.get_children ()) { foreach (var w in list_box.get_children ()) {
var group = (NotificationGroup) w; var noti = (Notification) w;
if (group != null) group.update (); if (noti != null) noti.set_time ();
} }
this.get_style_context ().add_class ("open");
}
else {
this.get_style_context ().remove_class ("open");
} }
swaync_daemon.subscribe_v2 (notification_count (), swaync_daemon.subscribe_v2 (notification_count (),
noti_daemon.dnd, noti_daemon.dnd,
this.visible, this.visible,
swaync_daemon.inhibited); swaync_daemon.inhibited);
} }
public bool toggle_visibility () { public bool toggle_visibility () {
@@ -606,114 +455,41 @@ namespace SwayNotificationCenter {
on_visibility_change (); on_visibility_change ();
} }
public void close_notification (uint32 id, bool dismiss) { public void close_notification (uint32 id, bool replaces = false) {
unowned NotificationGroup group = null; foreach (var w in list_box.get_children ()) {
if (!noti_groups_id.lookup_extended (id, null, out group))return;
foreach (var w in group.get_notifications ()) {
var noti = (Notification) w; var noti = (Notification) w;
if (noti != null && noti.param.applied_id == id) { if (noti != null && noti.param.applied_id == id) {
if (!dismiss) { if (replaces) {
noti.remove_noti_timeout (); noti.remove_noti_timeout ();
noti.destroy (); noti.destroy ();
} else { } else {
noti.close_notification (false); noti.close_notification (false);
group.remove_notification (noti); list_box.remove (w);
} }
noti_groups_id.remove (id);
break; break;
} }
} }
if (group.is_empty ()) {
if (group.name_id.length > 0) {
noti_groups_name.remove (group.name_id);
}
if (expanded_group == group) {
expanded_group = null;
fade_animate (1);
}
group.destroy ();
}
} }
public void replace_notification (uint32 id, NotifyParams new_params) { public void add_notification (NotifyParams param,
unowned NotificationGroup group = null; NotiDaemon noti_daemon) {
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, var noti = new Notification.regular (param,
noti_daemon, noti_daemon,
NotificationType.CONTROL_CENTER); NotificationType.CONTROL_CENTER);
noti.grab_focus.connect ((w) => { noti.grab_focus.connect ((w) => {
int i = list_box.get_children ().index (w); uint i = list_box.get_children ().index (w);
if (list_position != uint.MAX && list_position != i) { if (list_position != uint.MAX && list_position != i) {
list_position = i; list_position = i;
} }
}); });
noti.set_time (); 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); scroll_to_start (list_reverse);
try { try {
swaync_daemon.subscribe_v2 (notification_count (), swaync_daemon.subscribe_v2 (notification_count (),
swaync_daemon.get_dnd (), swaync_daemon.get_dnd (),
get_visibility (), get_visibility (),
swaync_daemon.inhibited); swaync_daemon.inhibited);
} catch (Error e) { } catch (Error e) {
stderr.printf (e.message + "\n"); stderr.printf (e.message + "\n");
} }
@@ -730,13 +506,8 @@ namespace SwayNotificationCenter {
/** Forces each notification EventBox to reload its style_context #27 */ /** Forces each notification EventBox to reload its style_context #27 */
private void reload_notifications_style () { private void reload_notifications_style () {
foreach (var c in list_box.get_children ()) { foreach (var c in list_box.get_children ()) {
NotificationGroup group = (NotificationGroup) c; Notification noti = (Notification) c;
if (group != null) { if (noti != null) noti.reload_style_context ();
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_draw_value (false);
slider.set_round_digits (0); slider.set_round_digits (0);
slider.value_changed.connect (() => { slider.value_changed.connect (() => {
this.client.set_brightness.begin ((float) slider.get_value ()); this.client.set_brightness ((float) slider.get_value ());
slider.tooltip_text = ((int) slider.get_value ()).to_string (); slider.tooltip_text = ((int) slider.get_value ()).to_string ();
}); });

View File

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

View File

@@ -1,3 +1,5 @@
using Posix;
namespace SwayNotificationCenter.Widgets { namespace SwayNotificationCenter.Widgets {
public abstract class BaseWidget : Gtk.Box { public abstract class BaseWidget : Gtk.Box {
public abstract string widget_name { get; } public abstract string widget_name { get; }
@@ -19,12 +21,8 @@ namespace SwayNotificationCenter.Widgets {
NORMAL; NORMAL;
public static ButtonType parse (string value) { public static ButtonType parse (string value) {
switch (value) { if (value == "toggle") return ButtonType.TOGGLE;
case "toggle": return ButtonType.NORMAL;
return ButtonType.TOGGLE;
default:
return ButtonType.NORMAL;
}
} }
} }
@@ -107,24 +105,28 @@ namespace SwayNotificationCenter.Widgets {
string command = actions.get_object_element (i).get_string_member_with_default ("command", ""); 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"); string t = actions.get_object_element (i).get_string_member_with_default ("type", "normal");
ButtonType type = ButtonType.parse (t); ButtonType type = ButtonType.parse (t);
string update_command = string active = actions.get_object_element (i).get_string_member_with_default ("active", "");
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 () { res[i] = Action () {
label = label, label = label,
command = command, command = command,
type = type, type = type,
update_command = update_command,
active = active active = active
}; };
} }
return res; return res;
} }
public static void execute_command (string cmd) {
protected async void execute_command (string cmd, string[] env_additions = {}) { pid_t pid;
string msg = ""; int status;
yield Functions.execute_command (cmd, env_additions, out msg); if ((pid = fork ()) < 0) {
perror ("fork()");
}
if (pid == 0) { // Child process
execl ("/bin/sh", "sh", "-c", cmd);
exit (EXIT_FAILURE); // should not return from execl
}
waitpid (pid, out status, 1);
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,9 +38,6 @@ namespace SwayNotificationCenter.Widgets.Mpris {
private string prev_art_url; private string prev_art_url;
private DesktopAppInfo ? desktop_entry = null; 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; private unowned Config mpris_config;
public MprisPlayer (MprisSource source, Config mpris_config) { public MprisPlayer (MprisSource source, Config mpris_config) {
@@ -96,84 +93,12 @@ namespace SwayNotificationCenter.Widgets.Mpris {
update_buttons (source.media_player.metadata); update_buttons (source.media_player.metadata);
}); });
}); });
album_art.set_pixel_size (mpris_config.image_size);
} }
public void before_destroy () { public void before_destroy () {
source.properties_changed.disconnect (properties_changed); 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, private void properties_changed (string iface,
HashTable<string, Variant> changed, HashTable<string, Variant> changed,
string[] invalid) { string[] invalid) {
@@ -321,6 +246,7 @@ namespace SwayNotificationCenter.Widgets.Mpris {
int scale = get_style_context ().get_scale (); int scale = get_style_context ().get_scale ();
Gdk.Pixbuf ? pixbuf = null;
// Cancel previous download, reset the state and download again // Cancel previous download, reset the state and download again
album_art_cancellable.cancel (); album_art_cancellable.cancel ();
album_art_cancellable.reset (); album_art_cancellable.reset ();
@@ -329,80 +255,23 @@ namespace SwayNotificationCenter.Widgets.Mpris {
InputStream stream = yield file.read_async (Priority.DEFAULT, InputStream stream = yield file.read_async (Priority.DEFAULT,
album_art_cancellable); album_art_cancellable);
this.album_art_pixbuf = yield new Gdk.Pixbuf.from_stream_async ( pixbuf = yield new Gdk.Pixbuf.from_stream_async (
stream, album_art_cancellable); stream, album_art_cancellable);
} catch (Error e) { } catch (Error e) {
debug ("Could not download album art for %s. Using fallback...", debug ("Could not download album art for %s. Using fallback...",
source.media_player.identity); source.media_player.identity);
this.album_art_pixbuf = null;
} }
if (this.album_art_pixbuf != null) { if (pixbuf != null) {
unowned Gtk.StyleContext style_ctx = album_art.get_style_context (); pixbuf = Functions.scale_round_pixbuf (pixbuf,
Value br_value = mpris_config.image_size,
style_ctx.get_property (Gtk.STYLE_PROPERTY_BORDER_RADIUS, mpris_config.image_size,
style_ctx.get_state ()); scale,
int radius = 0; mpris_config.image_radius);
if (!br_value.holds (Type.INT) || album_art.set_from_pixbuf (pixbuf);
(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); album_art.get_style_context ().set_scale (1);
this.queue_draw ();
return; return;
} }
} }
this.album_art_pixbuf = null;
this.blurred_cover_surface = null;
// Get the app icon
Icon ? icon = null;
if (desktop_entry is DesktopAppInfo) {
icon = desktop_entry.get_icon ();
}
Gtk.IconInfo ? icon_info = null;
if (icon != null) {
album_art.set_from_gicon (icon, Gtk.IconSize.INVALID);
icon_info = Gtk.IconTheme.get_default ().lookup_by_gicon (icon,
mpris_config.image_size,
Gtk.IconLookupFlags.USE_BUILTIN);
} else {
// Default icon
string icon_name = "audio-x-generic-symbolic";
album_art.set_from_icon_name (icon_name,
Gtk.IconSize.INVALID);
icon_info = Gtk.IconTheme.get_default ().lookup_icon (icon_name,
mpris_config.image_size,
Gtk.IconLookupFlags.USE_BUILTIN);
}
if (icon_info != null) {
try {
this.album_art_pixbuf = icon_info.load_icon ();
} catch (Error e) {
warning ("Could not load icon: %s", e.message);
}
}
} }
private void update_button_shuffle (HashTable<string, Variant> metadata) { private void update_button_shuffle (HashTable<string, Variant> metadata) {

View File

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

View File

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

View File

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

View File

@@ -50,16 +50,13 @@ widget_sources = [
app_sources = [ app_sources = [
'main.vala', 'main.vala',
'animation/animation.vala',
'orderedHashTable/orderedHashTable.vala', 'orderedHashTable/orderedHashTable.vala',
'configModel/configModel.vala', 'configModel/configModel.vala',
'swayncDaemon/swayncDaemon.vala', 'swayncDaemon/swayncDaemon.vala',
'notiDaemon/notiDaemon.vala', 'notiDaemon/notiDaemon.vala',
'notiModel/notiModel.vala', 'notiModel/notiModel.vala',
'fadedViewport/fadedViewport.vala',
'notificationWindow/notificationWindow.vala', 'notificationWindow/notificationWindow.vala',
'notification/notification.vala', 'notification/notification.vala',
'notificationGroup/notificationGroup.vala',
'controlCenter/controlCenter.vala', 'controlCenter/controlCenter.vala',
widget_sources, widget_sources,
'blankWindow/blankWindow.vala', 'blankWindow/blankWindow.vala',
@@ -67,19 +64,13 @@ app_sources = [
constants, constants,
] ]
assert(meson.get_compiler('vala').version() >= '0.56')
app_deps = [ app_deps = [
dependency('gio-2.0', version: '>= 2.50'), dependency('gio-2.0', version: '>= 2.50'),
dependency('gio-unix-2.0', version: '>= 2.50'), dependency('gio-unix-2.0', version: '>= 2.50'),
dependency('gtk+-3.0', version: '>= 3.22'), dependency('gtk+-3.0', version: '>= 3.22'),
dependency('json-glib-1.0', version: '>= 1.0'), dependency('json-glib-1.0', version: '>= 1.0'),
dependency('libhandy-1', version: '>= 1.8.0'), dependency('libhandy-1', version: '>= 1.2.3'),
dependency('granite', version: '>= 6.2.0'), meson.get_compiler('c').find_library('gtk-layer-shell'),
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('c').find_library('m', required : true),
meson.get_compiler('vala').find_library('posix'), meson.get_compiler('vala').find_library('posix'),
dependency('gee-0.8'), dependency('gee-0.8'),
@@ -107,18 +98,36 @@ if get_option('pulse-audio')
] ]
endif 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 = [ args = [
'--target-glib=2.50', '--target-glib=2.50',
'--pkg=GtkLayerShell-0.1', '--pkg=GtkLayerShell-0.1',
] ]
app_resources += gnome.compile_resources('sway_notification_center-resources', sysconfdir = get_option('sysconfdir')
gnome = import('gnome')
app_sources += gnome.compile_resources('sway_notification_center-resources',
'sway_notification_center.gresource.xml', 'sway_notification_center.gresource.xml',
c_name: 'sway_notification_center' c_name: 'sway_notification_center'
) )
executable('swaync', executable('swaync',
[ app_sources, app_resources ], app_sources,
vala_args: args, vala_args: args,
dependencies: app_deps, dependencies: app_deps,
install: true, install: true,
@@ -131,6 +140,8 @@ executable('swaync-client',
install: true, install: true,
) )
config_path = join_paths(sysconfdir, 'xdg/swaync')
config_data = configuration_data() config_data = configuration_data()
config_data.set_quoted('JSONPATH', join_paths('/', config_path, 'configSchema.json')) config_data.set_quoted('JSONPATH', join_paths('/', config_path, 'configSchema.json'))
config_json = configure_file( config_json = configure_file(
@@ -139,5 +150,6 @@ config_json = configure_file(
configuration : config_data configuration : config_data
) )
install_data('style.css', install_dir : config_path)
install_data(config_json, install_dir : config_path) install_data(config_json, install_dir : config_path)
install_data('configSchema.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 */ /** Method to close notification and send DISMISSED signal */
public void manually_close_notification (uint32 id, bool timeout) public void manually_close_notification (uint32 id, bool timeout)
throws DBusError, IOError { throws DBusError, IOError {
NotificationWindow.instance.close_notification (id, true); NotificationWindow.instance.close_notification (id, false);
if (!timeout) { if (!timeout) {
control_center.close_notification (id, true); control_center.close_notification (id);
NotificationClosed (id, ClosedReasons.DISMISSED); NotificationClosed (id, ClosedReasons.DISMISSED);
swaync_daemon.subscribe_v2 (control_center.notification_count (), swaync_daemon.subscribe_v2 (control_center.notification_count (),
@@ -170,10 +170,11 @@ namespace SwayNotificationCenter {
debug ("Notification: %s\n", param.to_string ()); debug ("Notification: %s\n", param.to_string ());
// Get the notification id to replace // Replace notification logic
uint32 replace_notification = 0;
if (id == replaces_id) { if (id == replaces_id) {
replace_notification = id; param.replaces = true;
NotificationWindow.instance.close_notification (id, true);
control_center.close_notification (id, true);
} else if (param.synchronous != null } else if (param.synchronous != null
&& param.synchronous.length > 0) { && param.synchronous.length > 0) {
// Tries replacing without replaces_id instead // Tries replacing without replaces_id instead
@@ -181,47 +182,29 @@ namespace SwayNotificationCenter {
// if there is any notification to replace // if there is any notification to replace
if (synchronous_ids.lookup_extended ( if (synchronous_ids.lookup_extended (
param.synchronous, null, out r_id)) { param.synchronous, null, out r_id)) {
replace_notification = r_id; param.replaces = true;
// Close the notification
NotificationWindow.instance.close_notification (r_id, true);
control_center.close_notification (r_id, true);
} }
synchronous_ids.set (param.synchronous, id); synchronous_ids.set (param.synchronous, id);
} }
bool show_notification = state == NotificationStatusEnum.ENABLED // Only show popup notification if it is ENABLED or TRANSIENT
|| state == NotificationStatusEnum.TRANSIENT; if ((state == NotificationStatusEnum.ENABLED || state == NotificationStatusEnum.TRANSIENT)
// Don't show the notification window if the control center is open && !control_center.get_visibility ()) {
if (control_center.get_visibility ()) { // Also check if urgency is Critical and not inhibited and dnd
show_notification = false; if (param.urgency == UrgencyLevels.CRITICAL ||
} (!dnd && !swaync_daemon.inhibited
&& param.urgency != UrgencyLevels.CRITICAL)) {
bool bypass_dnd = param.urgency == UrgencyLevels.CRITICAL || param.swaync_bypass_dnd; NotificationWindow.instance.add_notification (param, this);
// 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 // Only add notification to CC if it isn't IGNORED and not transient/TRANSIENT
if (state != NotificationStatusEnum.IGNORED if (state != NotificationStatusEnum.IGNORED
&& state != NotificationStatusEnum.TRANSIENT && state != NotificationStatusEnum.TRANSIENT
&& !param.transient) { && !param.transient) {
if (replace_notification > 0) { control_center.add_notification (param, this);
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 #if WANT_SCRIPTING
@@ -309,8 +292,8 @@ namespace SwayNotificationCenter {
*/ */
[DBus (name = "CloseNotification")] [DBus (name = "CloseNotification")]
public void close_notification (uint32 id) throws DBusError, IOError { public void close_notification (uint32 id) throws DBusError, IOError {
NotificationWindow.instance.close_notification (id, true); NotificationWindow.instance.close_notification (id, false);
control_center.close_notification (id, true); control_center.close_notification (id);
NotificationClosed (id, ClosedReasons.CLOSED_BY_CLOSENOTIFICATION); NotificationClosed (id, ClosedReasons.CLOSED_BY_CLOSENOTIFICATION);
} }

View File

@@ -110,16 +110,10 @@ namespace SwayNotificationCenter {
/** Disables scripting for notification */ /** Disables scripting for notification */
public bool swaync_no_script { get; set; } public bool swaync_no_script { get; set; }
/** Always show the notification, regardless of dnd/inhibit state */
public bool swaync_bypass_dnd { get; set; }
public Array<Action> actions { get; set; } public Array<Action> actions { get; set; }
public DesktopAppInfo ? desktop_app_info = null; /** If the notification replaces another */
public bool replaces { get; set; }
public string name_id;
public string display_name;
public NotifyParams (uint32 applied_id, public NotifyParams (uint32 applied_id,
string app_name, string app_name,
@@ -140,37 +134,12 @@ namespace SwayNotificationCenter {
this.expire_timeout = expire_timeout; this.expire_timeout = expire_timeout;
this.time = (int64) (get_real_time () * 0.000001); this.time = (int64) (get_real_time () * 0.000001);
this.replaces = false;
this.has_synch = false; this.has_synch = false;
parse_hints (); parse_hints ();
parse_actions (actions); 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 () { private void parse_hints () {
@@ -182,11 +151,6 @@ namespace SwayNotificationCenter {
swaync_no_script = hint_value.get_boolean (); swaync_no_script = hint_value.get_boolean ();
} }
break; break;
case "SWAYNC_BYPASS_DND":
if (hint_value.is_of_type (VariantType.BOOLEAN)) {
swaync_bypass_dnd = hint_value.get_boolean ();
}
break;
case "value": case "value":
if (hint_value.is_of_type (VariantType.INT32)) { if (hint_value.is_of_type (VariantType.INT32)) {
this.has_synch = true; this.has_synch = true;

View File

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

View File

@@ -30,7 +30,7 @@ namespace SwayNotificationCenter {
[GtkChild] [GtkChild]
unowned Gtk.Button close_button; unowned Gtk.Button close_button;
private Gtk.ButtonBox ? alt_actions_box = null; private Gtk.ButtonBox alt_actions_box = new Gtk.ButtonBox (Gtk.Orientation.HORIZONTAL);
[GtkChild] [GtkChild]
unowned Gtk.Label summary; unowned Gtk.Label summary;
@@ -41,8 +41,6 @@ namespace SwayNotificationCenter {
[GtkChild] [GtkChild]
unowned Gtk.Image img; unowned Gtk.Image img;
[GtkChild] [GtkChild]
unowned Gtk.Image img_app_icon;
[GtkChild]
unowned Gtk.Image body_image; unowned Gtk.Image body_image;
// Inline Reply // Inline Reply
@@ -72,8 +70,8 @@ namespace SwayNotificationCenter {
public bool is_timed { get; construct; default = false; } public bool is_timed { get; construct; default = false; }
public NotifyParams param { get; private set; } public NotifyParams param { get; construct; }
public unowned NotiDaemon noti_daemon { get; construct; } public NotiDaemon noti_daemon { get; construct; }
public NotificationType notification_type { public NotificationType notification_type {
get; get;
@@ -114,9 +112,8 @@ namespace SwayNotificationCenter {
NotiDaemon noti_daemon, NotiDaemon noti_daemon,
NotificationType notification_type) { NotificationType notification_type) {
Object (noti_daemon: noti_daemon, Object (noti_daemon: noti_daemon,
param: param,
notification_type: notification_type); notification_type: notification_type);
this.param = param;
build_noti ();
} }
/** Show a timed notification */ /** Show a timed notification */
@@ -127,6 +124,7 @@ namespace SwayNotificationCenter {
uint timeout_low, uint timeout_low,
uint timeout_critical) { uint timeout_critical) {
Object (noti_daemon: noti_daemon, Object (noti_daemon: noti_daemon,
param: param,
notification_type: notification_type, notification_type: notification_type,
is_timed: true, is_timed: true,
timeout_delay: timeout, timeout_delay: timeout,
@@ -134,13 +132,11 @@ namespace SwayNotificationCenter {
timeout_critical_delay: timeout_critical, timeout_critical_delay: timeout_critical,
number_of_body_lines: 5 number_of_body_lines: 5
); );
this.param = param;
build_noti ();
} }
construct { construct {
try { try {
code_regex = new Regex ("(?<= |^)(\\d{3}(-| )\\d{3}|\\d{4,8})(?= |$|\\.|,)", code_regex = new Regex ("(?<= |^)(\\d{3}(-| )\\d{3}|\\d{4,7})(?= |$|\\.|,)",
RegexCompileFlags.MULTILINE); RegexCompileFlags.MULTILINE);
string joined_tags = string.joinv ("|", TAGS); string joined_tags = string.joinv ("|", TAGS);
tag_regex = new Regex ("&lt;(/?(?:%s))&gt;".printf (joined_tags)); tag_regex = new Regex ("&lt;(/?(?:%s))&gt;".printf (joined_tags));
@@ -200,85 +196,12 @@ namespace SwayNotificationCenter {
}); });
this.transition_time = ConfigModel.instance.transition_time; 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 (); add_notification_timeout ();
return false; this.size_allocate.connect (on_size_allocation);
}); }
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 () { private void default_action_update_state () {
@@ -312,8 +235,50 @@ namespace SwayNotificationCenter {
this.summary.set_text (param.summary ?? param.app_name); this.summary.set_text (param.summary ?? param.app_name);
this.summary.set_ellipsize (Pango.EllipsizeMode.END); 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_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.revealer.set_transition_duration (this.transition_time);
this.carousel.set_animation_duration (this.transition_time); this.carousel.set_animation_duration (this.transition_time);
@@ -330,9 +295,20 @@ namespace SwayNotificationCenter {
this.carousel_empty_widget_index = 0; this.carousel_empty_widget_index = 0;
break; break;
} }
// Reset state this.carousel.page_changed.connect ((_, i) => {
this.carousel.scroll_to (event_box); if (i != this.carousel_empty_widget_index) return;
remove_noti_timeout ();
try {
noti_daemon.manually_close_notification (
param.applied_id, false);
} catch (Error e) {
print ("Error: %s\n", e.message);
this.destroy ();
}
});
#if HAVE_LATEST_LIBHANDY
this.carousel.allow_scroll_wheel = false; this.carousel.allow_scroll_wheel = false;
#endif
if (this.progress_bar.visible = param.has_synch) { if (this.progress_bar.visible = param.has_synch) {
this.progress_bar.set_fraction (param.value * 0.01); this.progress_bar.set_fraction (param.value * 0.01);
@@ -346,16 +322,13 @@ namespace SwayNotificationCenter {
this.show (); this.show ();
Timeout.add (0, () => { if (param.replaces) {
this.revealer.set_reveal_child (true); this.revealer.set_reveal_child (true);
return Source.REMOVE; } else {
}); Timeout.add (0, () => {
this.revealer.set_reveal_child (true);
remove_noti_timeout (); return Source.REMOVE;
this.size_allocate.disconnect (on_size_allocation); });
if (is_timed) {
add_notification_timeout ();
this.size_allocate.connect (on_size_allocation);
} }
} }
@@ -364,9 +337,6 @@ namespace SwayNotificationCenter {
this.body.set_lines (this.number_of_body_lines); 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 // Removes all image tags and adds them to an array
if (text.length > 0) { if (text.length > 0) {
try { try {
@@ -404,29 +374,22 @@ namespace SwayNotificationCenter {
// Markup // Markup
try { 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; Pango.AttrList ? attr = null;
string ? buf = null; string ? buf = null;
try { Pango.parse_markup (escaped, -1, 0, out attr, out buf, null);
// 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); this.body.set_text (buf);
if (attr != null) this.body.set_attributes (attr); if (attr != null) this.body.set_attributes (attr);
@@ -458,7 +421,6 @@ namespace SwayNotificationCenter {
} }
public void click_alt_action (uint index) { public void click_alt_action (uint index) {
if (alt_actions_box == null) return;
List<weak Gtk.Widget> ? children = alt_actions_box.get_children (); List<weak Gtk.Widget> ? children = alt_actions_box.get_children ();
uint length = children.length (); uint length = children.length ();
if (length == 0 || index >= length) return; if (length == 0 || index >= length) return;
@@ -490,11 +452,6 @@ namespace SwayNotificationCenter {
} }
private void set_style_urgency () { 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) { switch (param.urgency) {
case UrgencyLevels.LOW: case UrgencyLevels.LOW:
base_box.get_style_context ().add_class ("low"); base_box.get_style_context ().add_class ("low");
@@ -510,8 +467,6 @@ namespace SwayNotificationCenter {
} }
private void set_inline_reply () { private void set_inline_reply () {
// Reset state
inline_reply_box.hide ();
// Only show inline replies in popup notifications if the compositor // Only show inline replies in popup notifications if the compositor
// supports ON_DEMAND layer shell keyboard interactivity // supports ON_DEMAND layer shell keyboard interactivity
if (!ConfigModel.instance.notification_inline_replies if (!ConfigModel.instance.notification_inline_replies
@@ -539,23 +494,32 @@ namespace SwayNotificationCenter {
}, },
null); 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.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 () { 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 // Check for security codes
string ? code = parse_body_codes (); string ? code = parse_body_codes ();
if (param.actions.length > 0 || code != null) { if (param.actions.length > 0 || code != null) {
var viewport = new Gtk.Viewport (null, null); var viewport = new Gtk.Viewport (null, null);
var scroll = new Gtk.ScrolledWindow (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_homogeneous (true);
alt_actions_box.set_layout (Gtk.ButtonBoxStyle.EXPAND); alt_actions_box.set_layout (Gtk.ButtonBoxStyle.EXPAND);
@@ -650,104 +614,59 @@ namespace SwayNotificationCenter {
}); });
} }
public void replace_notification (NotifyParams new_params) {
this.param = new_params;
build_noti ();
}
private void set_icon () { 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; var image_visibility = ConfigModel.instance.image_visibility;
if (image_visibility == ImageVisibility.NEVER) { if (image_visibility == ImageVisibility.NEVER) {
img.set_visible (false); img.set_visible (false);
img_app_icon.set_visible (false);
return; return;
} }
img.set_pixel_size (notification_icon_size); img.set_pixel_size (notification_icon_size);
img.height_request = notification_icon_size; img.height_request = notification_icon_size;
img.width_request = notification_icon_size; img.width_request = notification_icon_size;
int app_icon_size = notification_icon_size / 3;
img_app_icon.set_pixel_size (app_icon_size);
var img_path_exists = File.new_for_uri ( var img_path_exists = File.new_for_path (
param.image_path ?? "").query_exists (); param.image_path ?? "").query_exists ();
if (param.image_path != null && !img_path_exists) { var app_icon_exists = File.new_for_path (
// Check if it's not a URI param.app_icon ?? "").query_exists ();
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) { if (param.image_data.is_initialized) {
Functions.set_image_data (param.image_data, img, Functions.set_image_data (param.image_data, img,
notification_icon_size, radius); notification_icon_size);
} else if (param.image_path != null && } else if (param.image_path != null &&
param.image_path != "" && param.image_path != "" &&
img_path_exists) { img_path_exists) {
Functions.set_image_uri (param.image_path, img, Functions.set_image_path (param.image_path, img,
notification_icon_size, notification_icon_size,
radius,
img_path_exists); 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) { } else if (param.icon_data.is_initialized) {
Functions.set_image_data (param.icon_data, img, Functions.set_image_data (param.icon_data, img,
notification_icon_size, radius); notification_icon_size);
} } else {
if (img.storage_type == Gtk.ImageType.EMPTY) {
// Get the app icon // Get the app icon
if (app_icon_uri != null) { Icon ? icon = null;
Functions.set_image_uri (app_icon_uri, img, if (param.desktop_entry != null) {
notification_icon_size, string entry = param.desktop_entry;
radius, entry = entry.replace (".desktop", "");
app_icon_exists); DesktopAppInfo entry_info = new DesktopAppInfo (
} else if (app_icon_name != null) { "%s.desktop".printf (entry));
img.set_from_gicon (app_icon_name, icon_size); // Checks if the .desktop file actually exists or not
if (entry_info is DesktopAppInfo) {
icon = entry_info.get_icon ();
}
}
if (icon != null) {
img.set_from_gicon (icon, icon_size);
} else if (image_visibility == ImageVisibility.ALWAYS) { } else if (image_visibility == ImageVisibility.ALWAYS) {
// Default icon // Default icon
img.set_from_icon_name ("image-missing", icon_size); img.set_from_icon_name ("image-missing", icon_size);
} else { } else {
img.set_visible (false); img.set_visible (false);
} }
} else {
// We only set the app icon if the main image is set
if (app_icon_uri != null) {
Functions.set_image_uri (app_icon_uri, img_app_icon,
app_icon_size,
0,
app_icon_exists);
} else if (app_icon_name != null) {
img_app_icon.set_from_gicon (app_icon_name, Gtk.IconSize.INVALID);
}
} }
} }

View File

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

View File

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

366
src/style.css Normal file
View File

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

View File

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