Move panel into an old-panel subdirectory

We expect to replace this with applets running under
https://github.com/pop-os/cosmic-dock-epoch.
This commit is contained in:
Ian Douglas Scott 2022-05-23 14:09:21 -07:00
parent 8c196a7ed3
commit 185dd48a6f
20 changed files with 30 additions and 31 deletions

View file

@ -0,0 +1,92 @@
use gtk4::{
gdk, gio,
glib::{self, clone},
prelude::*,
subclass::prelude::*,
};
use std::cell::Cell;
use crate::deref_cell::DerefCell;
use crate::notifications::Notifications;
use crate::status_notifier_watcher;
use crate::window;
#[derive(Default)]
pub struct PanelAppInner {
notifications: DerefCell<Notifications>,
activated: Cell<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for PanelAppInner {
const NAME: &'static str = "S76CosmicPanelApp";
type ParentType = gtk4::Application;
type Type = PanelApp;
}
impl ObjectImpl for PanelAppInner {
fn constructed(&self, obj: &PanelApp) {
obj.set_application_id(Some("com.system76.cosmicpanel"));
self.parent_constructed(obj);
glib::MainContext::default().spawn_local(status_notifier_watcher::start());
self.notifications.set(Notifications::new());
}
}
impl ApplicationImpl for PanelAppInner {
fn activate(&self, obj: &PanelApp) {
self.parent_activate(obj);
if self.activated.get() {
return;
}
self.activated.set(true);
let display = gdk::Display::default().unwrap();
let monitors = display.monitors();
for i in 0..monitors.n_items() {
obj.add_window_for_monitor(monitors.item(i).unwrap().downcast().unwrap());
}
monitors.connect_items_changed(
clone!(@weak obj => move |monitors, position, _removed, added| {
for i in position..position + added {
obj.add_window_for_monitor(monitors
.item(i)
.unwrap()
.downcast::<gdk::Monitor>()
.unwrap());
}
}),
);
}
}
impl GtkApplicationImpl for PanelAppInner {}
glib::wrapper! {
pub struct PanelApp(ObjectSubclass<PanelAppInner>)
@extends gtk4::Application, gio::Application,
@implements gio::ActionGroup, gio::ActionMap;
}
impl PanelApp {
pub fn new() -> Self {
glib::Object::new::<Self>(&[]).unwrap()
}
fn inner(&self) -> &PanelAppInner {
PanelAppInner::from_instance(self)
}
fn add_window_for_monitor(&self, monitor: gdk::Monitor) {
window::create(self, monitor);
}
pub fn notifications(&self) -> &Notifications {
&*self.inner().notifications
}
}

16
old-panel/src/config.rs Normal file
View file

@ -0,0 +1,16 @@
use std::str::FromStr;
use toml_edit::{Document, Table, Array, TomlError};
struct Buttons<'a>(&'a Table);
struct Config(Document);
impl Config {
fn new(s: &str) -> Result<Self, TomlError> {
Ok(Self(Document::from_str(s)?))
}
fn buttons(&self) -> Option<Buttons> {
Some(Buttons(self.0.as_table().get("buttons")?.as_table()?))
}
}

View file

@ -0,0 +1,53 @@
use futures::prelude::*;
use gtk4::glib::{self, clone};
use std::cell::Cell;
use zbus::fdo::{DBusProxy, RequestNameFlags, RequestNameReply};
use zbus_names::WellKnownName;
pub async fn create<
F: Fn(zbus::ConnectionBuilder<'static>) -> zbus::Result<zbus::ConnectionBuilder<'static>>,
>(
well_known_name: &'static str,
serve_cb: F,
) -> zbus::Result<zbus::Connection> {
let well_known_name = WellKnownName::try_from(well_known_name)?;
let connection = serve_cb(zbus::ConnectionBuilder::session()?)?
.build()
.await?;
let dbus_proxy = DBusProxy::new(&connection).await?;
let mut name_owner_changed_stream = dbus_proxy.receive_name_owner_changed().await?;
let flags = RequestNameFlags::AllowReplacement.into();
match dbus_proxy
.request_name(well_known_name.as_ref(), flags)
.await?
{
RequestNameReply::InQueue => {
eprintln!("Bus name '{}' already owned", well_known_name);
}
_ => {}
}
glib::MainContext::default().spawn_local(clone!(@strong connection => async move {
let have_bus_name = Cell::new(false);
let unique_name = connection.unique_name().map(|x| x.as_ref());
while let Some(evt) = name_owner_changed_stream.next().await {
let args = match evt.args() {
Ok(args) => args,
Err(_) => { continue; },
};
if args.name.as_ref() == well_known_name {
if args.new_owner.as_ref() == unique_name.as_ref() {
eprintln!("Acquired bus name: {}", well_known_name);
have_bus_name.set(true);
} else if have_bus_name.get() {
eprintln!("Lost bus name: {}", well_known_name);
have_bus_name.set(false);
}
}
}
}));
Ok(connection)
}

View file

@ -0,0 +1,31 @@
use once_cell::unsync::OnceCell;
/// Wrapper around `OnceCell` implementing `Deref`, and thus also panicking
/// when not set (or set twice).
///
/// To be used in place of `gtk::TemplateChild`, but without xml.
pub struct DerefCell<T>(OnceCell<T>);
impl<T> DerefCell<T> {
#[track_caller]
pub fn set(&self, value: T) {
if self.0.set(value).is_err() {
panic!("Initialized twice");
}
}
}
impl<T> Default for DerefCell<T> {
fn default() -> Self {
Self(OnceCell::default())
}
}
impl<T> std::ops::Deref for DerefCell<T> {
type Target = T;
#[track_caller]
fn deref(&self) -> &T {
self.0.get().unwrap()
}
}

25
old-panel/src/main.rs Normal file
View file

@ -0,0 +1,25 @@
use gtk4::{glib, prelude::*};
mod application;
mod dbus_service;
mod deref_cell;
mod mpris;
mod mpris_player;
mod notification_list;
mod notification_popover;
mod notification_widget;
mod notifications;
mod popover_container;
mod status_area;
mod status_menu;
mod status_notifier_watcher;
mod time_button;
mod window;
use application::PanelApp;
fn main() {
glib::MainContext::default()
.with_thread_default(|| PanelApp::new().run())
.unwrap();
}

134
old-panel/src/mpris.rs Normal file
View file

@ -0,0 +1,134 @@
use cascade::cascade;
use futures::stream::StreamExt;
use gtk4::{
glib::{self, clone},
prelude::*,
subclass::prelude::*,
};
use once_cell::unsync::OnceCell;
use std::{cell::RefCell, collections::HashMap};
use zbus::fdo::DBusProxy;
use crate::deref_cell::DerefCell;
use crate::mpris_player::MprisPlayer;
#[derive(Default)]
pub struct MprisControlsInner {
listbox: DerefCell<gtk4::ListBox>,
dbus: OnceCell<DBusProxy<'static>>,
players: RefCell<HashMap<String, MprisPlayer>>,
}
#[glib::object_subclass]
impl ObjectSubclass for MprisControlsInner {
const NAME: &'static str = "S76MprisControls";
type ParentType = gtk4::Widget;
type Type = MprisControls;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk4::BinLayout>();
}
}
impl ObjectImpl for MprisControlsInner {
fn constructed(&self, obj: &MprisControls) {
let listbox = cascade! {
gtk4::ListBox::new();
..set_parent(obj);
};
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
let (dbus, mut name_owner_changed_stream) = match async {
let connection = zbus::Connection::session().await?;
let dbus = DBusProxy::new(&connection).await?;
let stream = dbus.receive_name_owner_changed().await?;
Ok::<_, zbus::Error>((dbus, stream))
}.await {
Ok(value) => value,
Err(err) => {
eprintln!("Failed to connect to 'org.freedesktop.DBus': {}", err);
return;
}
};
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
while let Some(evt) = name_owner_changed_stream.next().await {
let args = match evt.args() {
Ok(args) => args,
Err(_) => { continue; },
};
if args.name.starts_with("org.mpris.MediaPlayer2.") {
if !args.old_owner.is_none() {
obj.player_removed(&args.name);
}
if !args.new_owner.is_none() {
obj.player_added(&args.name).await;
}
}
}
}));
match dbus.list_names().await {
Ok(names) => for name in names {
if name.starts_with("org.mpris.MediaPlayer2.") {
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
obj.player_added(&name).await;
}));
}
}
Err(err) => eprintln!("Failed to call 'ListNames: {}'", err)
}
let _ = obj.inner().dbus.set(dbus);
}));
self.listbox.set(listbox);
}
fn dispose(&self, _obj: &MprisControls) {
self.listbox.unparent();
}
}
impl WidgetImpl for MprisControlsInner {}
glib::wrapper! {
pub struct MprisControls(ObjectSubclass<MprisControlsInner>)
@extends gtk4::Widget;
}
impl MprisControls {
pub fn new() -> Self {
glib::Object::new(&[]).unwrap()
}
fn inner(&self) -> &MprisControlsInner {
MprisControlsInner::from_instance(self)
}
async fn player_added(&self, name: &str) {
let player = match MprisPlayer::new(&name).await {
Ok(player) => player,
Err(err) => {
eprintln!("Failed to connect to '{}': {}", name, err);
return;
}
};
let row = cascade! {
gtk4::ListBoxRow::new();
..set_selectable(false);
..set_child(Some(&player));
};
self.inner().listbox.append(&row);
self.inner()
.players
.borrow_mut()
.insert(name.to_owned(), player.clone());
}
fn player_removed(&self, name: &str) {
self.inner().players.borrow_mut().remove(name);
}
}

View file

@ -0,0 +1,264 @@
use cascade::cascade;
use futures::StreamExt;
use gtk4::{
gdk_pixbuf, gio,
glib::{self, clone},
pango,
prelude::*,
subclass::prelude::*,
};
use std::{cell::RefCell, collections::HashMap};
use zbus::dbus_proxy;
use zvariant::OwnedValue;
use crate::deref_cell::DerefCell;
#[derive(Default)]
pub struct MprisPlayerInner {
box_: DerefCell<gtk4::Box>,
backward_button: DerefCell<gtk4::Button>,
play_pause_button: DerefCell<gtk4::Button>,
forward_button: DerefCell<gtk4::Button>,
player: DerefCell<PlayerProxy<'static>>,
image: DerefCell<gtk4::Image>,
image_uri: RefCell<Option<String>>,
title_label: DerefCell<gtk4::Label>,
artist_label: DerefCell<gtk4::Label>,
}
#[glib::object_subclass]
impl ObjectSubclass for MprisPlayerInner {
const NAME: &'static str = "S76MprisPlayer";
type ParentType = gtk4::Widget;
type Type = MprisPlayer;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk4::BinLayout>();
}
}
impl ObjectImpl for MprisPlayerInner {
fn constructed(&self, obj: &MprisPlayer) {
let image = cascade! {
gtk4::Image::new();
..set_pixel_size(64);
};
let title_label = cascade! {
gtk4::Label::new(None);
..set_halign(gtk4::Align::Start);
..set_ellipsize(pango::EllipsizeMode::End);
..set_max_width_chars(20);
..set_attributes(Some(&cascade! {
pango::AttrList::new();
..insert(pango::AttrInt::new_weight(pango::Weight::Bold));
}));
};
let artist_label = cascade! {
gtk4::Label::new(None);
..set_halign(gtk4::Align::Start);
..set_ellipsize(pango::EllipsizeMode::End);
..set_max_width_chars(20);
};
let backward_button = cascade! {
gtk4::Button::from_icon_name("media-skip-backward-symbolic");
..connect_clicked(clone!(@strong obj => move |_| obj.call("Previous")));
};
let play_pause_button = cascade! {
gtk4::Button::from_icon_name("media-playback-start-symbolic");
..connect_clicked(clone!(@strong obj => move |_| obj.call("PlayPause")));
};
let forward_button = cascade! {
gtk4::Button::from_icon_name("media-skip-forward-symbolic");
..connect_clicked(clone!(@strong obj => move |_| obj.call("Next")));
};
let box_ = cascade! {
gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
..set_parent(obj);
..append(&image);
..append(&cascade! {
gtk4::Box::new(gtk4::Orientation::Vertical, 0);
..append(&title_label);
..append(&artist_label);
..append(&cascade! {
gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
..set_valign(gtk4::Align::Start);
..append(&backward_button);
..append(&play_pause_button);
..append(&forward_button);
});
});
};
self.box_.set(box_);
self.backward_button.set(backward_button);
self.play_pause_button.set(play_pause_button);
self.forward_button.set(forward_button);
self.image.set(image);
self.title_label.set(title_label);
self.artist_label.set(artist_label);
}
fn dispose(&self, _obj: &MprisPlayer) {
self.box_.unparent();
}
}
impl WidgetImpl for MprisPlayerInner {}
glib::wrapper! {
pub struct MprisPlayer(ObjectSubclass<MprisPlayerInner>)
@extends gtk4::Widget;
}
impl MprisPlayer {
pub async fn new(name: &str) -> zbus::Result<Self> {
let obj = glib::Object::new::<Self>(&[]).unwrap();
let connection = zbus::Connection::session().await?;
let player = PlayerProxy::builder(&connection)
.destination(name.to_string())?
.build()
.await?;
let mut status_stream = player.receive_playback_status_changed().await;
let mut metadata_stream = player.receive_metadata_changed().await;
obj.inner().player.set(player);
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
while status_stream.next().await.is_some() {
obj.update_status();
}
}));
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
while metadata_stream.next().await.is_some() {
obj.update_metadata();
}
}));
Ok(obj)
}
fn inner(&self) -> &MprisPlayerInner {
MprisPlayerInner::from_instance(self)
}
fn call(&self, method: &'static str) {
glib::MainContext::default().spawn_local(clone!(@strong self as self_ => async move {
if let Err(err) = self_.inner().player.call::<_, _, ()>(method, &()).await {
eprintln!("Failed to call '{}': {}", method, err);
}
}));
}
async fn update_arturl(&self, arturl: Option<&str>) {
let mut image_uri = self.inner().image_uri.borrow_mut();
if image_uri.as_deref() == arturl {
return;
}
*image_uri = arturl.map(String::from);
drop(image_uri);
let pixbuf = async {
// TODO: Security?
let file = gio::File::for_uri(&arturl?);
let stream = file.read_future(glib::PRIORITY_DEFAULT).await.ok()?;
gdk_pixbuf::Pixbuf::from_stream_future(&stream).await.ok()
}
.await;
if let Some(pixbuf) = pixbuf {
self.inner().image.set_from_pixbuf(Some(&pixbuf));
}
}
fn update_status(&self) {
let status = match self.inner().player.cached_playback_status() {
Ok(Some(status)) => status,
_ => return,
};
let play_pause_icon = if status == "Playing" {
"media-playback-pause-symbolic"
} else {
"media-playback-start-symbolic"
};
self.inner()
.play_pause_button
.set_icon_name(play_pause_icon);
}
fn update_metadata(&self) {
let metadata = match self.inner().player.cached_metadata() {
Ok(Some(metadata)) => metadata,
_ => return,
};
let title = metadata.title().unwrap_or_else(|| String::new());
// XXX correct way to handle multiple?
let artist = metadata
.artist()
.and_then(|x| x.get(0).cloned())
.unwrap_or_default();
let _album = metadata.album(); // TODO
let arturl = metadata.arturl();
glib::MainContext::default().spawn_local(clone!(@strong self as self_ => async move {
self_.update_arturl(arturl.as_deref()).await;
}));
self.inner().title_label.set_label(&title);
self.inner().artist_label.set_label(&artist);
}
}
pub struct Metadata(HashMap<String, OwnedValue>);
impl TryFrom<OwnedValue> for Metadata {
type Error = zbus::Error;
fn try_from(value: OwnedValue) -> zbus::Result<Self> {
Ok(Self(value.try_into()?))
}
}
impl Metadata {
fn lookup<'a, T: TryFrom<OwnedValue>>(&self, key: &str) -> Option<T> {
T::try_from(self.0.get(key)?.clone()).ok()
}
fn title(&self) -> Option<String> {
self.lookup("xesam:title")
}
fn album(&self) -> Option<String> {
self.lookup("xesam:album")
}
fn artist(&self) -> Option<Vec<String>> {
self.lookup("xesam:artist")
}
fn arturl(&self) -> Option<String> {
self.lookup("mpris:artUrl")
}
}
#[dbus_proxy(
interface = "org.mpris.MediaPlayer2.Player",
default_path = "/org/mpris/MediaPlayer2"
)]
trait Player {
#[dbus_proxy(property)]
fn metadata(&self) -> zbus::Result<Metadata>;
#[dbus_proxy(property)]
fn playback_status(&self) -> zbus::Result<String>;
}

View file

@ -0,0 +1,113 @@
use cascade::cascade;
use gtk4::{
glib::{self, clone},
prelude::*,
subclass::prelude::*,
};
use std::{cell::RefCell, collections::HashMap};
use crate::deref_cell::DerefCell;
use crate::notification_widget::NotificationWidget;
use crate::notifications::{Notification, NotificationId, Notifications};
#[derive(Default)]
pub struct NotificationListInner {
listbox: DerefCell<gtk4::ListBox>,
notifications: DerefCell<Notifications>,
ids: RefCell<Vec<glib::SignalHandlerId>>,
rows: RefCell<HashMap<NotificationId, gtk4::ListBoxRow>>,
}
#[glib::object_subclass]
impl ObjectSubclass for NotificationListInner {
const NAME: &'static str = "S76NotificationList";
type ParentType = gtk4::Widget;
type Type = NotificationList;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk4::BinLayout>();
}
}
impl ObjectImpl for NotificationListInner {
fn constructed(&self, obj: &NotificationList) {
let listbox = cascade! {
gtk4::ListBox::new();
..set_parent(obj);
..connect_row_activated(clone!(@weak obj => move |_, row| {
if let Some(id) = obj.id_for_row(row) {
let notifications = obj.inner().notifications.clone();
glib::MainContext::default().spawn_local(async move {
notifications.invoke_action(id, "default").await;
});
}
}));
};
self.listbox.set(listbox);
}
fn dispose(&self, obj: &NotificationList) {
self.listbox.unparent();
for i in obj.inner().ids.take().into_iter() {
obj.inner().notifications.disconnect(i);
}
}
}
impl WidgetImpl for NotificationListInner {}
glib::wrapper! {
pub struct NotificationList(ObjectSubclass<NotificationListInner>)
@extends gtk4::Widget;
}
impl NotificationList {
pub fn new(notifications: &Notifications) -> Self {
let obj = glib::Object::new::<Self>(&[]).unwrap();
obj.inner().notifications.set(notifications.clone());
*obj.inner().ids.borrow_mut() = vec![
notifications.connect_notification_received(clone!(@weak obj => move |notification| {
obj.handle_notification(&notification);
})),
notifications.connect_notification_closed(clone!(@weak obj => move |id| {
obj.remove_notification(id);
})),
];
obj
}
fn inner(&self) -> &NotificationListInner {
NotificationListInner::from_instance(self)
}
fn handle_notification(&self, notification: &Notification) {
let notification_widget = cascade! {
NotificationWidget::new(&*self.inner().notifications);
..set_notification(notification);
};
let row = cascade! {
gtk4::ListBoxRow::new();
..set_selectable(false);
..set_child(Some(&notification_widget));
};
self.inner().listbox.prepend(&row);
self.inner().rows.borrow_mut().insert(notification.id, row);
}
fn remove_notification(&self, id: NotificationId) {
if let Some(row) = self.inner().rows.borrow_mut().remove(&id) {
self.inner().listbox.remove(&row);
}
}
fn id_for_row(&self, row: &gtk4::ListBoxRow) -> Option<NotificationId> {
let rows = self.inner().rows.borrow();
Some(*rows.iter().find(|(_, i)| i == &row)?.0)
}
}

View file

@ -0,0 +1,136 @@
use cascade::cascade;
use gtk4::{
glib::{self, clone},
prelude::*,
subclass::prelude::*,
};
use std::cell::RefCell;
use crate::deref_cell::DerefCell;
use crate::notification_widget::NotificationWidget;
use crate::notifications::{Notification, NotificationId, Notifications};
#[derive(Default)]
pub struct NotificationPopoverInner {
notification_widget: DerefCell<NotificationWidget>,
notifications: DerefCell<Notifications>,
ids: RefCell<Vec<glib::SignalHandlerId>>,
source: RefCell<Option<glib::SourceId>>,
}
#[glib::object_subclass]
impl ObjectSubclass for NotificationPopoverInner {
const NAME: &'static str = "S76NotificationPopover";
type ParentType = gtk4::Popover;
type Type = NotificationPopover;
}
impl ObjectImpl for NotificationPopoverInner {
fn constructed(&self, obj: &NotificationPopover) {
cascade! {
obj;
..set_autohide(false);
..set_has_arrow(false);
..set_offset(0, 12);
..add_controller(&cascade! {
gtk4::GestureClick::new();
..connect_released(clone!(@weak obj => move |_, n_press, _, _| {
if n_press != 1 {
return;
}
if let Some(id) = obj.id() {
let notifications = obj.inner().notifications.clone();
glib::MainContext::default().spawn_local(async move {
notifications.invoke_action(id, "default").await;
});
}
obj.popdown();
}));
});
..add_controller(&cascade! {
gtk4::EventControllerMotion::new();
..connect_enter(clone!(@weak obj => move |_, _, _| {
obj.stop_timer();
}));
..connect_leave(clone!(@weak obj => move |_| {
obj.start_timer();
}));
});
};
}
fn dispose(&self, obj: &NotificationPopover) {
for i in obj.inner().ids.take().into_iter() {
obj.inner().notifications.disconnect(i);
}
}
}
impl WidgetImpl for NotificationPopoverInner {}
impl PopoverImpl for NotificationPopoverInner {}
glib::wrapper! {
pub struct NotificationPopover(ObjectSubclass<NotificationPopoverInner>)
@extends gtk4::Popover, gtk4::Widget;
}
impl NotificationPopover {
pub fn new(notifications: &Notifications) -> Self {
let obj = glib::Object::new::<Self>(&[]).unwrap();
let notification_widget = cascade! {
NotificationWidget::new(notifications);
};
obj.set_child(Some(&notification_widget));
obj.inner().notification_widget.set(notification_widget);
obj.inner().notifications.set(notifications.clone());
*obj.inner().ids.borrow_mut() = vec![
notifications.connect_notification_received(clone!(@weak obj => move |notification| {
obj.handle_notification(&notification);
})),
notifications.connect_notification_closed(clone!(@weak obj => move |id| {
if obj.id() == Some(id) {
obj.popdown();
}
})),
];
obj
}
fn inner(&self) -> &NotificationPopoverInner {
NotificationPopoverInner::from_instance(self)
}
fn id(&self) -> Option<NotificationId> {
self.inner().notification_widget.id()
}
fn handle_notification(&self, notification: &Notification) {
self.inner()
.notification_widget
.set_notification(notification);
self.popup();
self.start_timer();
}
fn stop_timer(&self) {
if let Some(source) = self.inner().source.borrow_mut().take() {
source.remove();
}
}
fn start_timer(&self) {
self.stop_timer();
let source = glib::timeout_add_seconds_local(
1,
clone!(@weak self as self_ => @default-return Continue(false), move || {
self_.popdown();
*self_.inner().source.borrow_mut() = None;
Continue(false)
}),
);
*self.inner().source.borrow_mut() = Some(source);
}
}

View file

@ -0,0 +1,117 @@
use cascade::cascade;
use gtk4::{
glib::{self, clone},
pango,
prelude::*,
subclass::prelude::*,
};
use std::cell::Cell;
use crate::deref_cell::DerefCell;
use crate::notifications::{Notification, NotificationId, Notifications};
#[derive(Default)]
pub struct NotificationWidgetInner {
box_: DerefCell<gtk4::Box>,
summary_label: DerefCell<gtk4::Label>,
body_label: DerefCell<gtk4::Label>,
notifications: DerefCell<Notifications>,
id: Cell<Option<NotificationId>>,
}
#[glib::object_subclass]
impl ObjectSubclass for NotificationWidgetInner {
const NAME: &'static str = "S76NotificationWidget";
type ParentType = gtk4::Widget;
type Type = NotificationWidget;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk4::BinLayout>();
}
}
impl ObjectImpl for NotificationWidgetInner {
fn constructed(&self, obj: &NotificationWidget) {
let summary_label = cascade! {
gtk4::Label::new(None);
..set_width_chars(20);
..set_max_width_chars(20);
..set_attributes(Some(&cascade! {
pango::AttrList::new();
..insert(pango::AttrInt::new_weight(pango::Weight::Bold));
}));
};
let body_label = cascade! {
gtk4::Label::new(None);
..set_width_chars(20);
..set_max_width_chars(20);
};
let box_ = cascade! {
gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
..set_parent(obj);
..append(&cascade! {
gtk4::Box::new(gtk4::Orientation::Vertical, 0);
..append(&summary_label);
..append(&body_label);
});
..append(&cascade! {
gtk4::Button::new();
..style_context().add_provider(&cascade! {
gtk4::CssProvider::new();
..load_from_data(b"button { min-width: 0; min-height: 0; padding: 4px 4px; }");
}, gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION);
..style_context().add_class("flat");
..set_valign(gtk4::Align::Start);
..set_child(Some(&cascade! {
gtk4::Image::from_icon_name("window-close-symbolic");
..set_pixel_size(8);
}));
..connect_clicked(clone!(@weak obj => move |_| {
if let Some(id) = obj.id() {
obj.inner().notifications.dismiss(id);
}
}));
});
};
self.box_.set(box_);
self.summary_label.set(summary_label);
self.body_label.set(body_label);
}
fn dispose(&self, _obj: &NotificationWidget) {
self.box_.unparent();
}
}
impl WidgetImpl for NotificationWidgetInner {}
glib::wrapper! {
pub struct NotificationWidget(ObjectSubclass<NotificationWidgetInner>)
@extends gtk4::Widget;
}
impl NotificationWidget {
pub fn new(notifications: &Notifications) -> Self {
let obj = glib::Object::new::<Self>(&[]).unwrap();
obj.inner().notifications.set(notifications.clone());
obj
}
fn inner(&self) -> &NotificationWidgetInner {
NotificationWidgetInner::from_instance(self)
}
pub fn set_notification(&self, notification: &Notification) {
self.inner().summary_label.set_label(&notification.summary);
self.inner().body_label.set_label(&notification.body);
self.inner().id.set(Some(notification.id));
}
pub fn id(&self) -> Option<NotificationId> {
self.inner().id.get()
}
}

View file

@ -0,0 +1,416 @@
#![allow(non_snake_case)]
use futures::stream::StreamExt;
use futures_channel::mpsc;
use gtk4::{
glib::{self, clone, subclass::Signal, SignalHandlerId},
prelude::*,
subclass::prelude::*,
};
use once_cell::sync::Lazy;
use once_cell::unsync::OnceCell;
use std::{
collections::HashMap,
convert::TryFrom,
fmt,
num::NonZeroU32,
sync::{Arc, Mutex},
};
use zbus::{dbus_interface, Result, SignalContext};
use zvariant::OwnedValue;
use crate::dbus_service;
use crate::deref_cell::DerefCell;
static PATH: &str = "/org/freedesktop/Notifications";
static INTERFACE: &str = "org.freedesktop.Notifications";
enum Event {
NotificationReceived(NotificationId),
CloseNotification(NotificationId),
}
pub struct NotificationsInterfaceInner {
next_id: Mutex<NotificationId>,
notifications: Mutex<HashMap<NotificationId, Arc<Notification>>>,
sender: mpsc::UnboundedSender<Event>,
}
#[derive(Clone)]
pub struct NotificationsInterface(Arc<NotificationsInterfaceInner>);
impl NotificationsInterface {
fn new() -> (Self, mpsc::UnboundedReceiver<Event>) {
let (sender, receiver) = mpsc::unbounded();
(
Self(Arc::new(NotificationsInterfaceInner {
next_id: Default::default(),
notifications: Default::default(),
sender,
})),
receiver,
)
}
fn next_id(&self) -> NotificationId {
let mut next_id = self.0.next_id.lock().unwrap();
let id = *next_id;
*next_id = NotificationId::new(u32::from(id).wrapping_add(1)).unwrap_or_default();
id
}
fn handle_notify(
&self,
app_name: String,
replaces_id: Option<NotificationId>,
app_icon: String,
summary: String,
body: String,
actions: Vec<String>,
hints: Hints,
_expire_timeout: i32,
) -> NotificationId {
// Ignores `expire-timeout`, like Gnome Shell
let id = replaces_id.unwrap_or_else(|| self.next_id());
let notification = Arc::new(Notification {
id,
app_name,
app_icon,
summary,
body,
actions,
hints,
});
self.0
.notifications
.lock()
.unwrap()
.insert(id, notification);
self.0
.sender
.unbounded_send(Event::NotificationReceived(id))
.unwrap();
id
}
}
// TODO: return value variable names in introspection data?
#[dbus_interface(name = "org.freedesktop.Notifications")]
impl NotificationsInterface {
fn Notify(
&self,
app_name: String,
replaces_id: u32,
app_icon: String,
summary: String,
body: String,
actions: Vec<String>,
hints: Hints,
expire_timeout: i32,
) -> u32 {
u32::from(self.handle_notify(
app_name,
NotificationId::new(replaces_id),
app_icon,
summary,
body,
actions,
hints,
expire_timeout,
))
}
async fn CloseNotification(&self, id: u32) {
if let Some(id) = NotificationId::new(id) {
self.0
.sender
.unbounded_send(Event::CloseNotification(id))
.unwrap();
}
// TODO error?
}
fn GetCapabilities(&self) -> Vec<&'static str> {
// TODO: body-markup, sound
vec!["actions", "body", "icon-static", "persistence"]
}
fn GetServerInformation(&self) -> (&'static str, &'static str, &'static str, &'static str) {
("cosmic-panel", "system76", env!("CARGO_PKG_VERSION"), "1.2")
}
#[dbus_interface(signal)]
async fn NotificationClosed(ctxt: &SignalContext<'_>, id: u32, reason: u32) -> Result<()>;
#[dbus_interface(signal)]
async fn ActionInvoked(ctxt: &SignalContext<'_>, id: u32, action_key: &str) -> Result<()>;
}
#[derive(Default)]
pub struct NotificationsInner {
interface: DerefCell<NotificationsInterface>,
connection: OnceCell<zbus::Connection>,
}
#[glib::object_subclass]
impl ObjectSubclass for NotificationsInner {
const NAME: &'static str = "S76Notifications";
type ParentType = glib::Object;
type Type = Notifications;
}
impl ObjectImpl for NotificationsInner {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![
Signal::builder(
"notification-received",
&[NotificationId::static_type().into()],
glib::Type::UNIT.into(),
)
.build(),
Signal::builder(
"notification-closed",
&[NotificationId::static_type().into()],
glib::Type::UNIT.into(),
)
.build(),
]
});
SIGNALS.as_ref()
}
}
glib::wrapper! {
pub struct Notifications(ObjectSubclass<NotificationsInner>);
}
#[derive(zvariant::Type, serde::Deserialize)]
struct Hints(HashMap<String, OwnedValue>);
#[allow(dead_code)]
impl Hints {
fn prop<T: TryFrom<OwnedValue>>(&self, name: &str) -> Option<T> {
T::try_from(self.0.get(name)?.clone()).ok()
}
fn actions_icon(&self) -> bool {
self.prop("actions-icon").unwrap_or(false)
}
fn category(&self) -> Option<String> {
self.prop("category")
}
fn desktop_entry(&self) -> Option<String> {
self.prop("desktop-entry")
}
fn image_data(&self) -> Option<(i32, i32, i32, bool, i32, i32, Vec<u8>)> {
self.prop("image-data")
.or_else(|| self.prop("image_data"))
.or_else(|| self.prop("icon_data"))
}
fn image_path(&self) -> Option<String> {
self.prop("image-path").or_else(|| self.prop("image_path"))
}
fn resident(&self) -> bool {
self.prop("resident").unwrap_or(false)
}
fn sound_file(&self) -> Option<String> {
self.prop("sound-file")
}
fn sound_name(&self) -> Option<String> {
self.prop("sound-name")
}
fn transient(&self) -> bool {
self.prop("transient").unwrap_or(false)
}
fn xy(&self) -> Option<(u8, u8)> {
Some((self.prop("x")?, self.prop("y")?))
}
fn urgency(&self) -> Option<u8> {
self.prop("urgency")
}
}
impl fmt::Debug for Hints {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut s = f.debug_struct("Hints");
for (k, v) in &self.0 {
if let Ok(v) = <&str>::try_from(v) {
s.field(k, &v);
} else if let Ok(v) = i32::try_from(v) {
s.field(k, &v);
} else if let Ok(v) = bool::try_from(v) {
s.field(k, &v);
} else if let Ok(v) = u8::try_from(v) {
s.field(k, &v);
} else {
s.field(k, v);
};
}
s.finish()
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, Hash, glib::Boxed, PartialEq, Eq)]
#[boxed_type(name = "S76NotificationId")]
pub struct NotificationId(NonZeroU32);
impl Default for NotificationId {
fn default() -> Self {
Self(NonZeroU32::new(1).unwrap())
}
}
impl From<NotificationId> for u32 {
fn from(id: NotificationId) -> u32 {
id.0.into()
}
}
impl NotificationId {
fn new(value: u32) -> Option<Self> {
NonZeroU32::new(value).map(Self)
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct Notification {
pub id: NotificationId,
pub app_name: String,
pub app_icon: String, // decode?
pub summary: String,
pub body: String,
pub actions: Vec<String>, // enum?
hints: Hints,
}
#[repr(u32)]
#[allow(dead_code)]
enum CloseReason {
Expire = 1,
Dismiss,
Call,
Undefined,
}
impl Notifications {
pub fn new() -> Self {
let notifications = glib::Object::new::<Self>(&[]).unwrap();
let (interface, mut receiver) = NotificationsInterface::new();
notifications.inner().interface.set(interface);
glib::MainContext::default().spawn_local(clone!(@strong notifications => async move {
let connection = match dbus_service::create(INTERFACE, |builder| builder.serve_at(PATH, notifications.inner().interface.clone())).await {
Ok(connection) => connection,
Err(err) => {
eprintln!("Failed to start `Notifications` service: {}", err);
return;
}
};
let _ = notifications.inner().connection.set(connection.clone());
if let Some(event) = receiver.next().await {
match event {
Event::NotificationReceived(id) => {
notifications.emit_by_name::<()>("notification-received", &[&id]);
}
Event::CloseNotification(id) => {
notifications.close_notification(id, CloseReason::Call).await
}
}
}
}));
notifications
}
fn inner(&self) -> &NotificationsInner {
NotificationsInner::from_instance(self)
}
async fn close_notification(&self, id: NotificationId, reason: CloseReason) {
self.inner()
.interface
.0
.notifications
.lock()
.unwrap()
.remove(&id);
self.emit_by_name::<()>("notification-closed", &[&id]);
if let Some(connection) = self.inner().connection.get() {
let ctxt = SignalContext::new(connection, PATH).unwrap(); // XXX unwrap?
let _ =
NotificationsInterface::NotificationClosed(&ctxt, id.into(), reason as u32).await;
}
}
pub fn dismiss(&self, id: NotificationId) {
glib::MainContext::default().spawn_local(clone!(@strong self as self_ => async move {
self_.close_notification(id, CloseReason::Dismiss).await
}));
}
pub async fn invoke_action(&self, id: NotificationId, action_key: &str) {
if let Some(connection) = self.inner().connection.get() {
let ctxt = SignalContext::new(connection, PATH).unwrap(); // XXX unwrap?
let _ = NotificationsInterface::ActionInvoked(&ctxt, id.into(), action_key).await;
}
}
pub fn get(&self, id: NotificationId) -> Option<Arc<Notification>> {
self.inner()
.interface
.0
.notifications
.lock()
.unwrap()
.get(&id)
.cloned()
}
pub fn connect_notification_received<F: Fn(Arc<Notification>) + 'static>(
&self,
cb: F,
) -> SignalHandlerId {
self.connect_local("notification-received", false, move |values| {
let obj = values[0].get::<Self>().unwrap();
let id = values[1].get().unwrap();
if let Some(notification) = obj.get(id) {
cb(notification);
}
None
})
}
pub fn connect_notification_closed<F: Fn(NotificationId) + 'static>(
&self,
cb: F,
) -> SignalHandlerId {
self.connect_local("notification-closed", false, move |values| {
let id = values[1].get().unwrap();
cb(id);
None
})
}
}

View file

@ -0,0 +1,89 @@
use cascade::cascade;
use gtk4::{glib, prelude::*, subclass::prelude::*};
use crate::deref_cell::DerefCell;
/// Unlike gtk4's `MenuButton`, this supports a custom child.
#[derive(Default)]
pub struct PopoverContainerInner {
child: DerefCell<gtk4::Widget>,
popover: DerefCell<gtk4::Popover>,
}
#[glib::object_subclass]
impl ObjectSubclass for PopoverContainerInner {
const NAME: &'static str = "S76PopoverContainer";
type ParentType = gtk4::Widget;
type Type = PopoverContainer;
}
impl ObjectImpl for PopoverContainerInner {
fn constructed(&self, obj: &PopoverContainer) {
let popover = cascade! {
gtk4::Popover::new();
..set_parent(obj);
};
self.popover.set(popover);
}
fn dispose(&self, _obj: &PopoverContainer) {
self.child.unparent();
self.popover.unparent();
}
}
impl WidgetImpl for PopoverContainerInner {
fn measure(
&self,
_obj: &PopoverContainer,
orientation: gtk4::Orientation,
for_size: i32,
) -> (i32, i32, i32, i32) {
self.child.measure(orientation, for_size)
}
fn size_allocate(&self, _obj: &PopoverContainer, width: i32, height: i32, baseline: i32) {
self.child
.size_allocate(&gtk4::Allocation::new(0, 0, width, height), baseline);
self.popover.present();
}
fn focus(&self, _obj: &PopoverContainer, direction: gtk4::DirectionType) -> bool {
if self.popover.is_visible() {
self.popover.child_focus(direction)
} else {
self.child.child_focus(direction)
}
}
}
glib::wrapper! {
pub struct PopoverContainer(ObjectSubclass<PopoverContainerInner>)
@extends gtk4::Widget;
}
impl PopoverContainer {
pub fn new<T: IsA<gtk4::Widget>>(child: &T) -> Self {
let obj = glib::Object::new::<Self>(&[]).unwrap();
child.set_parent(&obj);
obj.inner().child.set(child.clone().upcast());
obj
}
fn inner(&self) -> &PopoverContainerInner {
PopoverContainerInner::from_instance(self)
}
pub fn popover(&self) -> &gtk4::Popover {
&self.inner().popover
}
pub fn popup(&self) {
self.popover().popup();
}
pub fn popdown(&self) {
self.popover().popdown();
}
}

View file

@ -0,0 +1,145 @@
use cascade::cascade;
use futures::stream::StreamExt;
use gtk4::{
glib::{self, clone},
prelude::*,
subclass::prelude::*,
};
use once_cell::unsync::OnceCell;
use std::{cell::RefCell, collections::HashMap};
use zbus::dbus_proxy;
use crate::deref_cell::DerefCell;
use crate::status_menu::StatusMenu;
#[derive(Default)]
pub struct StatusAreaInner {
box_: DerefCell<gtk4::Box>,
watcher: OnceCell<StatusNotifierWatcherProxy<'static>>,
icons: RefCell<HashMap<String, StatusMenu>>,
}
#[glib::object_subclass]
impl ObjectSubclass for StatusAreaInner {
const NAME: &'static str = "S76StatusArea";
type ParentType = gtk4::Widget;
type Type = StatusArea;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk4::BinLayout>();
}
}
impl ObjectImpl for StatusAreaInner {
fn constructed(&self, obj: &StatusArea) {
let box_ = cascade! {
gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
..set_parent(obj);
};
self.box_.set(box_);
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
async {
let connection = zbus::Connection::session().await?;
let watcher = StatusNotifierWatcherProxy::new(&connection).await?;
let name = connection.unique_name().unwrap().as_str();
if let Err(err) = watcher.register_status_notifier_host(name).await {
eprintln!("Failed to register status notifier host: {}", err);
}
let mut registered_stream = watcher.receive_status_notifier_item_registered().await?;
let mut unregistered_stream = watcher.receive_status_notifier_item_unregistered().await?;
for name in watcher.registered_status_notifier_items().await? {
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
obj.item_registered(&name).await;
}));
}
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
if let Some(evt) = registered_stream.next().await {
if let Ok(args) = evt.args() {
obj.item_registered(&args.name).await;
}
}
}));
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
if let Some(evt) = unregistered_stream.next().await {
if let Ok(args) = evt.args() {
obj.item_unregistered(&args.name);
}
}
}));
let _ = obj.inner().watcher.set(watcher);
Ok::<_, zbus::Error>(())
}.await.unwrap_or_else(|err| {
eprintln!("Failed to connect to 'org.kde.StatusNotifierWatcher': {}", err);
});
}));
}
fn dispose(&self, _obj: &StatusArea) {
self.box_.unparent();
}
}
impl WidgetImpl for StatusAreaInner {}
glib::wrapper! {
pub struct StatusArea(ObjectSubclass<StatusAreaInner>)
@extends gtk4::Widget;
}
impl StatusArea {
pub fn new() -> Self {
glib::Object::new(&[]).unwrap()
}
fn inner(&self) -> &StatusAreaInner {
StatusAreaInner::from_instance(self)
}
async fn item_registered(&self, name: &str) {
match StatusMenu::new(&name).await {
Ok(item) => {
self.inner().box_.append(&item);
self.item_unregistered(name);
self.inner()
.icons
.borrow_mut()
.insert(name.to_owned(), item);
}
Err(err) => eprintln!("Failed to connect to '{}': {}", name, err),
}
}
fn item_unregistered(&self, name: &str) {
if let Some(icon) = self.inner().icons.borrow_mut().remove(name) {
self.inner().box_.remove(&icon);
}
}
}
#[dbus_proxy(
interface = "org.kde.StatusNotifierWatcher",
default_service = "org.kde.StatusNotifierWatcher",
default_path = "/StatusNotifierWatcher"
)]
trait StatusNotifierWatcher {
fn register_status_notifier_host(&self, name: &str) -> zbus::Result<()>;
#[dbus_proxy(property)]
fn registered_status_notifier_items(&self) -> zbus::Result<Vec<String>>;
#[dbus_proxy(signal)]
fn status_notifier_item_registered(&self, name: &str) -> zbus::Result<()>;
#[dbus_proxy(signal)]
fn status_notifier_item_unregistered(&self, name: &str) -> zbus::Result<()>;
}

View file

@ -0,0 +1,354 @@
use cascade::cascade;
use futures::StreamExt;
use gtk4::{
gdk_pixbuf,
glib::{self, clone},
prelude::*,
subclass::prelude::*,
};
use std::{cell::RefCell, collections::HashMap, io};
use zbus::dbus_proxy;
use zvariant::OwnedValue;
use crate::deref_cell::DerefCell;
use crate::popover_container::PopoverContainer;
struct Menu {
box_: gtk4::Box,
children: Vec<i32>,
}
#[derive(Default)]
pub struct StatusMenuInner {
button: DerefCell<gtk4::ToggleButton>,
popover_container: DerefCell<PopoverContainer>,
vbox: DerefCell<gtk4::Box>,
item: DerefCell<StatusNotifierItemProxy<'static>>,
dbus_menu: DerefCell<DBusMenuProxy<'static>>,
menus: RefCell<HashMap<i32, Menu>>,
}
#[glib::object_subclass]
impl ObjectSubclass for StatusMenuInner {
const NAME: &'static str = "S76StatusMenu";
type ParentType = gtk4::Widget;
type Type = StatusMenu;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk4::BinLayout>();
}
}
impl ObjectImpl for StatusMenuInner {
fn constructed(&self, obj: &StatusMenu) {
let vbox = cascade! {
gtk4::Box::new(gtk4::Orientation::Vertical, 0);
};
let button = cascade! {
gtk4::ToggleButton::new();
..set_has_frame(false);
};
let popover_container = cascade! {
PopoverContainer::new(&button);
..set_parent(obj);
..popover().set_child(Some(&vbox));
..popover().bind_property("visible", &button, "active").flags(glib::BindingFlags::BIDIRECTIONAL).build();
};
self.button.set(button);
self.popover_container.set(popover_container);
self.vbox.set(vbox);
}
fn dispose(&self, _obj: &StatusMenu) {
self.button.unparent();
}
}
impl WidgetImpl for StatusMenuInner {}
glib::wrapper! {
pub struct StatusMenu(ObjectSubclass<StatusMenuInner>)
@extends gtk4::Widget;
}
impl StatusMenu {
pub async fn new(name: &str) -> zbus::Result<Self> {
let (dest, path) = if let Some(idx) = name.find('/') {
(&name[..idx], &name[idx..])
} else {
(name, "/StatusNotifierItem")
};
let connection = zbus::Connection::session().await?;
let item = StatusNotifierItemProxy::builder(&connection)
.destination(dest.to_string())?
.path(path.to_string())?
.build()
.await?;
let obj = glib::Object::new::<Self>(&[]).unwrap();
let icon_name = item.icon_name().await?;
obj.inner().button.set_icon_name(&icon_name);
let menu = item.menu().await?;
let menu = DBusMenuProxy::builder(&connection)
.destination(dest.to_string())?
.path(menu)?
.build()
.await?;
let layout = menu.get_layout(0, -1, &[]).await?.1;
let mut layout_updated_stream = menu.receive_layout_updated().await?;
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
while let Some(evt) = layout_updated_stream.next().await {
let args = match evt.args() {
Ok(args) => args,
Err(_) => { continue; },
};
obj.layout_updated(args.revision, args.parent);
}
}));
obj.inner().item.set(item);
obj.inner().dbus_menu.set(menu);
println!("{:#?}", layout);
obj.populate_menu(&obj.inner().vbox, &layout);
Ok(obj)
}
fn inner(&self) -> &StatusMenuInner {
StatusMenuInner::from_instance(self)
}
fn layout_updated(&self, _revision: u32, parent: i32) {
let mut menus = self.inner().menus.borrow_mut();
if let Some(Menu { box_, children }) = menus.remove(&parent) {
let mut next_child = box_.first_child();
while let Some(child) = next_child {
next_child = child.next_sibling();
box_.remove(&child);
}
fn remove_child_menus(menus: &mut HashMap<i32, Menu>, children: Vec<i32>) {
for i in children {
if let Some(menu) = menus.remove(&i) {
remove_child_menus(menus, menu.children);
}
}
}
remove_child_menus(&mut menus, children);
glib::MainContext::default().spawn_local(clone!(@weak self as self_ => async move {
match self_.inner().dbus_menu.get_layout(parent, -1, &[]).await {
Ok((_, layout)) => self_.populate_menu(&box_, &layout),
Err(err) => eprintln!("Failed to call 'GetLayout': {}", err),
}
}));
}
}
fn populate_menu(&self, box_: &gtk4::Box, layout: &Layout) {
let mut children = Vec::new();
for i in layout.children() {
children.push(i.id());
if i.type_().as_deref() == Some("separator") {
let separator = cascade! {
gtk4::Separator::new(gtk4::Orientation::Horizontal);
..set_visible(i.visible());
};
box_.append(&separator);
} else if let Some(label) = i.label() {
let mut label = label.to_string();
if let Some(toggle_state) = i.toggle_state() {
if toggle_state != 0 {
label = format!("{}", label);
}
}
let label_widget = cascade! {
gtk4::Label::new(Some(&label));
..set_halign(gtk4::Align::Start);
..set_hexpand(true);
..set_use_underline(true);
};
let hbox = cascade! {
gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
..append(&label_widget);
};
if let Some(icon_data) = i.icon_data() {
let icon_data = io::Cursor::new(icon_data.to_vec());
let pixbuf = gdk_pixbuf::Pixbuf::from_read(icon_data).unwrap(); // XXX unwrap
let image = cascade! {
gtk4::Image::from_pixbuf(Some(&pixbuf));
..set_halign(gtk4::Align::End);
};
hbox.append(&image);
}
let id = i.id();
let close_on_click = i.children_display().as_deref() != Some("submenu");
let button = cascade! {
gtk4::Button::new();
..set_child(Some(&hbox));
..style_context().add_class("flat");
..set_visible(i.visible());
..set_sensitive(i.enabled());
..connect_clicked(clone!(@weak self as self_ => move |_| {
// XXX data, timestamp
if close_on_click {
self_.inner().popover_container.popdown();
}
glib::MainContext::default().spawn_local(clone!(@strong self_ => async move {
let _ = self_.inner().dbus_menu.event(id, "clicked", &0.into(), 0).await;
}));
}));
};
box_.append(&button);
if i.children_display().as_deref() == Some("submenu") {
let vbox = cascade! {
gtk4::Box::new(gtk4::Orientation::Vertical, 0);
};
let revealer = cascade! {
gtk4::Revealer::new();
..set_child(Some(&vbox));
};
self.populate_menu(&vbox, &i);
box_.append(&revealer);
button.connect_clicked(move |_| {
revealer.set_reveal_child(!revealer.reveals_child());
});
}
}
}
self.inner().menus.borrow_mut().insert(
layout.id(),
Menu {
box_: box_.clone(),
children,
},
);
}
}
#[dbus_proxy(interface = "org.kde.StatusNotifierItem")]
trait StatusNotifierItem {
#[dbus_proxy(property)]
fn icon_name(&self) -> zbus::Result<String>;
#[dbus_proxy(property)]
fn menu(&self) -> zbus::Result<zvariant::OwnedObjectPath>;
}
#[derive(Debug)]
pub struct Layout(i32, LayoutProps, Vec<Layout>);
impl<'a> serde::Deserialize<'a> for Layout {
fn deserialize<D: serde::Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
let (id, props, children) =
<(i32, LayoutProps, Vec<(zvariant::Signature<'_>, Self)>)>::deserialize(deserializer)?;
Ok(Self(id, props, children.into_iter().map(|x| x.1).collect()))
}
}
impl zvariant::Type for Layout {
fn signature() -> zvariant::Signature<'static> {
zvariant::Signature::try_from("(ia{sv}av)").unwrap()
}
}
#[derive(Debug, zvariant::DeserializeDict, zvariant::Type)]
pub struct LayoutProps {
#[zvariant(rename = "accessible-desc")]
accessible_desc: Option<String>,
#[zvariant(rename = "children-display")]
children_display: Option<String>,
label: Option<String>,
enabled: Option<bool>,
visible: Option<bool>,
#[zvariant(rename = "type")]
type_: Option<String>,
#[zvariant(rename = "toggle-type")]
toggle_type: Option<String>,
#[zvariant(rename = "toggle-state")]
toggle_state: Option<i32>,
#[zvariant(rename = "icon-data")]
icon_data: Option<Vec<u8>>,
}
#[allow(dead_code)]
impl Layout {
fn id(&self) -> i32 {
self.0
}
fn children(&self) -> &[Self] {
&self.2
}
fn accessible_desc(&self) -> Option<&str> {
self.1.accessible_desc.as_deref()
}
fn children_display(&self) -> Option<&str> {
self.1.children_display.as_deref()
}
fn label(&self) -> Option<&str> {
self.1.label.as_deref()
}
fn enabled(&self) -> bool {
self.1.enabled.unwrap_or(true)
}
fn visible(&self) -> bool {
self.1.visible.unwrap_or(true)
}
fn type_(&self) -> Option<&str> {
self.1.type_.as_deref()
}
fn toggle_type(&self) -> Option<&str> {
self.1.toggle_type.as_deref()
}
fn toggle_state(&self) -> Option<i32> {
self.1.toggle_state
}
fn icon_data(&self) -> Option<&[u8]> {
self.1.icon_data.as_deref()
}
}
#[dbus_proxy(interface = "com.canonical.dbusmenu")]
trait DBusMenu {
fn get_layout(
&self,
parent_id: i32,
recursion_depth: i32,
property_names: &[&str],
) -> zbus::Result<(u32, Layout)>;
fn event(&self, id: i32, event_id: &str, data: &OwnedValue, timestamp: u32)
-> zbus::Result<()>;
#[dbus_proxy(signal)]
fn layout_updated(&self, revision: u32, parent: i32) -> zbus::Result<()>;
}

View file

@ -0,0 +1,70 @@
#![allow(non_snake_case)]
use std::sync::{Arc, Mutex};
use zbus::{dbus_interface, MessageHeader, Result, SignalContext};
use crate::dbus_service;
#[derive(Default)]
struct StatusNotifierWatcher {
items: Arc<Mutex<Vec<String>>>,
}
#[dbus_interface(name = "org.kde.StatusNotifierWatcher")]
impl StatusNotifierWatcher {
async fn RegisterStatusNotifierItem(
&self,
service: &str,
#[zbus(header)] hdr: MessageHeader<'_>,
#[zbus(signal_context)] ctxt: SignalContext<'_>,
) {
let service = format!("{}{}", hdr.sender().unwrap().unwrap(), service);
Self::StatusNotifierItemRegistered(&ctxt, &service)
.await
.unwrap();
// XXX emit unreigstered
self.items.lock().unwrap().push(service);
}
fn RegisterStatusNotifierHost(&self, _service: &str) {
// XXX emit registed/unregistered
}
#[dbus_interface(property)]
fn RegisteredStatusNotifierItems(&self) -> Vec<String> {
self.items.lock().unwrap().clone()
}
#[dbus_interface(property)]
fn IsStatusNotifierHostRegistered(&self) -> bool {
true
}
#[dbus_interface(property)]
fn ProtocolVersion(&self) -> i32 {
0
}
#[dbus_interface(signal)]
async fn StatusNotifierItemRegistered(ctxt: &SignalContext<'_>, service: &str) -> Result<()>;
#[dbus_interface(signal)]
async fn StatusNotifierItemUnregistered(ctxt: &SignalContext<'_>, service: &str) -> Result<()>;
#[dbus_interface(signal)]
async fn StatusNotifierHostRegistered(ctxt: &SignalContext<'_>) -> Result<()>;
#[dbus_interface(signal)]
async fn StatusNotifierHostUnregistered(ctxt: &SignalContext<'_>) -> Result<()>;
}
pub async fn start() {
if let Err(err) = dbus_service::create("org.kde.StatusNotifierWatcher", |builder| {
builder.serve_at("/StatusNotifierWatcher", StatusNotifierWatcher::default())
})
.await
{
eprintln!("Failed to start `StatusNotifierWatcher` service: {}", err);
}
}

View file

@ -0,0 +1,136 @@
use cascade::cascade;
use gtk4::{
glib::{self, clone},
pango,
prelude::*,
subclass::prelude::*,
};
use crate::application::PanelApp;
use crate::deref_cell::DerefCell;
use crate::mpris::MprisControls;
use crate::notification_list::NotificationList;
use crate::notification_popover::NotificationPopover;
use crate::popover_container::PopoverContainer;
#[derive(Default)]
pub struct TimeButtonInner {
calendar: DerefCell<gtk4::Calendar>,
button: DerefCell<gtk4::ToggleButton>,
label: DerefCell<gtk4::Label>,
notification_popover: DerefCell<NotificationPopover>,
left_box: DerefCell<gtk4::Box>,
}
#[glib::object_subclass]
impl ObjectSubclass for TimeButtonInner {
const NAME: &'static str = "S76TimeButton";
type ParentType = gtk4::Widget;
type Type = TimeButton;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk4::BinLayout>();
}
}
impl ObjectImpl for TimeButtonInner {
fn constructed(&self, obj: &TimeButton) {
let calendar = cascade! {
gtk4::Calendar::new();
};
let label = cascade! {
gtk4::Label::new(None);
..set_attributes(Some(&cascade! {
pango::AttrList::new();
..insert(pango::AttrInt::new_weight(pango::Weight::Bold));
}));
};
let button = cascade! {
gtk4::ToggleButton::new();
..set_has_frame(false);
..set_child(Some(&label));
};
let left_box = cascade! {
gtk4::Box::new(gtk4::Orientation::Vertical, 0);
..append(&MprisControls::new());
};
cascade! {
PopoverContainer::new(&button);
..set_parent(obj);
..popover().set_child(Some(&cascade! {
gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
..append(&left_box);
..append(&calendar);
}));
..popover().connect_show(clone!(@strong obj => move |_| obj.opening()));
..popover().bind_property("visible", &button, "active").flags(glib::BindingFlags::BIDIRECTIONAL).build();
};
self.calendar.set(calendar);
self.button.set(button);
self.label.set(label);
self.left_box.set(left_box);
// TODO: better way to do this?
glib::timeout_add_seconds_local(
1,
clone!(@weak obj => @default-return glib::Continue(false), move || {
obj.update_time();
glib::Continue(true)
}),
);
obj.update_time();
}
fn dispose(&self, _obj: &TimeButton) {
self.button.unparent();
self.notification_popover.unparent();
}
}
impl WidgetImpl for TimeButtonInner {}
glib::wrapper! {
pub struct TimeButton(ObjectSubclass<TimeButtonInner>)
@extends gtk4::Widget;
}
impl TimeButton {
pub fn new(app: &PanelApp) -> Self {
let obj = glib::Object::new::<Self>(&[]).unwrap();
let notification_list = NotificationList::new(app.notifications());
obj.inner().left_box.prepend(&notification_list);
let notification_popover = cascade! {
NotificationPopover::new(app.notifications());
..set_parent(&obj);
};
obj.inner().notification_popover.set(notification_popover);
obj
}
fn inner(&self) -> &TimeButtonInner {
TimeButtonInner::from_instance(self)
}
fn opening(&self) {
let date = glib::DateTime::now(&glib::TimeZone::local()).unwrap();
self.inner().calendar.clear_marks();
self.inner().calendar.select_day(&date);
}
fn update_time(&self) {
// TODO: Locale-based formatting?
let time = chrono::Local::now();
self.inner()
.label
.set_label(&time.format("%b %-d %-I:%M %p").to_string());
// time.format("%B %-d %Y")
}
}

238
old-panel/src/window.rs Normal file
View file

@ -0,0 +1,238 @@
use cascade::cascade;
use glib::clone;
use gtk4::{gdk, glib, pango, prelude::*, subclass::prelude::*};
use libcosmic::x;
use std::cell::Cell;
use crate::application::PanelApp;
use crate::deref_cell::DerefCell;
use crate::status_area::StatusArea;
use crate::time_button::TimeButton;
const BOTTOM: bool = false;
pub fn create(app: &PanelApp, monitor: gdk::Monitor) {
#[cfg(feature = "layer-shell")]
if let Some(wayland_monitor) = monitor.downcast_ref() {
wayland_create(app, wayland_monitor);
return;
}
cascade! {
PanelWindow::new(app, monitor);
..show();
};
}
#[cfg(feature = "layer-shell")]
fn wayland_create(app: &PanelApp, monitor: &gdk4_wayland::WaylandMonitor) {
use libcosmic::wayland::{Anchor, Layer, LayerShellWindow};
let window = LayerShellWindow::new(Some(monitor), Layer::Top, "");
window.connect_realize(|window| {
let surface = window.surface();
surface.connect_layout(clone!(@weak window => move |_surface, _width, height| {
window.set_exclusive_zone(height);
}));
});
window.set_child(Some(&window_box(app)));
window.set_size_request(monitor.geometry().width(), 0);
window.set_anchor(if BOTTOM { Anchor::Bottom } else { Anchor::Top });
window.show();
// XXX
unsafe { window.set_data("cosmic-app-hold", app.hold()) };
}
// XXX better handle duplication
#[cfg(feature = "layer-shell")]
fn window_box(app: &PanelApp) -> gtk4::Widget {
let widget = cascade! {
gtk4::CenterBox::new();
..set_start_widget(Some(&cascade! {
gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
..append(&button("Workspaces"));
..append(&button("Applications"));
}));
..set_center_widget(Some(&TimeButton::new(app)));
..set_end_widget(Some(&StatusArea::new()));
};
widget.upcast()
}
fn button(text: &str) -> gtk4::Button {
let label = cascade! {
gtk4::Label::new(Some(text));
..set_attributes(Some(&cascade! {
pango::AttrList::new();
..insert(pango::AttrInt::new_weight(pango::Weight::Bold));
}));
};
cascade! {
gtk4::Button::new();
..set_has_frame(false);
..set_child(Some(&label));
}
}
#[derive(Default)]
pub struct PanelWindowInner {
size: Cell<Option<(i32, i32)>>,
monitor: DerefCell<gdk::Monitor>,
box_: DerefCell<gtk4::CenterBox>,
}
#[glib::object_subclass]
impl ObjectSubclass for PanelWindowInner {
const NAME: &'static str = "S76PanelWindow";
type ParentType = gtk4::ApplicationWindow;
type Type = PanelWindow;
}
impl ObjectImpl for PanelWindowInner {
fn constructed(&self, obj: &PanelWindow) {
let box_ = cascade! {
gtk4::CenterBox::new();
..set_start_widget(Some(&cascade! {
gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
..append(&button("Workspaces"));
..append(&button("Applications"));
}));
..set_end_widget(Some(&StatusArea::new()));
};
cascade! {
obj;
..set_decorated(false);
..set_child(Some(&box_));
};
self.box_.set(box_);
}
}
impl WidgetImpl for PanelWindowInner {
fn realize(&self, obj: &PanelWindow) {
self.parent_realize(obj);
let surface = obj.surface();
surface.connect_layout(clone!(@weak obj => move |_surface, width, height| {
let size = Some((width, height));
if obj.inner().size.replace(size) != size {
obj.monitor_geometry_changed();
}
}));
}
fn show(&self, obj: &PanelWindow) {
self.parent_show(obj);
if let Some((display, surface)) = x::get_window_x11(obj) {
unsafe {
surface.set_skip_pager_hint(true);
surface.set_skip_taskbar_hint(true);
x::wm_state_add(&display, &surface, "_NET_WM_STATE_ABOVE");
x::wm_state_add(&display, &surface, "_NET_WM_STATE_STICKY");
x::change_property(
&display,
&surface,
"_NET_WM_ALLOWED_ACTIONS",
x::PropMode::Replace,
&[
x::Atom::new(&display, "_NET_WM_ACTION_CHANGE_DESKTOP").unwrap(),
x::Atom::new(&display, "_NET_WM_ACTION_ABOVE").unwrap(),
x::Atom::new(&display, "_NET_WM_ACTION_BELOW").unwrap(),
],
);
x::change_property(
&display,
&surface,
"_NET_WM_WINDOW_TYPE",
x::PropMode::Replace,
&[x::Atom::new(&display, "_NET_WM_WINDOW_TYPE_DOCK").unwrap()],
);
}
}
self.monitor
.connect_geometry_notify(clone!(@strong obj => move |_| {
obj.monitor_geometry_changed();
}));
obj.monitor_geometry_changed();
}
}
impl WindowImpl for PanelWindowInner {}
impl ApplicationWindowImpl for PanelWindowInner {}
glib::wrapper! {
pub struct PanelWindow(ObjectSubclass<PanelWindowInner>)
@extends gtk4::ApplicationWindow, gtk4::Window, gtk4::Widget,
@implements gtk4::Accessible, gtk4::Buildable, gtk4::ConstraintTarget, gtk4::Native, gtk4::Root, gtk4::ShortcutManager;
}
impl PanelWindow {
pub fn new(app: &PanelApp, monitor: gdk::Monitor) -> Self {
let obj = glib::Object::new::<Self>(&[]).unwrap();
monitor.connect_invalidate(clone!(@weak obj => move |_| obj.close()));
obj.set_size_request(monitor.geometry().width(), 0);
obj.inner().monitor.set(monitor);
obj.inner()
.box_
.set_center_widget(Some(&TimeButton::new(app)));
app.add_window(&obj);
obj
}
fn inner(&self) -> &PanelWindowInner {
PanelWindowInner::from_instance(self)
}
fn monitor_geometry_changed(&self) {
let geometry = self.inner().monitor.geometry();
self.set_size_request(geometry.width(), 0);
let height = if let Some((_width, height)) = self.inner().size.get() {
height as x::c_ulong
} else {
return;
};
if let Some((display, surface)) = x::get_window_x11(self) {
let start_x = geometry.x() as x::c_ulong;
let end_x = start_x + geometry.width() as x::c_ulong - 1;
unsafe {
let y = if BOTTOM {
geometry.height() as x::c_int - height as x::c_int
} else {
0
};
x::set_position(&display, &surface, start_x as _, y);
let strut = if BOTTOM {
[0, 0, 0, height, 0, 0, 0, 0, 0, 0, start_x, end_x]
} else {
[0, 0, height, 0, 0, 0, 0, 0, start_x, end_x, 0, 0]
};
x::change_property(
&display,
&surface,
"_NET_WM_STRUT_PARTIAL",
x::PropMode::Replace,
&strut,
);
}
}
}
}