Unverified Commit 3d719a82 authored by Erik Reider's avatar Erik Reider Committed by GitHub

Ported to GTK 4 (#539)

* Ported to GTK 4 * Updated README dependencies * Updated GitHub actions * Added blueprint-compiler to README dependencies * Fixed expanded_group not being set to null when group only contains a single notification * Added a overlay close button for grouped notifications, just like regular notifications * Fixed control center kb shortcuts not always working * Fixed Notification sizing in Control Center When adding a large notification with a body image and actions, and add a seperate non-grouped notification, the first notification would take up more space than needed * Simplified Notification Group offset calculation * Limit the control center height to the monitors height * Fixed vala-lint issues * Fixed MPRIS background not using album art gicon * Show --skip-system-css in --help print * Fixed CC adaptive height being short with fit-to-screen enabled * Fixed grouped notifications not notifying CC that it's not the expanded one * Fixed notification groups focus being buggy and not visually visible * Fixed notification focus resetting to the top when removing the last notification
parent 5a6ae71d
......@@ -21,7 +21,7 @@ jobs:
- name: Install packages
run: |
pacman-key --init
pacman -Syu --noconfirm
pacman -Sy --noconfirm
- name: Add builduser
run: |
......@@ -34,13 +34,23 @@ jobs:
- name: Check out sources
uses: actions/checkout@v3
with:
fetch-depth: 0
path: swaync
- name: Test PKGBUILD
run: |
build_dir="/tmp/PKGBUILD-git/"
cd swaync
build_dir="/tmp/PKGBUILD-git"
cd build-scripts
mkdir $build_dir
cp ./PKGBUILD-git $build_dir/PKGBUILD
mkdir $build_dir/src
cp -r ../ $build_dir/src/swaync
cd $build_dir
sudo chown builduser $build_dir
sudo -H -u builduser bash -c 'makepkg -s --noconfirm'
sudo chown -R builduser $build_dir
# Use the locally checked-out branch instead of downloading the source
sudo -H -u builduser bash -c "makepkg -s --noconfirm --noextract SRCDEST=$build_dir"
......@@ -15,11 +15,11 @@ on:
jobs:
ubuntu-build:
container: ubuntu:24.10
container: ubuntu:25.04
runs-on: ubuntu-latest
env:
DEBIAN_FRONTEND: noninteractive
PACKAGES: meson libwayland-dev libgtk-3-dev gobject-introspection libgirepository1.0-dev valac libjson-glib-dev libhandy-1-dev libgtk-layer-shell-dev scdoc libgee-0.8-dev libpulse-dev sassc libgranite-dev
PACKAGES: meson libwayland-dev libgtk-4-dev gobject-introspection libgirepository1.0-dev valac libjson-glib-dev libgtk4-layer-shell-dev scdoc libgee-0.8-dev libpulse-dev sassc libgranite-7-dev blueprint-compiler libadwaita-1-dev
steps:
- name: Install packages
run: |
......
SwayNotificationCenter
======================
# SwayNotificationCenter
[![Check PKGBUILD builds for Arch.](https://github.com/ErikReider/SwayNotificationCenter/actions/workflows/PKGBUILD-buildd.yml/badge.svg)](https://github.com/ErikReider/SwayNotificationCenter/actions/workflows/PKGBUILD-buildd.yml)
[![Check build for Fedora.](https://github.com/ErikReider/SwayNotificationCenter/actions/workflows/fedora-build.yml/badge.svg)](https://github.com/ErikReider/SwayNotificationCenter/actions/workflows/fedora-build.yml)
......@@ -21,8 +20,7 @@ https://github.com/ErikReider/SwayNotificationCenter/assets/35975961/93ff072f-e6
![Screenshot of panel](./assets/panel.png)
Table of Contents
=================
## Table of Contents
* [Want to show off your sick config?](#want-to-show-off-your-sick-config)
* [Features](#features)
......@@ -49,6 +47,7 @@ Table of Contents
* [Disable scripting](#disable-scripting)
* [i3status-rs Example](#i3status-rs-example)
* [Waybar Example](#waybar-example)
* [Debugging Environment Variables](#debugging-environment-variables)
## Want to show off your sick config?
......@@ -154,7 +153,6 @@ Lunar and later:
sudo apt install sway-notification-center
```
### Debian
Bookworm and later:
......@@ -166,6 +164,7 @@ sudo apt install sway-notification-center
### Guix
The simplest way is to install it to user's profile:
```zsh
guix install swaynotificationcenter
```
......@@ -173,7 +172,8 @@ guix install swaynotificationcenter
But we recommend to use [Guix Home](https://guix.gnu.org/manual/devel/en/html_node/Home-Configuration.html) to manage packages and their configurations declaratively.
### rde
```
```scm
(use-modules (rde features wm))
;; Include the following code into the list of your rde features:
......@@ -186,19 +186,20 @@ But we recommend to use [Guix Home](https://guix.gnu.org/manual/devel/en/html_no
- `vala >= 0.56`
- `meson`
- `blueprint-compiler`
- `git`
- `scdoc`
- `sassc`
- `gtk3`
- `gtk-layer-shell`
- `gtk4`
- `gtk4-layer-shell`
- `dbus`
- `glib2`
- `gobject-introspection`
- `libgee`
- `json-glib`
- `libhandy`
- `libadwaita`
- `gvfs`
- `granite`
- `granite7`
##### Optional Dependencies
......@@ -446,3 +447,15 @@ Alternatively, the number of notifications can be shown by adding `{}` anywhere
// ...
},
```
## Debugging Environment Variables
- `G_MESSAGES_DEBUG=all`: Displays all of the debug messages.
- `GTK_DEBUG=interactive`: Opens the GTK Inspector.
- `G_ENABLE_DIAGNOSTIC=1`: If set to a non-zero value, this environment variable
enables diagnostic messages, like deprecation messages for GObject properties
and signals.
- `G_DEBUG=fatal_criticals` or `G_DEBUG=fatal_warnings`: Causes GLib to abort
the program at the first call to g_warning() or g_critical().
More can be read [here](https://www.manpagez.com/html/glib/glib-2.56.0/glib-running.php)
......@@ -2,7 +2,7 @@
# Maintainer: Erik Reider <erik.reider@protonmail.com>
pkgname=swaync-git
_pkgname=swaync
pkgver=r558.8d1b813
pkgver=r560.a935000
pkgrel=1
pkgdesc="A simple notification daemon with a GTK panel for checking previous notifications like other DEs"
url="https://github.com/ErikReider/SwayNotificationCenter"
......@@ -12,7 +12,7 @@ arch=(
'armv7h' # ARM v7 hardfloat
)
license=('GPL3')
depends=("gtk3" "gtk-layer-shell" "dbus" "glib2" "gobject-introspection" "libgee" "json-glib" "libhandy" "libpulse" "gvfs" "libnotify" "granite")
depends=("gtk3" "gtk4-layer-shell" "dbus" "glib2" "gobject-introspection" "libgee" "json-glib" "libpulse" "gvfs" "libnotify" "granite7" "blueprint-compiler" "libadwaita")
conflicts=("swaync" "swaync-client")
provides=("swaync" "swaync-client" "notification-daemon")
makedepends=("vala>=0.56" meson git scdoc sassc)
......@@ -30,12 +30,15 @@ prepare() {
}
build() {
arch-meson $_pkgname build -Dscripting=true
# Very important to be in the directory! Otherwise somehow breaks linking to the .ui files
cd $_pkgname
arch-meson build -Dscripting=true
ninja -C build
}
package() {
cd $_pkgname
DESTDIR="$pkgdir/" ninja -C build install
install -Dm644 "$_pkgname/COPYING" -t "$pkgdir/usr/share/licenses/$pkgname"
install -Dm644 "$_pkgname/README.md" -t "$pkgdir/usr/share/doc/$pkgname"
install -Dm644 "COPYING" -t "$pkgdir/usr/share/licenses/$pkgname"
install -Dm644 "README.md" -t "$pkgdir/usr/share/doc/$pkgname"
}
......@@ -14,23 +14,24 @@ URL: https://github.com/ErikReider/SwayNotificationCenter
VCS: {{{ git_repo_vcs }}}
Source: {{{ git_repo_pack }}}
BuildRequires: meson >= 0.51.0
BuildRequires: meson >= 1.5.1
BuildRequires: vala >= 0.56
BuildRequires: scdoc
BuildRequires: pkgconfig(gtk+-3.0) >= 3.22
BuildRequires: pkgconfig(gtk-layer-shell-0) >= 0.1
BuildRequires: pkgconfig(gtk4) >= 4.16
BuildRequires: pkgconfig(gtk4-layer-shell-0) >= 1.0.3
BuildRequires: pkgconfig(json-glib-1.0) >= 1.0
BuildRequires: pkgconfig(libhandy-1) >= 1.4.0
BuildRequires: pkgconfig(libadwaita-1) >= 1.6.5
BuildRequires: pkgconfig(glib-2.0) >= 2.50
BuildRequires: pkgconfig(gobject-introspection-1.0) >= 1.68
BuildRequires: pkgconfig(gee-0.8) >= 0.20
BuildRequires: pkgconfig(bash-completion)
BuildRequires: pkgconfig(fish)
BuildRequires: pkgconfig(libpulse)
BuildRequires: pkgconfig(granite)
BuildRequires: pkgconfig(granite-7)
BuildRequires: systemd-devel
BuildRequires: systemd
BuildRequires: sassc
BuildRequires: blueprint-compiler >= 0.16
BuildRequires: granite-devel
Requires: gvfs
......
......@@ -13,23 +13,24 @@ URL: https://github.com/ErikReider/SwayNotificationCenter
VCS: {{{ git_repo_vcs }}}
Source: {{{ git_repo_pack }}}
BuildRequires: meson >= 0.51.0
BuildRequires: meson >= 1.5.1
BuildRequires: vala >= 0.56
BuildRequires: scdoc
BuildRequires: pkgconfig(gtk+-3.0) >= 3.22
BuildRequires: pkgconfig(gtk-layer-shell-0) >= 0.1
BuildRequires: pkgconfig(gtk4) >= 4.16
BuildRequires: pkgconfig(gtk4-layer-shell-0) >= 1.0.3
BuildRequires: pkgconfig(json-glib-1.0) >= 1.0
BuildRequires: pkgconfig(libhandy-1) >= 1.4.0
BuildRequires: pkgconfig(libadwaita-1) >= 1.6.5
BuildRequires: pkgconfig(glib-2.0) >= 2.50
BuildRequires: pkgconfig(gobject-introspection-1.0) >= 1.68
BuildRequires: pkgconfig(gee-0.8) >= 0.20
BuildRequires: pkgconfig(bash-completion)
BuildRequires: pkgconfig(fish)
BuildRequires: pkgconfig(libpulse)
BuildRequires: pkgconfig(granite)
BuildRequires: pkgconfig(granite-7)
BuildRequires: systemd-devel
BuildRequires: systemd
BuildRequires: sassc
BuildRequires: blueprint-compiler >= 0.16
BuildRequires: granite-devel
Requires: gvfs
......
app_resources += gnome.compile_resources('icon-resources',
'swaync_icons.gresource.xml',
c_name: 'sway_notification_center_icons'
)
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 3.292969 6.707031 l 4 4 c 0.390625 0.390625 1.023437 0.390625 1.414062 0 l 4 -4 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 s -1.023437 -0.390625 -1.414062 0 l -3.292969 3.292969 l -3.292969 -3.292969 c -0.390625 -0.390625 -1.023437 -0.390625 -1.414062 0 s -0.390625 1.023437 0 1.414062 z m 0 0" fill="#222222" fill-rule="evenodd"/></svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 7.292969 5.292969 l -4 4 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 s 1.023437 0.390625 1.414062 0 l 3.292969 -3.292969 l 3.292969 3.292969 c 0.390625 0.390625 1.023437 0.390625 1.414062 0 s 0.390625 -1.023437 0 -1.414062 l -4 -4 c -0.390625 -0.390625 -1.023437 -0.390625 -1.414062 0 z m 0 0" fill="#222222" fill-rule="evenodd"/></svg>
<?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>
......@@ -2,8 +2,6 @@ install_data('org.erikreider.swaync.gschema.xml',
install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')
)
subdir('icons')
compile_schemas = find_program('glib-compile-schemas', required: false)
if compile_schemas.found()
test('Validate schema file', compile_schemas,
......@@ -34,3 +32,21 @@ style_css = custom_target(
)
message(style_css.full_path())
blueprints = custom_target('blueprints',
input: files(run_command('find', '.', '-name', '*.blp').stdout().strip().split('\n')),
output: '.',
command: [
find_program('blueprint-compiler', required: true),
'batch-compile',
'@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'
],
build_always_stale: true,
build_by_default: true,
)
app_resources += gnome.compile_resources('sway_notification_center-resources',
'sway_notification_center.gresource.xml',
dependencies: [ blueprints ],
c_name: 'sway_notification_center'
)
@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);
:root {
--cc-bg: rgba(46, 46, 46, 0.7);
--noti-border-color: rgba(255, 255, 255, 0.15);
--noti-bg: 48, 48, 48;
--noti-bg-alpha: 0.8;
--noti-bg-darker: rgb(38, 38, 38);
--noti-bg-hover: rgb(56, 56, 56);
--noti-bg-focus: rgba(68, 68, 68, 0.6);
--noti-close-bg: rgb(78, 78, 78);
--noti-close-bg-hover: rgb(94, 94, 94);
--text-color: rgb(255, 255, 255);
--text-color-disabled: rgb(150, 150, 150);
--bg-selected: rgb(0, 128, 255);
}
$border: 1px solid #{"@noti-border-color"};
$border: 1px solid var(--noti-border-color);
$border-radius: 12px;
$margin: 8px;
$font-size-body: 15px;
$font-size-summary: 16px;
......@@ -27,37 +29,39 @@ $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);
.close-button {
/* The notification Close Button */
background: var(--noti-close-bg);
color: var(--text-color);
text-shadow: none;
padding: 0;
border-radius: 100%;
margin-top: $margin;
margin-right: $margin;
box-shadow: none;
border: none;
min-width: 24px;
min-height: 24px;
&:hover {
box-shadow: none;
background: var(--noti-close-bg-hover);
transition: $hover-tranistion;
border: none;
}
}
.notification-row {
background: none;
outline: none;
&:focus,
&:hover {
background: #{"@noti-bg-focus"};
&:focus {
background: var(--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 {
......@@ -66,7 +70,7 @@ $notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
border: $border;
padding: 0;
transition: $hover-tranistion;
background: #{"@noti-bg"};
background: rgba(var(--noti-bg), var(--noti-bg-alpha));
&.low {
/* Low Priority Notification */
......@@ -78,27 +82,20 @@ $notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
/* Critical Priority Notification */
}
%action {
padding: 4px;
.notification-default-action {
/* The large action that also displays the notification summary and body */
padding: $margin / 2;
margin: 0;
box-shadow: none;
background: transparent;
border: none;
color: #{"@text-color"};
color: var(--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;
-gtk-icon-filter: none;
background: var(--noti-bg-hover);
}
&:not(:only-child) {
......@@ -111,28 +108,33 @@ $notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
background: transparent;
border-radius: $border-radius;
$margin: 4px;
padding: $margin;
padding: 0;
.image {
/* Notification Primary Image */
-gtk-icon-effect: none;
-gtk-icon-filter: 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-filter: none;
-gtk-icon-shadow: 0 1px 4px black;
margin: 6px;
}
.text-box {
label {
/* Fixes base GTK 4 CSS setting a filter of opacity 50% for some odd reason */
filter: none;
}
.summary {
/* Notification summary/title */
font-size: $font-size-summary;
font-weight: bold;
background: transparent;
color: #{"@text-color"};
color: var(--text-color);
text-shadow: none;
}
.time {
......@@ -140,7 +142,7 @@ $notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
font-size: $font-size-summary;
font-weight: bold;
background: transparent;
color: #{"@text-color"};
color: var(--text-color);
text-shadow: none;
margin-right: 30px;
}
......@@ -149,7 +151,7 @@ $notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
font-size: $font-size-body;
font-weight: normal;
background: transparent;
color: #{"@text-color"};
color: var(--text-color);
text-shadow: none;
}
}
......@@ -163,8 +165,7 @@ $notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
/* The "extra" optional bottom notification image */
margin-top: $margin;
background-color: white;
border-radius: $border-radius;
-gtk-icon-effect: none;
-gtk-icon-filter: none;
}
.inline-reply {
......@@ -172,51 +173,47 @@ $notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
margin-top: $margin;
.inline-reply-entry {
background: #{"@noti-bg-darker"};
color: #{"@text-color"};
caret-color: #{"@text-color"};
background: var(--noti-bg-darker);
color: var(--text-color);
caret-color: var(--text-color);
border: $border;
border-radius: $border-radius;
}
.inline-reply-button {
margin-left: 4px;
background: #{"@noti-bg"};
background: rgba(var(--noti-bg), var(--noti-bg-alpha));
border: $border;
border-radius: $border-radius;
color: #{"@text-color"};
color: var(--text-color);
&:disabled {
background: initial;
color: #{"@text-color-disabled"};
color: var(--text-color-disabled);
border: $border;
border-color: transparent;
}
&:hover {
background: #{"@noti-bg-hover"};
background: var(--noti-bg-hover);
}
}
}
}
}
.notification-alt-actions {
background: none;
border-bottom-left-radius: $border-radius;
border-bottom-right-radius: $border-radius;
padding: $margin / 2;
}
.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;
}
margin: $margin / 2;
padding: 0;
&:hover {
@extend %action-hover;
& > button {
border-radius: $border-radius;
}
}
}
......@@ -225,6 +222,11 @@ $notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
.notification-group {
/* Styling only for Grouped Notifications */
transition: opacity 200ms ease-in-out;
&:focus {
background: var(--noti-bg-focus);
}
&.low {
/* Low Priority Group */
......@@ -236,9 +238,14 @@ $notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
/* Low Priority Group */
}
/* The groups close button */
.notification-group-close-button .close-button {
margin: $margin * 1.5 $margin * 2.5;
}
%header {
margin: 0 16px;
color: #{"@text-color"};
margin: 0 $margin * 2;
color: var(--text-color);
}
.notification-group-headers {
......@@ -246,10 +253,10 @@ $notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
@extend %header;
.notification-group-icon {
color: #{"@text-color"};
color: var(--text-color);
}
.notification-group-header {
color: #{"@text-color"};
color: var(--text-color);
}
}
.notification-group-buttons {
......@@ -258,9 +265,14 @@ $notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
}
&.collapsed {
/* When another group is expanded, lower the opacity of the collapsed ones */
&.not-expanded {
opacity: 0.4;
}
.notification-row {
.notification {
background-color: #{"@noti-bg-opaque"};
background-color: rgba(var(--noti-bg), 1);
}
&:not(:last-child) {
......@@ -276,7 +288,7 @@ $notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
&:hover {
.notification-row:not(:only-child) {
.notification {
background-color: #{"@noti-bg-hover-opaque"};
background-color: var(--noti-bg-hover);
}
}
}
......@@ -285,8 +297,8 @@ $notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
.control-center {
/* The Control Center which contains the old notifications + widgets */
background: #{"@cc-bg"};
color: #{"@text-color"};
background: var(--cc-bg);
color: var(--text-color);
border-radius: $border-radius;
.control-center-list-placeholder {
......@@ -306,7 +318,7 @@ $notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
transition: $group-collapse-tranistion, $hover-tranistion;
&:hover {
background-color: #{"@noti-bg-hover"};
background-color: var(--noti-bg-hover);
}
}
}
......@@ -322,7 +334,7 @@ $notification-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3),
background: transparent;
.notification {
box-shadow: none;
box-shadow: none;
}
}
......
.widget-backlight {
background-color: #{"@noti-bg"};
padding: 8px;
margin: 8px;
border-radius: 12px;
padding: $margin;
margin: $margin;
border-radius: $border-radius;
}
.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;
}
padding: $margin;
margin: $margin;
border-radius: $border-radius;
.widget-buttons-grid > flowbox > flowboxchild > button.toggle:checked {
/* style given to the active toggle button */
}
& flowboxchild > button {
border-radius: $border-radius;
&.toggle:checked {
/* style given to the active toggle button */
}
.widget-buttons-grid > flowbox > flowboxchild > button:hover {
&:hover {
}
}
}
.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;
label {
color: var(--text-color);
margin: $margin;
font-size: 1.1rem;
}
switch {
border-radius: $border-radius;
margin: $margin;
&:checked {
}
slider {
border-radius: $border-radius;
}
}
}
.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"};
& > label {
margin: $margin;
font-size: 1.5rem;
}
& > button {
margin: $margin;
border-radius: $border-radius;
}
}
.widget-label {
margin: 8px;
}
.widget-label > label {
font-size: 1.1rem;
margin: $margin;
& > label {
font-size: 1.1rem;
}
}
.widget-menubar > box > .menu-button-bar > button {
border: none;
background: transparent;
}
.widget-menubar {
& > .menu-button-bar {
/* The left button container */
& > .start {
margin-left: $margin;
}
/* .AnyName { Name defined in config after #
background-color: @noti-bg;
padding: 8px;
margin: 8px;
border-radius: 12px;
}
/* The right button container */
& > .end {
margin-right: $margin;
}
.AnyName>button {
background: transparent;
border: none;
}
/* The left and right button container */
& > .widget-menubar-container {
button {
border-radius: $border-radius;
margin: 0 $margin / 2;
}
}
}
/* The revealer buttons */
& > revealer {
margin-top: $margin;
button {
border-radius: $border-radius;
margin: $margin;
margin-top: 0;
}
}
/* .AnyName { Name defined in config after #
background-color: rgba(var(--noti-bg), 1.0);
padding: 8px;
margin: 8px;
border-radius: 12px;
}
.AnyName>button:hover {
background-color: @noti-bg-hover;
} */
.AnyName>button {
background: transparent;
border: none;
}
.topbar-buttons > button {
/* Name defined in config after # */
border: none;
background: transparent;
.AnyName>button:hover {
background-color: var(--noti-bg-hover);
} */
}
@define-color mpris-album-art-overlay rgba(0, 0, 0, 0.55);
@define-color mpris-button-hover rgba(0, 0, 0, 0.50);
:root {
--mpris-album-art-overlay: rgba(0, 0, 0, 0.55);
--mpris-button-hover: rgba(0, 0, 0, 0.5);
}
$mpris-shadow: 0px 0px 10px rgba(0, 0, 0, 0.75);
.widget-mpris {
margin: $margin;
/* 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"};
.mpris-background {
filter: blur(10px);
}
.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;
}
.mpris-overlay {
padding: 16px;
background-color: var(--mpris-album-art-overlay);
button:hover {
/* The media player buttons (play, pause, next, etc...) */
background: var(--noti-bg-hover);
}
.widget-mpris-album-art {
border-radius: $border-radius;
box-shadow: $mpris-shadow;
-gtk-icon-size: 96px;
}
.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 control buttons */
&:hover {
background-color: var(--mpris-button-hover);
}
}
}
}
......
.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"};
& > label {
margin: $margin;
font-size: 1.5rem;
}
& > button {
margin: $margin;
border-radius: $border-radius;
}
}
.widget-volume {
background-color: #{"@noti-bg"};
padding: 8px;
margin: 8px;
border-radius: 12px;
padding: $margin;
margin: $margin;
border-radius: $border-radius;
}
.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;
background-color: var(--noti-bg-alt);
padding: $margin/2 $margin $margin $margin;
margin: 0px $margin $margin $margin;
border-radius: $border-radius;
}
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/erikreider/swaync">
<file preprocess="xml-stripblanks">icons/scalable/actions/swaync-collapse-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/swaync-close-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/swaync-up-small-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/swaync-down-small-symbolic.svg</file>
<file preprocess="xml-stripblanks">ui/notification_window.ui</file>
<file preprocess="xml-stripblanks">ui/notification.ui</file>
<file preprocess="xml-stripblanks">ui/control_center.ui</file>
<file preprocess="xml-stripblanks">ui/mpris_player.ui</file>
</gresource>
</gresources>
using Gtk 4.0;
template $SwayNotificationCenterControlCenter: ApplicationWindow {
name: "control-center-window";
styles [
"blank-window",
]
ScrolledWindow window {
hscrollbar-policy: never;
styles [
"control-center",
]
child: Viewport {
child: $IterBox box {
orientation: vertical;
vexpand: true;
Box notifications_box {
orientation: vertical;
Stack stack {
hhomogeneous: true;
vhomogeneous: true;
transition-type: crossfade;
transition-duration: 200;
vexpand: true;
StackPage {
name: "notifications-list";
child: ScrolledWindow scrolled_window {
hscrollbar-policy: never;
has-frame: false;
child: Viewport viewport {
vexpand: true;
child: ListBox list_box {
valign: fill;
styles [
"control-center-list",
]
};
};
};
}
StackPage {
name: "notifications-placeholder";
child: Box {
halign: center;
valign: center;
hexpand: true;
vexpand: true;
orientation: vertical;
spacing: 12;
styles [
"control-center-list-placeholder",
]
Image {
pixel-size: 96;
icon-name: "preferences-system-notifications-symbolic";
use-fallback: true;
}
Label text_empty_label {
label: "No Notifications";
}
};
}
}
}
};
};
}
}
using Gtk 4.0;
template $SwayNotificationCenterWidgetsMprisMprisPlayer: $Underlay {
overflow: hidden;
valign: start;
hexpand: true;
underlay_child: Picture background_picture {
content-fit: cover;
styles [
"mpris-background",
]
};
child: Box {
orientation: vertical;
styles [
"mpris-overlay",
]
Box {
spacing: 12;
Image album_art {
icon-name: "audio-x-generic-symbolic";
use-fallback: true;
pixel-size: 96;
overflow: hidden;
styles [
"widget-mpris-album-art",
]
}
Box {
orientation: vertical;
spacing: 4;
hexpand: true;
valign: center;
Label title {
justify: left;
ellipsize: end;
wrap-mode: word_char;
xalign: 0;
yalign: 0;
styles [
"widget-mpris-title",
]
}
Label sub_title {
justify: left;
ellipsize: end;
wrap-mode: word_char;
xalign: 0;
yalign: 0;
styles [
"widget-mpris-subtitle",
]
}
}
}
Box {
spacing: 6;
halign: center;
Button button_shuffle {
has-frame: false;
icon-name: "media-playlist-shuffle-symbolic";
styles [
"circular",
"image-button",
"flat",
]
}
Button button_prev {
has-frame: false;
icon-name: "media-seek-backward-symbolic";
styles [
"circular",
"image-button",
"flat",
]
}
Button button_play_pause {
has-frame: false;
icon-name: "media-playback-pause-symbolic";
styles [
"circular",
"image-button",
"flat",
]
}
Button button_next {
has-frame: false;
icon-name: "media-seek-forward-symbolic";
styles [
"circular",
"image-button",
"flat",
]
}
Button button_repeat {
has-frame: false;
icon-name: "media-playlist-repeat-symbolic";
styles [
"circular",
"image-button",
"flat",
]
}
}
};
}
using Gtk 4.0;
template $SwayNotificationCenterNotification: Box {
hexpand: true;
styles [
"notification-row",
]
Revealer revealer {
transition-type: crossfade;
reveal-child: false;
hexpand: true;
child: $DismissibleWidget dismissible_widget {
halign: fill;
valign: fill;
child: Overlay base_widget {
hexpand: true;
styles [
"notification-background",
]
$IterBox base_box {
orientation: vertical;
overflow: hidden;
styles [
"notification",
]
Box default_action {
orientation: vertical;
styles [
"notification-default-action",
]
Box {
orientation: vertical;
spacing: 8;
styles [
"notification-content",
]
Box {
// Box with images/icons
Overlay {
halign: center;
valign: center;
child: Image img {
// pixel-size: 48;
valign: center;
overflow: hidden;
styles [
"image",
]
};
[overlay]
Image img_app_icon {
// pixel-size: 48;
halign: end;
valign: end;
overflow: hidden;
styles [
"app-icon",
]
}
}
// Body, summary, and time
Box {
hexpand: true;
vexpand: true;
valign: center;
orientation: vertical;
styles [
"text-box",
]
Box {
orientation: horizontal;
valign: center;
Label summary {
justify: left;
ellipsize: end;
wrap: false;
xalign: 0;
hexpand: true;
styles [
"summary",
]
}
Label time {
justify: left;
ellipsize: none;
valign: start;
styles [
"time",
]
}
}
Label body {
ellipsize: end;
justify: left;
wrap-mode: word_char;
natural-wrap-mode: word;
lines: 1;
xalign: 0;
yalign: 0;
hexpand: true;
styles [
"body",
]
}
}
}
ProgressBar progress_bar {
orientation: horizontal;
}
Picture body_image {
halign: center;
overflow: hidden;
}
Box inline_reply_box {
styles [
"inline-reply",
]
Entry inline_reply_entry {
input-purpose: free_form;
input-hints: spellcheck | emoji;
hexpand: true;
styles [
"inline-reply-entry",
]
}
Button inline_reply_button {
styles [
"inline-reply-button",
]
}
}
}
}
FlowBox alt_actions_box {
orientation: horizontal;
homogeneous: true;
overflow: hidden;
selection-mode: none;
activate-on-single-click: true;
styles [
"notification-alt-actions",
]
}
}
[overlay]
$SwayNotificationCenterNotificationCloseButton close_button {}
};
};
}
}
using Gtk 4.0;
template $SwayNotificationCenterNotificationWindow: Gtk.ApplicationWindow {
styles [
"floating-notifications",
]
Gtk.ScrolledWindow scrolled_window {
propagate-natural-height: true;
hscrollbar-policy: never;
vscrollbar-policy: automatic;
has-frame: false;
Gtk.Viewport viewport {
vexpand: true;
$IterBox box {
orientation: vertical;
}
}
}
}
......@@ -19,6 +19,9 @@ swaync - A simple notification daemon with a GTK gui for notifications and the c
*-s, --style* <CSS file>
Use a custom Stylesheet file
*--skip-system-css*
Skip trying to parse the packaged Stylesheet file. Useful for CSS debugging
*-c, --config* <Config file>
Use a custom config file
......
......@@ -330,11 +330,6 @@ config file to be able to detect config errors
optional: true ++
default: 96 ++
description: The size of the album art. ++
image-radius: ++
type: integer ++
optional: true ++
default: 12 ++
description: The border radius of the album art. ++
blacklist: ++
type: array ++
optional: true ++
......@@ -615,7 +610,6 @@ config file to be able to detect config errors
},
"mpris": {
"image-size": 96,
"image-radius": 12,
"blacklist": ["playerctld"],
"autohide": true
},
......
project('sway-notificaton-center', ['c', 'vala'],
version: '0.11.0',
meson_version: '>= 0.59.0',
meson_version: '>= 0.60.0',
default_options: [ 'warning_level=2' ],
)
......
#include <gdk/gdk.h>
#include <gdk/gdkwayland.h>
#include <glib.h>
#include <stdio.h>
#include <wayland-client.h>
/** GDK doesn't provide a vapi file for GDK Wayland... */
#define PRINT_ERROR \
g_error("Gdk Display isn't a Wayland display! Only Wayland is supported")
struct wl_display *get_wl_display() {
GdkDisplay *display = gdk_display_get_default();
if (GDK_IS_WAYLAND_DISPLAY(display)) {
return gdk_wayland_display_get_wl_display(display);
}
PRINT_ERROR;
return NULL;
}
struct wl_surface *get_wl_surface(GdkWindow *window) {
if (GDK_IS_WAYLAND_WINDOW(window)) {
return gdk_wayland_window_get_wl_surface(window);
}
PRINT_ERROR;
return NULL;
}
extern Wl.Display * get_wl_display ();
extern Wl.Surface * get_wl_surface (Gdk.Window window);
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);
}
}
}
namespace SwayNotificationCenter {
public class BlankWindow : Gtk.Window {
public unowned Gdk.Display display { get; private set; }
public class BlankWindow : Gtk.ApplicationWindow {
public unowned Gdk.Monitor monitor { get; private set; }
unowned SwayncDaemon daemon;
Gtk.Button button;
private Gtk.GestureClick blank_window_gesture;
private bool blank_window_down = false;
private bool blank_window_in = false;
public BlankWindow (Gdk.Display disp,
Gdk.Monitor mon,
SwayncDaemon dae) {
display = disp;
monitor = mon;
daemon = dae;
public BlankWindow (Gdk.Monitor monitor) {
this.monitor = monitor;
// Use button click event instead of Window button_press_event due
// to Gtk layer shell bug. This would grab focus instead of ControlCenter
button = new Gtk.Button () {
expand = true,
opacity = 0,
relief = Gtk.ReliefStyle.NONE,
visible = true,
};
button.clicked.connect (() => {
try {
daemon.set_visibility (false);
} catch (Error e) {
stderr.printf ("BlankWindow Click Error: %s\n", e.message);
blank_window_gesture = new Gtk.GestureClick ();
((Gtk.Widget) this).add_controller (blank_window_gesture);
blank_window_gesture.touch_only = false;
blank_window_gesture.exclusive = true;
blank_window_gesture.button = Gdk.BUTTON_PRIMARY;
blank_window_gesture.propagation_phase = Gtk.PropagationPhase.BUBBLE;
blank_window_gesture.pressed.connect ((n_press, x, y) => {
blank_window_in = true;
blank_window_down = true;
});
blank_window_gesture.released.connect ((n_press, x, y) => {
// Emit released
if (!blank_window_down) return;
blank_window_down = false;
if (blank_window_in) {
try {
swaync_daemon.set_visibility (false);
} catch (Error e) {
stderr.printf ("ControlCenter BlankWindow Click Error: %s\n",
e.message);
}
}
if (blank_window_gesture.get_current_sequence () == null) {
blank_window_in = false;
}
});
blank_window_gesture.update.connect ((gesture, sequence) => {
Gtk.GestureSingle gesture_single = (Gtk.GestureSingle) gesture;
if (sequence != gesture_single.get_current_sequence ()) return;
// Calculate if the clicked coords intersect other monitors
double x, y;
gesture.get_point (sequence, out x, out y);
Graphene.Point click_point = Graphene.Point ()
.init ((float) x, (float) y);
Graphene.Rect ? bounds = null;
this.compute_bounds (this, out bounds);
if (bounds != null && bounds.contains_point (click_point)) {
blank_window_in = false;
}
});
add (button);
blank_window_gesture.cancel.connect (() => {
blank_window_down = false;
});
if (!GtkLayerShell.is_supported ()) {
stderr.printf ("GTKLAYERSHELL IS NOT SUPPORTED!\n");
......@@ -48,25 +72,22 @@ namespace SwayNotificationCenter {
GtkLayerShell.set_exclusive_zone (this, -1);
GtkLayerShell.Layer layer;
switch (ConfigModel.instance.control_center_layer) {
case Layer.BACKGROUND:
layer = GtkLayerShell.Layer.BACKGROUND;
break;
case Layer.BOTTOM:
layer = GtkLayerShell.Layer.BOTTOM;
break;
case Layer.TOP:
layer = GtkLayerShell.Layer.TOP;
break;
default:
case Layer.OVERLAY:
layer = GtkLayerShell.Layer.OVERLAY;
break;
}
GtkLayerShell.set_layer (this, layer);
GtkLayerShell.set_layer (
this, ConfigModel.instance.control_center_layer.to_layer ());
get_style_context ().add_class ("blank-window");
add_css_class ("blank-window");
}
protected override void snapshot (Gtk.Snapshot snapshot) {
// HACK: Fixes fully transparent windows not being mapped
Gdk.RGBA color = Gdk.RGBA () {
red = 0,
green = 0,
blue = 0,
alpha = 0,
};
snapshot.append_color (color, Graphene.Rect.zero ());
base.snapshot (snapshot);
}
}
}
......@@ -75,7 +75,6 @@
},
"mpris": {
"image-size": 96,
"image-radius": 12,
"blacklist": [],
"autohide": false
},
......
......@@ -38,6 +38,20 @@ namespace SwayNotificationCenter {
return "overlay";
}
}
public GtkLayerShell.Layer to_layer () {
switch (this) {
case BACKGROUND:
return GtkLayerShell.Layer.BACKGROUND;
case BOTTOM:
return GtkLayerShell.Layer.BOTTOM;
case TOP:
return GtkLayerShell.Layer.TOP;
default:
case OVERLAY:
return GtkLayerShell.Layer.OVERLAY;
}
}
}
public enum CssPriority {
......@@ -75,8 +89,7 @@ namespace SwayNotificationCenter {
public string ? category { get; set; default = null; }
private const RegexCompileFlags REGEX_COMPILE_OPTIONS =
RegexCompileFlags.MULTILINE
| RegexCompileFlags.JAVASCRIPT_COMPAT;
RegexCompileFlags.MULTILINE;
private const RegexMatchFlags REGEX_MATCH_FLAGS = RegexMatchFlags.NOTEMPTY;
......
......@@ -145,7 +145,7 @@
},
"control-center-height": {
"type": "integer",
"description": "Height of the control center in pixels. Ignored when 'fit-to-screen' is set to 'true'",
"description": "Height of the control center in pixels. Ignored when 'fit-to-screen' is set to 'true'. Also limited to the height of the monitor, unless 'layer-shell-cover-screen' is set to false.",
"default": 600,
"minimum": 300
},
......@@ -409,17 +409,6 @@
"type": "integer",
"description": "The size of the album art",
"default": 96
},
"image-radius": {
"type": "integer",
"description": "The border radius of the album art. Will be overriden by setting the border-radius in the style.css for the \".widget-mpris-album-art\" class",
"default": 12
},
"blur": {
"type": "boolean",
"description": "Appy the artwork as the MPRIS background and blur it",
"default": true
}
}
},
......
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.40.0 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<template class="SwayNotificationCenterControlCenter" parent="GtkApplicationWindow">
<property name="name">control-center-window</property>
<property name="can-focus">False</property>
<property name="resizable">False</property>
<property name="skip-taskbar-hint">True</property>
<property name="skip-pager-hint">True</property>
<property name="urgency-hint">True</property>
<property name="decorated">False</property>
<property name="deletable">False</property>
<property name="gravity">north-east</property>
<property name="show-menubar">False</property>
<child>
<object class="GtkBox" id="box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="notifications_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hscrollbar-policy">never</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">notifications-list</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">96</property>
<property name="icon-name">preferences-system-notifications-symbolic</property>
<property name="use-fallback">True</property>
<property name="icon_size">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="text_empty_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">No Notifications</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="control-center-list-placeholder"/>
</style>
</object>
<packing>
<property name="name">notifications-placeholder</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<style>
<class name="control-center"/>
</style>
</object>
</child>
<style>
<class name="blank-window"/>
</style>
</template>
</interface>
namespace SwayNotificationCenter {
[GtkTemplate (ui = "/org/erikreider/sway-notification-center/controlCenter/controlCenter.ui")]
/* Only to be used by the Viewport in the Control Center */
private class FixedViewportLayout : Gtk.LayoutManager {
private unowned Gtk.ScrolledWindow parent;
public FixedViewportLayout (Gtk.ScrolledWindow parent) {
this.parent = parent;
}
public override void measure (Gtk.Widget widget,
Gtk.Orientation orientation, int for_size,
out int minimum, out int natural,
out int minimum_baseline, out int natural_baseline) {
minimum = 0;
natural = 0;
minimum_baseline = 0;
natural_baseline = 0;
if (widget == null || !widget.should_layout ()) {
return;
}
unowned Gtk.Widget child = ((Gtk.Viewport) widget).child;
if (!child.should_layout ()) {
return;
}
child.measure (orientation, for_size,
out minimum, out natural,
out minimum_baseline, out natural_baseline);
}
public override void allocate (Gtk.Widget widget,
int width, int height, int baseline) {
if (widget == null || !widget.should_layout ()) {
return;
}
unowned Gtk.Widget child = ((Gtk.Viewport) widget).child;
if (!child.should_layout ()) {
return;
}
if (ConfigModel.instance.fit_to_screen) {
child.allocate (width, height, baseline, null);
return;
}
int m_height, n_height;
child.measure (Gtk.Orientation.VERTICAL, width,
out m_height, out n_height,
null, null);
int m_width, n_width;
child.measure (Gtk.Orientation.HORIZONTAL, height,
out m_width, out n_width,
null, null);
int parent_width = parent.get_width ();
int parent_height = parent.get_height ();
// Limit the size to the ScrolledWindows size
child.allocate (
n_width.clamp ((int) Math.fmin (m_width, parent_width), parent_width),
n_height.clamp ((int) Math.fmin (m_height, parent_height), parent_height),
baseline, null);
}
}
[GtkTemplate (ui = "/org/erikreider/swaync/ui/control_center.ui")]
public class ControlCenter : Gtk.ApplicationWindow {
[GtkChild]
unowned Gtk.ScrolledWindow window;
[GtkChild]
unowned Gtk.Box notifications_box;
[GtkChild]
unowned Gtk.Label text_empty_label;
......@@ -9,17 +78,18 @@ namespace SwayNotificationCenter {
unowned Gtk.Stack stack;
[GtkChild]
unowned Gtk.ScrolledWindow scrolled_window;
FadedViewport viewport = new FadedViewport (20);
Gtk.ListBox list_box = new Gtk.ListBox ();
[GtkChild]
unowned Gtk.Viewport viewport;
[GtkChild]
unowned Gtk.ListBox list_box;
IterListBoxController list_box_controller;
[GtkChild]
unowned Gtk.Box box;
unowned IterBox 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;
uint scroll_timer_id = 0;
HashTable<uint32, unowned NotificationGroup> noti_groups_id =
new HashTable<uint32, unowned NotificationGroup> (direct_hash, direct_equal);
......@@ -30,10 +100,12 @@ namespace SwayNotificationCenter {
const string STACK_NOTIFICATIONS_PAGE = "notifications-list";
const string STACK_PLACEHOLDER_PAGE = "notifications-placeholder";
private Gtk.GestureMultiPress blank_window_gesture;
private Gtk.GestureClick blank_window_gesture;
private bool blank_window_down = false;
private bool blank_window_in = false;
private Gtk.EventControllerKey key_controller;
private SwayncDaemon swaync_daemon;
private NotiDaemon noti_daemon;
......@@ -49,19 +121,7 @@ namespace SwayNotificationCenter {
this.swaync_daemon = swaync_daemon;
this.noti_daemon = noti_daemon;
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);
list_box_controller = new IterListBoxController (list_box);
text_empty_label.set_text (ConfigModel.instance.text_empty);
......@@ -75,23 +135,20 @@ namespace SwayNotificationCenter {
}
GtkLayerShell.init_for_window (this);
GtkLayerShell.set_namespace (this, "swaync-control-center");
GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.TOP, true);
GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.LEFT, true);
GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.RIGHT, true);
GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.BOTTOM, true);
set_anchor ();
}
this.map.connect (() => {
set_anchor ();
// Wait until the layer has attached
unowned Gdk.Surface surface = get_surface ();
if (!(surface is Gdk.Surface)) {
return;
}
ulong id = 0;
id = notify["has-toplevel-focus"].connect (() => {
disconnect (id);
unowned Gdk.Monitor monitor = null;
unowned Gdk.Window ? win = get_window ();
if (win != null) {
monitor = get_display ().get_monitor_at_window (win);
}
id = surface.enter_monitor.connect ((monitor) => {
surface.disconnect (id);
swaync_daemon.show_blank_windows (monitor);
});
});
......@@ -100,23 +157,22 @@ namespace SwayNotificationCenter {
/*
* Handling of bank window presses (pressing outside of ControlCenter)
*/
blank_window_gesture = new Gtk.GestureMultiPress (this);
blank_window_gesture.set_touch_only (false);
blank_window_gesture.set_exclusive (true);
blank_window_gesture.set_button (Gdk.BUTTON_PRIMARY);
blank_window_gesture.set_propagation_phase (Gtk.PropagationPhase.BUBBLE);
blank_window_gesture.pressed.connect ((_gesture, _n_press, x, y) => {
blank_window_gesture = new Gtk.GestureClick ();
((Gtk.Widget) this).add_controller (blank_window_gesture);
blank_window_gesture.touch_only = false;
blank_window_gesture.exclusive = true;
blank_window_gesture.button = Gdk.BUTTON_PRIMARY;
blank_window_gesture.propagation_phase = Gtk.PropagationPhase.BUBBLE;
blank_window_gesture.pressed.connect ((n_press, x, y) => {
// Calculate if the clicked coords intersect the ControlCenter
Gdk.Rectangle click_rectangle = Gdk.Rectangle () {
width = 1,
height = 1,
x = (int) x,
y = (int) y,
};
blank_window_in = !box.intersect (click_rectangle, null);
Graphene.Point click_point = Graphene.Point ()
.init ((float) x, (float) y);
Graphene.Rect ? bounds = null;
window.compute_bounds (this, out bounds);
blank_window_in = !(bounds != null && bounds.contains_point (click_point));
blank_window_down = true;
});
blank_window_gesture.released.connect ((gesture, _n_press, _x, _y) => {
blank_window_gesture.released.connect ((n_press, x, y) => {
// Emit released
if (!blank_window_down) return;
blank_window_down = false;
......@@ -129,8 +185,7 @@ namespace SwayNotificationCenter {
}
}
Gdk.EventSequence ? sequence = gesture.get_current_sequence ();
if (sequence == null) {
if (blank_window_gesture.get_current_sequence () == null) {
blank_window_in = false;
}
});
......@@ -140,220 +195,151 @@ namespace SwayNotificationCenter {
// Calculate if the clicked coords intersect the ControlCenter
double x, y;
gesture.get_point (sequence, out x, out y);
Gdk.Rectangle click_rectangle = Gdk.Rectangle () {
width = 1,
height = 1,
x = (int) x,
y = (int) y,
};
if (box.intersect (click_rectangle, null)) {
Graphene.Point click_point = Graphene.Point ()
.init ((float) x, (float) y);
Graphene.Rect ? bounds = null;
window.compute_bounds (this, out bounds);
if (bounds != null && bounds.contains_point (click_point)) {
blank_window_in = false;
}
});
blank_window_gesture.cancel.connect ((gesture, sequence) => {
blank_window_gesture.cancel.connect (() => {
blank_window_down = false;
});
// Only use release for closing notifications due to Escape key
// sometimes being passed through to unfucused application
// Ex: Firefox in a fullscreen YouTube video
this.key_release_event.connect ((w, event_key) => {
if (this.get_focus () is Gtk.Entry) {
switch (Gdk.keyval_name (event_key.keyval)) {
case "Escape":
this.set_focus (null);
return true;
}
return false;
}
if (event_key.type == Gdk.EventType.KEY_RELEASE) {
switch (Gdk.keyval_name (event_key.keyval)) {
case "Escape":
case "Caps_Lock":
this.set_visibility (false);
return true;
}
}
return true;
});
key_press_event.connect (key_press_event_cb);
key_controller = new Gtk.EventControllerKey ();
key_controller.set_propagation_phase (Gtk.PropagationPhase.CAPTURE);
((Gtk.Widget) this).add_controller (key_controller);
key_controller.key_released.connect (key_released_event_cb);
key_controller.key_pressed.connect (key_press_event_cb);
stack.set_visible_child_name (STACK_PLACEHOLDER_PAGE);
// Switches the stack page depending on the amount of notifications
list_box.add.connect (() => {
stack.set_visible_child_name (STACK_NOTIFICATIONS_PAGE);
});
list_box.remove.connect ((container, _widget) => {
if (container.get_children ().length () > 0) return;
stack.set_visible_child_name (STACK_PLACEHOLDER_PAGE);
});
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);
}
}
// Scroll to the expanded group once said group has fully expanded
void scroll_animate (double to) {
scroll_animation.stop ();
scroll_animation.start (scroll_animation_progress, to);
if (scroll_timer_id > 0) {
Source.remove (scroll_timer_id);
scroll_timer_id = 0;
}
scroll_timer_id = Timeout.add_once (Constants.ANIMATION_DURATION, () => {
scroll_timer_id = 0;
if (expanded_group == null) {
return;
}
float y = expanded_group.get_relative_y (list_box);
if (y > 0) {
viewport.scroll_to (expanded_group, null);
}
});
}
/// 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 ();
private void key_released_event_cb (uint keyval, uint keycode, Gdk.ModifierType state) {
if (this.get_focus () is Gtk.Entry) {
switch (Gdk.keyval_name (keyval)) {
case "Escape":
this.set_focus (null);
return;
}
cr.pop_group_to_source ();
cr.paint ();
cr.restore ();
return;
}
switch (Gdk.keyval_name (keyval)) {
case "Escape":
case "Caps_Lock":
this.set_visibility (false);
return;
}
return true;
}
private bool key_press_event_cb (Gdk.EventKey event_key) {
private bool key_press_event_cb (uint keyval, uint keycode, Gdk.ModifierType state) {
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 ());
var children = list_box_controller.get_children ();
var group = (NotificationGroup) list_box.get_focus_child ();
switch (Gdk.keyval_name (keyval)) {
case "Return":
if (group != null) {
var noti = group.get_latest_notification ();
if (group.only_single_notification () && noti != null) {
noti.click_default_action ();
break;
}
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;
}
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;
}
var noti = group.get_latest_notification ();
if (group.only_single_notification () && noti != null) {
close_notification (noti.param.applied_id, true);
break;
list_position--;
} else {
if (list_position > 0) list_position--;
if (children.last ().data == group) {
delta = 0;
}
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;
var noti = group.get_latest_notification ();
if (group.only_single_notification () && noti != null) {
close_notification (noti.param.applied_id, true);
break;
}
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;
}
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 num_keyval = Gdk.keyval_from_name (
(i + 1).to_string ());
if (keyval == num_keyval && group != null) {
var noti = group.get_latest_notification ();
noti.click_alt_action (i);
break;
}
break;
}
navigate_list (list_position);
}
break;
}
navigate_list (list_position);
// Override the builtin list navigation
return true;
}
......@@ -371,10 +357,13 @@ namespace SwayNotificationCenter {
bool has_notification = false;
foreach (string key in w) {
// Reposition the notifications_box
// TODO: Move notifications into its own widget
if (key == "notifications") {
has_notification = true;
uint pos = box.get_children ().length ();
box.reorder_child (notifications_box, (int) (pos > 0 ? --pos : 0));
unowned Gtk.Widget ? sibling = box.get_last_child ();
if (sibling != notifications_box) {
box.reorder_child_after (notifications_box, sibling);
}
continue;
}
// Add the widget if it is valid
......@@ -382,13 +371,14 @@ namespace SwayNotificationCenter {
key, swaync_daemon, noti_daemon);
if (widget == null) continue;
widgets.append_val (widget);
box.pack_start (widgets.index (widgets.length - 1),
false, true, 0);
box.append (widgets.index (widgets.length - 1));
}
if (!has_notification) {
warning ("Notification widget not included in \"widgets\" config. Using default bottom position");
uint pos = box.get_children ().length ();
box.reorder_child (notifications_box, (int) (pos > 0 ? --pos : 0));
unowned Gtk.Widget ? sibling = box.get_last_child ();
if (sibling != notifications_box) {
box.reorder_child_after (notifications_box, sibling);
}
}
}
......@@ -411,23 +401,8 @@ namespace SwayNotificationCenter {
GtkLayerShell.set_keyboard_mode (this, mode);
// Set layer
GtkLayerShell.Layer layer;
switch (ConfigModel.instance.control_center_layer) {
case Layer.BACKGROUND:
layer = GtkLayerShell.Layer.BACKGROUND;
break;
case Layer.BOTTOM:
layer = GtkLayerShell.Layer.BOTTOM;
break;
case Layer.TOP:
layer = GtkLayerShell.Layer.TOP;
break;
default:
case Layer.OVERLAY:
layer = GtkLayerShell.Layer.OVERLAY;
break;
}
GtkLayerShell.set_layer (this, layer);
GtkLayerShell.set_layer (
this, ConfigModel.instance.control_center_layer.to_layer ());
// Set whether the control center should cover the whole screen or not
bool cover_screen = ConfigModel.instance.layer_shell_cover_screen;
......@@ -470,13 +445,13 @@ namespace SwayNotificationCenter {
}
}
// Set the box margins
box.set_margin_top (ConfigModel.instance.control_center_margin_top);
box.set_margin_start (ConfigModel.instance.control_center_margin_left);
box.set_margin_end (ConfigModel.instance.control_center_margin_right);
box.set_margin_bottom (ConfigModel.instance.control_center_margin_bottom);
// Set the window margins
window.set_margin_top (ConfigModel.instance.control_center_margin_top);
window.set_margin_start (ConfigModel.instance.control_center_margin_left);
window.set_margin_end (ConfigModel.instance.control_center_margin_right);
window.set_margin_bottom (ConfigModel.instance.control_center_margin_bottom);
// Anchor box to north/south edges as needed
// Anchor window to north/south edges as needed
Gtk.Align align_x = Gtk.Align.END;
switch (pos_x) {
case PositionX.LEFT:
......@@ -515,15 +490,22 @@ namespace SwayNotificationCenter {
// Fit the ControlCenter to the monitor height
if (ConfigModel.instance.fit_to_screen) align_y = Gtk.Align.FILL;
// Set the ControlCenter alignment
box.set_halign (align_x);
box.set_valign (align_y);
window.set_halign (align_x);
window.set_valign (align_y);
list_box.set_valign (list_align);
list_box.set_sort_func (list_box_sort_func);
list_box.set_selection_mode (Gtk.SelectionMode.NONE);
list_box.set_activate_on_single_click (false);
// Always set the size request in all events.
window.set_propagate_natural_height (true);
// Re-set the minimum size
box.set_size_request (ConfigModel.instance.control_center_width,
ConfigModel.instance.control_center_height);
// Use a custom layout to limit the minimum size above to the size
// of the window so that it doesn't exceed the monitors edge
window.child.set_layout_manager (new FixedViewportLayout (window));
}
/**
......@@ -562,7 +544,7 @@ namespace SwayNotificationCenter {
public uint notification_count () {
uint count = 0;
foreach (unowned Gtk.Widget widget in list_box.get_children ()) {
foreach (unowned Gtk.Widget widget in list_box_controller.get_children ()) {
if (widget is NotificationGroup) {
count += ((NotificationGroup) widget).get_num_notifications ();
}
......@@ -571,7 +553,7 @@ namespace SwayNotificationCenter {
}
public void close_all_notifications () {
foreach (var w in list_box.get_children ()) {
foreach (unowned Gtk.Widget w in list_box_controller.get_children ()) {
NotificationGroup group = (NotificationGroup) w;
if (group != null) group.close_all_notifications ();
}
......@@ -591,14 +573,14 @@ namespace SwayNotificationCenter {
}
private void navigate_list (int i) {
unowned Gtk.Widget ? widget = list_box.get_children ().nth_data (i);
unowned Gtk.ListBoxRow ? widget = list_box.get_row_at_index (i);
if (widget == null) {
// Try getting the last widget
if (list_reverse) {
widget = list_box.get_children ().nth_data (0);
widget = list_box.get_row_at_index (0);
} else {
int len = ((int) list_box.get_children ().length ()) - 1;
widget = list_box.get_children ().nth_data (len);
int len = ((int) list_box_controller.length) - 1;
widget = list_box.get_row_at_index (len);
}
}
if (widget != null) {
......@@ -615,20 +597,18 @@ namespace SwayNotificationCenter {
if (this.visible) {
// Focus the first notification
list_position = list_reverse ?
(((int) list_box.get_children ().length ()) - 1) : 0;
if (list_position == uint.MAX) list_position = 0;
list_position = (list_reverse ? (((int) list_box_controller.length) - 1) : 0)
.clamp (0, (int) list_box_controller.length);
list_box.grab_focus ();
navigate_list (list_position);
foreach (var w in list_box.get_children ()) {
foreach (unowned Gtk.Widget w in list_box_controller.get_children ()) {
var group = (NotificationGroup) w;
if (group != null) group.update ();
}
this.get_style_context ().add_class ("open");
}
else {
this.get_style_context ().remove_class ("open");
add_css_class ("open");
} else {
remove_css_class ("open");
}
swaync_daemon.subscribe_v2 (notification_count (),
noti_daemon.dnd,
......@@ -638,45 +618,56 @@ namespace SwayNotificationCenter {
public bool toggle_visibility () {
var cc_visibility = !this.visible;
if (this.visible != cc_visibility) {
this.set_visible (cc_visibility);
on_visibility_change ();
}
set_visibility (cc_visibility);
return cc_visibility;
}
public void set_visibility (bool visibility) {
if (this.visible == visibility) return;
if (visibility) {
// Destroy the wl_surface to get a new "enter-monitor" signal
((Gtk.Widget) this).unrealize ();
}
this.set_visible (visibility);
on_visibility_change ();
}
public void close_notification (uint32 id, bool dismiss) {
unowned NotificationGroup group = null;
if (!noti_groups_id.lookup_extended (id, null, out group))return;
if (!noti_groups_id.lookup_extended (id, null, out group)) {
return;
}
foreach (var w in group.get_notifications ()) {
var noti = (Notification) w;
if (noti != null && noti.param.applied_id == id) {
if (!dismiss) {
noti.remove_noti_timeout ();
noti.destroy ();
} else {
if (dismiss) {
noti.close_notification (false);
group.remove_notification (noti);
}
group.remove_notification (noti);
noti_groups_id.remove (id);
break;
}
}
if (group.is_empty ()) {
if (group.only_single_notification ()) {
if (expanded_group == group) {
expanded_group = null;
}
} else 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 ();
list_box_controller.remove (group);
navigate_list (--list_position);
// Switches the stack page depending on the amount of notifications
if (list_box_controller.length < 1) {
stack.set_visible_child_name (STACK_PLACEHOLDER_PAGE);
}
}
}
......@@ -704,12 +695,6 @@ namespace SwayNotificationCenter {
var noti = new Notification.regular (param,
noti_daemon,
NotificationType.CONTROL_CENTER);
noti.grab_focus.connect ((w) => {
int i = list_box.get_children ().index (w);
if (list_position != uint.MAX && list_position != i) {
list_position = i;
}
});
noti.set_time ();
NotificationGroup ? group = null;
......@@ -721,34 +706,53 @@ namespace SwayNotificationCenter {
// 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);
foreach (unowned Gtk.Widget child in list_box_controller.get_children ()) {
if (child is NotificationGroup) {
child.remove_css_class ("not-expanded");
}
}
expanded_group = null;
return;
}
expanded_group = group;
expanded_group.set_sensitive (true);
fade_animate (0);
int y = expanded_group.get_relative_y (list_box);
float y = expanded_group.get_relative_y (list_box);
if (y > 0) {
scroll_animate (y);
}
foreach (unowned Gtk.Widget child in list_box.get_children ()) {
foreach (unowned Gtk.Widget child in list_box_controller.get_children ()) {
NotificationGroup g = (NotificationGroup) child;
if (g != null && g != group) {
g.set_expanded (false);
if (g.only_single_notification ()) {
g.set_sensitive (false);
}
child.add_css_class ("not-expanded");
}
}
});
if (param.name_id.length > 0) {
noti_groups_name.set (param.name_id, group);
}
list_box.add (group);
// Set the new list position when the group receives keyboard focus
Gtk.EventControllerFocus focus_controller = new Gtk.EventControllerFocus ();
group.add_controller (focus_controller);
focus_controller.enter.connect (() => {
int i = list_box_controller.get_children ().index (group);
if (list_position != int.MAX && list_position != i) {
list_position = i;
}
});
// Switches the stack page depending on the amount of notifications
stack.set_visible_child_name (STACK_NOTIFICATIONS_PAGE);
list_box_controller.append (group);
}
// Set the group as not-expanded (reduce opacity) if there's
// already a group that's expanded.
if (expanded_group != null) {
group.add_css_class ("not-expanded");
}
noti_groups_id.set (param.applied_id, group);
group.add_notification (noti);
......@@ -771,18 +775,5 @@ namespace SwayNotificationCenter {
public bool get_visibility () {
return this.visible;
}
/** Forces each notification EventBox to reload its style_context #27 */
private void reload_notifications_style () {
foreach (var c in list_box.get_children ()) {
NotificationGroup group = (NotificationGroup) c;
if (group != null) {
foreach (unowned var widget in group.get_notifications ()) {
Notification noti = (Notification) widget;
noti.reload_style_context ();
}
}
}
}
}
}
......@@ -51,15 +51,14 @@ namespace SwayNotificationCenter.Widgets {
slider.set_draw_value (false);
slider.set_round_digits (0);
slider.set_hexpand (true);
slider.value_changed.connect (() => {
this.client.set_brightness.begin ((float) slider.get_value ());
slider.tooltip_text = ((int) slider.get_value ()).to_string ();
});
add (label_widget);
pack_start (slider, true, true, 0);
show_all ();
append (label_widget);
append (slider);
}
public override void on_cc_visibility_change (bool val) {
......
......@@ -79,15 +79,11 @@ namespace SwayNotificationCenter.Widgets {
public async void set_brightness (float percent) {
this.close ();
try {
if (subsystem == "backlight") {
int actual = calc_actual (percent);
login1.set_brightness.begin (subsystem, device, actual);
} else {
login1.set_brightness.begin (subsystem, device, (uint32) percent);
}
} catch (Error e) {
error ("Error %s\n", e.message);
if (subsystem == "backlight") {
int actual = calc_actual (percent);
login1.set_brightness.begin (subsystem, device, actual);
} else {
login1.set_brightness.begin (subsystem, device, (uint32) percent);
}
connect_monitor ();
}
......
......@@ -34,8 +34,8 @@ namespace SwayNotificationCenter.Widgets {
this.swaync_daemon = swaync_daemon;
this.noti_daemon = noti_daemon;
get_style_context ().add_class (css_class_name);
if (suffix.length > 0) get_style_context ().add_class (suffix);
add_css_class (css_class_name);
if (suffix.length > 0) add_css_class (suffix);
}
protected Json.Object ? get_config (Gtk.Widget widget) {
......@@ -108,7 +108,7 @@ namespace SwayNotificationCenter.Widgets {
string t = actions.get_object_element (i).get_string_member_with_default ("type", "normal");
ButtonType type = ButtonType.parse (t);
string update_command =
actions.get_object_element (i).get_string_member_with_default ("update-command", "");
actions.get_object_element (i).get_string_member_with_default ("update-command", "");
bool active = actions.get_object_element (i).get_boolean_member_with_default ("active", false);
res[i] = Action () {
label = label,
......
......@@ -23,7 +23,8 @@ namespace SwayNotificationCenter.Widgets {
Gtk.FlowBox container = new Gtk.FlowBox ();
container.set_selection_mode (Gtk.SelectionMode.NONE);
pack_start (container, true, true, 0);
container.set_hexpand (true);
append (container);
// add action to container
foreach (var act in actions) {
......@@ -40,8 +41,6 @@ namespace SwayNotificationCenter.Widgets {
break;
}
}
show_all ();
}
public override void on_cc_visibility_change (bool value) {
......
......@@ -24,7 +24,9 @@ namespace SwayNotificationCenter.Widgets {
// Title
title_widget = new Gtk.Label (title);
add (title_widget);
title_widget.set_hexpand (true);
title_widget.set_halign (Gtk.Align.START);
append (title_widget);
// Dnd button
dnd_button = new Gtk.Switch () {
......@@ -40,10 +42,8 @@ namespace SwayNotificationCenter.Widgets {
dnd_button.set_can_focus (false);
dnd_button.valign = Gtk.Align.CENTER;
// Backwards compatible towards older CSS stylesheets
dnd_button.get_style_context ().add_class ("control-center-dnd");
pack_end (dnd_button, false);
show_all ();
dnd_button.add_css_class ("control-center-dnd");
append (dnd_button);
}
private bool state_set (Gtk.Widget widget, bool state) {
......
......@@ -42,8 +42,10 @@ namespace SwayNotificationCenter.Widgets {
}
title_widget = new Gtk.Label (title);
title_widget.set_halign (Gtk.Align.START);
title_widget.set_hexpand (true);
title_widget.show ();
add (title_widget);
append (title_widget);
if (has_clear_all_button) {
clear_all_button = new Gtk.Button.with_label (button_text);
......@@ -57,7 +59,7 @@ namespace SwayNotificationCenter.Widgets {
clear_all_button.set_can_focus (false);
clear_all_button.valign = Gtk.Align.CENTER;
clear_all_button.show ();
pack_end (clear_all_button, false);
append (clear_all_button);
}
hide ();
......
......@@ -29,18 +29,17 @@ namespace SwayNotificationCenter.Widgets {
label_widget.set_text (text);
label_widget.set_ellipsize (Pango.EllipsizeMode.END);
label_widget.set_line_wrap (true);
label_widget.set_wrap (true);
label_widget.set_lines (max_lines);
// Without this and pack_start fill, the label would expand to
// the monitors full width... GTK bug!...
label_widget.set_max_width_chars (0);
label_widget.set_line_wrap_mode (Pango.WrapMode.WORD_CHAR);
label_widget.set_wrap_mode (Pango.WrapMode.WORD_CHAR);
label_widget.set_justify (Gtk.Justification.LEFT);
label_widget.set_alignment (0, 0);
label_widget.set_xalign (0.0f);
label_widget.set_yalign (0.0f);
pack_start (label_widget, true, true, 0);
show_all ();
append (label_widget);
}
}
}
......@@ -37,35 +37,48 @@ namespace SwayNotificationCenter.Widgets {
}
}
Gtk.Box menus_container;
Gtk.Box topbar_container;
Gtk.Box left_container;
Gtk.Box right_container;
List<ConfigObject ?> menu_objects;
List<ToggleButton> toggle_buttons;
public Menubar (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) {
base (suffix, swaync_daemon, noti_daemon);
set_orientation (Gtk.Orientation.VERTICAL);
set_hexpand (true);
Json.Object ? config = get_config (this);
if (config != null) {
parse_config_objects (config);
}
menus_container = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
topbar_container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
topbar_container.get_style_context ().add_class ("menu-button-bar");
menus_container.add (topbar_container);
Gtk.Box topbar_container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
topbar_container.add_css_class ("menu-button-bar");
append (topbar_container);
left_container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) {
overflow = Gtk.Overflow.HIDDEN,
hexpand = true,
halign = Gtk.Align.START,
};
left_container.add_css_class ("widget-menubar-container");
left_container.add_css_class ("start");
topbar_container.append (left_container);
right_container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) {
overflow = Gtk.Overflow.HIDDEN,
hexpand = true,
halign = Gtk.Align.END,
};
right_container.add_css_class ("widget-menubar-container");
right_container.add_css_class ("end");
topbar_container.append (right_container);
for (int i = 0; i < menu_objects.length (); i++) {
unowned ConfigObject ? obj = menu_objects.nth_data (i);
add_menu (ref obj);
}
pack_start (menus_container, true, true, 0);
show_all ();
foreach (var obj in menu_objects) {
obj.revealer ?.set_reveal_child (false);
}
......@@ -75,43 +88,48 @@ namespace SwayNotificationCenter.Widgets {
switch (obj.type) {
case MenuType.BUTTONS:
Gtk.Box container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
if (obj.name != null) container.get_style_context ().add_class (obj.name);
if (obj.name != null) container.add_css_class (obj.name);
foreach (Action a in obj.actions) {
switch (a.type) {
case ButtonType.TOGGLE:
ToggleButton tb = new ToggleButton (a.label, a.command, a.update_command, a.active);
container.add (tb);
container.append (tb);
toggle_buttons.append (tb);
break;
default:
Gtk.Button b = new Gtk.Button.with_label (a.label);
b.clicked.connect (() => execute_command.begin (a.command));
container.add (b);
container.append (b);
break;
}
}
switch (obj.position) {
case Position.LEFT:
topbar_container.pack_start (container, false, false, 0);
left_container.append (container);
break;
case Position.RIGHT:
topbar_container.pack_end (container, false, false, 0);
right_container.append (container);
break;
}
break;
case MenuType.MENU:
Gtk.Button show_button = new Gtk.Button.with_label (obj.label);
Gtk.ToggleButton show_button = new Gtk.ToggleButton.with_label (obj.label);
Gtk.Box menu = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
if (obj.name != null) menu.get_style_context ().add_class (obj.name);
if (obj.name != null) menu.add_css_class (obj.name);
Gtk.Revealer r = new Gtk.Revealer ();
r.add (menu);
r.set_child (menu);
r.set_transition_duration (obj.animation_duration);
r.set_transition_type (obj.animation_type);
obj.revealer = r;
// Make sure that the toggle buttons state is always synced
// with the revealers visibility.
r.bind_property ("child-revealed",
show_button, "active", BindingFlags.SYNC_CREATE, null, null);
show_button.clicked.connect (() => {
bool visible = !r.get_reveal_child ();
foreach (var o in menu_objects) {
......@@ -124,27 +142,29 @@ namespace SwayNotificationCenter.Widgets {
switch (a.type) {
case ButtonType.TOGGLE:
ToggleButton tb = new ToggleButton (a.label, a.command, a.update_command, a.active);
menu.pack_start (tb, true, true, 0);
tb.set_hexpand (true);
menu.append (tb);
toggle_buttons.append (tb);
break;
default:
Gtk.Button b = new Gtk.Button.with_label (a.label);
b.set_hexpand (true);
b.clicked.connect (() => execute_command.begin (a.command));
menu.pack_start (b, true, true, 0);
menu.append (b);
break;
}
}
switch (obj.position) {
case Position.RIGHT:
topbar_container.pack_end (show_button, false, false, 0);
right_container.append (show_button);
break;
case Position.LEFT:
topbar_container.pack_start (show_button, false, false, 0);
left_container.append (show_button);
break;
}
menus_container.add (r);
append (r);
break;
}
}
......
namespace SwayNotificationCenter.Widgets.Mpris {
public struct Config {
int image_size;
int image_radius;
bool blur;
bool autohide;
string[] blacklist;
}
......@@ -24,15 +22,13 @@ namespace SwayNotificationCenter.Widgets.Mpris {
Gtk.Button button_prev;
Gtk.Button button_next;
Gtk.Box carousel_box;
Hdy.Carousel carousel;
Hdy.CarouselIndicatorDots carousel_dots;
Adw.Carousel carousel;
Adw.CarouselIndicatorDots carousel_dots;
// Default config values
Config mpris_config = Config () {
image_size = 96,
image_radius = 12,
blur = true,
autohide = false
autohide = false,
};
public Mpris (string suffix, SwayncDaemon swaync_daemon, NotiDaemon noti_daemon) {
......@@ -45,43 +41,41 @@ namespace SwayNotificationCenter.Widgets.Mpris {
visible = true,
};
button_prev = new Gtk.Button.from_icon_name ("go-previous", Gtk.IconSize.BUTTON) {
relief = Gtk.ReliefStyle.NONE,
button_prev = new Gtk.Button.from_icon_name ("go-previous") {
has_frame = false,
visible = false,
};
button_prev.clicked.connect (() => change_carousel_position (-1));
button_next = new Gtk.Button.from_icon_name ("go-next", Gtk.IconSize.BUTTON) {
relief = Gtk.ReliefStyle.NONE,
button_next = new Gtk.Button.from_icon_name ("go-next") {
has_frame = false,
visible = false,
};
button_next.clicked.connect (() => change_carousel_position (1));
carousel = new Hdy.Carousel () {
carousel = new Adw.Carousel () {
visible = true,
};
carousel.allow_scroll_wheel = true;
carousel.page_changed.connect ((index) => {
GLib.List<weak Gtk.Widget> children = carousel.get_children ();
int children_length = (int) children.length ();
if (children_length <= 1) {
if (carousel.n_pages <= 1) {
button_prev.sensitive = false;
button_next.sensitive = false;
return;
}
button_prev.sensitive = index > 0;
button_next.sensitive = index < children_length - 1;
button_next.sensitive = index < carousel.n_pages - 1;
});
carousel_box.add (button_prev);
carousel_box.add (carousel);
carousel_box.add (button_next);
add (carousel_box);
carousel_box.append (button_prev);
carousel_box.append (carousel);
carousel_box.append (button_next);
append (carousel_box);
carousel_dots = new Hdy.CarouselIndicatorDots ();
carousel_dots = new Adw.CarouselIndicatorDots ();
carousel_dots.set_carousel (carousel);
carousel_dots.show ();
add (carousel_dots);
append (carousel_dots);
// Config
Json.Object ? config = get_config (this);
......@@ -90,18 +84,6 @@ namespace SwayNotificationCenter.Widgets.Mpris {
int? image_size = get_prop<int> (config, "image-size");
if (image_size != null) mpris_config.image_size = image_size;
// Get image-border-radius
int? image_radius = get_prop<int> (config, "image-radius");
if (image_radius != null) mpris_config.image_radius = image_radius;
// Clamp the radius
mpris_config.image_radius = mpris_config.image_radius.clamp (
0, (int) (mpris_config.image_size * 0.5));
// Get blur
bool blur_found;
bool? blur = get_prop<bool> (config, "blur", out blur_found);
if (blur_found) mpris_config.blur = blur;
Json.Array ? blacklist = get_prop_array (config, "blacklist");
if (blacklist != null) {
mpris_config.blacklist = new string[blacklist.get_length ()];
......@@ -128,19 +110,6 @@ namespace SwayNotificationCenter.Widgets.Mpris {
}
}
/**
* Forces the carousel to reload its style_context.
* Fixes carousel items not redrawing when window isn't visible.
* Probably related to: https://gitlab.gnome.org/GNOME/libhandy/-/issues/363
*/
public override void on_cc_visibility_change (bool value) {
if (!value) return;
carousel.get_style_context ().changed ();
foreach (var child in carousel.get_children ()) {
child.get_style_context ().changed ();
}
}
private void setup_mpris () throws Error {
dbus_iface = Bus.get_proxy_sync (BusType.SESSION,
"org.freedesktop.DBus",
......@@ -175,7 +144,6 @@ namespace SwayNotificationCenter.Widgets.Mpris {
return false;
}
private bool check_player_metadata_empty (string name) {
MprisPlayer ? player = players.lookup (name);
if (player == null) return true;
......@@ -188,19 +156,21 @@ namespace SwayNotificationCenter.Widgets.Mpris {
}
private bool check_carousel_has_player (MprisPlayer player) {
return carousel.get_children ().find (player) != null;
return player != null && player.parent == carousel;
}
private void add_player_to_carousel (string name) {
MprisPlayer ? player = players.lookup (name);
if (player == null || check_carousel_has_player (player)) return;
carousel.prepend (player);
uint children_length = carousel.get_children ().length ();
if (children_length > 1) {
// HACK: The carousel doesn't focus the prepended player when not mapped.
carousel.append (player);
carousel.reorder (player, 0);
if (carousel.n_pages > 1) {
button_prev.show ();
button_next.show ();
// Scroll to the new player
carousel.scroll_to (player);
carousel.scroll_to (player, false);
}
if (!visible) show ();
......@@ -208,7 +178,7 @@ namespace SwayNotificationCenter.Widgets.Mpris {
private void add_player (string name, MprisSource source) {
MprisPlayer player = new MprisPlayer (source, mpris_config);
player.get_style_context ().add_class ("%s-player".printf (css_class_name));
player.add_css_class ("%s-player".printf (css_class_name));
players.set (name, player);
if (mpris_config.autohide) {
......@@ -229,10 +199,11 @@ namespace SwayNotificationCenter.Widgets.Mpris {
MprisPlayer ? player = players.lookup (name);
if (player == null || !check_carousel_has_player (player)) return;
carousel.remove (player);
uint children_length = carousel.get_children ().length ();
if (children_length == 0) hide ();
uint children_length = carousel.n_pages;
if (children_length == 0) {
hide ();
}
if (children_length <= 1) {
button_prev.hide ();
button_next.hide ();
......@@ -248,17 +219,15 @@ namespace SwayNotificationCenter.Widgets.Mpris {
remove_player_from_carousel (name);
player.before_destroy ();
player.destroy ();
players.remove (name);
}
private void change_carousel_position (int delta) {
GLib.List<weak Gtk.Widget> children = carousel.get_children ();
int children_length = (int) children.length ();
uint children_length = carousel.n_pages;
if (children_length == 0) return;
int position = ((int) carousel.position + delta).clamp (
0, children_length - 1);
carousel.scroll_to (children.nth_data (position));
uint position = ((uint) carousel.position + delta)
.clamp (0, (children_length - 1));
carousel.scroll_to (carousel.get_nth_page (position), true);
}
private bool is_blacklisted (string name) {
......@@ -266,7 +235,7 @@ namespace SwayNotificationCenter.Widgets.Mpris {
if (blacklistedPattern == null || blacklistedPattern.length == 0) {
continue;
}
if (GLib.Regex.match_simple (blacklistedPattern, name, GLib.RegexCompileFlags.JAVASCRIPT_COMPAT, 0)) {
if (GLib.Regex.match_simple (blacklistedPattern, name, RegexCompileFlags.DEFAULT, 0)) {
message ("\"%s\" is blacklisted", name);
return true;
}
......
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.40.0 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<template class="SwayNotificationCenterWidgetsMprisMprisPlayer" parent="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">start</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">4</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">12</property>
<child>
<object class="GtkImage" id="album_art">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">96</property>
<property name="icon-name">audio-x-generic-symbolic</property>
<property name="use-fallback">True</property>
<property name="icon_size">0</property>
<style>
<class name="widget-mpris-album-art"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">4</property>
<child>
<object class="GtkLabel" id="title">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="ellipsize">end</property>
<property name="width-chars">0</property>
<property name="max-width-chars">0</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
<style>
<class name="widget-mpris-title"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="sub_title">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="ellipsize">end</property>
<property name="width-chars">0</property>
<property name="max-width-chars">0</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
<style>
<class name="widget-mpris-subtitle"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="spacing">6</property>
<property name="homogeneous">True</property>
<child>
<object class="GtkButton" id="button_shuffle">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="relief">none</property>
<child>
<object class="GtkImage" id="button_shuffle_img">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="xpad">4</property>
<property name="ypad">4</property>
<property name="pixel-size">20</property>
<property name="icon-name">media-playlist-shuffle-symbolic</property>
<property name="icon_size">0</property>
</object>
</child>
<style>
<class name="circular"/>
<class name="image-button"/>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button_prev">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="relief">none</property>
<child>
<object class="GtkImage" id="button_prev_img">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="xpad">4</property>
<property name="ypad">4</property>
<property name="pixel-size">20</property>
<property name="icon-name">media-seek-backward-symbolic</property>
<property name="icon_size">0</property>
</object>
</child>
<style>
<class name="circular"/>
<class name="image-button"/>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button_play_pause">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="relief">none</property>
<child>
<object class="GtkImage" id="button_play_pause_img">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="xpad">4</property>
<property name="ypad">4</property>
<property name="pixel-size">20</property>
<property name="icon-name">media-playback-pause-symbolic</property>
<property name="icon_size">0</property>
</object>
</child>
<style>
<class name="circular"/>
<class name="image-button"/>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button_next">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="relief">none</property>
<child>
<object class="GtkImage" id="button_next_img">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="xpad">4</property>
<property name="ypad">4</property>
<property name="pixel-size">20</property>
<property name="icon-name">media-seek-forward-symbolic</property>
<property name="icon_size">0</property>
</object>
</child>
<style>
<class name="circular"/>
<class name="image-button"/>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button_repeat">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="relief">none</property>
<child>
<object class="GtkImage" id="button_repeat_img">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="xpad">4</property>
<property name="ypad">4</property>
<property name="pixel-size">20</property>
<property name="icon-name">media-playlist-repeat-symbolic</property>
<property name="icon_size">0</property>
</object>
</child>
<style>
<class name="circular"/>
<class name="image-button"/>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</template>
</interface>
namespace SwayNotificationCenter.Widgets.Mpris {
[GtkTemplate (ui = "/org/erikreider/sway-notification-center/controlCenter/widgets/mpris/mpris_player.ui")]
public class MprisPlayer : Gtk.Box {
[GtkTemplate (ui = "/org/erikreider/swaync/ui/mpris_player.ui")]
public class MprisPlayer : Underlay {
[GtkChild]
unowned Gtk.Label title;
public unowned Gtk.Label title;
[GtkChild]
unowned Gtk.Label sub_title;
......@@ -10,19 +10,18 @@ namespace SwayNotificationCenter.Widgets.Mpris {
unowned Gtk.Image album_art;
[GtkChild]
unowned Gtk.Picture background_picture;
[GtkChild]
unowned Gtk.Button button_shuffle;
[GtkChild]
unowned Gtk.Button button_prev;
[GtkChild]
unowned Gtk.Button button_play_pause;
[GtkChild]
unowned Gtk.Image button_play_pause_img;
[GtkChild]
unowned Gtk.Button button_next;
[GtkChild]
unowned Gtk.Button button_repeat;
[GtkChild]
unowned Gtk.Image button_repeat_img;
public MprisSource source { construct; get; }
......@@ -38,9 +37,6 @@ namespace SwayNotificationCenter.Widgets.Mpris {
private string prev_art_url;
private DesktopAppInfo ? desktop_entry = null;
private Gdk.Pixbuf ? album_art_pixbuf = null;
private Granite.Drawing.BufferSurface ? blurred_cover_surface = null;
private unowned Config mpris_config;
public signal void content_updated ();
......@@ -105,77 +101,6 @@ namespace SwayNotificationCenter.Widgets.Mpris {
source.properties_changed.disconnect (properties_changed);
}
public override bool draw (Cairo.Context cr) {
unowned Gdk.Window ? window = get_window ();
if (!mpris_config.blur || window == null ||
!(album_art_pixbuf is Gdk.Pixbuf)) {
return base.draw (cr);
}
const double DEGREES = Math.PI / 180.0;
unowned Gtk.StyleContext style_ctx = this.get_style_context ();
unowned Gtk.StateFlags state = style_ctx.get_state ();
Gtk.Border border = style_ctx.get_border (state);
Gtk.Border margin = style_ctx.get_margin (state);
Value radius_value = style_ctx.get_property (
Gtk.STYLE_PROPERTY_BORDER_RADIUS, state);
int radius = 0;
if (!radius_value.holds (Type.INT) ||
(radius = radius_value.get_int ()) == 0) {
radius = mpris_config.image_radius;
}
int scale = style_ctx.get_scale ();
int width = get_allocated_width ()
- margin.left - margin.right
- border.left - border.right;
int height = get_allocated_height ()
- margin.top - margin.bottom
- border.top - border.bottom;
cr.save ();
cr.new_sub_path ();
// Top Right
cr.arc (width - radius + margin.right,
radius + margin.top,
radius, -90 * DEGREES, 0 * DEGREES);
// Bottom Right
cr.arc (width - radius + margin.right,
height - radius + margin.bottom,
radius, 0 * DEGREES, 90 * DEGREES);
// Bottom Left
cr.arc (radius + margin.left,
height - radius + margin.bottom,
radius, 90 * DEGREES, 180 * DEGREES);
// Top Left
cr.arc (radius + margin.left,
radius + margin.top,
radius, 180 * DEGREES, 270 * DEGREES);
cr.close_path ();
cr.set_source_rgba (0, 0, 0, 0);
cr.clip ();
// Draw blurred player background
if (blurred_cover_surface == null
|| blurred_cover_surface.width != width
|| blurred_cover_surface.height != height) {
var buffer = new Granite.Drawing.BufferSurface (width, height);
Cairo.Surface surface = Functions.scale_pixbuf (
album_art_pixbuf, width, height, scale);
buffer.context.set_source_surface (surface, 0, 0);
buffer.context.paint ();
buffer.fast_blur (8, 2);
blurred_cover_surface = buffer;
}
cr.set_source_surface (blurred_cover_surface.surface, margin.left, margin.top);
cr.paint ();
cr.restore ();
return base.draw (cr);
}
private void properties_changed (string iface,
HashTable<string, Variant> changed,
string[] invalid) {
......@@ -325,89 +250,61 @@ namespace SwayNotificationCenter.Widgets.Mpris {
if (url == prev_art_url) return;
prev_art_url = url;
int scale = get_style_context ().get_scale ();
// Cancel previous download, reset the state and download again
album_art_cancellable.cancel ();
album_art_cancellable.reset ();
Gdk.Texture ? album_art_texture = null;
try {
File file = File.new_for_uri (url);
InputStream stream = yield file.read_async (Priority.DEFAULT,
album_art_cancellable);
this.album_art_pixbuf = yield new Gdk.Pixbuf.from_stream_async (
Gdk.Pixbuf pixbuf = yield new Gdk.Pixbuf.from_stream_async (
stream, album_art_cancellable);
album_art_texture = Gdk.Texture.for_pixbuf (pixbuf);
} catch (Error e) {
debug ("Could not download album art for %s. Using fallback...",
source.media_player.identity);
this.album_art_pixbuf = null;
}
if (this.album_art_pixbuf != null) {
unowned Gtk.StyleContext style_ctx = album_art.get_style_context ();
Value br_value =
style_ctx.get_property (Gtk.STYLE_PROPERTY_BORDER_RADIUS,
style_ctx.get_state ());
int radius = 0;
if (!br_value.holds (Type.INT) ||
(radius = br_value.get_int ()) == 0) {
radius = mpris_config.image_radius;
}
var surface = Functions.scale_pixbuf (this.album_art_pixbuf,
mpris_config.image_size,
mpris_config.image_size,
scale);
this.album_art_pixbuf = Gdk.pixbuf_get_from_surface (surface,
0, 0,
mpris_config.image_size,
mpris_config.image_size);
this.blurred_cover_surface = null;
surface = Functions.round_surface (surface,
mpris_config.image_size,
mpris_config.image_size,
scale,
radius);
var pix_buf = Gdk.pixbuf_get_from_surface (surface,
0, 0,
mpris_config.image_size,
mpris_config.image_size);
surface.finish ();
album_art.set_from_pixbuf (pix_buf);
album_art.get_style_context ().set_scale (1);
this.queue_draw ();
if (album_art_texture != null) {
// Set album art
Gtk.Snapshot snapshot = new Gtk.Snapshot ();
Functions.scale_texture (album_art_texture,
mpris_config.image_size, mpris_config.image_size,
get_scale_factor (), snapshot);
Graphene.Size size = Graphene.Size ().init (mpris_config.image_size, mpris_config.image_size);
album_art.set_from_paintable (snapshot.free_to_paintable (size));
// Set background album art
background_picture.set_paintable (album_art_texture);
this.queue_draw ();
return;
}
}
this.album_art_pixbuf = null;
this.blurred_cover_surface = null;
// Get the app icon
Icon ? icon = null;
if (desktop_entry is DesktopAppInfo) {
icon = desktop_entry.get_icon ();
}
Gtk.IconInfo ? icon_info = null;
unowned Gtk.IconTheme icon_theme = Gtk.IconTheme.get_for_display (get_display ());
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);
album_art.set_from_gicon (icon);
Gtk.IconPaintable ? icon_info = icon_theme.lookup_by_gicon (
icon, mpris_config.image_size, get_scale_factor (), Gtk.TextDirection.NONE, 0);
background_picture.set_paintable (icon_info);
} 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);
}
album_art.set_from_icon_name (icon_name);
if (icon_info != null) {
try {
this.album_art_pixbuf = icon_info.load_icon ();
} catch (Error e) {
warning ("Could not load icon: %s", e.message);
}
Gtk.IconPaintable ? icon_info = icon_theme.lookup_icon (
icon_name, null, mpris_config.image_size, get_scale_factor (),
Gtk.TextDirection.NONE, 0);
background_picture.set_paintable (icon_info);
}
}
......@@ -455,7 +352,7 @@ namespace SwayNotificationCenter.Widgets.Mpris {
check = source.media_player.can_play;
break;
}
button_play_pause_img.icon_name = icon_name;
button_play_pause.set_icon_name (icon_name);
button_play_pause.sensitive = check && source.media_player.can_control;
}
......@@ -493,12 +390,14 @@ namespace SwayNotificationCenter.Widgets.Mpris {
icon_name = ICON_REPEAT_SONG;
break;
}
unowned Gtk.StyleContext ctx = button_repeat.get_style_context ();
if (remove_flat_css_class) ctx.remove_class ("flat");
else ctx.add_class ("flat");
if (remove_flat_css_class) {
button_repeat.remove_css_class ("flat");
} else {
button_repeat.add_css_class ("flat");
}
button_repeat.get_child ().opacity = opacity;
button_repeat.sensitive = true;
button_repeat_img.icon_name = icon_name;
button_repeat.set_icon_name (icon_name);
button_repeat.show ();
}
} else {
......
......@@ -10,6 +10,8 @@ namespace SwayNotificationCenter.Widgets {
this.update_command = update_command;
this.label = label;
this.set_has_frame (true);
if (active) {
this.active = true;
}
......
......@@ -33,7 +33,9 @@ namespace SwayNotificationCenter.Widgets {
}
title_widget = new Gtk.Label (title);
add (title_widget);
title_widget.set_hexpand (true);
title_widget.set_halign (Gtk.Align.START);
append (title_widget);
if (has_clear_all_button) {
clear_all_button = new Gtk.Button.with_label (button_text);
......@@ -44,14 +46,19 @@ namespace SwayNotificationCenter.Widgets {
error ("Error: %s\n", e.message);
}
});
if (noti_daemon.control_center != null) {
clear_all_button.set_sensitive (
noti_daemon.control_center.notification_count () > 0);
}
swaync_daemon.subscribe_v2.connect ((count) => {
clear_all_button.set_sensitive (count > 0);
});
clear_all_button.set_can_focus (false);
clear_all_button.valign = Gtk.Align.CENTER;
// Backwards compatible towards older CSS stylesheets
clear_all_button.get_style_context ().add_class ("control-center-clear-all");
pack_end (clear_all_button, false);
clear_all_button.add_css_class ("control-center-clear-all");
append (clear_all_button);
}
show_all ();
}
}
}
......@@ -19,24 +19,27 @@ namespace SwayNotificationCenter.Widgets {
this.show_per_app_icon = show_per_app_icon;
this.show_per_app_label = show_per_app_label;
set_activatable (false);
update (sink_input);
scale.draw_value = false;
scale.set_hexpand (true);
icon.pixel_size = icon_size;
container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
if (show_per_app_icon) {
container.add (icon);
container.append (icon);
}
if (show_per_app_label) {
container.add (label);
container.append (label);
}
container.pack_start (scale);
container.append (scale);
add (container);
set_child (container);
scale.value_changed.connect (() => {
client.set_sink_input_volume (sink_input, (float) scale.get_value ());
......@@ -49,9 +52,7 @@ namespace SwayNotificationCenter.Widgets {
if (show_per_app_icon) {
icon.set_from_icon_name (
sink_input.application_icon_name ?? "application-x-executable",
Gtk.IconSize.DIALOG
);
sink_input.application_icon_name ?? "application-x-executable");
}
if (show_per_app_label) {
......@@ -60,8 +61,6 @@ namespace SwayNotificationCenter.Widgets {
scale.set_value (sink_input.volume);
scale.tooltip_text = ((int) scale.get_value ()).to_string ();
this.show_all ();
}
}
}
......@@ -10,15 +10,16 @@ namespace SwayNotificationCenter.Widgets {
Gtk.Label label_widget = new Gtk.Label (null);
Gtk.Scale slider = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 0, 100, 1);
// Per app volume controll
// Per app volume control
Gtk.ListBox levels_listbox;
Gtk.Button reveal_button;
IterListBoxController list_box_controller;
Gtk.ToggleButton reveal_button;
Gtk.Revealer revealer;
Gtk.Label no_sink_inputs_label;
string empty_label = "No active sink input";
string expand_label = "⇧";
string collapse_label = "⇩";
string ? expand_label = null;
string ? collapse_label = null;
int icon_size = 24;
Gtk.RevealerTransitionType revealer_type = Gtk.RevealerTransitionType.SLIDE_DOWN;
......@@ -98,49 +99,65 @@ namespace SwayNotificationCenter.Widgets {
this.orientation = Gtk.Orientation.VERTICAL;
slider.draw_value = false;
slider.set_hexpand (true);
main_volume_slider_container.add (label_widget);
main_volume_slider_container.pack_start (slider, true, true, 0);
add (main_volume_slider_container);
main_volume_slider_container.append (label_widget);
main_volume_slider_container.append (slider);
append (main_volume_slider_container);
if (show_per_app) {
reveal_button = new Gtk.Button.with_label (expand_label);
revealer = new Gtk.Revealer ();
revealer.transition_type = revealer_type;
revealer.transition_duration = revealer_duration;
levels_listbox = new Gtk.ListBox ();
levels_listbox.get_style_context ().add_class ("per-app-volume");
revealer.add (levels_listbox);
levels_listbox.add_css_class ("per-app-volume");
levels_listbox.set_activate_on_single_click (true);
levels_listbox.set_selection_mode (Gtk.SelectionMode.NONE);
revealer.set_child (levels_listbox);
list_box_controller = new IterListBoxController (levels_listbox);
if (this.client.active_sinks.size == 0) {
no_sink_inputs_label = new Gtk.Label (empty_label);
levels_listbox.add (no_sink_inputs_label);
list_box_controller.append (no_sink_inputs_label);
}
foreach (var item in this.client.active_sinks.values) {
levels_listbox.add (new SinkInputRow (item, client,
icon_size, show_per_app_icon, show_per_app_label));
SinkInputRow row = new SinkInputRow (item, client,
icon_size, show_per_app_icon,
show_per_app_label);
list_box_controller.append (row);
}
this.client.change_active_sink.connect (active_sink_change);
this.client.new_active_sink.connect (active_sink_added);
this.client.remove_active_sink.connect (active_sink_removed);
reveal_button.clicked.connect (() => {
bool show = revealer.reveal_child;
revealer.set_reveal_child (!show);
if (show) {
reveal_button.label = expand_label;
} else {
reveal_button.label = collapse_label;
}
reveal_button = new Gtk.ToggleButton ();
set_button_icon ();
reveal_button.toggled.connect (() => {
set_button_icon ();
revealer.set_reveal_child (reveal_button.active);
});
main_volume_slider_container.pack_end (reveal_button, false, false, 0);
add (revealer);
main_volume_slider_container.append (reveal_button);
append (revealer);
}
}
show_all ();
private void set_button_icon () {
if (!reveal_button.active) {
if (expand_label == null) {
reveal_button.set_icon_name ("swaync-up-small-symbolic");
} else {
reveal_button.set_label (expand_label);
}
} else {
if (collapse_label == null) {
reveal_button.set_icon_name ("swaync-down-small-symbolic");
} else {
reveal_button.set_label (collapse_label);
}
}
}
public override void on_cc_visibility_change (bool val) {
......@@ -160,8 +177,10 @@ namespace SwayNotificationCenter.Widgets {
}
private void active_sink_change (PulseSinkInput sink) {
foreach (var row in levels_listbox.get_children ()) {
if (row == null) continue;
foreach (unowned Gtk.Widget row in list_box_controller.get_children ()) {
if (!(row is SinkInputRow)) {
continue;
}
var s = (SinkInputRow) row;
if (s.sink_input.cmp (sink)) {
s.update (sink);
......@@ -173,25 +192,28 @@ namespace SwayNotificationCenter.Widgets {
private void active_sink_added (PulseSinkInput sink) {
// one element added -> remove the empty label
if (this.client.active_sinks.size == 1) {
var label = levels_listbox.get_children ().first ().data;
levels_listbox.remove ((Gtk.Widget) label);
var label = levels_listbox.get_first_child ();
list_box_controller.remove ((Gtk.Widget) label);
}
levels_listbox.add (new SinkInputRow (sink, client, icon_size, show_per_app_icon, show_per_app_label));
show_all ();
SinkInputRow row = new SinkInputRow (sink, client,
icon_size, show_per_app_icon,
show_per_app_label);
list_box_controller.append (row);
}
private void active_sink_removed (PulseSinkInput sink) {
foreach (var row in levels_listbox.get_children ()) {
if (row == null) continue;
foreach (unowned Gtk.Widget row in list_box_controller.get_children ()) {
if (!(row is SinkInputRow)) {
continue;
}
var s = (SinkInputRow) row;
if (s.sink_input.cmp (sink)) {
levels_listbox.remove (row);
list_box_controller.remove (row);
break;
}
}
if (levels_listbox.get_children ().length () == 0) {
levels_listbox.add (no_sink_inputs_label);
show_all ();
if (levels_listbox.get_first_child () == null) {
list_box_controller.append (no_sink_inputs_label);
}
}
}
......
public enum SwipeDirection {
SWIPE_LEFT, SWIPE_RIGHT;
}
public class DismissibleWidget : Gtk.Widget, Adw.Swipeable {
protected unowned Gtk.Widget _child = null;
public unowned Gtk.Widget child {
get {
return _child;
}
set {
_child = value;
if (_child != null) {
_child.unparent ();
_child.set_parent (this);
}
}
}
public Gtk.Orientation orientation {
get;
private set;
default = Gtk.Orientation.HORIZONTAL;
}
// Animation
Adw.SpringAnimation animation;
Adw.AnimationTarget target;
// Swipe Gesture
Adw.SwipeTracker swipe_tracker;
bool transition_running = false;
bool gesture_active = false;
double child_offset = 0;
double swipe_progress = 0.0;
SwipeDirection swipe_direction = SwipeDirection.SWIPE_RIGHT;
construct {
swipe_tracker = new Adw.SwipeTracker (this);
swipe_tracker.set_orientation (orientation);
swipe_tracker.set_reversed (true);
swipe_tracker.set_upper_overshoot (true);
swipe_tracker.set_lower_overshoot (true);
swipe_tracker.set_allow_long_swipes (true);
swipe_tracker.set_enabled (true);
swipe_tracker.set_allow_mouse_drag (true);
swipe_tracker.prepare.connect (swipe_prepare_cb);
swipe_tracker.update_swipe.connect (swipe_update_swipe_cb);
swipe_tracker.end_swipe.connect (swipe_end_swipe_cb);
double[] snap_dir = get_snap_points ();
target = new Adw.CallbackAnimationTarget (animate_value_cb);
animation = new Adw.SpringAnimation (this, snap_dir[0], snap_dir[1],
new Adw.SpringParams (1, 0.5, 500),
target);
animation.set_clamp (true);
animation.done.connect (animation_done_cb);
}
public signal void dismissed ();
public void set_can_dismiss (bool state) {
swipe_tracker.set_enabled (state);
}
/*
* Overrides
*/
protected override Gtk.SizeRequestMode get_request_mode () {
return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
}
protected override void measure (Gtk.Orientation orientation, int for_size,
out int minimum, out int natural,
out int minimum_baseline, out int natural_baseline) {
minimum = 0;
natural = 0;
minimum_baseline = -1;
natural_baseline = -1;
int child_min, child_nat;
if (!child.visible) {
return;
}
child.measure (orientation, for_size,
out child_min, out child_nat, null, null);
minimum = int.max (minimum, child_min);
natural = int.max (natural, child_nat);
}
protected override void size_allocate (int width, int height, int baseline) {
if (!child.visible) {
return;
}
int child_width, child_height;
int min = 0, nat = 0;
if (orientation == Gtk.Orientation.HORIZONTAL) {
child.measure (orientation,
height, out min, out nat, null, null);
} else {
child.measure (orientation,
width, out min, out nat, null, null);
}
child_width = width;
child_height = height;
double x = 0;
if (get_direction () == Gtk.TextDirection.RTL) {
x -= ((width * swipe_progress) - (width - child_width) / 2.0)
+ (width * child_offset * 2);
} else {
x -= - ((width * swipe_progress) - (width - child_width) / 2.0)
- (width * child_offset * 2);
}
Gsk.Transform transform = new Gsk.Transform ()
.translate (Graphene.Point ().init ((float) x, 0));
child.allocate (child_width, child_height, baseline, transform);
}
protected override void snapshot (Gtk.Snapshot snapshot) {
snapshot.save ();
snapshot.push_opacity (1 - swipe_progress.abs ());
snapshot_child (child, snapshot);
snapshot.pop ();
snapshot.restore ();
}
/*
* Callbacks
*/
private void animate_value_cb (double value) {
set_position (value);
}
private void animation_done_cb () {
transition_running = false;
if (swipe_progress != 0) {
dismissed ();
}
}
private void swipe_prepare_cb (Adw.NavigationDirection direction) {
gesture_active = true;
if (transition_running) {
animation.pause ();
} else {
transition_running = true;
}
}
private void swipe_update_swipe_cb (double distance) {
set_position (distance);
}
private void swipe_end_swipe_cb (double velocity, double to) {
if (!gesture_active) {
return;
}
animation.set_value_from (swipe_progress);
animation.set_value_to (to);
animation.set_initial_velocity (velocity);
animation.play ();
gesture_active = false;
}
/*
* Methods
*/
private void set_position (double value) {
this.swipe_progress = value;
queue_allocate ();
}
public void set_gesture_direction (SwipeDirection swipe_direction) {
this.swipe_direction = swipe_direction;
// Reset the position
set_position (0.0);
}
/*
* Swipe gesture
*/
/** Gets the progress this will snap back to after the gesture is canceled. */
public double get_cancel_progress () {
return 0;
}
/** Gets the swipe distance of this. */
public double get_distance () {
return get_width ();
}
/** Gets the current progress of this. */
public double get_progress () {
if (!transition_running) {
return 0;
}
return this.swipe_progress;
}
/** Gets the snap points of this. */
public double[] get_snap_points () {
switch (swipe_direction) {
case SwipeDirection.SWIPE_LEFT:
return new double[] { -1, 0 };
default:
case SwipeDirection.SWIPE_RIGHT:
return new double[] { 0, 1 };
}
}
/**
* Gets the area this can start a swipe from for the given direction
* and gesture type.
*/
public Gdk.Rectangle get_swipe_area (Adw.NavigationDirection direction,
bool is_drag) {
Gtk.Allocation alloc = Gtk.Allocation ();
Graphene.Rect bounds;
this.compute_bounds (this, out bounds);
alloc.width = (int) bounds.size.width;
alloc.height = (int) bounds.size.height;
alloc.x = (int) bounds.origin.x;
alloc.y = (int) bounds.origin.y;
return alloc;
}
}
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;
}
}
namespace SwayNotificationCenter {
public class Functions {
const Gsk.ScalingFilter SCALING_FILTER = Gsk.ScalingFilter.NEAREST;
private static Gtk.CssProvider system_css_provider;
private static Gtk.CssProvider user_css_provider;
......@@ -10,7 +12,7 @@ namespace SwayNotificationCenter {
user_css_provider = new Gtk.CssProvider ();
// Init resources
var theme = Gtk.IconTheme.get_default ();
var theme = Gtk.IconTheme.get_for_display (Gdk.Display.get_default ());
theme.add_resource_path ("/org/erikreider/swaync/icons");
}
......@@ -29,7 +31,6 @@ namespace SwayNotificationCenter {
public static void set_image_uri (owned string uri,
Gtk.Image img,
int icon_size,
int radius,
bool file_exists,
bool is_theme_icon = false) {
const string URI_PREFIX = "file://";
......@@ -40,18 +41,8 @@ namespace SwayNotificationCenter {
try {
if (is_uri) uri = uri.slice (URI_PREFIX.length, uri.length);
var pixbuf = new Gdk.Pixbuf.from_file_at_scale (
Uri.unescape_string (uri),
icon_size * img.scale_factor,
icon_size * img.scale_factor,
true);
// Scale and round the image. Scales to fit the size
var surface = scale_round_pixbuf (pixbuf,
icon_size,
icon_size,
img.scale_factor,
radius);
img.set_from_surface (surface);
Gdk.Texture texture = Gdk.Texture.from_filename (Uri.unescape_string (uri));
img.set_from_paintable (texture);
} catch (Error e) {
stderr.printf (e.message + "\n");
}
......@@ -59,30 +50,26 @@ namespace SwayNotificationCenter {
// Try as icon name
if (img.storage_type == Gtk.ImageType.EMPTY) {
img.set_from_icon_name (uri, Gtk.IconSize.INVALID);
unowned Gdk.Display display = Gdk.Display.get_default ();
unowned Gtk.IconTheme icon_theme = Gtk.IconTheme.get_for_display (display);
if (icon_theme.has_icon (uri)) {
img.set_from_icon_name (uri);
}
}
}
public static void set_image_data (ImageData data,
Gtk.Image img,
int icon_size,
int radius) {
// Rebuild and scale the image
var pixbuf = new Gdk.Pixbuf.with_unowned_data (data.data,
Gdk.Colorspace.RGB,
data.has_alpha,
data.bits_per_sample,
data.width,
data.height,
data.rowstride,
null);
var surface = scale_round_pixbuf (pixbuf,
icon_size,
icon_size,
img.scale_factor,
radius);
img.set_from_surface (surface);
int icon_size) {
Gdk.MemoryFormat format = Gdk.MemoryFormat.R8G8B8;
if (data.has_alpha) {
format = Gdk.MemoryFormat.R8G8B8A8;
}
var texture = new Gdk.MemoryTexture (data.width, data.height,
format,
new Bytes.static (data.data),
data.rowstride);
img.set_from_paintable (texture);
}
/** Load the package provided CSS file as a base.
......@@ -95,31 +82,26 @@ namespace SwayNotificationCenter {
// Load packaged CSS as backup
string system_css = get_style_path (null, true);
system_css = File.new_for_path (system_css).get_path () ?? system_css;
message ("Loading CSS: \"%s\"", system_css);
try {
if (!skip_packaged_css) {
message ("Loading CSS: \"%s\"", system_css);
system_css_provider.load_from_path (system_css);
Gtk.StyleContext.add_provider_for_screen (
Gdk.Screen.get_default (),
Gtk.StyleContext.add_provider_for_display (
Gdk.Display.get_default (),
system_css_provider,
css_priority);
} catch (Error e) {
critical ("Load packaged CSS Error (\"%s\"):\n\t%s\n", system_css, e.message);
} else {
message ("Skipping system CSS: \"%s\"", system_css);
}
// Load user CSS
string user_css = get_style_path (style_path);
user_css = File.new_for_path (user_css).get_path () ?? user_css;
message ("Loading CSS: \"%s\"", user_css);
try {
user_css_provider.load_from_path (user_css);
Gtk.StyleContext.add_provider_for_screen (
Gdk.Screen.get_default (),
user_css_provider,
css_priority);
} catch (Error e) {
critical ("Load user CSS Error (\"%s\"):\n\t%s\n", user_css, e.message);
return false;
}
user_css_provider.load_from_path (user_css);
Gtk.StyleContext.add_provider_for_display (
Gdk.Display.get_default (),
user_css_provider,
css_priority);
return true;
}
......@@ -217,122 +199,64 @@ namespace SwayNotificationCenter {
return type;
}
/** Roundes the Cairo Surface to the given radii */
public static Cairo.Surface round_surface (Cairo.Surface base_surf,
int buffer_width,
int buffer_height,
int img_scale,
int radius) {
// Limit radii size
radius = int.min (radius, int.min (buffer_width / 2, buffer_height / 2));
Cairo.Surface surface = new Cairo.ImageSurface (Cairo.Format.ARGB32,
buffer_width,
buffer_height);
var cr = new Cairo.Context (surface);
// Border radius
const double DEGREES = Math.PI / 180.0;
cr.new_sub_path ();
cr.arc (buffer_width - radius, radius, radius, -90 * DEGREES, 0 * DEGREES);
cr.arc (buffer_width - radius, buffer_height - radius, radius, 0 * DEGREES, 90 * DEGREES);
cr.arc (radius, buffer_height - radius, radius, 90 * DEGREES, 180 * DEGREES);
cr.arc (radius, radius, radius, 180 * DEGREES, 270 * DEGREES);
cr.close_path ();
cr.set_source_rgba (0, 0, 0, 0);
cr.clip ();
cr.paint ();
cr.save ();
cr.set_source_surface (base_surf, 0, 0);
cr.paint ();
cr.restore ();
return surface;
}
/** Scales the pixbuf to fit the given dimensions */
public static Cairo.Surface scale_pixbuf (Gdk.Pixbuf pixbuf,
int buffer_width,
int buffer_height,
int img_scale) {
Cairo.Surface surface = new Cairo.ImageSurface (Cairo.Format.ARGB32,
buffer_width,
buffer_height);
var cr = new Cairo.Context (surface);
cr.save ();
Cairo.Surface base_surf = Gdk.cairo_surface_create_from_pixbuf (pixbuf,
img_scale,
null);
int width = pixbuf.width / img_scale;
int height = pixbuf.height / img_scale;
/** Scales the Texture to fit the given dimensions */
public static void scale_texture (Gdk.Texture texture,
int buffer_width,
int buffer_height,
int img_scale,
Gtk.Snapshot snapshot) {
int width = texture.width / img_scale;
int height = texture.height / img_scale;
double window_ratio = (double) buffer_width / buffer_height;
double bg_ratio = width / height;
snapshot.save ();
if (window_ratio > bg_ratio) { // Taller wallpaper than monitor
double scale = (double) buffer_width / width;
if (scale * height < buffer_height) {
draw_scale_wide (buffer_width, width, buffer_height, height, cr, base_surf);
draw_scale_wide (buffer_width, width, buffer_height, height, snapshot, texture);
} else {
draw_scale_tall (buffer_width, width, buffer_height, height, cr, base_surf);
draw_scale_tall (buffer_width, width, buffer_height, height, snapshot, texture);
}
} else { // Wider wallpaper than monitor
double scale = (double) buffer_height / height;
if (scale * width < buffer_width) {
draw_scale_tall (buffer_width, width, buffer_height, height, cr, base_surf);
draw_scale_tall (buffer_width, width, buffer_height, height, snapshot, texture);
} else {
draw_scale_wide (buffer_width, width, buffer_height, height, cr, base_surf);
draw_scale_wide (buffer_width, width, buffer_height, height, snapshot, texture);
}
}
cr.paint ();
cr.restore ();
base_surf.finish ();
return surface;
}
/** Scales the pixbuf to fit the given dimensions */
public static Cairo.Surface scale_round_pixbuf (Gdk.Pixbuf pixbuf,
int buffer_width,
int buffer_height,
int img_scale,
int radius) {
var surface = Functions.scale_pixbuf (pixbuf,
buffer_width,
buffer_height,
img_scale);
surface = Functions.round_surface (surface,
buffer_width,
buffer_height,
img_scale,
radius);
return surface;
snapshot.restore ();
}
private static void draw_scale_tall (int buffer_width,
int width,
int buffer_height,
int height,
Cairo.Context cr,
Cairo.Surface surface) {
double scale = (double) buffer_width / width;
cr.scale (scale, scale);
cr.set_source_surface (surface,
0, (double) buffer_height / 2 / scale - height / 2);
Gtk.Snapshot snapshot,
Gdk.Texture texture) {
float scale = (float) buffer_width / width;
snapshot.scale (scale, scale);
float x = 0;
float y = (float) (buffer_height / 2 / scale - height / 2);
snapshot.append_scaled_texture (texture,
SCALING_FILTER,
{ { x, y }, { width, height } });
}
private static void draw_scale_wide (int buffer_width,
int width,
int buffer_height,
int height,
Cairo.Context cr,
Cairo.Surface surface) {
double scale = (double) buffer_height / height;
cr.scale (scale, scale);
cr.set_source_surface (
surface,
(double) buffer_width / 2 / scale - width / 2, 0);
Gtk.Snapshot snapshot,
Gdk.Texture texture) {
float scale = (float) buffer_height / height;
snapshot.scale (scale, scale);
float x = (float) (buffer_width / 2 / scale - width / 2);
float y = 0;
snapshot.append_scaled_texture (texture,
SCALING_FILTER,
{ { x, y }, { width, height } });
}
public delegate bool FilterFunc (char character);
......@@ -412,5 +336,24 @@ namespace SwayNotificationCenter {
return false;
}
}
public static unowned Wl.Display get_wl_display () {
unowned var display = Gdk.Display.get_default ();
if (display is Gdk.Wayland.Display) {
return ((Gdk.Wayland.Display) display).get_wl_display ();
}
GLib.error ("Only supports Wayland!");
}
public static Wl.Surface * get_wl_surface (Gdk.Surface surface) {
if (surface is Gdk.Wayland.Surface) {
return ((Gdk.Wayland.Surface) surface).get_wl_surface ();
}
GLib.error ("Only supports Wayland!");
}
public static double lerp (double a, double b, double t) {
return a * (1.0 - t) + b * t;
}
}
}
public class IterBox : Gtk.Box {
public uint length { get; private set; default = 0; }
private List<Gtk.Widget> children = new List<Gtk.Widget> ();
public IterBox (Gtk.Orientation orientation, int spacing) {
Object (orientation: orientation, spacing: spacing);
set_name ("iterbox");
}
private void on_add (Gtk.Widget child) {
length++;
child.destroy.connect (() => {
children.remove (child);
});
}
public List<weak Gtk.Widget> get_children () {
return children.copy ();
}
public new void append (Gtk.Widget child) {
children.append (child);
base.append (children.last ().data);
on_add (child);
}
public new void prepend (Gtk.Widget child) {
children.prepend (child);
base.prepend (children.first ().data);
on_add (child);
}
public new void remove (Gtk.Widget child) {
children.remove (child);
base.remove (child);
length--;
}
}
public class IterListBoxController : Object {
public uint length { get; private set; default = 0; }
private List<Gtk.Widget> children = new List<Gtk.Widget> ();
public unowned Gtk.ListBox list_box {
get;
private set;
}
public IterListBoxController (Gtk.ListBox list_box) {
this.list_box = list_box;
}
private void on_add (Gtk.Widget child) {
length++;
child.destroy.connect (() => {
children.remove (child);
});
}
public List<weak Gtk.Widget> get_children () {
return children.copy ();
}
public void append (Gtk.Widget child) {
children.append (child);
list_box.append (children.last ().data);
on_add (child);
}
public void prepend (Gtk.Widget child) {
children.prepend (child);
list_box.prepend (children.first ().data);
on_add (child);
}
public void insert (Gtk.Widget child, int position) {
children.insert (child, position);
list_box.insert (children.nth (position).data, position);
on_add (child);
}
public void remove (Gtk.Widget child) {
children.remove (child);
list_box.remove (child);
length--;
}
}
namespace SwayNotificationCenter {
static SwayncDaemon swaync_daemon;
// Args
static string ? style_path;
static string ? config_path;
static uint layer_shell_protocol_version = 3;
// Dev args
static bool skip_packaged_css = false;
static Settings self_settings;
public void main (string[] args) {
Gtk.init (ref args);
Hdy.init ();
Gtk.init ();
Adw.init ();
Functions.init ();
self_settings = new Settings ("org.erikreider.swaync");
......@@ -22,6 +23,9 @@ namespace SwayNotificationCenter {
case "--style":
style_path = args[++i];
break;
case "--skip-system-css":
skip_packaged_css = true;
break;
case "-c":
case "--config":
config_path = args[++i];
......@@ -39,25 +43,28 @@ namespace SwayNotificationCenter {
}
}
ConfigModel.init (config_path);
Functions.load_css (style_path);
var app = new Gtk.Application ("org.erikreider.swaync",
ApplicationFlags.DEFAULT_FLAGS);
app.hold ();
app.activate.connect (() => {
ConfigModel.init (config_path);
Functions.load_css (style_path);
if (ConfigModel.instance.layer_shell) {
layer_shell_protocol_version = GtkLayerShell.get_protocol_version ();
}
swaync_daemon = new SwayncDaemon ();
Bus.own_name (BusType.SESSION, "org.erikreider.swaync.cc",
BusNameOwnerFlags.NONE,
on_cc_bus_aquired,
() => {},
() => {
stderr.printf (
"Could not acquire swaync name!...\n");
Process.exit (1);
});
swaync_daemon = new SwayncDaemon ();
Bus.own_name (BusType.SESSION, "org.erikreider.swaync.cc",
BusNameOwnerFlags.NONE,
on_cc_bus_aquired,
() => {},
() => {
stderr.printf (
"Could not acquire swaync name!...\n");
Process.exit (1);
app.add_window (swaync_daemon.noti_daemon.control_center);
});
Gtk.main ();
app.run ();
}
void on_cc_bus_aquired (DBusConnection conn) {
......@@ -78,5 +85,6 @@ namespace SwayNotificationCenter {
print ("Options:\n");
print ("\t -s, --style \t\t Use a custom Stylesheet file\n");
print ("\t -c, --config \t\t Use a custom config file\n");
print ("\t --skip-system-css \t Skip trying to parse the packaged Stylesheet file. Useful for CSS debugging\n");
}
}
......@@ -50,22 +50,22 @@ widget_sources = [
app_sources = [
'main.vala',
'animation/animation.vala',
'orderedHashTable/orderedHashTable.vala',
'iterHelpers/iterBox.vala',
'iterHelpers/iterListBoxController.vala',
'underlay/underlay.vala',
'configModel/configModel.vala',
'swayncDaemon/swayncDaemon.vala',
'notiDaemon/notiDaemon.vala',
'notiModel/notiModel.vala',
'fadedViewport/fadedViewport.vala',
'notificationWindow/notificationWindow.vala',
'dismissableWidget/dismissableWidget.vala',
'notification/notification.vala',
'notificationGroup/notificationGroup.vala',
'controlCenter/controlCenter.vala',
widget_sources,
'blankWindow/blankWindow.vala',
'functions.vala',
'WaylandUtils/WaylandUtils.c',
'WaylandUtils/WaylandUtils.vala',
'xdg_activation.vala',
constants,
]
......@@ -73,20 +73,18 @@ app_sources = [
assert(vala.version() >= '0.56')
app_deps = [
dependency('gtk4-layer-shell-0', version: '>= 1.0.3'),
dependency('gio-2.0', version: '>= 2.50'),
dependency('gio-unix-2.0', version: '>= 2.50'),
dependency('gtk+-3.0', version: '>= 3.22'),
dependency('gtk4', version: '>= 4.16.13'),
dependency('gtk4-wayland', version: '>= 4.16.5'),
dependency('json-glib-1.0', version: '>= 1.0'),
dependency('libhandy-1', version: '>= 1.8.0'),
dependency('granite', version: '>= 6.2.0'),
dependency('gtk-layer-shell-0',
fallback: ['gtk-layer-shell-0', 'gtk-layer-shell'],
version: '>= 0.8.0'
),
dependency('libadwaita-1', version: '>= 1.6.1'),
dependency('granite-7', version: '>= 7.5.0'),
cc.find_library('m', required : true),
vala.find_library('posix'),
dependency('gee-0.8'),
dependency('wayland-client'),
dependency('gee-0.8', version: '>=0.20.6'),
dependency('wayland-client', version: '>=1.23.0'),
protocol_dep,
]
......@@ -113,15 +111,9 @@ if get_option('pulse-audio')
endif
args = [
'--target-glib=2.50',
'--pkg=GtkLayerShell-0.1',
'--target-glib=2.82',
]
app_resources += gnome.compile_resources('sway_notification_center-resources',
'sway_notification_center.gresource.xml',
c_name: 'sway_notification_center'
)
executable('swaync',
[ app_sources, app_resources ],
vala_args: args,
......
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.40.0 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.2"/>
<template class="SwayNotificationCenterNotification" parent="GtkListBoxRow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkRevealer" id="revealer">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="HdyCarousel" id="carousel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<child>
<placeholder/>
</child>
</object>
</child>
<child>
<object class="GtkEventBox" id="event_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkOverlay" id="overlay">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkBox" id="base_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkEventBox" id="default_action">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="events">GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_STRUCTURE_MASK</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">8</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkOverlay">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<child>
<object class="GtkImage" id="img">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">center</property>
<property name="icon_size">6</property>
<style>
<class name="image"/>
</style>
</object>
<packing>
<property name="index">-1</property>
</packing>
</child>
<child type="overlay">
<object class="GtkImage" id="img_app_icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="valign">end</property>
<property name="icon_size">6</property>
<style>
<class name="app-icon"/>
</style>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">center</property>
<child>
<object class="GtkLabel" id="summary">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="ellipsize">end</property>
<property name="max-width-chars">0</property>
<property name="xalign">0</property>
<style>
<class name="summary"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="time">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">start</property>
<property name="margin-start">6</property>
<property name="track-visited-links">False</property>
<style>
<class name="time"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="body">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="ellipsize">end</property>
<property name="lines">1</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
<style>
<class name="body"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="text-box"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkProgressBar" id="progress_bar">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkImage" id="body_image">
<property name="can-focus">False</property>
<property name="halign">center</property>
<style>
<class name="body-image"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="inline_reply_box">
<property name="can-focus">False</property>
<child>
<object class="GtkEntry" id="inline_reply_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="input-hints">GTK_INPUT_HINT_SPELLCHECK | GTK_INPUT_HINT_EMOJI | GTK_INPUT_HINT_NONE</property>
<style>
<class name="inline-reply-entry"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="inline_reply_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<style>
<class name="inline-reply-button"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="inline-reply"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<style>
<class name="notification-content"/>
</style>
</object>
</child>
<style>
<class name="notification-default-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<style>
<class name="notification"/>
</style>
</object>
<packing>
<property name="index">-1</property>
</packing>
</child>
<child type="overlay">
<object class="GtkRevealer" id="close_revealer">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="valign">start</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkButton" id="close_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin-start">4</property>
<property name="margin-end">4</property>
<property name="margin-top">2</property>
<property name="margin-bottom">2</property>
<property name="relief">none</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">swaync-close-symbolic</property>
</object>
</child>
<style>
<class name="close-button"/>
<class name="circular"/>
</style>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<style>
<class name="notification-background"/>
</style>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<style>
<class name="notification-row"/>
</style>
</template>
</interface>
namespace SwayNotificationCenter {
public class NotificationCloseButton : Adw.Bin {
Gtk.Revealer revealer;
Gtk.Button button;
construct {
valign = Gtk.Align.START;
// TODO: Configurable
halign = Gtk.Align.END;
revealer = new Gtk.Revealer () {
transition_type = Gtk.RevealerTransitionType.CROSSFADE,
reveal_child = false,
};
revealer.notify["child-revealed"].connect (() => {
set_visible (revealer.reveal_child);
});
set_child (revealer);
button = new Gtk.Button.from_icon_name ("swaync-close-symbolic") {
has_frame = false,
halign = Gtk.Align.CENTER,
valign = Gtk.Align.CENTER,
};
button.add_css_class ("close-button");
button.add_css_class ("circular");
button.clicked.connect (() => this.clicked ());
revealer.set_child (button);
}
public signal void clicked ();
public void set_reveal (bool state) {
if (state == revealer.reveal_child) {
return;
}
if (state) {
set_visible (true);
}
revealer.set_reveal_child (state);
}
public void set_transition_duration (uint duration) {
revealer.set_transition_duration (duration);
}
}
public enum NotificationType { CONTROL_CENTER, POPUP }
[GtkTemplate (ui = "/org/erikreider/sway-notification-center/notification/notification.ui")]
public class Notification : Gtk.ListBoxRow {
[GtkTemplate (ui = "/org/erikreider/swaync/ui/notification.ui")]
public class Notification : Gtk.Box {
[GtkChild]
unowned Gtk.Revealer revealer;
[GtkChild]
unowned Hdy.Carousel carousel;
unowned DismissibleWidget dismissible_widget;
[GtkChild]
unowned Gtk.EventBox event_box;
unowned Gtk.Overlay base_widget;
[GtkChild]
unowned Gtk.EventBox default_action;
unowned Gtk.Box default_action;
[GtkChild]
unowned Gtk.FlowBox alt_actions_box;
/** The default_action gesture. Allows clicks while not in swipe gesture. */
private Gtk.GestureMultiPress gesture;
private Gtk.GestureClick gesture;
/** Detects when hovering over the widget */
private Gtk.EventControllerMotion motion_controller;
[GtkChild]
unowned Gtk.ProgressBar progress_bar;
[GtkChild]
unowned Gtk.Box base_box;
unowned IterBox base_box;
[GtkChild]
unowned Gtk.Revealer close_revealer;
[GtkChild]
unowned Gtk.Button close_button;
private Gtk.ButtonBox ? alt_actions_box = null;
unowned NotificationCloseButton close_button;
[GtkChild]
unowned Gtk.Label summary;
......@@ -43,7 +90,7 @@ namespace SwayNotificationCenter {
[GtkChild]
unowned Gtk.Image img_app_icon;
[GtkChild]
unowned Gtk.Image body_image;
unowned Gtk.Picture body_image;
// Inline Reply
[GtkChild]
......@@ -56,7 +103,6 @@ namespace SwayNotificationCenter {
private bool default_action_down = false;
private bool default_action_in = false;
public static Gtk.IconSize icon_size = Gtk.IconSize.INVALID;
private int notification_icon_size { get; default = ConfigModel.instance.notification_icon_size; }
private int notification_body_image_height {
......@@ -91,8 +137,6 @@ namespace SwayNotificationCenter {
public bool has_inline_reply { get; private set; default = false; }
private int carousel_empty_widget_index = 0;
private static Regex code_regex;
private static Regex tag_regex;
......@@ -151,51 +195,57 @@ namespace SwayNotificationCenter {
stderr.printf ("Invalid regex: %s", e.message);
}
// Build the default_action gesture. Makes clickes compatible with
// the Hdy Swipe gesture unlike a regular ::button_release_event
gesture = new Gtk.GestureMultiPress (default_action);
// Build the default_action gesture
gesture = new Gtk.GestureClick ();
default_action.add_controller (gesture);
gesture.set_touch_only (false);
gesture.set_exclusive (true);
gesture.set_button (Gdk.BUTTON_PRIMARY);
gesture.set_button (0);
gesture.set_propagation_phase (Gtk.PropagationPhase.BUBBLE);
gesture.pressed.connect ((_gesture, _n_press, _x, _y) => {
default_action_in = true;
default_action_down = true;
default_action_update_state ();
});
gesture.released.connect ((gesture, _n_press, _x, _y) => {
// Emit released
if (!default_action_down) return;
default_action_down = false;
if (default_action_in) {
click_default_action ();
// Close notification on middle and right button click
switch (gesture.get_current_button ()) {
default:
case Gdk.BUTTON_PRIMARY:
click_default_action ();
break;
case Gdk.BUTTON_MIDDLE:
case Gdk.BUTTON_SECONDARY:
this.close_notification ();
break;
}
}
Gdk.EventSequence ? sequence = gesture.get_current_sequence ();
if (sequence == null) {
default_action_in = false;
default_action_update_state ();
}
});
gesture.update.connect ((gesture, sequence) => {
Gtk.GestureSingle gesture_single = (Gtk.GestureSingle) gesture;
if (sequence != gesture_single.get_current_sequence ()) return;
Gtk.Allocation allocation;
int width = default_action.get_width ();
int height = default_action.get_height ();
double x, y;
default_action.get_allocation (out allocation);
gesture.get_point (sequence, out x, out y);
bool in_button = (x >= 0 && y >= 0 && x < allocation.width && y < allocation.height);
bool in_button = (x >= 0 && y >= 0 && x < width && y < height);
if (default_action_in != in_button) {
default_action_in = in_button;
default_action_update_state ();
}
});
gesture.cancel.connect ((_gesture, _sequence) => {
if (default_action_down) {
default_action_down = false;
default_action_update_state ();
}
});
......@@ -205,72 +255,41 @@ namespace SwayNotificationCenter {
/// 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);
motion_controller = new Gtk.EventControllerMotion ();
base_widget.add_controller (motion_controller);
motion_controller.enter.connect ((event) => {
close_button.set_reveal (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);
motion_controller.leave.connect ((controller) => {
close_button.set_reveal (false);
add_notification_timeout ();
return false;
});
this.carousel.page_changed.connect ((_, i) => {
if (i != this.carousel_empty_widget_index) return;
// Remove notification when it has been swiped
dismissible_widget.dismissed.connect (() => {
remove_noti_timeout ();
try {
noti_daemon.manually_close_notification (
param.applied_id, false);
} catch (Error e) {
print ("Error: %s\n", e.message);
printerr ("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;
Gtk.EventControllerKey reply_key_controller = new Gtk.EventControllerKey ();
reply_key_controller.key_released.connect ((keyval, keycode, state) => {
if (Gdk.keyval_name (keyval) == "Return") {
inline_reply_button.clicked ();
}
});
inline_reply_entry.add_controller (reply_key_controller);
inline_reply_button.clicked.connect (() => {
string text = inline_reply_entry.get_text ().strip ();
if (text.length == 0) return;
......@@ -281,58 +300,31 @@ namespace SwayNotificationCenter {
}
private void default_action_update_state () {
bool pressed = default_action_in && default_action_down;
Gtk.StateFlags flags = default_action.get_state_flags () &
~(Gtk.StateFlags.PRELIGHT | Gtk.StateFlags.ACTIVE);
if (default_action_in) flags |= Gtk.StateFlags.PRELIGHT;
if (pressed) flags |= Gtk.StateFlags.ACTIVE;
default_action.set_state_flags (flags, true);
}
private void on_size_allocation (Gtk.Allocation _ignored) {
// Force recomputing the allocated size of the wrapped GTK label in the body.
// `queue_resize` alone DOES NOT WORK because it does not properly invalidate
// the cache, this is a GTK bug!
// See https://gitlab.gnome.org/GNOME/gtk/-/issues/2556
if (body != null) {
body.set_size_request (-1, body.get_allocated_height ());
}
}
private void build_noti () {
this.body.set_line_wrap (true);
this.body.set_line_wrap_mode (Pango.WrapMode.WORD_CHAR);
this.body.set_wrap (true);
this.body.set_wrap_mode (Pango.WrapMode.WORD_CHAR);
this.body.set_natural_wrap_mode (Gtk.NaturalWrapMode.WORD);
this.body.set_ellipsize (Pango.EllipsizeMode.END);
this.summary.set_line_wrap (false);
this.summary.set_wrap (false);
this.summary.set_text (param.summary ?? param.app_name);
this.summary.set_ellipsize (Pango.EllipsizeMode.END);
close_revealer.set_transition_duration (this.transition_time);
close_button.set_transition_duration (this.transition_time);
this.revealer.set_transition_duration (this.transition_time);
this.carousel.set_animation_duration (this.transition_time);
// Changes the swipe direction depending on the notifications X position
switch (ConfigModel.instance.positionX) {
case PositionX.LEFT:
this.carousel.reorder (event_box, 0);
this.carousel_empty_widget_index = 1;
dismissible_widget.set_gesture_direction (SwipeDirection.SWIPE_LEFT);
break;
default:
case PositionX.RIGHT:
case PositionX.CENTER:
this.carousel.scroll_to (event_box);
this.carousel_empty_widget_index = 0;
dismissible_widget.set_gesture_direction (SwipeDirection.SWIPE_RIGHT);
break;
}
// Reset state
this.carousel.scroll_to (event_box);
this.carousel.allow_scroll_wheel = false;
if (this.progress_bar.visible = param.has_synch) {
this.progress_bar.set_fraction (param.value * 0.01);
......@@ -344,18 +336,14 @@ namespace SwayNotificationCenter {
set_actions ();
set_style_urgency ();
this.show ();
Timeout.add (0, () => {
this.revealer.set_reveal_child (true);
revealer.set_reveal_child (true);
return Source.REMOVE;
});
remove_noti_timeout ();
this.size_allocate.disconnect (on_size_allocation);
if (is_timed) {
add_notification_timeout ();
this.size_allocate.connect (on_size_allocation);
}
}
......@@ -365,7 +353,7 @@ namespace SwayNotificationCenter {
this.body.set_lines (this.number_of_body_lines);
// Reset state
this.body_image.hide ();
body_image.hide ();
// Removes all image tags and adds them to an array
if (text.length > 0) {
......@@ -396,16 +384,20 @@ namespace SwayNotificationCenter {
// Set the image if exists and is valid
if (img_paths.length > 0) {
var img = Functions.uri_to_path (img_paths[0]);
var file = File.new_for_path (img);
string img = Functions.uri_to_path (img_paths[0]);
File file = File.new_for_path (img);
if (img.length > 0 && file.query_exists ()) {
var buf = new Gdk.Pixbuf.from_file_at_scale (
file.get_path (),
notification_body_image_width,
notification_body_image_height,
true);
this.body_image.set_from_pixbuf (buf);
this.body_image.show ();
Gdk.Texture texture = Gdk.Texture.from_file (file);
body_image.set_paintable (texture);
body_image.set_can_shrink (true);
body_image.set_content_fit (Gtk.ContentFit.SCALE_DOWN);
body_image.width_request = notification_body_image_width;
body_image.height_request = notification_body_image_height;
// Fixes the Picture taking up too much space:
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7092
Gtk.LayoutManager layout = new Gtk.CenterLayout ();
body_image.set_layout_manager (layout);
body_image.show ();
}
}
} catch (Error e) {
......@@ -471,13 +463,20 @@ namespace SwayNotificationCenter {
}
public void click_alt_action (uint index) {
if (alt_actions_box == null) return;
List<weak Gtk.Widget> ? children = alt_actions_box.get_children ();
uint length = children.length ();
if (length == 0 || index >= length) return;
if (!alt_actions_box.visible) return;
unowned Gtk.Widget button = children.nth_data (index);
if (button is Gtk.Button) {
unowned Gtk.Widget? button = null;
int i = 0;
foreach (unowned Gtk.Widget child in base_box.get_children ()) {
if (i == index) {
button = child;
}
i++;
}
if (button == null) {
return;
} else if (button is Gtk.Button) {
((Gtk.Button) button).clicked ();
return;
}
......@@ -511,20 +510,20 @@ namespace SwayNotificationCenter {
private void set_style_urgency () {
// Reset state
base_box.get_style_context ().remove_class ("low");
base_box.get_style_context ().remove_class ("normal");
base_box.get_style_context ().remove_class ("critical");
base_box.remove_css_class ("low");
base_box.remove_css_class ("normal");
base_box.remove_css_class ("critical");
switch (param.urgency) {
case UrgencyLevels.LOW:
base_box.get_style_context ().add_class ("low");
base_box.add_css_class ("low");
break;
case UrgencyLevels.NORMAL:
default:
base_box.get_style_context ().add_class ("normal");
base_box.add_css_class ("normal");
break;
case UrgencyLevels.CRITICAL:
base_box.get_style_context ().add_class ("critical");
base_box.add_css_class ("critical");
break;
}
}
......@@ -536,7 +535,7 @@ namespace SwayNotificationCenter {
// supports ON_DEMAND layer shell keyboard interactivity
if (!ConfigModel.instance.notification_inline_replies
|| (ConfigModel.instance.layer_shell
&& layer_shell_protocol_version < 4
&& swaync_daemon.has_layer_on_demand
&& notification_type == NotificationType.POPUP)) {
return;
}
......@@ -563,51 +562,51 @@ namespace SwayNotificationCenter {
}
private void set_actions () {
// Reset state
foreach (Gtk.Widget child in base_box.get_children ()) {
if (child is Gtk.ScrolledWindow) {
child.destroy ();
}
alt_actions_box.set_visible (false);
// Remove all of the old alt actions
for (unowned Gtk.Widget child = alt_actions_box.get_first_child ();
child != null;
child = alt_actions_box.get_first_child ()) {
alt_actions_box.remove (child);
}
// Check for security codes
string ? code = parse_body_codes ();
// Display all of the actions
if (param.actions.length > 0 || code != null) {
var viewport = new Gtk.Viewport (null, null);
var scroll = new Gtk.ScrolledWindow (null, null);
alt_actions_box = new Gtk.ButtonBox (Gtk.Orientation.HORIZONTAL);
alt_actions_box.set_homogeneous (true);
alt_actions_box.set_layout (Gtk.ButtonBoxStyle.EXPAND);
alt_actions_box.set_visible (true);
// Add "Copy code" Action if available and copy it to clipboard when clicked
if (code != null && code.length > 0) {
string action_name = "COPY \"%s\"".printf (code);
var action_button = new Gtk.Button.with_label (action_name);
Gtk.FlowBoxChild flowbox_child = new Gtk.FlowBoxChild ();
flowbox_child.add_css_class ("notification-action");
alt_actions_box.append (flowbox_child);
Gtk.Button action_button = new Gtk.Button.with_label (
"COPY \"%s\"".printf (code));
action_button.clicked.connect (() => {
// Copy to clipboard
get_clipboard (Gdk.SELECTION_CLIPBOARD).set_text (code, -1);
get_clipboard ().set_text (code);
// Dismiss notification
action_clicked (null);
});
action_button
.get_style_context ().add_class ("notification-action");
action_button.set_can_focus (false);
alt_actions_box.add (action_button);
flowbox_child.set_child (action_button);
}
// Add notification specified actions
foreach (var action in param.actions.data) {
var action_button = new Gtk.Button.with_label (action.name);
Gtk.FlowBoxChild flowbox_child = new Gtk.FlowBoxChild ();
flowbox_child.add_css_class ("notification-action");
alt_actions_box.append (flowbox_child);
Gtk.Button action_button = new Gtk.Button.with_label (action.name);
action_button.clicked.connect (() => action_clicked (action));
action_button
.get_style_context ().add_class ("notification-action");
action_button.set_can_focus (false);
alt_actions_box.add (action_button);
flowbox_child.set_child (action_button);
}
viewport.add (alt_actions_box);
scroll.add (viewport);
base_box.add (scroll);
scroll.show_all ();
}
}
......@@ -712,11 +711,8 @@ namespace SwayNotificationCenter {
// Check if it's a freedesktop.org-compliant icon
if (!img_path_exists) {
unowned Gtk.IconTheme icon_theme = Gtk.IconTheme.get_default ();
Gtk.IconInfo? info = icon_theme.lookup_icon (param.image_path,
notification_icon_size,
Gtk.IconLookupFlags.USE_BUILTIN);
img_path_exists = info != null;
unowned Gtk.IconTheme icon_theme = Gtk.IconTheme.get_for_display (get_display ());
img_path_exists = icon_theme.has_icon (param.image_path);
img_path_is_theme_icon = img_path_exists;
}
}
......@@ -726,30 +722,20 @@ namespace SwayNotificationCenter {
app_icon_exists = File.new_for_path (app_icon_uri ?? "").query_exists ();
}
// Get the image CSS corner radius in pixels
int radius = 0;
unowned var ctx = img.get_style_context ();
var value = ctx.get_property (Gtk.STYLE_PROPERTY_BORDER_RADIUS,
ctx.get_state ());
if (value.type () == Type.INT) {
radius = value.get_int ();
}
// Set the main image to the provided image
if (param.image_data.is_initialized) {
Functions.set_image_data (param.image_data, img,
notification_icon_size, radius);
notification_icon_size);
} else if (param.image_path != null &&
param.image_path != "" &&
img_path_exists) {
Functions.set_image_uri (param.image_path, img,
notification_icon_size,
radius,
img_path_exists,
img_path_is_theme_icon);
} else if (param.icon_data.is_initialized) {
Functions.set_image_data (param.icon_data, img,
notification_icon_size, radius);
notification_icon_size);
}
if (img.storage_type == Gtk.ImageType.EMPTY) {
......@@ -757,13 +743,12 @@ namespace SwayNotificationCenter {
if (app_icon_uri != null) {
Functions.set_image_uri (app_icon_uri, img,
notification_icon_size,
radius,
app_icon_exists);
} else if (app_icon_name != null) {
img.set_from_gicon (app_icon_name, icon_size);
img.set_from_gicon (app_icon_name);
} else if (image_visibility == ImageVisibility.ALWAYS) {
// Default icon
img.set_from_icon_name ("image-missing", icon_size);
img.set_from_icon_name ("image-missing");
} else {
img.set_visible (false);
}
......@@ -772,10 +757,9 @@ namespace SwayNotificationCenter {
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);
img_app_icon.set_from_gicon (app_icon_name);
}
}
}
......@@ -816,11 +800,5 @@ namespace SwayNotificationCenter {
timeout_id = 0;
}
}
/** Forces the EventBox to reload its style_context #27 */
public void reload_style_context () {
event_box.get_style_context ().changed ();
default_action.get_style_context ().changed ();
}
}
}
......@@ -5,12 +5,15 @@ namespace SwayNotificationCenter {
public string name_id;
private NotificationCloseButton close_button;
private DismissibleWidget dismissible;
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 Gtk.EventControllerMotion motion_controller;
private Gtk.GestureClick gesture;
private bool gesture_down = false;
private bool gesture_in = false;
......@@ -21,9 +24,23 @@ namespace SwayNotificationCenter {
public NotificationGroup (string name_id, string display_name) {
this.name_id = name_id;
get_style_context ().add_class ("notification-group");
add_css_class ("notification-group");
dismissible = new DismissibleWidget ();
dismissible.dismissed.connect (close_all_notifications);
set_child (dismissible);
Gtk.Overlay overlay = new Gtk.Overlay ();
dismissible.child = overlay;
Gtk.Box box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
box.set_hexpand (true);
overlay.set_child (box);
close_button = new NotificationCloseButton ();
close_button.clicked.connect (close_all_notifications);
close_button.add_css_class ("notification-group-close-button");
overlay.add_overlay (close_button);
revealer.set_transition_type (Gtk.RevealerTransitionType.SLIDE_UP);
revealer.set_reveal_child (false);
......@@ -33,59 +50,56 @@ namespace SwayNotificationCenter {
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");
end_box.add_css_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);
"swaync-collapse-symbolic");
collapse_button.add_css_class ("circular");
collapse_button.add_css_class ("notification-group-collapse-button");
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);
end_box.append (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);
"swaync-close-symbolic");
close_all_button.add_css_class ("circular");
close_all_button.add_css_class ("notification-group-close-all-button");
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);
end_box.append (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");
start_box.set_hexpand (true);
start_box.add_css_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_icon.add_css_class ("notification-group-icon");
start_box.append (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);
app_label.add_css_class ("title-1");
app_label.add_css_class ("notification-group-header");
start_box.append (app_label);
controls_box.pack_start (start_box);
controls_box.pack_end (end_box);
revealer.add (controls_box);
box.add (revealer);
controls_box.prepend (start_box);
controls_box.append (end_box);
revealer.set_child (controls_box);
box.append (revealer);
set_activatable (false);
......@@ -98,15 +112,13 @@ namespace SwayNotificationCenter {
}
});
set_classes ();
box.add (group);
add (box);
show_all ();
box.append (group);
/*
* Handling of group presses
*/
gesture = new Gtk.GestureMultiPress (this);
gesture = new Gtk.GestureClick ();
box.add_controller (gesture);
gesture.set_touch_only (false);
gesture.set_exclusive (true);
gesture.set_button (Gdk.BUTTON_PRIMARY);
......@@ -122,7 +134,7 @@ namespace SwayNotificationCenter {
if (gesture_in) {
bool single_noti = only_single_notification ();
if (!group.is_expanded && !single_noti) {
group.set_expanded (true);
set_expanded (true);
on_expand_change (true);
}
group.set_sensitive (single_noti || group.is_expanded);
......@@ -137,12 +149,12 @@ namespace SwayNotificationCenter {
Gtk.GestureSingle gesture_single = (Gtk.GestureSingle) gesture;
if (sequence != gesture_single.get_current_sequence ()) return;
Gtk.Allocation allocation;
int width = get_width ();
int height = get_height ();
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);
bool intersects = (x >= 0 && y >= 0 && x < width && y < height);
if (gesture_in != intersects) {
gesture_in = intersects;
}
......@@ -152,14 +164,25 @@ namespace SwayNotificationCenter {
gesture_down = false;
}
});
/*
* Handling of group hover
*/
motion_controller = new Gtk.EventControllerMotion ();
this.add_controller (motion_controller);
motion_controller.motion.connect ((event) => {
close_button.set_reveal (!group.is_expanded && !only_single_notification ());
});
motion_controller.leave.connect ((controller) => {
close_button.set_reveal (false);
});
}
private void set_classes () {
unowned Gtk.StyleContext ctx = get_style_context ();
ctx.remove_class (STYLE_CLASS_COLLAPSED);
remove_css_class (STYLE_CLASS_COLLAPSED);
if (!group.is_expanded) {
if (!ctx.has_class (STYLE_CLASS_COLLAPSED)) {
ctx.add_class (STYLE_CLASS_COLLAPSED);
if (!has_css_class (STYLE_CLASS_COLLAPSED)) {
add_css_class (STYLE_CLASS_COLLAPSED);
}
}
}
......@@ -173,23 +196,22 @@ namespace SwayNotificationCenter {
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.set_from_gicon (icon);
app_icon.show ();
} else {
app_icon.set_from_icon_name ("application-x-executable-symbolic",
Gtk.IconSize.INVALID);
app_icon.set_from_icon_name ("application-x-executable-symbolic");
}
}
/// 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;
return group.widgets.nth_data (0) != null && group.widgets.nth_data (1) == null;
}
public void set_expanded (bool state) {
group.set_expanded (state);
group.set_sensitive (only_single_notification () || group.is_expanded);
dismissible.set_can_dismiss (!state);
}
public bool toggle_expanded () {
......@@ -201,9 +223,8 @@ namespace SwayNotificationCenter {
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);
if (!has_css_class (STYLE_CLASS_URGENT)) {
add_css_class (STYLE_CLASS_URGENT);
}
}
group.add (noti);
......@@ -219,7 +240,7 @@ namespace SwayNotificationCenter {
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);
remove_css_class (STYLE_CLASS_URGENT);
}
group.remove (noti);
if (only_single_notification ()) {
......@@ -254,10 +275,13 @@ namespace SwayNotificationCenter {
}
public void close_all_notifications () {
close_button.set_reveal (false);
urgent_notifications.remove_all ();
foreach (unowned Gtk.Widget widget in group.widgets) {
var noti = (Notification) widget;
if (noti != null) noti.close_notification (false);
if (noti != null) {
noti.close_notification (false);
}
}
}
......@@ -269,14 +293,15 @@ namespace SwayNotificationCenter {
}
}
public int get_relative_y (Gtk.Widget parent) {
int dest_y;
translate_coordinates (parent, 0, 0, null, out dest_y);
return dest_y;
public float get_relative_y (Gtk.Widget parent) {
Graphene.Point point = Graphene.Point.zero ();
Graphene.Point dest_point;
compute_point (parent, point, out dest_point);
return dest_point.y;
}
}
private class ExpandableGroup : Gtk.Container {
private class ExpandableGroup : Gtk.Widget {
const int NUM_STACKED_NOTIFICATIONS = 3;
const int COLLAPSED_NOTIFICATION_OFFSET = 8;
......@@ -288,7 +313,8 @@ namespace SwayNotificationCenter {
return (1 - animation_progress);
}
}
private Animation ? animation;
private Adw.TimedAnimation animation;
private Adw.CallbackAnimationTarget animation_target;
private unowned on_expand_change change_cb;
......@@ -297,15 +323,16 @@ namespace SwayNotificationCenter {
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);
animation_target = new Adw.CallbackAnimationTarget (animation_value_cb);
animation = new Adw.TimedAnimation (this, 1.0, 0.0,
animation_duration,
animation_target);
animation.set_easing (Adw.Easing.EASE_IN_OUT_CUBIC);
animation.done.connect (animation_done_cb);
this.show ();
......@@ -323,36 +350,84 @@ namespace SwayNotificationCenter {
change_cb (is_expanded);
}
public override void add (Gtk.Widget widget) {
public void add (Gtk.Widget widget) {
widget.set_parent (this);
widgets.append (widget);
}
public override void remove (Gtk.Widget widget) {
public void remove (Gtk.Widget widget) {
widget.unparent ();
widgets.remove (widget);
if (this.get_visible () && widget.get_visible ()) {
this.queue_resize_no_redraw ();
queue_resize ();
}
}
public override void forall_internal (bool include_internals, Gtk.Callback callback) {
foreach (unowned Gtk.Widget widget in widgets) {
callback (widget);
}
private Gtk.Allocation get_alloc (Gtk.Widget w) {
Gtk.Allocation alloc = Gtk.Allocation ();
Graphene.Rect bounds;
w.compute_bounds (this, out bounds);
alloc.width = w.get_width ();
alloc.height = w.get_height ();
alloc.x = (int) bounds.origin.x;
alloc.y = (int) bounds.origin.y;
return alloc;
}
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);
protected override void measure (Gtk.Orientation orientation, int for_size,
out int minimum, out int natural,
out int minimum_baseline, out int natural_baseline) {
minimum = 0;
natural = 0;
minimum_baseline = -1;
natural_baseline = -1;
if (orientation == Gtk.Orientation.HORIZONTAL) {
return;
}
foreach (unowned Gtk.Widget widget in widgets) {
if (widget != null && widget.get_visible ()) {
int widget_minimum_height = 0;
int widget_natural_height = 0;
widget.measure (orientation, for_size,
out widget_minimum_height,
out widget_natural_height,
null, null);
minimum += widget_minimum_height;
natural += widget_natural_height;
}
}
int target_natural_height;
int target_minimum_height;
get_height_for_latest_notifications (for_size, out target_minimum_height,
out target_natural_height);
// TODO: Always use natural as minimum?
// Fixes large (tall) Notification body Pictures
minimum = (int) Functions.lerp (minimum,
target_natural_height,
animation_progress_inv);
natural = (int) Functions.lerp (natural,
target_natural_height,
animation_progress_inv);
}
protected override void size_allocate (int alloc_width, int alloc_height, int baseline) {
base.size_allocate (alloc_width, alloc_height, baseline);
int length = (int) widgets.length ();
if (length == 0) return;
uint border_width = get_border_width ();
Gtk.Allocation allocation = get_alloc (this);
allocation.width = alloc_width;
allocation.height = alloc_height;
Gtk.Allocation prev_allocation = Gtk.Allocation ();
prev_allocation.y = allocation.y;
......@@ -360,29 +435,28 @@ namespace SwayNotificationCenter {
// 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);
last.measure (Gtk.Orientation.VERTICAL, allocation.width, null, out target_height, null, 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);
widget.measure (Gtk.Orientation.VERTICAL, allocation.width,
null, out height,
null, null);
Gtk.Allocation alloc = Gtk.Allocation ();
alloc.x = allocation.x + (int) border_width;
alloc.x = allocation.x;
alloc.y = (int) (prev_allocation.y +
animation_progress * prev_allocation.height +
border_width);
alloc.width = allocation.width - 2 * (int) border_width;
animation_progress * prev_allocation.height);
alloc.width = allocation.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
......@@ -391,7 +465,9 @@ namespace SwayNotificationCenter {
}
prev_allocation = alloc;
widget.size_allocate (alloc);
Gsk.Transform transform = new Gsk.Transform ();
transform = transform.translate (Graphene.Point ().init (alloc.x, alloc.y));
widget.allocate (alloc.width, alloc.height, baseline, transform);
if (get_realized ()) {
widget.show ();
......@@ -403,62 +479,22 @@ namespace SwayNotificationCenter {
}
}
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) {
// Draw the widget
protected override void snapshot (Gtk.Snapshot snapshot) {
int length = (int) widgets.length ();
if (length == 0) return true;
if (length == 0) {
return;
}
Gtk.Allocation alloc;
get_allocated_size (out alloc, null);
Graphene.Rect bounds = Graphene.Rect ();
if (!compute_bounds (this, out bounds)) {
return;
}
Gtk.Allocation alloc = get_alloc (this);
int width = alloc.width;
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;
Gtk.Allocation latest_alloc = get_alloc (latest);
for (int i = 0; i < length; i++) {
// Skip drawing excess notifications
......@@ -469,101 +505,100 @@ namespace SwayNotificationCenter {
}
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);
Gtk.Allocation widget_alloc = get_alloc (widget);
int height_diff = latest_alloc.height - widget_alloc.height;
cr.save ();
snapshot.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);
double translate_y = height_diff * animation_progress_inv;
snapshot.translate (Graphene.Point ().init (0, (float) 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);
snapshot.translate (Graphene.Point ().init (
(float) ((widget_alloc.width - width * scale) * 0.5),
(float) (widget_alloc.height * (1 - scale))));
snapshot.scale ((float) scale, (float) scale);
}
int lerped_y = (int) Animation.lerp (-height_diff, 0, animation_progress);
int lerped_height = (int) Animation.lerp (latest_alloc.height,
int lerped_y = (int) Functions.lerp (-height_diff, 0, animation_progress);
lerped_y += (int) widget_alloc.y - alloc.y;
int lerped_height = (int) Functions.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 ();
Graphene.Rect clip_bounds = Graphene.Rect ().init (0f,
(float) lerped_y,
(float) width,
(float) lerped_height);
snapshot.push_clip (clip_bounds);
// TODO: Fades from the bottom at 0.5 -> top at 0.0 opacity
// 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 ();
// 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 ();
// }
// cr.pop_group_to_source ();
// cr.paint ();
// Cross-fades in the non visible stacked notifications when expanded
if (i < length - NUM_STACKED_NOTIFICATIONS) {
snapshot.push_opacity (1.5 * animation_progress);
}
// Draw notification cross-fade
snapshot_child (widget, snapshot);
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 ();
snapshot.pop (); // Cross-fade
}
cr.pop_group_to_source ();
cr.paint ();
snapshot.pop (); // Clip
cr.restore ();
snapshot.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;
private void get_height_for_latest_notifications (int for_size,
out int minimum,
out int natural) {
minimum = 0;
natural = 0;
uint length = widgets.length ();
int length = (int) widgets.length ();
if (length == 0) {
return;
}
if (length == 0) return;
unowned GLib.List<weak Gtk.Widget> last = widgets.last ();
if (last != null) {
unowned Gtk.Widget last_widget = widgets.last ().data;
int offset = 0;
for (uint i = 1;
i < length && i < NUM_STACKED_NOTIFICATIONS;
i++) {
offset += COLLAPSED_NOTIFICATION_OFFSET;
last_widget.measure (Gtk.Orientation.VERTICAL, for_size,
out minimum, out natural,
null, null);
}
unowned Gtk.Widget last = widgets.last ().data;
last.get_preferred_height_for_width (width,
out minimum_height,
out natural_height);
int offset = (length - 1).clamp (0, NUM_STACKED_NOTIFICATIONS - 1)
* COLLAPSED_NOTIFICATION_OFFSET;
minimum_height += offset;
natural_height += offset;
natural += offset;
}
void animation_value_cb (double progress) {
......@@ -573,14 +608,14 @@ namespace SwayNotificationCenter {
}
void animation_done_cb () {
animation.dispose ();
this.queue_allocate ();
}
void animate (double to) {
animation.stop ();
animation.start (animation_progress, to);
animation.set_value_from (animation_progress);
animation.set_value_to (to);
animation.reset ();
animation.play ();
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<template class="SwayNotificationCenterNotificationWindow" parent="GtkApplicationWindow">
<property name="can-focus">False</property>
<property name="role">NotificationWindow</property>
<property name="resizable">False</property>
<property name="type-hint">notification</property>
<property name="skip-taskbar-hint">True</property>
<property name="skip-pager-hint">True</property>
<property name="urgency-hint">True</property>
<property name="decorated">False</property>
<property name="deletable">False</property>
<property name="gravity">north-east</property>
<property name="show-menubar">False</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hscrollbar-policy">never</property>
<property name="window-placement">top-right</property>
<property name="propagate-natural-height">True</property>
<child>
<object class="GtkViewport" id="viewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="vexpand">True</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkBox" id="box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<style>
<class name="floating-notifications"/>
</style>
</template>
</interface>
namespace SwayNotificationCenter {
[GtkTemplate (ui = "/org/erikreider/sway-notification-center/notificationWindow/notificationWindow.ui")]
[GtkTemplate (ui = "/org/erikreider/swaync/ui/notification_window.ui")]
public class NotificationWindow : Gtk.ApplicationWindow {
private static NotificationWindow ? window = null;
/**
......@@ -34,11 +34,10 @@ namespace SwayNotificationCenter {
[GtkChild]
unowned Gtk.Viewport viewport;
[GtkChild]
unowned Gtk.Box box;
unowned IterBox box;
private bool list_reverse = false;
private double last_upper = 0;
private uint scroll_to_source_id = 0;
Gee.HashSet<uint32> inline_reply_notifications = new Gee.HashSet<uint32> ();
......@@ -63,30 +62,25 @@ namespace SwayNotificationCenter {
scrolled_window.set_max_content_height (MAX_HEIGHT);
scrolled_window.set_propagate_natural_height (true);
viewport.size_allocate.connect (size_alloc);
set_resizable (false);
default_width = ConfigModel.instance.notification_window_width;
}
this.default_width = ConfigModel.instance.notification_window_width;
protected override void snapshot (Gtk.Snapshot snapshot) {
// HACK: Fixes fully transparent windows not being mapped
Gdk.RGBA color = Gdk.RGBA () {
red = 0,
green = 0,
blue = 0,
alpha = 0,
};
snapshot.append_color (color, Graphene.Rect.zero ());
base.snapshot (snapshot);
}
private void set_anchor () {
if (swaync_daemon.use_layer_shell) {
GtkLayerShell.Layer layer;
switch (ConfigModel.instance.layer) {
case Layer.BACKGROUND:
layer = GtkLayerShell.Layer.BACKGROUND;
break;
case Layer.BOTTOM:
layer = GtkLayerShell.Layer.BOTTOM;
break;
case Layer.TOP:
layer = GtkLayerShell.Layer.TOP;
break;
default:
case Layer.OVERLAY:
layer = GtkLayerShell.Layer.OVERLAY;
break;
}
GtkLayerShell.set_layer (this, layer);
GtkLayerShell.set_layer (this, ConfigModel.instance.layer.to_layer ());
switch (ConfigModel.instance.positionX) {
case PositionX.LEFT:
......@@ -133,21 +127,6 @@ namespace SwayNotificationCenter {
list_reverse = ConfigModel.instance.positionY == PositionY.BOTTOM;
}
private void size_alloc () {
var adj = viewport.vadjustment;
double upper = adj.get_upper ();
if (last_upper < upper) {
scroll_to_start (list_reverse);
}
last_upper = upper;
}
private void scroll_to_start (bool reverse) {
var adj = viewport.vadjustment;
var val = (reverse ? adj.get_upper () : adj.get_lower ());
adj.set_value (val);
}
public void change_visibility (bool value) {
if (!value) {
close_all_notifications ();
......@@ -162,8 +141,8 @@ namespace SwayNotificationCenter {
public void close_all_notifications (remove_iter_func ? func = null) {
inline_reply_notifications.clear ();
if (!this.get_realized ()) return;
foreach (var w in box.get_children ()) {
Notification notification = (Notification) w;
foreach (unowned Gtk.Widget child in box.get_children ()) {
Notification notification = (Notification) child;
if (func == null || func (notification)) {
remove_notification (notification, false);
}
......@@ -178,6 +157,7 @@ namespace SwayNotificationCenter {
if (noti.has_inline_reply) {
inline_reply_notifications.remove (noti.param.applied_id);
if (inline_reply_notifications.size == 0
&& swaync_daemon.use_layer_shell
&& GtkLayerShell.get_keyboard_mode (this)
!= GtkLayerShell.KeyboardMode.NONE) {
GtkLayerShell.set_keyboard_mode (
......@@ -185,19 +165,34 @@ namespace SwayNotificationCenter {
}
}
noti.remove_noti_timeout ();
noti.destroy ();
box.remove (noti);
}
if (dismiss
&& (!get_realized ()
|| !get_mapped ()
|| !(get_child () is Gtk.Widget)
|| box.get_children ().length () == 0)) {
|| box.length == 0)) {
close ();
return;
}
}
/** Scroll to the latest notification */
private void scroll_to_latest_notification () {
if (scroll_to_source_id > 0) {
Source.remove (scroll_to_source_id);
}
scroll_to_source_id = Idle.add_once (() => {
scroll_to_source_id = 0;
if (list_reverse) {
viewport.scroll_to (box.get_last_child (), null);
} else {
viewport.scroll_to (box.get_first_child (), null);
}
});
}
public void add_notification (NotifyParams param) {
var noti = new Notification.timed (param,
swaync_daemon.noti_daemon,
......@@ -208,32 +203,31 @@ namespace SwayNotificationCenter {
if (noti.has_inline_reply) {
inline_reply_notifications.add (param.applied_id);
if (GtkLayerShell.get_keyboard_mode (this)
!= GtkLayerShell.KeyboardMode.ON_DEMAND) {
if (swaync_daemon.use_layer_shell &&
GtkLayerShell.get_keyboard_mode (this)
!= GtkLayerShell.KeyboardMode.ON_DEMAND
&& swaync_daemon.has_layer_on_demand) {
GtkLayerShell.set_keyboard_mode (
this, GtkLayerShell.KeyboardMode.ON_DEMAND);
}
}
if (list_reverse) {
box.pack_start (noti);
box.append (noti);
} else {
box.pack_end (noti);
box.prepend (noti);
}
this.grab_focus ();
if (!this.get_mapped () || !this.get_realized ()) {
this.set_anchor ();
this.show ();
}
// IMPORTANT: queue a resize event to force the layout to be recomputed
noti.queue_resize ();
scroll_to_start (list_reverse);
scroll_to_latest_notification ();
}
public void close_notification (uint32 id, bool dismiss) {
foreach (var w in box.get_children ()) {
var noti = (Notification) w;
foreach (unowned Gtk.Widget child in box.get_children ()) {
var noti = (Notification) child;
if (noti != null && noti.param.applied_id == id) {
remove_notification (noti, dismiss);
break;
......@@ -242,12 +236,17 @@ namespace SwayNotificationCenter {
}
public void replace_notification (uint32 id, NotifyParams new_params) {
foreach (var w in box.get_children ()) {
var noti = (Notification) w;
foreach (unowned Gtk.Widget child in box.get_children ()) {
var noti = (Notification) child;
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 ());
// Position the notification in the beginning/end of the list
if (list_reverse) {
box.reorder_child_after (noti, box.get_last_child ());
} else {
box.reorder_child_after (noti, null);
}
scroll_to_latest_notification ();
return;
}
}
......@@ -257,14 +256,15 @@ namespace SwayNotificationCenter {
}
public uint32 ? get_latest_notification () {
List<weak Gtk.Widget> children = box.get_children ();
if (children.is_empty ()) return null;
if (box.length == 0) {
return null;
}
Gtk.Widget ? child = null;
unowned Gtk.Widget ? child = null;
if (list_reverse) {
child = children.last ().data;
child = box.get_last_child ();
} else {
child = children.first ().data;
child = box.get_first_child ();
}
if (child == null || !(child is Notification)) return null;
......@@ -273,14 +273,15 @@ namespace SwayNotificationCenter {
}
public void latest_notification_action (uint32 action) {
List<weak Gtk.Widget> children = box.get_children ();
if (children.is_empty ()) return;
if (box.length == 0) {
return;
}
Gtk.Widget ? child = null;
if (list_reverse) {
child = children.last ().data;
child = box.get_last_child ();
} else {
child = children.first ().data;
child = box.get_first_child ();
}
if (child == null || !(child is Notification)) return;
......
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/erikreider/sway-notification-center">
<file>notificationWindow/notificationWindow.ui</file>
<file>notification/notification.ui</file>
<file>controlCenter/controlCenter.ui</file>
<file>controlCenter/widgets/mpris/mpris_player.ui</file>
</gresource>
</gresources>
......@@ -18,17 +18,18 @@ namespace SwayNotificationCenter {
private Array<BlankWindow> blank_windows = new Array<BlankWindow> ();
private unowned Gdk.Display ? display = Gdk.Display.get_default ();
[DBus (visible = false)]
public signal void reloading_css ();
private unowned GLib.ListModel ? monitors = null;
// Only set on swaync start due to some limitations of GtkLayerShell
[DBus (visible = false)]
public bool use_layer_shell { get; private set; }
[DBus (visible = false)]
public bool has_layer_on_demand { get; private set; }
public SwayncDaemon () {
// Init noti_daemon
this.use_layer_shell = ConfigModel.instance.layer_shell;
this.has_layer_on_demand = use_layer_shell && GtkLayerShell.get_protocol_version () >= 4;
this.noti_daemon = new NotiDaemon (this);
this.xdg_activation = new XdgActivationHelper ();
Bus.own_name (BusType.SESSION, "org.freedesktop.Notifications",
......@@ -67,27 +68,13 @@ namespace SwayNotificationCenter {
/// Blank windows
if (display == null) return;
init_blank_windows (false);
display.closed.connect ((is_error) => {
clear_blank_windows ();
if (is_error) stderr.printf ("Display closed due to error!\n");
});
display.opened.connect ((d) => {
bool visibility = noti_daemon.control_center.get_visibility ();
init_blank_windows (visibility);
});
display.monitor_added.connect ((d, m) => {
bool visibility = noti_daemon.control_center.get_visibility ();
add_blank_window (d, m, visibility);
});
display.monitor_removed.connect ((monitor) => {
monitors = display.get_monitors ();
monitors.items_changed.connect (() => {
close_blank_windows ();
bool visibility = noti_daemon.control_center.get_visibility ();
init_blank_windows (visibility);
});
init_blank_windows (false);
}
private void on_noti_bus_aquired (DBusConnection conn) {
......@@ -100,25 +87,20 @@ namespace SwayNotificationCenter {
}
}
private void add_blank_window (Gdk.Display display,
Gdk.Monitor monitor,
bool visible) {
var win = new BlankWindow (display, monitor, this);
win.set_visible (visible);
blank_windows.append_val (win);
}
private void init_blank_windows (bool visible) {
clear_blank_windows ();
close_blank_windows ();
// Add a window to all monitors
for (int i = 0; i < display.get_n_monitors (); i++) {
unowned Gdk.Monitor ? monitor = display.get_monitor (i);
if (monitor == null) continue;
add_blank_window (display, monitor, visible);
for (int i = 0; i < monitors.get_n_items (); i++) {
Object ? obj = monitors.get_item (i);
if (obj == null || !(obj is Gdk.Monitor)) continue;
BlankWindow win = new BlankWindow ((Gdk.Monitor) obj);
win.set_visible (visible);
blank_windows.append_val (win);
}
}
private void clear_blank_windows () {
private void close_blank_windows () {
while (blank_windows.length > 0) {
uint i = blank_windows.length - 1;
unowned BlankWindow ? win = blank_windows.index (i);
......@@ -128,12 +110,14 @@ namespace SwayNotificationCenter {
}
[DBus (visible = false)]
public void show_blank_windows (Gdk.Monitor ? monitor) {
public void show_blank_windows (Gdk.Monitor ? ref_monitor) {
if (!use_layer_shell || !ConfigModel.instance.layer_shell_cover_screen) {
return;
}
foreach (unowned BlankWindow win in blank_windows.data) {
if (win.monitor != monitor) win.show ();
if (win.monitor != ref_monitor) {
win.show ();
}
}
}
......@@ -174,7 +158,6 @@ namespace SwayNotificationCenter {
/** Reloads the CSS file */
public bool reload_css () throws Error {
bool result = Functions.load_css (style_path);
if (result) reloading_css ();
return result;
}
......
public class Underlay : Gtk.Widget {
protected unowned Gtk.Widget _child = null;
public unowned Gtk.Widget child {
get {
return _child;
}
set {
_child = value;
set_children ();
}
}
protected unowned Gtk.Widget _underlay_child = null;
public unowned Gtk.Widget underlay_child {
get {
return _underlay_child;
}
set {
_underlay_child = value;
set_children ();
}
}
private void set_children () {
if (_underlay_child != null) {
_underlay_child.unparent ();
_underlay_child.insert_after (this, null);
}
if (_child != null) {
_child.unparent ();
_child.insert_after (this, _underlay_child);
}
}
/*
* Overrides
*/
protected override Gtk.SizeRequestMode get_request_mode () {
return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
}
protected override void measure (Gtk.Orientation orientation, int for_size,
out int minimum, out int natural,
out int minimum_baseline, out int natural_baseline) {
minimum = 0;
natural = 0;
minimum_baseline = -1;
natural_baseline = -1;
int child_min, child_nat;
if (!child.visible) {
return;
}
child.measure (orientation, for_size,
out child_min, out child_nat, null, null);
minimum = int.max (minimum, child_min);
natural = int.max (natural, child_nat);
}
protected override void size_allocate (int width, int height, int baseline) {
if (child != null && child.should_layout ()) {
child.allocate (width, height, baseline, null);
}
if (underlay_child != null && underlay_child.should_layout ()) {
underlay_child.allocate (width, height, baseline, null);
}
}
}
......@@ -8,7 +8,7 @@ public class XdgActivationHelper : Object {
private Activation * xdg_activation = null;
public XdgActivationHelper () {
unowned Wl.Display wl_display = get_wl_display ();
unowned Wl.Display wl_display = Functions.get_wl_display ();
var wl_registry = wl_display.get_registry ();
wl_registry.add_listener (registry_listener, this);
......@@ -48,13 +48,13 @@ public class XdgActivationHelper : Object {
return null;
}
unowned Wl.Display wl_display = get_wl_display ();
unowned Gdk.Window ? window = widget.get_window ();
if (window == null) {
unowned Wl.Display wl_display = Functions.get_wl_display ();
unowned Gtk.Root ? root = widget.get_root ();
if (root == null) {
warning ("GDK Window is null");
return null;
}
unowned Wl.Surface wl_surface = get_wl_surface (window);
unowned Wl.Surface wl_surface = Functions.get_wl_surface (root.get_surface ());
Value token_value = Value (typeof (string));
token_value.set_string (null);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment