Move Cosmic Applets into new Dir & remove old applets
|
|
@ -1,17 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "cosmic-applet-status-area"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
license = "GPL-3.0-or-later"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
cascade = "1"
|
|
||||||
futures = "0.3"
|
|
||||||
gtk4 = { git = "https://github.com/gtk-rs/gtk4-rs" }
|
|
||||||
adw = { git = "https://gitlab.gnome.org/World/Rust/libadwaita-rs", package = "libadwaita"}
|
|
||||||
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false }
|
|
||||||
libcosmic-applet = { path = "../../libcosmic-applet" }
|
|
||||||
once_cell = "1.12"
|
|
||||||
serde = "1"
|
|
||||||
zbus = "3"
|
|
||||||
zvariant = "3"
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
[Desktop Entry]
|
|
||||||
Name=Cosmic Applet Status Area
|
|
||||||
Type=Application
|
|
||||||
Exec=cosmic-applet-status-area
|
|
||||||
Terminal=false
|
|
||||||
Categories=GNOME;GTK;
|
|
||||||
Keywords=Gnome;GTK;
|
|
||||||
# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
|
|
||||||
Icon=com.system76.CosmicAppletStatusArea
|
|
||||||
NoDisplay=true
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128px" height="128px" viewBox="0 0 128 128" version="1.1">
|
|
||||||
<defs>
|
|
||||||
<filter id="alpha" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">
|
|
||||||
<feColorMatrix type="matrix" in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
|
||||||
</filter>
|
|
||||||
<mask id="mask0">
|
|
||||||
<g filter="url(#alpha)">
|
|
||||||
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
|
|
||||||
</g>
|
|
||||||
</mask>
|
|
||||||
<clipPath id="clip1">
|
|
||||||
<rect x="0" y="0" width="192" height="152"/>
|
|
||||||
</clipPath>
|
|
||||||
<g id="surface10632" clip-path="url(#clip1)">
|
|
||||||
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 123.503906 236 C 123.503906 268.863281 96.863281 295.503906 64 295.503906 C 31.136719 295.503906 4.496094 268.863281 4.496094 236 C 4.496094 203.136719 31.136719 176.496094 64 176.496094 C 96.863281 176.496094 123.503906 203.136719 123.503906 236 Z M 123.503906 236 " transform="matrix(1,0,0,1,8,-156)"/>
|
|
||||||
</g>
|
|
||||||
<mask id="mask1">
|
|
||||||
<g filter="url(#alpha)">
|
|
||||||
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
|
|
||||||
</g>
|
|
||||||
</mask>
|
|
||||||
<clipPath id="clip2">
|
|
||||||
<rect x="0" y="0" width="192" height="152"/>
|
|
||||||
</clipPath>
|
|
||||||
<g id="surface10635" clip-path="url(#clip2)">
|
|
||||||
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 29.195312 180.496094 L 98.804688 180.496094 C 103.609375 180.496094 107.503906 184.046875 107.503906 188.425781 L 107.503906 283.574219 C 107.503906 287.953125 103.609375 291.503906 98.804688 291.503906 L 29.195312 291.503906 C 24.390625 291.503906 20.496094 287.953125 20.496094 283.574219 L 20.496094 188.425781 C 20.496094 184.046875 24.390625 180.496094 29.195312 180.496094 Z M 29.195312 180.496094 " transform="matrix(1,0,0,1,8,-156)"/>
|
|
||||||
</g>
|
|
||||||
<mask id="mask2">
|
|
||||||
<g filter="url(#alpha)">
|
|
||||||
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
|
|
||||||
</g>
|
|
||||||
</mask>
|
|
||||||
<clipPath id="clip3">
|
|
||||||
<rect x="0" y="0" width="192" height="152"/>
|
|
||||||
</clipPath>
|
|
||||||
<g id="surface10638" clip-path="url(#clip3)">
|
|
||||||
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 20.417969 184.496094 L 107.582031 184.496094 C 111.957031 184.496094 115.503906 188.042969 115.503906 192.417969 L 115.503906 279.582031 C 115.503906 283.957031 111.957031 287.503906 107.582031 287.503906 L 20.417969 287.503906 C 16.042969 287.503906 12.496094 283.957031 12.496094 279.582031 L 12.496094 192.417969 C 12.496094 188.042969 16.042969 184.496094 20.417969 184.496094 Z M 20.417969 184.496094 " transform="matrix(1,0,0,1,8,-156)"/>
|
|
||||||
</g>
|
|
||||||
<mask id="mask3">
|
|
||||||
<g filter="url(#alpha)">
|
|
||||||
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
|
|
||||||
</g>
|
|
||||||
</mask>
|
|
||||||
<clipPath id="clip4">
|
|
||||||
<rect x="0" y="0" width="192" height="152"/>
|
|
||||||
</clipPath>
|
|
||||||
<g id="surface10641" clip-path="url(#clip4)">
|
|
||||||
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 16.425781 200.496094 L 111.574219 200.496094 C 115.953125 200.496094 119.503906 204.390625 119.503906 209.195312 L 119.503906 278.804688 C 119.503906 283.609375 115.953125 287.503906 111.574219 287.503906 L 16.425781 287.503906 C 12.046875 287.503906 8.496094 283.609375 8.496094 278.804688 L 8.496094 209.195312 C 8.496094 204.390625 12.046875 200.496094 16.425781 200.496094 Z M 16.425781 200.496094 " transform="matrix(1,0,0,1,8,-156)"/>
|
|
||||||
</g>
|
|
||||||
</defs>
|
|
||||||
<g id="surface10578">
|
|
||||||
<rect x="0" y="0" width="128" height="128" style="fill:rgb(94.117647%,94.117647%,94.117647%);fill-opacity:1;stroke:none;"/>
|
|
||||||
<use xlink:href="#surface10632" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask0)"/>
|
|
||||||
<use xlink:href="#surface10635" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask1)"/>
|
|
||||||
<use xlink:href="#surface10638" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask2)"/>
|
|
||||||
<use xlink:href="#surface10641" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask3)"/>
|
|
||||||
<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(38.431373%,62.7451%,91.764706%);stroke-opacity:1;stroke-miterlimit:4;" d="M 0 289 L 128 289 " transform="matrix(1,0,0,1,0,-172)"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.5 KiB |
|
|
@ -1,31 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
use cascade::cascade;
|
|
||||||
use gtk4::{glib, prelude::*};
|
|
||||||
|
|
||||||
mod deref_cell;
|
|
||||||
mod status_area;
|
|
||||||
mod status_menu;
|
|
||||||
mod status_notifier_watcher;
|
|
||||||
|
|
||||||
use status_area::StatusArea;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let _monitors = libcosmic::init();
|
|
||||||
|
|
||||||
// XXX Implement DBus service somewhere other than applet?
|
|
||||||
glib::MainContext::default().spawn_local(status_notifier_watcher::start());
|
|
||||||
|
|
||||||
let status_area = StatusArea::new();
|
|
||||||
cascade! {
|
|
||||||
libcosmic_applet::AppletWindow::new();
|
|
||||||
..set_child(Some(&status_area));
|
|
||||||
..show();
|
|
||||||
};
|
|
||||||
|
|
||||||
let main_loop = glib::MainLoop::new(None, false);
|
|
||||||
main_loop.run();
|
|
||||||
}
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
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<()>;
|
|
||||||
}
|
|
||||||
|
|
@ -1,345 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
||||||
struct Menu {
|
|
||||||
box_: gtk4::Box,
|
|
||||||
children: Vec<i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct StatusMenuInner {
|
|
||||||
menu_button: DerefCell<libcosmic_applet::AppletButton>,
|
|
||||||
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 menu_button = cascade! {
|
|
||||||
libcosmic_applet::AppletButton::new();
|
|
||||||
..set_parent(obj);
|
|
||||||
..set_popover_child(Some(&vbox));
|
|
||||||
};
|
|
||||||
|
|
||||||
self.menu_button.set(menu_button);
|
|
||||||
self.vbox.set(vbox);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dispose(&self, _obj: &StatusMenu) {
|
|
||||||
self.menu_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().menu_button.set_button_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().menu_button.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<()>;
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
#![allow(non_snake_case)]
|
|
||||||
|
|
||||||
use futures::prelude::*;
|
|
||||||
use gtk4::glib::{self, clone};
|
|
||||||
use std::cell::Cell;
|
|
||||||
use zbus::{
|
|
||||||
dbus_interface,
|
|
||||||
fdo::{DBusProxy, RequestNameFlags, RequestNameReply},
|
|
||||||
names::{BusName, UniqueName, WellKnownName},
|
|
||||||
MessageHeader, Result, SignalContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
const OBJECT_PATH: &str = "/StatusNotifierWatcher";
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct StatusNotifierWatcher {
|
|
||||||
items: Vec<(UniqueName<'static>, String)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[dbus_interface(name = "org.kde.StatusNotifierWatcher")]
|
|
||||||
impl StatusNotifierWatcher {
|
|
||||||
async fn register_status_notifier_item(
|
|
||||||
&mut self,
|
|
||||||
service: &str,
|
|
||||||
#[zbus(header)] hdr: MessageHeader<'_>,
|
|
||||||
#[zbus(signal_context)] ctxt: SignalContext<'_>,
|
|
||||||
) {
|
|
||||||
let sender = hdr.sender().unwrap().unwrap();
|
|
||||||
let service = if service.starts_with('/') {
|
|
||||||
format!("{}{}", sender, service)
|
|
||||||
} else {
|
|
||||||
service.to_string()
|
|
||||||
};
|
|
||||||
Self::status_notifier_item_registered(&ctxt, &service)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
self.items.push((sender.to_owned(), service));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_status_notifier_host(&self, _service: &str) {
|
|
||||||
// XXX emit registed/unregistered
|
|
||||||
}
|
|
||||||
|
|
||||||
#[dbus_interface(property)]
|
|
||||||
fn registered_status_notifier_items(&self) -> Vec<String> {
|
|
||||||
self.items.iter().map(|(_, x)| x.clone()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[dbus_interface(property)]
|
|
||||||
fn is_status_notifier_host_registered(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[dbus_interface(property)]
|
|
||||||
fn protocol_version(&self) -> i32 {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[dbus_interface(signal)]
|
|
||||||
async fn status_notifier_item_registered(ctxt: &SignalContext<'_>, service: &str)
|
|
||||||
-> Result<()>;
|
|
||||||
|
|
||||||
#[dbus_interface(signal)]
|
|
||||||
async fn status_notifier_item_unregistered(
|
|
||||||
ctxt: &SignalContext<'_>,
|
|
||||||
service: &str,
|
|
||||||
) -> Result<()>;
|
|
||||||
|
|
||||||
#[dbus_interface(signal)]
|
|
||||||
async fn status_notifier_host_registered(ctxt: &SignalContext<'_>) -> Result<()>;
|
|
||||||
|
|
||||||
#[dbus_interface(signal)]
|
|
||||||
async fn status_notifier_host_unregistered(ctxt: &SignalContext<'_>) -> Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_service() -> zbus::Result<zbus::Connection> {
|
|
||||||
let well_known_name = WellKnownName::try_from("org.kde.StatusNotifierWatcher")?;
|
|
||||||
|
|
||||||
let connection = zbus::ConnectionBuilder::session()?.build().await?;
|
|
||||||
connection
|
|
||||||
.object_server()
|
|
||||||
.at(OBJECT_PATH, StatusNotifierWatcher::default())
|
|
||||||
.await?;
|
|
||||||
let interface = connection
|
|
||||||
.object_server()
|
|
||||||
.interface::<_, StatusNotifierWatcher>(OBJECT_PATH)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
} else if let BusName::Unique(name) = &args.name {
|
|
||||||
let mut interface = interface.get_mut().await;
|
|
||||||
if let Some(idx) = interface.items.iter().position(|(unique_name, _)| unique_name == name) {
|
|
||||||
let ctxt = zbus::SignalContext::new(&connection, OBJECT_PATH).unwrap();
|
|
||||||
let service = interface.items.remove(idx).1;
|
|
||||||
StatusNotifierWatcher::status_notifier_item_unregistered(&ctxt, &service)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
Ok(connection)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn start() {
|
|
||||||
if let Err(err) = create_service().await {
|
|
||||||
eprintln!("Failed to start `StatusNotifierWatcher` service: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "cosmic-panel-button"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
[dependencies]
|
|
||||||
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", features = ["gtk4"] }
|
|
||||||
cascade = "1.0.0"
|
|
||||||
gtk4 = { git = "https://github.com/gtk-rs/gtk4-rs", features = ["v4_4"] }
|
|
||||||
adw = { git = "https://gitlab.gnome.org/World/Rust/libadwaita-rs", package = "libadwaita"}
|
|
||||||
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false }
|
|
||||||
once_cell = "1.9.0"
|
|
||||||
pretty_env_logger = "0.4"
|
|
||||||
anyhow = "1.0.50"
|
|
||||||
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }
|
|
||||||
i18n-embed-fl = "0.6.4"
|
|
||||||
rust-embed = "6.3.0"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
glib-build-tools = { git = "https://github.com/gtk-rs/gtk-rs-core" }
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fn main() {
|
|
||||||
glib_build_tools::compile_resources(
|
|
||||||
"data/resources",
|
|
||||||
"data/resources/resources.gresource.xml",
|
|
||||||
"compiled.gresource",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
fallback_language = "en"
|
|
||||||
|
|
||||||
[fluent]
|
|
||||||
assets_dir = "i18n"
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
cosmic-panel-button = Cosmic Panel Button
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
cosmic-panel-button = Bouton du panneau Cosmic
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0-only
|
|
||||||
|
|
||||||
use gtk4::{glib, subclass::prelude::*};
|
|
||||||
// Object holding the state
|
|
||||||
#[derive(Default)]
|
|
||||||
|
|
||||||
pub struct CosmicPanelAppButtonWindow {}
|
|
||||||
|
|
||||||
// The central trait for subclassing a GObject
|
|
||||||
#[glib::object_subclass]
|
|
||||||
impl ObjectSubclass for CosmicPanelAppButtonWindow {
|
|
||||||
// `NAME` needs to match `class` attribute of template
|
|
||||||
const NAME: &'static str = "CosmicPanelAppButtonWindow";
|
|
||||||
type Type = super::CosmicPanelAppButtonWindow;
|
|
||||||
type ParentType = gtk4::ApplicationWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trait shared by all GObjects
|
|
||||||
impl ObjectImpl for CosmicPanelAppButtonWindow {}
|
|
||||||
|
|
||||||
// Trait shared by all widgets
|
|
||||||
impl WidgetImpl for CosmicPanelAppButtonWindow {}
|
|
||||||
|
|
||||||
// Trait shared by all windows
|
|
||||||
impl WindowImpl for CosmicPanelAppButtonWindow {}
|
|
||||||
|
|
||||||
// Trait shared by all application
|
|
||||||
impl ApplicationWindowImpl for CosmicPanelAppButtonWindow {}
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0-only
|
|
||||||
|
|
||||||
use crate::fl;
|
|
||||||
use cascade::cascade;
|
|
||||||
use cosmic_panel_config::{CosmicPanelConfig, PanelSize};
|
|
||||||
use gtk4::{
|
|
||||||
gio::{self, DesktopAppInfo, Icon},
|
|
||||||
glib::{self, Object},
|
|
||||||
prelude::*,
|
|
||||||
Align, Application, Button, Orientation,
|
|
||||||
};
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
mod imp;
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct CosmicPanelAppButtonWindow(ObjectSubclass<imp::CosmicPanelAppButtonWindow>)
|
|
||||||
@extends gtk4::ApplicationWindow, gtk4::Window, gtk4::Widget,
|
|
||||||
@implements gio::ActionGroup, gio::ActionMap, gtk4::Accessible, gtk4::Buildable,
|
|
||||||
gtk4::ConstraintTarget, gtk4::Native, gtk4::Root, gtk4::ShortcutManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CosmicPanelAppButtonWindow {
|
|
||||||
pub fn new(app: &Application, app_desktop_file_name: &str) -> Self {
|
|
||||||
let self_: Self = Object::new(&[("application", app)])
|
|
||||||
.expect("Failed to create `CosmicPanelButtonWindow`.");
|
|
||||||
cascade! {
|
|
||||||
&self_;
|
|
||||||
..set_width_request(1);
|
|
||||||
..set_height_request(1);
|
|
||||||
..set_decorated(false);
|
|
||||||
..set_resizable(false);
|
|
||||||
..set_title(Some(app_desktop_file_name));
|
|
||||||
..add_css_class("root_window");
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(apps_desktop_info) =
|
|
||||||
DesktopAppInfo::new(&format!("{}.desktop", app_desktop_file_name))
|
|
||||||
{
|
|
||||||
let app_button = cascade! {
|
|
||||||
Button::new();
|
|
||||||
..add_css_class("apps");
|
|
||||||
};
|
|
||||||
let pixels = std::env::var("COSMIC_PANEL_SIZE")
|
|
||||||
.ok()
|
|
||||||
.and_then(|size| match size.parse::<PanelSize>() {
|
|
||||||
Ok(PanelSize::XL) => Some(64),
|
|
||||||
Ok(PanelSize::L) => Some(48),
|
|
||||||
Ok(PanelSize::M) => Some(36),
|
|
||||||
Ok(PanelSize::S) => Some(24),
|
|
||||||
Ok(PanelSize::XS) => Some(18),
|
|
||||||
Err(_) => Some(36),
|
|
||||||
})
|
|
||||||
.unwrap_or(36);
|
|
||||||
let icon = apps_desktop_info.icon().unwrap_or_else(|| {
|
|
||||||
Icon::for_string("image-missing").expect("Failed to set default icon")
|
|
||||||
});
|
|
||||||
let container = gtk4::Box::new(Orientation::Horizontal, 0);
|
|
||||||
let image = cascade! {
|
|
||||||
gtk4::Image::from_gicon(&icon);
|
|
||||||
..set_hexpand(true);
|
|
||||||
..set_halign(Align::Center);
|
|
||||||
..set_pixel_size(pixels);
|
|
||||||
..set_tooltip_text(Some(&apps_desktop_info.name()));
|
|
||||||
};
|
|
||||||
container.append(&image);
|
|
||||||
|
|
||||||
app_button.set_child(Some(&container));
|
|
||||||
let app_id = app_desktop_file_name.to_string();
|
|
||||||
app_button.connect_clicked(move |_| {
|
|
||||||
let _ = Command::new("xdg-shell-wrapper")
|
|
||||||
.env_remove("WAYLAND_SOCKET")
|
|
||||||
.arg(&app_id)
|
|
||||||
.spawn();
|
|
||||||
});
|
|
||||||
self_.set_child(Some(&app_button));
|
|
||||||
} else {
|
|
||||||
panic!("{} is not installed", app_desktop_file_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
self_
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0-only
|
|
||||||
|
|
||||||
use i18n_embed::{
|
|
||||||
fluent::{fluent_language_loader, FluentLanguageLoader},
|
|
||||||
DefaultLocalizer, LanguageLoader, Localizer,
|
|
||||||
};
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use rust_embed::RustEmbed;
|
|
||||||
|
|
||||||
#[derive(RustEmbed)]
|
|
||||||
#[folder = "i18n/"]
|
|
||||||
struct Localizations;
|
|
||||||
|
|
||||||
pub static LANGUAGE_LOADER: Lazy<FluentLanguageLoader> = Lazy::new(|| {
|
|
||||||
let loader: FluentLanguageLoader = fluent_language_loader!();
|
|
||||||
|
|
||||||
loader
|
|
||||||
.load_fallback_language(&Localizations)
|
|
||||||
.expect("Error while loading fallback language");
|
|
||||||
|
|
||||||
loader
|
|
||||||
});
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! fl {
|
|
||||||
($message_id:literal) => {{
|
|
||||||
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id)
|
|
||||||
}};
|
|
||||||
|
|
||||||
($message_id:literal, $($args:expr),*) => {{
|
|
||||||
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *)
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the `Localizer` to be used for localizing this library.
|
|
||||||
pub fn localizer() -> Box<dyn Localizer> {
|
|
||||||
Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0-only
|
|
||||||
|
|
||||||
use apps_window::CosmicPanelAppButtonWindow;
|
|
||||||
use gtk4::gdk::Display;
|
|
||||||
use gtk4::{
|
|
||||||
gio::{self, ApplicationFlags},
|
|
||||||
glib,
|
|
||||||
prelude::*,
|
|
||||||
CssProvider, StyleContext,
|
|
||||||
};
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
|
|
||||||
mod apps_window;
|
|
||||||
mod localize;
|
|
||||||
mod utils;
|
|
||||||
|
|
||||||
static ID: OnceCell<String> = OnceCell::new();
|
|
||||||
|
|
||||||
pub fn localize() {
|
|
||||||
let localizer = crate::localize::localizer();
|
|
||||||
let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();
|
|
||||||
|
|
||||||
if let Err(error) = localizer.select(&requested_languages) {
|
|
||||||
eprintln!("Error while loading language for App List {}", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_css() {
|
|
||||||
let provider = CssProvider::new();
|
|
||||||
provider.load_from_data(include_bytes!("style.css"));
|
|
||||||
|
|
||||||
StyleContext::add_provider_for_display(
|
|
||||||
&Display::default().unwrap(),
|
|
||||||
&provider,
|
|
||||||
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let _monitors = libcosmic::init();
|
|
||||||
|
|
||||||
// Initialize logger
|
|
||||||
pretty_env_logger::init();
|
|
||||||
glib::set_application_name("Cosmic Panel App Button");
|
|
||||||
|
|
||||||
localize();
|
|
||||||
gio::resources_register_include!("compiled.gresource").unwrap();
|
|
||||||
let app = gtk4::Application::new(None, ApplicationFlags::default());
|
|
||||||
app.add_main_option(
|
|
||||||
"id",
|
|
||||||
glib::Char::from(b'i'),
|
|
||||||
glib::OptionFlags::NONE,
|
|
||||||
glib::OptionArg::String,
|
|
||||||
"id of the launched application",
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
app.connect_handle_local_options(|_app, args| {
|
|
||||||
if let Ok(Some(id)) = args.lookup::<String>("id") {
|
|
||||||
ID.set(id).unwrap();
|
|
||||||
-1
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.connect_activate(|app| {
|
|
||||||
load_css();
|
|
||||||
let id = ID.get().unwrap().clone();
|
|
||||||
let window = CosmicPanelAppButtonWindow::new(app, &id);
|
|
||||||
|
|
||||||
window.show();
|
|
||||||
});
|
|
||||||
app.run();
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
button {
|
|
||||||
border-radius: 12px;
|
|
||||||
transition: 100ms;
|
|
||||||
padding: 4px;
|
|
||||||
border-color: transparent;
|
|
||||||
background: transparent;
|
|
||||||
outline-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
border-radius: 12px;
|
|
||||||
transition: 100ms;
|
|
||||||
padding: 4px;
|
|
||||||
border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
outline-color: rgba(255, 255, 255, 0.1);
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
image {
|
|
||||||
border-color: transparent;
|
|
||||||
background: transparent;
|
|
||||||
outline-color: transparent;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.root_window {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0-only
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use gtk4::glib;
|
|
||||||
use std::future::Future;
|
|
||||||
|
|
||||||
pub fn data_path(id: &str) -> PathBuf {
|
|
||||||
let mut path = glib::user_data_dir();
|
|
||||||
path.push(id);
|
|
||||||
std::fs::create_dir_all(&path).expect("Could not create directory.");
|
|
||||||
path.push("data.json");
|
|
||||||
path
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn thread_context() -> glib::MainContext {
|
|
||||||
glib::MainContext::thread_default().unwrap_or_else(|| glib::MainContext::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn block_on<F>(future: F) -> F::Output
|
|
||||||
where
|
|
||||||
F: Future,
|
|
||||||
{
|
|
||||||
let ctx = thread_context();
|
|
||||||
ctx.with_thread_default(|| ctx.block_on(future)).unwrap()
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<gresources>
|
|
||||||
<gresource prefix="/com/system76/CosmicPanelAppButton/">
|
|
||||||
<!-- see https://gtk-rs.org/gtk4-rs/git/docs/gtk4/struct.Application.html#automatic-resources -->
|
|
||||||
</gresource>
|
|
||||||
</gresources>
|
|
||||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |