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:
parent
8c196a7ed3
commit
185dd48a6f
20 changed files with 30 additions and 31 deletions
92
old-panel/src/application.rs
Normal file
92
old-panel/src/application.rs
Normal 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
16
old-panel/src/config.rs
Normal 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()?))
|
||||
}
|
||||
}
|
||||
53
old-panel/src/dbus_service.rs
Normal file
53
old-panel/src/dbus_service.rs
Normal 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)
|
||||
}
|
||||
31
old-panel/src/deref_cell.rs
Normal file
31
old-panel/src/deref_cell.rs
Normal 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
25
old-panel/src/main.rs
Normal 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
134
old-panel/src/mpris.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
264
old-panel/src/mpris_player.rs
Normal file
264
old-panel/src/mpris_player.rs
Normal 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>;
|
||||
}
|
||||
113
old-panel/src/notification_list.rs
Normal file
113
old-panel/src/notification_list.rs
Normal 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(¬ification);
|
||||
})),
|
||||
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(¬ification_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: >k4::ListBoxRow) -> Option<NotificationId> {
|
||||
let rows = self.inner().rows.borrow();
|
||||
Some(*rows.iter().find(|(_, i)| i == &row)?.0)
|
||||
}
|
||||
}
|
||||
136
old-panel/src/notification_popover.rs
Normal file
136
old-panel/src/notification_popover.rs
Normal 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(¬ification_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(¬ification);
|
||||
})),
|
||||
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);
|
||||
}
|
||||
}
|
||||
117
old-panel/src/notification_widget.rs
Normal file
117
old-panel/src/notification_widget.rs
Normal 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(¬ification.summary);
|
||||
self.inner().body_label.set_label(¬ification.body);
|
||||
self.inner().id.set(Some(notification.id));
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Option<NotificationId> {
|
||||
self.inner().id.get()
|
||||
}
|
||||
}
|
||||
416
old-panel/src/notifications.rs
Normal file
416
old-panel/src/notifications.rs
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
||||
89
old-panel/src/popover_container.rs
Normal file
89
old-panel/src/popover_container.rs
Normal 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(>k4::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) -> >k4::Popover {
|
||||
&self.inner().popover
|
||||
}
|
||||
|
||||
pub fn popup(&self) {
|
||||
self.popover().popup();
|
||||
}
|
||||
|
||||
pub fn popdown(&self) {
|
||||
self.popover().popdown();
|
||||
}
|
||||
}
|
||||
145
old-panel/src/status_area.rs
Normal file
145
old-panel/src/status_area.rs
Normal 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<()>;
|
||||
}
|
||||
354
old-panel/src/status_menu.rs
Normal file
354
old-panel/src/status_menu.rs
Normal 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_: >k4::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<()>;
|
||||
}
|
||||
70
old-panel/src/status_notifier_watcher.rs
Normal file
70
old-panel/src/status_notifier_watcher.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
136
old-panel/src/time_button.rs
Normal file
136
old-panel/src/time_button.rs
Normal 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(¬ification_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
238
old-panel/src/window.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue