wip: use toplevel protocols
This commit is contained in:
parent
647a402206
commit
560ebc0bf5
20 changed files with 1073 additions and 623 deletions
|
|
@ -1,9 +1,10 @@
|
|||
use std::env;
|
||||
|
||||
use crate::TX;
|
||||
// SPDX-License-Identifier: MPL-2.0-only
|
||||
use crate::dock_list::DockList;
|
||||
use crate::dock_list::DockListType;
|
||||
use crate::utils::Event;
|
||||
use crate::utils::AppListEvent;
|
||||
use cascade::cascade;
|
||||
use cosmic_panel_config::{PanelAnchor, CosmicPanelConfig};
|
||||
use gtk4::prelude::*;
|
||||
|
|
@ -21,7 +22,7 @@ glib::wrapper! {
|
|||
}
|
||||
|
||||
impl AppsContainer {
|
||||
pub fn new(tx: Sender<Event>) -> Self {
|
||||
pub fn new() -> Self {
|
||||
let self_: Self = glib::Object::new(&[]).expect("Failed to create AppsContainer");
|
||||
let imp = imp::AppsContainer::from_instance(&self_);
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ impl AppsContainer {
|
|||
|
||||
let config = CosmicPanelConfig::load_from_env().unwrap_or_default();
|
||||
|
||||
let saved_app_list_view = DockList::new(DockListType::Saved, tx.clone(), config.clone());
|
||||
let saved_app_list_view = DockList::new(DockListType::Saved, config.clone());
|
||||
self_.append(&saved_app_list_view);
|
||||
|
||||
// let separator_container = cascade! {
|
||||
|
|
@ -52,7 +53,7 @@ impl AppsContainer {
|
|||
// ..add_css_class("dock_separator");
|
||||
// };
|
||||
// separator_container.append(&separator);
|
||||
let active_app_list_view = DockList::new(DockListType::Active, tx, config.clone());
|
||||
let active_app_list_view = DockList::new(DockListType::Active, config.clone());
|
||||
self_.append(&active_app_list_view);
|
||||
// self_.connect_orientation_notify(glib::clone!(@weak separator => move |c| {
|
||||
// dbg!(c.orientation());
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// SPDX-License-Identifier: MPL-2.0-only
|
||||
|
||||
use crate::{apps_container::AppsContainer, fl, Event};
|
||||
use crate::{apps_container::AppsContainer, fl, AppListEvent};
|
||||
use cascade::cascade;
|
||||
use gtk4::{
|
||||
gio,
|
||||
|
|
@ -20,7 +20,7 @@ glib::wrapper! {
|
|||
}
|
||||
|
||||
impl CosmicAppListWindow {
|
||||
pub fn new(app: >k4::Application, tx: mpsc::Sender<Event>) -> Self {
|
||||
pub fn new(app: >k4::Application) -> Self {
|
||||
let self_: Self =
|
||||
Object::new(&[("application", app)]).expect("Failed to create `CosmicAppListWindow`.");
|
||||
let imp = imp::CosmicAppListWindow::from_instance(&self_);
|
||||
|
|
@ -34,7 +34,7 @@ impl CosmicAppListWindow {
|
|||
..set_title(Some(&fl!("cosmic-app-list")));
|
||||
..add_css_class("transparent");
|
||||
};
|
||||
let app_list = AppsContainer::new(tx);
|
||||
let app_list = AppsContainer::new();
|
||||
self_.set_child(Some(&app_list));
|
||||
imp.inner.set(app_list).unwrap();
|
||||
|
||||
|
|
|
|||
31
applets/cosmic-app-list/src/config.rs
Normal file
31
applets/cosmic-app-list/src/config.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use std::fmt::Debug;
|
||||
use std::fs::File;
|
||||
use anyhow::anyhow;
|
||||
use serde::Deserialize;
|
||||
use xdg::BaseDirectories;
|
||||
use crate::ID;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub enum TopLevelFilter {
|
||||
ActiveWorkspace,
|
||||
ConfiguredOutput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
pub struct AppListConfig {
|
||||
pub filter_top_levels: Option<TopLevelFilter>,
|
||||
}
|
||||
|
||||
impl AppListConfig {
|
||||
/// load config with the provided name
|
||||
pub fn load() -> anyhow::Result<AppListConfig> {
|
||||
let file= match BaseDirectories::new().ok().and_then(|dirs| dirs.find_config_file(format!("{ID}/config.ron"))).and_then(|p| File::open(p).ok()) {
|
||||
Some(path) => path,
|
||||
_ => {
|
||||
anyhow::bail!("Failed to load config");
|
||||
}
|
||||
};
|
||||
|
||||
ron::de::from_reader::<_, AppListConfig>(file).map_err(|err| anyhow!("Failed to parse config file: {}", err))
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ use std::rc::Rc;
|
|||
use tokio::sync::mpsc::Sender;
|
||||
|
||||
use crate::dock_popover::DockPopover;
|
||||
use crate::utils::Event;
|
||||
use crate::utils::AppListEvent;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DockItem {
|
||||
|
|
@ -20,7 +20,7 @@ pub struct DockItem {
|
|||
pub item_box: Rc<RefCell<gtk4::Box>>,
|
||||
pub popover: Rc<RefCell<gtk4::Popover>>,
|
||||
pub popover_menu: Rc<RefCell<Option<DockPopover>>>,
|
||||
pub tx: OnceCell<Sender<Event>>,
|
||||
pub tx: OnceCell<Sender<AppListEvent>>,
|
||||
pub icon_size: Rc<Cell<u32>>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
use crate::dock_object::DockObject;
|
||||
use crate::dock_popover::DockPopover;
|
||||
use crate::utils::BoxedWindowList;
|
||||
use crate::utils::Event;
|
||||
use crate::utils::AppListEvent;
|
||||
use cascade::cascade;
|
||||
use cosmic_panel_config::PanelAnchor;
|
||||
use gtk4::glib;
|
||||
|
|
@ -25,7 +25,7 @@ glib::wrapper! {
|
|||
}
|
||||
|
||||
impl DockItem {
|
||||
pub fn new(tx: Sender<Event>, icon_size: u32) -> Self {
|
||||
pub fn new(icon_size: u32) -> Self {
|
||||
let self_: DockItem = glib::Object::new(&[]).expect("Failed to create DockItem");
|
||||
|
||||
let item_box = Box::new(Orientation::Vertical, 0);
|
||||
|
|
@ -66,7 +66,7 @@ impl DockItem {
|
|||
});
|
||||
|
||||
let popover_menu = cascade! {
|
||||
DockPopover::new(tx.clone());
|
||||
DockPopover::new();
|
||||
..add_css_class("popover_menu");
|
||||
};
|
||||
popover.set_child(Some(&popover_menu));
|
||||
|
|
@ -87,7 +87,6 @@ impl DockItem {
|
|||
imp.item_box.replace(item_box);
|
||||
imp.popover.replace(popover);
|
||||
imp.popover_menu.replace(Some(popover_menu));
|
||||
imp.tx.set(tx).unwrap();
|
||||
self_
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use std::cell::{Cell, RefCell};
|
|||
use std::rc::Rc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::utils::Event;
|
||||
use crate::utils::AppListEvent;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DockList {
|
||||
|
|
@ -24,7 +24,7 @@ pub struct DockList {
|
|||
pub drag_cancel_signal: Rc<RefCell<Option<SignalHandlerId>>>,
|
||||
pub popover_menu_index: Rc<Cell<Option<u32>>>,
|
||||
pub position: Rc<Cell<PanelAnchor>>,
|
||||
pub tx: OnceCell<mpsc::Sender<Event>>,
|
||||
pub tx: OnceCell<mpsc::Sender<AppListEvent>>,
|
||||
pub config: OnceCell<CosmicPanelConfig>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
// SPDX-License-Identifier: MPL-2.0-only
|
||||
|
||||
use crate::{TX, WAYLAND_TX};
|
||||
use crate::dock_item::DockItem;
|
||||
use crate::dock_object::DockObject;
|
||||
use crate::utils::data_path;
|
||||
use crate::utils::{BoxedWindowList, Event, Item};
|
||||
use crate::utils::{BoxedWindowList, AppListEvent};
|
||||
use crate::wayland::{Toplevel, ToplevelEvent};
|
||||
use cascade::cascade;
|
||||
use gio::DesktopAppInfo;
|
||||
use gio::Icon;
|
||||
|
|
@ -49,11 +51,10 @@ impl Default for DockListType {
|
|||
}
|
||||
|
||||
impl DockList {
|
||||
pub fn new(type_: DockListType, tx: Sender<Event>, config: CosmicPanelConfig) -> Self {
|
||||
pub fn new(type_: DockListType, config: CosmicPanelConfig) -> Self {
|
||||
let self_: DockList = glib::Object::new(&[]).expect("Failed to create DockList");
|
||||
let imp = imp::DockList::from_instance(&self_);
|
||||
imp.type_.set(type_).unwrap();
|
||||
imp.tx.set(tx).unwrap();
|
||||
imp.config.set(config).unwrap();
|
||||
self_.layout();
|
||||
//dnd behavior is different for each type, as well as the data in the model
|
||||
|
|
@ -219,7 +220,6 @@ impl DockList {
|
|||
let model = self.model();
|
||||
let list_view = &imp.list_view.get().unwrap();
|
||||
let popover_menu_index = &imp.popover_menu_index;
|
||||
let tx = imp.tx.get().unwrap().clone();
|
||||
controller.connect_released(glib::clone!(@weak model, @weak list_view, @weak popover_menu_index => move |self_, _, x, y| {
|
||||
let max_x = list_view.allocated_width();
|
||||
let max_y = list_view.allocated_height();
|
||||
|
|
@ -238,13 +238,10 @@ impl DockList {
|
|||
// dbg!(click_modifier);
|
||||
// Launch the application when an item of the list is activated
|
||||
|
||||
let tx = tx.clone();
|
||||
let focus_window = move |first_focused_item: &Item| {
|
||||
let entity = first_focused_item.entity;
|
||||
let tx = tx.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
let _ = tx.clone().send(Event::Activate(entity)).await;
|
||||
});
|
||||
let focus_window = move |first_focused_item: &Toplevel| {
|
||||
let toplevel_handle = first_focused_item.toplevel_handle.clone();
|
||||
let tx = WAYLAND_TX.get().unwrap().clone();
|
||||
let _ = tx.clone().send(ToplevelEvent::Activate(toplevel_handle));
|
||||
};
|
||||
let old_index = popover_menu_index.get();
|
||||
if let Some(old_index) = old_index {
|
||||
|
|
@ -322,7 +319,6 @@ impl DockList {
|
|||
let list_view = &imp.list_view.get().unwrap();
|
||||
let drag_end = &imp.drag_end_signal;
|
||||
let drag_source = &imp.drag_source.get().unwrap();
|
||||
let tx = imp.tx.get().unwrap().clone();
|
||||
drop_controller.connect_drop(
|
||||
glib::clone!(@weak model, @weak list_view, @weak drag_end, @weak drag_source => @default-return true, move |_self, drop_value, x, y| {
|
||||
//calculate insertion location
|
||||
|
|
@ -388,10 +384,8 @@ impl DockList {
|
|||
// dbg!("rejecting drop");
|
||||
_self.reject();
|
||||
}
|
||||
let tx = tx.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
let _ = tx.send(Event::RefreshFromCache).await;
|
||||
});
|
||||
let tx = TX.get().unwrap().clone();
|
||||
let _ = tx.send(AppListEvent::Refresh);
|
||||
true
|
||||
}),
|
||||
);
|
||||
|
|
@ -419,7 +413,6 @@ impl DockList {
|
|||
let drag_end = &imp.drag_end_signal;
|
||||
let drag_cancel = &imp.drag_cancel_signal;
|
||||
let type_ = *type_;
|
||||
let tx = imp.tx.get().unwrap().clone();
|
||||
list_view.add_controller(&drag_source);
|
||||
drag_source.connect_prepare(glib::clone!(@weak model, @weak list_view, @weak drag_end, @weak drag_cancel => @default-return None, move |self_, x, _y| {
|
||||
let max_x = list_view.allocated_width();
|
||||
|
|
@ -430,30 +423,25 @@ impl DockList {
|
|||
let index = (x * n_buckets as f64 / (max_x as f64 + 0.1)) as u32;
|
||||
if let Some(item) = model.item(index) {
|
||||
if type_ == DockListType::Saved {
|
||||
let tx1 = tx.clone();
|
||||
if let Some(old_handle) = drag_end.replace(Some(self_.connect_drag_end(
|
||||
glib::clone!(@weak model => move |_self, _drag, _delete_data| {
|
||||
if _delete_data {
|
||||
model.remove(index);
|
||||
let tx = tx1.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
let _ = tx.send(Event::RefreshFromCache).await;
|
||||
});
|
||||
let tx = TX.get().unwrap().clone();
|
||||
let _ = tx.send(AppListEvent::Refresh);
|
||||
|
||||
};
|
||||
}),
|
||||
))) {
|
||||
glib::signal_handler_disconnect(self_, old_handle);
|
||||
}
|
||||
|
||||
let tx = tx.clone();
|
||||
if let Some(old_handle) = drag_cancel.replace(Some(self_.connect_drag_cancel(
|
||||
glib::clone!(@weak model => @default-return false, move |_self, _drag, cancel_reason| {
|
||||
if cancel_reason != gdk::DragCancelReason::UserCancelled {
|
||||
model.remove(index);
|
||||
let tx = tx.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
let _ = tx.send(Event::RefreshFromCache).await;
|
||||
});
|
||||
let tx = TX.get().unwrap().clone();
|
||||
let _ = tx.send(AppListEvent::Refresh);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
|
@ -515,11 +503,10 @@ impl DockList {
|
|||
let popover_menu_index = &imp.popover_menu_index;
|
||||
let factory = SignalListItemFactory::new();
|
||||
let model = imp.model.get().expect("Failed to get saved app model.");
|
||||
let tx = imp.tx.get().unwrap().clone();
|
||||
let icon_size = imp.config.get().unwrap().get_applet_icon_size();
|
||||
factory.connect_setup(
|
||||
glib::clone!(@weak popover_menu_index, @weak model => move |_, list_item| {
|
||||
let dock_item = DockItem::new(tx.clone(), icon_size);
|
||||
let dock_item = DockItem::new(icon_size);
|
||||
dock_item
|
||||
.connect_local("popover-closed", false, move |_| {
|
||||
if let Some(old_index) = popover_menu_index.replace(None) {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ impl DockObject {
|
|||
if let Some(path) = path.to_str() {
|
||||
if let Some(app_info) = gio::DesktopAppInfo::new(path) {
|
||||
if app_info.should_show()
|
||||
&& first.description.as_str() == app_info.name().as_str()
|
||||
&& first.name.as_str() == app_info.name().as_str()
|
||||
{
|
||||
return Some(app_info);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use once_cell::sync::OnceCell;
|
|||
use tokio::sync::mpsc::Sender;
|
||||
|
||||
use crate::dock_object::DockObject;
|
||||
use crate::utils::Event;
|
||||
use crate::utils::AppListEvent;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DockPopover {
|
||||
|
|
@ -26,7 +26,7 @@ pub struct DockPopover {
|
|||
pub quit_all_item: Rc<RefCell<Button>>,
|
||||
//TODO figure out how to use lifetimes with glib::wrapper! macro
|
||||
pub dock_object: Rc<RefCell<Option<DockObject>>>,
|
||||
pub tx: OnceCell<Sender<Event>>,
|
||||
pub tx: OnceCell<Sender<AppListEvent>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ use gtk4::{prelude::*, Label};
|
|||
use gtk4::{Box, Button, Image, ListBox, Orientation};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
|
||||
use crate::wayland::ToplevelEvent;
|
||||
use crate::{TX, WAYLAND_TX};
|
||||
use crate::dock_object::DockObject;
|
||||
use crate::utils::BoxedWindowList;
|
||||
use crate::utils::Event;
|
||||
use crate::utils::AppListEvent;
|
||||
|
||||
mod imp;
|
||||
|
||||
|
|
@ -22,10 +24,9 @@ glib::wrapper! {
|
|||
}
|
||||
|
||||
impl DockPopover {
|
||||
pub fn new(tx: Sender<Event>) -> Self {
|
||||
pub fn new() -> Self {
|
||||
let self_: DockPopover = glib::Object::new(&[]).expect("Failed to create DockList");
|
||||
let imp = imp::DockPopover::from_instance(&self_);
|
||||
imp.tx.set(tx).unwrap();
|
||||
self_.layout();
|
||||
//dnd behavior is different for each type, as well as the data in the model
|
||||
self_
|
||||
|
|
@ -192,30 +193,24 @@ impl DockPopover {
|
|||
self_.emit_hide();
|
||||
}));
|
||||
|
||||
let tx = imp.tx.get().unwrap().clone();
|
||||
let self_ = self.clone();
|
||||
quit_all_item.connect_clicked(glib::clone!(@weak dock_object => move |_| {
|
||||
let active = dock_object.property::<BoxedWindowList>("active").0;
|
||||
for w in active {
|
||||
let entity = w.entity;
|
||||
let tx = tx.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
let _ = tx.clone().send(Event::Close(entity)).await;
|
||||
});
|
||||
let t = w.toplevel_handle.clone();
|
||||
let tx = WAYLAND_TX.get().unwrap().clone();
|
||||
let _ = tx.clone().send(ToplevelEvent::Close(t));
|
||||
}
|
||||
self_.emit_hide();
|
||||
}));
|
||||
|
||||
let tx = imp.tx.get().unwrap().clone();
|
||||
let self_ = self.clone();
|
||||
favorite_item.connect_clicked(glib::clone!(@weak dock_object => move |_| {
|
||||
let saved = dock_object.property::<bool>("saved");
|
||||
let tx = tx.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
if let Some(name) = dock_object.get_name() {
|
||||
let _ = tx.clone().send(Event::Favorite((name, !saved))).await;
|
||||
}
|
||||
});
|
||||
if let Some(name) = dock_object.get_name() {
|
||||
let tx = TX.get().unwrap().clone();
|
||||
let _ = tx.clone().send(AppListEvent::Favorite((name, !saved)));
|
||||
}
|
||||
self_.emit_hide();
|
||||
}));
|
||||
|
||||
|
|
@ -227,16 +222,13 @@ impl DockPopover {
|
|||
// }),
|
||||
// );
|
||||
|
||||
let tx = imp.tx.get().unwrap().clone();
|
||||
let self_ = self.clone();
|
||||
window_listbox.connect_row_activated(
|
||||
glib::clone!(@weak dock_object => move |_, item| {
|
||||
let active = dock_object.property::<BoxedWindowList>("active").0;
|
||||
let entity = active[usize::try_from(item.index()).unwrap()].entity;
|
||||
let tx = tx.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
let _ = tx.send(Event::Activate(entity)).await;
|
||||
});
|
||||
let t = active[usize::try_from(item.index()).unwrap()].toplevel_handle.clone();
|
||||
let tx = WAYLAND_TX.get().unwrap().clone();
|
||||
let _ = tx.send(ToplevelEvent::Activate(t));
|
||||
self_.emit_hide();
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
// SPDX-License-Identifier: MPL-2.0-only
|
||||
|
||||
use apps_window::CosmicAppListWindow;
|
||||
use calloop::channel::SyncSender;
|
||||
use dock_list::DockListType;
|
||||
use dock_object::DockObject;
|
||||
use gio::{ApplicationFlags, DesktopAppInfo};
|
||||
use gtk4::gdk::Display;
|
||||
use gtk4::{glib, prelude::*, CssProvider, StyleContext};
|
||||
use once_cell::sync::OnceCell;
|
||||
use wayland::{ToplevelEvent, Toplevel};
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use utils::{block_on, BoxedWindowList, Event, Item, DEST, PATH};
|
||||
use utils::{block_on, BoxedWindowList, AppListEvent, DEST, PATH};
|
||||
|
||||
mod apps_container;
|
||||
mod apps_window;
|
||||
|
|
@ -21,9 +23,13 @@ mod dock_object;
|
|||
mod dock_popover;
|
||||
mod localize;
|
||||
mod utils;
|
||||
mod wayland;
|
||||
mod wayland_source;
|
||||
mod config;
|
||||
|
||||
const ID: &str = "com.system76.CosmicAppList";
|
||||
static TX: OnceCell<mpsc::Sender<Event>> = OnceCell::new();
|
||||
static TX: OnceCell<glib::Sender<AppListEvent>> = OnceCell::new();
|
||||
static WAYLAND_TX: OnceCell<SyncSender<ToplevelEvent>> = OnceCell::new();
|
||||
|
||||
pub fn localize() {
|
||||
let localizer = crate::localize::localizer();
|
||||
|
|
@ -57,202 +63,206 @@ fn main() {
|
|||
|
||||
app.connect_activate(|app| {
|
||||
load_css();
|
||||
let (tx, mut rx) = mpsc::channel(100);
|
||||
let (tx, rx) = glib::MainContext::channel(glib::Priority::default());
|
||||
|
||||
let window = CosmicAppListWindow::new(app, tx.clone());
|
||||
let window = CosmicAppListWindow::new(app);
|
||||
let apps_container = apps_container::AppsContainer::new();
|
||||
let wayland_tx = wayland::spawn_toplevels();
|
||||
|
||||
let apps_container = apps_container::AppsContainer::new(tx.clone());
|
||||
let cached_results = Arc::new(Mutex::new(Vec::new()));
|
||||
WAYLAND_TX.set(wayland_tx).unwrap();
|
||||
|
||||
|
||||
|
||||
let mut cached_results = Vec::new();
|
||||
// let zbus_conn = spawn_zbus(tx.clone(), Arc::clone(&cached_results));
|
||||
TX.set(tx.clone()).unwrap();
|
||||
|
||||
let _ = glib::MainContext::default().spawn_local(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
Event::Activate(_) => {
|
||||
// let _activate_window = zbus_conn
|
||||
// .call_method(Some(DEST), PATH, Some(DEST), "WindowFocus", &((e,)))
|
||||
// .await
|
||||
// .expect("Failed to focus selected window");
|
||||
}
|
||||
Event::Close(_) => {
|
||||
// let _activate_window = zbus_conn
|
||||
// .call_method(Some(DEST), PATH, Some(DEST), "WindowQuit", &((e,)))
|
||||
// .await
|
||||
// .expect("Failed to close selected window");
|
||||
}
|
||||
Event::Favorite((name, should_favorite)) => {
|
||||
let saved_app_model = apps_container.model(DockListType::Saved);
|
||||
let active_app_model = apps_container.model(DockListType::Active);
|
||||
if should_favorite {
|
||||
let mut cur: u32 = 0;
|
||||
let mut index: Option<u32> = None;
|
||||
while let Some(item) = active_app_model.item(cur) {
|
||||
if let Ok(cur_dock_object) = item.downcast::<DockObject>() {
|
||||
if cur_dock_object.get_path() == Some(name.clone()) {
|
||||
cur_dock_object.set_saved(true);
|
||||
index = Some(cur);
|
||||
}
|
||||
rx.attach(None, glib::clone!(@weak window => @default-return glib::prelude::Continue(true), move |event| {
|
||||
match event {
|
||||
AppListEvent::Activate(_) => {
|
||||
// let _activate_window = zbus_conn
|
||||
// .call_method(Some(DEST), PATH, Some(DEST), "WindowFocus", &((e,)))
|
||||
// .await
|
||||
// .expect("Failed to focus selected window");
|
||||
}
|
||||
AppListEvent::Close(_) => {
|
||||
// let _activate_window = zbus_conn
|
||||
// .call_method(Some(DEST), PATH, Some(DEST), "WindowQuit", &((e,)))
|
||||
// .await
|
||||
// .expect("Failed to close selected window");
|
||||
}
|
||||
AppListEvent::Favorite((name, should_favorite)) => {
|
||||
let saved_app_model = apps_container.model(DockListType::Saved);
|
||||
let active_app_model = apps_container.model(DockListType::Active);
|
||||
if should_favorite {
|
||||
let mut cur: u32 = 0;
|
||||
let mut index: Option<u32> = None;
|
||||
while let Some(item) = active_app_model.item(cur) {
|
||||
if let Ok(cur_dock_object) = item.downcast::<DockObject>() {
|
||||
if cur_dock_object.get_path() == Some(name.clone()) {
|
||||
cur_dock_object.set_saved(true);
|
||||
index = Some(cur);
|
||||
}
|
||||
cur += 1;
|
||||
}
|
||||
if let Some(index) = index {
|
||||
let object = active_app_model.item(index).unwrap();
|
||||
active_app_model.remove(index);
|
||||
saved_app_model.append(&object);
|
||||
}
|
||||
} else {
|
||||
let mut cur: u32 = 0;
|
||||
let mut index: Option<u32> = None;
|
||||
while let Some(item) = saved_app_model.item(cur) {
|
||||
if let Ok(cur_dock_object) = item.downcast::<DockObject>() {
|
||||
if cur_dock_object.get_path() == Some(name.clone()) {
|
||||
cur_dock_object.set_saved(false);
|
||||
index = Some(cur);
|
||||
}
|
||||
}
|
||||
cur += 1;
|
||||
}
|
||||
if let Some(index) = index {
|
||||
let object = saved_app_model.item(index).unwrap();
|
||||
saved_app_model.remove(index);
|
||||
active_app_model.append(&object);
|
||||
}
|
||||
cur += 1;
|
||||
}
|
||||
let _ = tx.send(Event::RefreshFromCache).await;
|
||||
}
|
||||
Event::RefreshFromCache => {
|
||||
// println!("refreshing model from cache");
|
||||
let cached_results = cached_results.as_ref().lock().unwrap();
|
||||
let stack_active = cached_results.iter().fold(
|
||||
BTreeMap::new(),
|
||||
|mut acc: BTreeMap<String, BoxedWindowList>, elem: &Item| {
|
||||
if let Some(v) = acc.get_mut(&elem.description) {
|
||||
v.0.push(elem.clone());
|
||||
} else {
|
||||
acc.insert(
|
||||
elem.description.clone(),
|
||||
BoxedWindowList(vec![elem.clone()]),
|
||||
);
|
||||
if let Some(index) = index {
|
||||
let object = active_app_model.item(index).unwrap();
|
||||
active_app_model.remove(index);
|
||||
saved_app_model.append(&object);
|
||||
}
|
||||
} else {
|
||||
let mut cur: u32 = 0;
|
||||
let mut index: Option<u32> = None;
|
||||
while let Some(item) = saved_app_model.item(cur) {
|
||||
if let Ok(cur_dock_object) = item.downcast::<DockObject>() {
|
||||
if cur_dock_object.get_path() == Some(name.clone()) {
|
||||
cur_dock_object.set_saved(false);
|
||||
index = Some(cur);
|
||||
}
|
||||
acc
|
||||
},
|
||||
);
|
||||
let mut stack_active: Vec<BoxedWindowList> =
|
||||
stack_active.into_values().collect();
|
||||
}
|
||||
cur += 1;
|
||||
}
|
||||
if let Some(index) = index {
|
||||
let object = saved_app_model.item(index).unwrap();
|
||||
saved_app_model.remove(index);
|
||||
active_app_model.append(&object);
|
||||
}
|
||||
}
|
||||
let _ = tx.send(AppListEvent::Refresh);
|
||||
}
|
||||
AppListEvent::Refresh => {
|
||||
// println!("refreshing model from cache");
|
||||
let stack_active = cached_results.iter().fold(
|
||||
BTreeMap::new(),
|
||||
|mut acc: BTreeMap<String, BoxedWindowList>, elem: &Toplevel| {
|
||||
if let Some(v) = acc.get_mut(&elem.name) {
|
||||
v.0.push(elem.clone());
|
||||
} else {
|
||||
acc.insert(
|
||||
elem.name.clone(),
|
||||
BoxedWindowList(vec![elem.clone()]),
|
||||
);
|
||||
}
|
||||
acc
|
||||
},
|
||||
);
|
||||
let mut stack_active: Vec<BoxedWindowList> =
|
||||
stack_active.into_values().collect();
|
||||
|
||||
// update active app stacks for saved apps into the saved app model
|
||||
// then put the rest in the active app model (which doesn't include saved apps)
|
||||
let saved_app_model = apps_container.model(DockListType::Saved);
|
||||
// update active app stacks for saved apps into the saved app model
|
||||
// then put the rest in the active app model (which doesn't include saved apps)
|
||||
let saved_app_model = apps_container.model(DockListType::Saved);
|
||||
|
||||
let mut saved_i: u32 = 0;
|
||||
while let Some(item) = saved_app_model.item(saved_i) {
|
||||
if let Ok(dock_obj) = item.downcast::<DockObject>() {
|
||||
if let Some(cur_app_info) =
|
||||
dock_obj.property::<Option<DesktopAppInfo>>("appinfo")
|
||||
let mut saved_i: u32 = 0;
|
||||
while let Some(item) = saved_app_model.item(saved_i) {
|
||||
if let Ok(dock_obj) = item.downcast::<DockObject>() {
|
||||
if let Some(cur_app_info) =
|
||||
dock_obj.property::<Option<DesktopAppInfo>>("appinfo")
|
||||
{
|
||||
if let Some((i, _s)) = stack_active
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_i, s)| s.0[0].name == cur_app_info.name())
|
||||
{
|
||||
if let Some((i, _s)) = stack_active
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_i, s)| s.0[0].description == cur_app_info.name())
|
||||
{
|
||||
// println!(
|
||||
// "found active saved app {} at {}",
|
||||
// _s.0[0].name, i
|
||||
// );
|
||||
let active = stack_active.remove(i);
|
||||
dock_obj.set_property("active", active.to_value());
|
||||
saved_app_model.items_changed(saved_i, 0, 0);
|
||||
} else if cached_results
|
||||
.iter()
|
||||
.any(|s| s.description == cur_app_info.name())
|
||||
{
|
||||
dock_obj.set_property(
|
||||
"active",
|
||||
BoxedWindowList(Vec::new()).to_value(),
|
||||
);
|
||||
saved_app_model.items_changed(saved_i, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
saved_i += 1;
|
||||
}
|
||||
|
||||
let active_app_model = apps_container.model(DockListType::Active);
|
||||
let model_len = active_app_model.n_items();
|
||||
let new_results: Vec<glib::Object> = stack_active
|
||||
.into_iter()
|
||||
.map(|v| DockObject::from_search_results(v).upcast())
|
||||
.collect();
|
||||
active_app_model.splice(0, model_len, &new_results[..]);
|
||||
}
|
||||
Event::WindowList => {
|
||||
// sort to make comparison with cache easier
|
||||
let results = cached_results.as_ref().lock().unwrap();
|
||||
|
||||
// build active app stacks for each app
|
||||
let stack_active = results.iter().fold(
|
||||
BTreeMap::new(),
|
||||
|mut acc: BTreeMap<String, BoxedWindowList>, elem| {
|
||||
if let Some(v) = acc.get_mut(&elem.description) {
|
||||
v.0.push(elem.clone());
|
||||
} else {
|
||||
acc.insert(
|
||||
elem.description.clone(),
|
||||
BoxedWindowList(vec![elem.clone()]),
|
||||
);
|
||||
}
|
||||
acc
|
||||
},
|
||||
);
|
||||
let mut stack_active: Vec<BoxedWindowList> =
|
||||
stack_active.into_values().collect();
|
||||
|
||||
// update active app stacks for saved apps into the saved app model
|
||||
// then put the rest in the active app model (which doesn't include saved apps)
|
||||
let saved_app_model = apps_container.model(DockListType::Saved);
|
||||
|
||||
let mut saved_i: u32 = 0;
|
||||
while let Some(item) = saved_app_model.item(saved_i) {
|
||||
if let Ok(dock_obj) = item.downcast::<DockObject>() {
|
||||
if let Some(cur_app_info) =
|
||||
dock_obj.property::<Option<DesktopAppInfo>>("appinfo")
|
||||
// println!(
|
||||
// "found active saved app {} at {}",
|
||||
// _s.0[0].name, i
|
||||
// );
|
||||
let active = stack_active.remove(i);
|
||||
dock_obj.set_property("active", active.to_value());
|
||||
saved_app_model.items_changed(saved_i, 0, 0);
|
||||
} else if cached_results
|
||||
.iter()
|
||||
.any(|s| s.name == cur_app_info.name())
|
||||
{
|
||||
if let Some((i, _s)) = stack_active
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_i, s)| s.0[0].description == cur_app_info.name())
|
||||
{
|
||||
// println!("found active saved app {} at {}", s.0[0].name, i);
|
||||
let active = stack_active.remove(i);
|
||||
dock_obj.set_property("active", active.to_value());
|
||||
saved_app_model.items_changed(saved_i, 0, 0);
|
||||
} else if results
|
||||
.iter()
|
||||
.any(|s| s.description == cur_app_info.name())
|
||||
{
|
||||
dock_obj.set_property(
|
||||
"active",
|
||||
BoxedWindowList(Vec::new()).to_value(),
|
||||
);
|
||||
saved_app_model.items_changed(saved_i, 0, 0);
|
||||
}
|
||||
dock_obj.set_property(
|
||||
"active",
|
||||
BoxedWindowList(Vec::new()).to_value(),
|
||||
);
|
||||
saved_app_model.items_changed(saved_i, 0, 0);
|
||||
}
|
||||
}
|
||||
saved_i += 1;
|
||||
}
|
||||
|
||||
let active_app_model = apps_container.model(DockListType::Active);
|
||||
let model_len = active_app_model.n_items();
|
||||
let new_results: Vec<glib::Object> = stack_active
|
||||
.into_iter()
|
||||
.map(|v| DockObject::from_search_results(v).upcast())
|
||||
.collect();
|
||||
active_app_model.splice(0, model_len, &new_results[..]);
|
||||
saved_i += 1;
|
||||
}
|
||||
|
||||
let active_app_model = apps_container.model(DockListType::Active);
|
||||
let model_len = active_app_model.n_items();
|
||||
let new_results: Vec<glib::Object> = stack_active
|
||||
.into_iter()
|
||||
.map(|v| DockObject::from_search_results(v).upcast())
|
||||
.collect();
|
||||
active_app_model.splice(0, model_len, &new_results[..]);
|
||||
}
|
||||
AppListEvent::WindowList(results) => {
|
||||
// sort to make comparison with cache easier
|
||||
cached_results = results.clone();
|
||||
|
||||
// build active app stacks for each app
|
||||
let stack_active = results.iter().fold(
|
||||
BTreeMap::new(),
|
||||
|mut acc: BTreeMap<String, BoxedWindowList>, elem| {
|
||||
if let Some(v) = acc.get_mut(&elem.name) {
|
||||
v.0.push(elem.clone());
|
||||
} else {
|
||||
acc.insert(
|
||||
elem.name.clone(),
|
||||
BoxedWindowList(vec![elem.clone()]),
|
||||
);
|
||||
}
|
||||
acc
|
||||
},
|
||||
);
|
||||
let mut stack_active: Vec<BoxedWindowList> =
|
||||
stack_active.into_values().collect();
|
||||
|
||||
// update active app stacks for saved apps into the saved app model
|
||||
// then put the rest in the active app model (which doesn't include saved apps)
|
||||
let saved_app_model = apps_container.model(DockListType::Saved);
|
||||
|
||||
let mut saved_i: u32 = 0;
|
||||
while let Some(item) = saved_app_model.item(saved_i) {
|
||||
if let Ok(dock_obj) = item.downcast::<DockObject>() {
|
||||
if let Some(cur_app_info) =
|
||||
dock_obj.property::<Option<DesktopAppInfo>>("appinfo")
|
||||
{
|
||||
if let Some((i, _s)) = stack_active
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_i, s)| s.0[0].name == cur_app_info.name())
|
||||
{
|
||||
// println!("found active saved app {} at {}", s.0[0].name, i);
|
||||
let active = stack_active.remove(i);
|
||||
dock_obj.set_property("active", active.to_value());
|
||||
saved_app_model.items_changed(saved_i, 0, 0);
|
||||
} else if results
|
||||
.iter()
|
||||
.any(|s| s.name == cur_app_info.name())
|
||||
{
|
||||
dock_obj.set_property(
|
||||
"active",
|
||||
BoxedWindowList(Vec::new()).to_value(),
|
||||
);
|
||||
saved_app_model.items_changed(saved_i, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
saved_i += 1;
|
||||
}
|
||||
|
||||
let active_app_model = apps_container.model(DockListType::Active);
|
||||
let model_len = active_app_model.n_items();
|
||||
let new_results: Vec<glib::Object> = stack_active
|
||||
.into_iter()
|
||||
.map(|v| DockObject::from_search_results(v).upcast())
|
||||
.collect();
|
||||
active_app_model.splice(0, model_len, &new_results[..]);
|
||||
}
|
||||
}
|
||||
});
|
||||
glib::prelude::Continue(true)
|
||||
}));
|
||||
|
||||
window.show();
|
||||
});
|
||||
app.run();
|
||||
|
|
|
|||
|
|
@ -3,32 +3,25 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use gtk4::glib;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::future::Future;
|
||||
|
||||
use crate::wayland::Toplevel;
|
||||
|
||||
pub const DEST: &str = "com.System76.PopShell";
|
||||
pub const PATH: &str = "/com/System76/PopShell";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event {
|
||||
WindowList,
|
||||
pub enum AppListEvent {
|
||||
WindowList(Vec<Toplevel>),
|
||||
Activate((u32, u32)),
|
||||
Close((u32, u32)),
|
||||
Favorite((String, bool)),
|
||||
RefreshFromCache,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Item {
|
||||
pub(crate) entity: (u32, u32),
|
||||
pub(crate) name: String,
|
||||
pub(crate) description: String,
|
||||
pub(crate) desktop_entry: String,
|
||||
Refresh,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, glib::Boxed)]
|
||||
#[boxed_type(name = "BoxedWindowList")]
|
||||
pub struct BoxedWindowList(pub Vec<Item>);
|
||||
pub struct BoxedWindowList(pub Vec<Toplevel>);
|
||||
|
||||
pub fn data_path() -> PathBuf {
|
||||
let mut path = glib::user_data_dir();
|
||||
|
|
|
|||
512
applets/cosmic-app-list/src/wayland.rs
Normal file
512
applets/cosmic-app-list/src/wayland.rs
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
use crate::{
|
||||
wayland_source::WaylandSource, config::TopLevelFilter, TX, utils::AppListEvent,
|
||||
};
|
||||
use cosmic_panel_config::CosmicPanelConfig;
|
||||
use gtk4::glib;
|
||||
use std::{
|
||||
env, os::unix::net::UnixStream, path::PathBuf,time::Duration,
|
||||
};
|
||||
use wayland_client::{
|
||||
event_created_child,
|
||||
protocol::{
|
||||
wl_output::{self, WlOutput},
|
||||
wl_registry,
|
||||
},
|
||||
ConnectError, Proxy,
|
||||
};
|
||||
use cosmic_protocols::{workspace::v1::client::{
|
||||
zcosmic_workspace_manager_v1::{self, ZcosmicWorkspaceManagerV1},
|
||||
zcosmic_workspace_group_handle_v1::{self, ZcosmicWorkspaceGroupHandleV1},
|
||||
zcosmic_workspace_handle_v1::{self, ZcosmicWorkspaceHandleV1},
|
||||
}, toplevel_info::v1::client::{zcosmic_toplevel_info_v1::{ZcosmicToplevelInfoV1, self}, zcosmic_toplevel_handle_v1::{ZcosmicToplevelHandleV1, self}}, toplevel_management::v1::client::zcosmic_toplevel_manager_v1::{self, ZcosmicToplevelManagerV1}};
|
||||
use wayland_client::{Connection, Dispatch, QueueHandle};
|
||||
use calloop::channel::*;
|
||||
use crate::config::AppListConfig;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToplevelEvent {
|
||||
Activate(ZcosmicToplevelHandleV1),
|
||||
Close(ZcosmicToplevelHandleV1),
|
||||
}
|
||||
|
||||
pub fn spawn_toplevels() -> SyncSender<ToplevelEvent> {
|
||||
let config = AppListConfig::load().unwrap_or_default();
|
||||
|
||||
let (workspaces_tx, workspaces_rx) = calloop::channel::sync_channel(100);
|
||||
|
||||
if let Ok(Ok(conn)) = std::env::var("WAYLAND_DISPLAY")
|
||||
.map_err(anyhow::Error::msg)
|
||||
.map(|display_str| {
|
||||
let mut socket_path = env::var_os("XDG_RUNTIME_DIR")
|
||||
.map(Into::<PathBuf>::into)
|
||||
.ok_or(ConnectError::NoCompositor)?;
|
||||
socket_path.push(display_str);
|
||||
|
||||
Ok(UnixStream::connect(socket_path).map_err(|_| ConnectError::NoCompositor)?)
|
||||
})
|
||||
.and_then(|s| s.map(|s| Connection::from_socket(s).map_err(anyhow::Error::msg)))
|
||||
{
|
||||
std::thread::spawn(move || {
|
||||
let output = match config.filter_top_levels {
|
||||
Some(TopLevelFilter::ConfiguredOutput) => CosmicPanelConfig::load_from_env()
|
||||
.ok().map(|c| c.output),
|
||||
_ => None,
|
||||
};
|
||||
let mut event_loop = calloop::EventLoop::<State>::try_new().unwrap();
|
||||
let loop_handle = event_loop.handle();
|
||||
let event_queue = conn.new_event_queue::<State>();
|
||||
let qhandle = event_queue.handle();
|
||||
|
||||
WaylandSource::new(event_queue)
|
||||
.expect("Failed to create wayland source")
|
||||
.insert(loop_handle)
|
||||
.unwrap();
|
||||
|
||||
let display = conn.display();
|
||||
display.get_registry(&qhandle, ()).unwrap();
|
||||
|
||||
let mut state = State {
|
||||
workspace_manager: None,
|
||||
workspace_groups: Vec::new(),
|
||||
toplevel_info: None,
|
||||
toplevel_manager: None,
|
||||
config,
|
||||
configured_output: output,
|
||||
expected_output: None,
|
||||
running: true,
|
||||
toplevels: vec![]
|
||||
};
|
||||
let loop_handle = event_loop.handle();
|
||||
loop_handle
|
||||
.insert_source(workspaces_rx, |e, _, state| match e {
|
||||
Event::Msg(ToplevelEvent::Activate(_t)) => {
|
||||
todo!()
|
||||
}
|
||||
Event::Msg(ToplevelEvent::Close(_t)) => {
|
||||
todo!()
|
||||
}
|
||||
Event::Closed => {
|
||||
if let Some(workspace_manager) = &mut state.workspace_manager {
|
||||
for g in &mut state.workspace_groups {
|
||||
g.workspace_group_handle.destroy();
|
||||
}
|
||||
workspace_manager.stop();
|
||||
}
|
||||
if let Some(toplevel_manager) = &mut state.toplevel_manager {
|
||||
toplevel_manager.destroy();
|
||||
}
|
||||
if let Some(toplevel_info) = &mut state.toplevel_info {
|
||||
for toplevel in &state.toplevels {
|
||||
toplevel.toplevel_handle.destroy();
|
||||
}
|
||||
toplevel_info.stop();
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
while state.running {
|
||||
event_loop
|
||||
.dispatch(Duration::from_millis(16), &mut state)
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
eprintln!("ENV variable WAYLAND_DISPLAY is missing. Exiting...");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
workspaces_tx
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State {
|
||||
running: bool,
|
||||
config: AppListConfig,
|
||||
configured_output: Option<String>,
|
||||
expected_output: Option<WlOutput>,
|
||||
workspace_manager: Option<ZcosmicWorkspaceManagerV1>,
|
||||
workspace_groups: Vec<WorkspaceGroup>,
|
||||
toplevel_info: Option<ZcosmicToplevelInfoV1>,
|
||||
toplevel_manager: Option<ZcosmicToplevelManagerV1>,
|
||||
toplevels: Vec<Toplevel>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn workspace_list(&self) -> impl Iterator<Item = (String, u32)> + '_ {
|
||||
self.workspace_groups
|
||||
.iter()
|
||||
.filter_map(|g| {
|
||||
if g.output == self.expected_output {
|
||||
Some(g.workspaces.iter().map(|w| (w.name.clone(), match &w.states {
|
||||
x if x.contains(&zcosmic_workspace_handle_v1::State::Active) => 0,
|
||||
x if x.contains(&zcosmic_workspace_handle_v1::State::Urgent) => 1,
|
||||
x if x.contains(&zcosmic_workspace_handle_v1::State::Hidden) => 2,
|
||||
_ => 3,
|
||||
})))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Toplevel {
|
||||
pub name: String,
|
||||
pub app_id: String,
|
||||
pub toplevel_handle: ZcosmicToplevelHandleV1,
|
||||
pub states: Vec<zcosmic_toplevel_handle_v1::State>,
|
||||
pub output: Option<WlOutput>,
|
||||
pub workspace: Option<ZcosmicWorkspaceHandleV1>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct WorkspaceGroup {
|
||||
workspace_group_handle: ZcosmicWorkspaceGroupHandleV1,
|
||||
output: Option<WlOutput>,
|
||||
workspaces: Vec<Workspace>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Workspace {
|
||||
workspace_handle: ZcosmicWorkspaceHandleV1,
|
||||
name: String,
|
||||
coordinates: Vec<u32>,
|
||||
states: Vec<zcosmic_workspace_handle_v1::State>,
|
||||
}
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
registry: &wl_registry::WlRegistry,
|
||||
event: wl_registry::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
if let wl_registry::Event::Global {
|
||||
name,
|
||||
interface,
|
||||
version,
|
||||
} = event
|
||||
{
|
||||
match &interface[..] {
|
||||
"zcosmic_workspace_info_v1" => {
|
||||
let ti = registry
|
||||
.bind::<ZcosmicToplevelInfoV1, _, _>(
|
||||
name,
|
||||
1,
|
||||
qh,
|
||||
(),
|
||||
)
|
||||
.unwrap();
|
||||
state.toplevel_info = Some(ti);
|
||||
}
|
||||
"zcosmic_toplevel_manager_v1" => {
|
||||
let tm = registry
|
||||
.bind::<ZcosmicToplevelManagerV1, _, _>(
|
||||
name,
|
||||
1,
|
||||
qh,
|
||||
(),
|
||||
)
|
||||
.unwrap();
|
||||
state.toplevel_manager = Some(tm);
|
||||
}
|
||||
"zcosmic_toplevel_manager_v1" => {
|
||||
let workspace_manager = registry
|
||||
.bind::<ZcosmicWorkspaceManagerV1, _, _>(
|
||||
name,
|
||||
1,
|
||||
qh,
|
||||
(),
|
||||
)
|
||||
.unwrap();
|
||||
state.workspace_manager = Some(workspace_manager);
|
||||
}
|
||||
"wl_output" => {
|
||||
registry.bind::<WlOutput, _, _>(name, 1, qh, ()).unwrap();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZcosmicToplevelInfoV1, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &ZcosmicToplevelInfoV1,
|
||||
event: <ZcosmicToplevelInfoV1 as Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
dbg!(&event);
|
||||
match event {
|
||||
zcosmic_toplevel_info_v1::Event::Toplevel { toplevel } => {
|
||||
state.toplevels.push(Toplevel { name: "".into(), app_id: "".into(), toplevel_handle: toplevel, states: vec![], output: None, workspace: None });
|
||||
},
|
||||
zcosmic_toplevel_info_v1::Event::Finished => {
|
||||
dbg!(&state.toplevels);
|
||||
let tx = TX.get().unwrap().clone();
|
||||
|
||||
let _ = tx.send(AppListEvent::WindowList(state.toplevels.iter().filter(|t| {
|
||||
match state.config.filter_top_levels {
|
||||
Some(TopLevelFilter::ActiveWorkspace) => true,
|
||||
_ => false,
|
||||
}
|
||||
}).cloned().collect()));
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Dispatch<ZcosmicToplevelManagerV1, ()> for State {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
_: &ZcosmicToplevelManagerV1,
|
||||
event: <ZcosmicToplevelManagerV1 as Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
zcosmic_toplevel_manager_v1::Event::Capabilities { .. } => {
|
||||
// TODO capabilities affect what is shown to user in applet
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZcosmicToplevelHandleV1, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
p: &ZcosmicToplevelHandleV1,
|
||||
event: <ZcosmicToplevelHandleV1 as Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
dbg!(&event);
|
||||
match event {
|
||||
zcosmic_toplevel_handle_v1::Event::Closed => {
|
||||
if let Some(i) = state.toplevels.iter().position(|t| &t.toplevel_handle == p) {
|
||||
state.toplevels.remove(i);
|
||||
}
|
||||
},
|
||||
zcosmic_toplevel_handle_v1::Event::Done => {},
|
||||
zcosmic_toplevel_handle_v1::Event::Title { title } => {
|
||||
if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p) {
|
||||
i.name = title;
|
||||
}
|
||||
},
|
||||
zcosmic_toplevel_handle_v1::Event::AppId { app_id } => {
|
||||
if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p) {
|
||||
i.app_id = app_id;
|
||||
}
|
||||
},
|
||||
zcosmic_toplevel_handle_v1::Event::OutputEnter { output } => {
|
||||
if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p) {
|
||||
i.output.replace(output);
|
||||
}
|
||||
},
|
||||
zcosmic_toplevel_handle_v1::Event::OutputLeave { output } => {
|
||||
if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p && t.output.as_ref() == Some(&output)) {
|
||||
i.output.take();
|
||||
}
|
||||
},
|
||||
zcosmic_toplevel_handle_v1::Event::WorkspaceEnter { workspace } => {
|
||||
if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p) {
|
||||
i.workspace.replace(workspace);
|
||||
}
|
||||
},
|
||||
zcosmic_toplevel_handle_v1::Event::WorkspaceLeave { workspace } => {
|
||||
if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p && t.workspace.as_ref() == Some(&workspace)) {
|
||||
i.workspace.take();
|
||||
}
|
||||
},
|
||||
zcosmic_toplevel_handle_v1::Event::State { state: t_state } => {
|
||||
if let Some(i) = state.toplevels.iter_mut().find(|t| &t.toplevel_handle == p) {
|
||||
i.states = t_state.chunks(4).map(|chunk| zcosmic_toplevel_handle_v1::State::try_from(u32::from_ne_bytes(chunk.try_into().unwrap())).unwrap()).collect();
|
||||
}
|
||||
},
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Dispatch<ZcosmicWorkspaceManagerV1, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &ZcosmicWorkspaceManagerV1,
|
||||
event: zcosmic_workspace_manager_v1::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
zcosmic_workspace_manager_v1::Event::WorkspaceGroup { workspace_group } => {
|
||||
state.workspace_groups.push(WorkspaceGroup {
|
||||
workspace_group_handle: workspace_group,
|
||||
output: None,
|
||||
workspaces: Vec::new(),
|
||||
});
|
||||
}
|
||||
zcosmic_workspace_manager_v1::Event::Done => {
|
||||
for group in &mut state.workspace_groups {
|
||||
group.workspaces.sort_by(|w1, w2| {
|
||||
w1.coordinates.iter().zip(w2.coordinates.iter())
|
||||
.skip_while(|(coord1, coord2)| coord1 == coord2)
|
||||
.next()
|
||||
.map(|(coord1, coord2)| coord1.cmp(coord2))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
}
|
||||
}
|
||||
zcosmic_workspace_manager_v1::Event::Finished => {
|
||||
state.workspace_manager.take();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
event_created_child!(State, ZcosmicWorkspaceManagerV1, [
|
||||
0 => (ZcosmicWorkspaceGroupHandleV1, ())
|
||||
]);
|
||||
}
|
||||
|
||||
impl Dispatch<ZcosmicWorkspaceGroupHandleV1, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
group: &ZcosmicWorkspaceGroupHandleV1,
|
||||
event: zcosmic_workspace_group_handle_v1::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
zcosmic_workspace_group_handle_v1::Event::OutputEnter { output } => {
|
||||
if let Some(group) = state
|
||||
.workspace_groups
|
||||
.iter_mut()
|
||||
.find(|g| &g.workspace_group_handle == group)
|
||||
{
|
||||
group.output = Some(output);
|
||||
}
|
||||
}
|
||||
zcosmic_workspace_group_handle_v1::Event::OutputLeave { output } => {
|
||||
if let Some(group) = state.workspace_groups.iter_mut().find(|g| {
|
||||
&g.workspace_group_handle == group && g.output.as_ref() == Some(&output)
|
||||
}) {
|
||||
group.output = None;
|
||||
}
|
||||
}
|
||||
zcosmic_workspace_group_handle_v1::Event::Workspace { workspace } => {
|
||||
if let Some(group) = state
|
||||
.workspace_groups
|
||||
.iter_mut()
|
||||
.find(|g| &g.workspace_group_handle == group)
|
||||
{
|
||||
group.workspaces.push(Workspace {
|
||||
workspace_handle: workspace,
|
||||
name: String::new(),
|
||||
coordinates: Vec::new(),
|
||||
states: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
zcosmic_workspace_group_handle_v1::Event::Remove => {
|
||||
if let Some(group) = state
|
||||
.workspace_groups
|
||||
.iter()
|
||||
.position(|g| &g.workspace_group_handle == group)
|
||||
{
|
||||
state.workspace_groups.remove(group);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
event_created_child!(State, ZcosmicWorkspaceGroupHandleV1, [
|
||||
3 => (ZcosmicWorkspaceHandleV1, ())
|
||||
]);
|
||||
}
|
||||
|
||||
impl Dispatch<ZcosmicWorkspaceHandleV1, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
workspace: &ZcosmicWorkspaceHandleV1,
|
||||
event: zcosmic_workspace_handle_v1::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
zcosmic_workspace_handle_v1::Event::Name { name } => {
|
||||
if let Some(w) = state.workspace_groups.iter_mut().find_map(|g| {
|
||||
g.workspaces
|
||||
.iter_mut()
|
||||
.find(|w| &w.workspace_handle == workspace)
|
||||
}) {
|
||||
w.name = name;
|
||||
}
|
||||
}
|
||||
zcosmic_workspace_handle_v1::Event::Coordinates { coordinates } => {
|
||||
if let Some(w) = state.workspace_groups.iter_mut().find_map(|g| {
|
||||
g.workspaces
|
||||
.iter_mut()
|
||||
.find(|w| &w.workspace_handle == workspace)
|
||||
}) {
|
||||
// wayland is host byte order
|
||||
w.coordinates = coordinates.chunks(4).map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap())).collect();
|
||||
}
|
||||
}
|
||||
zcosmic_workspace_handle_v1::Event::State { state: workspace_state } => {
|
||||
if let Some(w) = state.workspace_groups.iter_mut().find_map(|g| {
|
||||
g.workspaces
|
||||
.iter_mut()
|
||||
.find(|w| &w.workspace_handle == workspace)
|
||||
}) {
|
||||
// wayland is host byte order
|
||||
w.states = workspace_state.chunks(4).map(|chunk| zcosmic_workspace_handle_v1::State::try_from(u32::from_ne_bytes(chunk.try_into().unwrap())).unwrap()).collect();
|
||||
}
|
||||
}
|
||||
zcosmic_workspace_handle_v1::Event::Remove => {
|
||||
if let Some((g, w_i)) = state.workspace_groups.iter_mut().find_map(|g| {
|
||||
g.workspaces
|
||||
.iter_mut()
|
||||
.position(|w| &w.workspace_handle == workspace)
|
||||
.map(|p| (g, p))
|
||||
}) {
|
||||
g.workspaces.remove(w_i);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlOutput, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
o: &WlOutput,
|
||||
e: wl_output::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
match e {
|
||||
wl_output::Event::Name { name } if Some(&name) == state.configured_output.as_ref() => {
|
||||
state.expected_output.replace(o.clone());
|
||||
}
|
||||
_ => {} // ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
219
applets/cosmic-app-list/src/wayland_source.rs
Normal file
219
applets/cosmic-app-list/src/wayland_source.rs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
//! Utilities for using an [`EventQueue`] from wayland-client with an event loop that performs polling with
|
||||
//! [`calloop`](https://crates.io/crates/calloop).
|
||||
|
||||
use std::{io, os::unix::prelude::RawFd};
|
||||
|
||||
use calloop::{
|
||||
generic::Generic, EventSource, InsertError, Interest, LoopHandle, Mode, Poll, PostAction,
|
||||
Readiness, RegistrationToken, Token, TokenFactory,
|
||||
};
|
||||
use nix::errno::Errno;
|
||||
use wayland_backend::client::{ReadEventsGuard, WaylandError};
|
||||
use wayland_client::{DispatchError, EventQueue};
|
||||
|
||||
/// An adapter to insert an [`EventQueue`] into a calloop [`EventLoop`](calloop::EventLoop).
|
||||
///
|
||||
/// This type implements [`EventSource`] which generates an event whenever events on the display need to be
|
||||
/// dispatched. The event queue available in the callback calloop registers may be used to dispatch pending
|
||||
/// events using [`EventQueue::dispatch_pending`].
|
||||
///
|
||||
/// [`WaylandSource::insert`] can be used to insert this source into an event loop and automatically dispatch
|
||||
/// pending events on the display.
|
||||
#[derive(Debug)]
|
||||
pub struct WaylandSource<D> {
|
||||
queue: EventQueue<D>,
|
||||
fd: Generic<RawFd>,
|
||||
read_guard: Option<ReadEventsGuard>,
|
||||
}
|
||||
|
||||
impl<D> WaylandSource<D> {
|
||||
/// Wrap an [`EventQueue`] as a [`WaylandSource`].
|
||||
pub fn new(queue: EventQueue<D>) -> Result<WaylandSource<D>, WaylandError> {
|
||||
let guard = queue.prepare_read()?;
|
||||
let fd = Generic::new(guard.connection_fd(), Interest::READ, Mode::Level);
|
||||
drop(guard);
|
||||
|
||||
Ok(WaylandSource {
|
||||
queue,
|
||||
fd,
|
||||
read_guard: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Access the underlying event queue
|
||||
///
|
||||
/// Note that you should be careful when interacting with it if you invoke methods that
|
||||
/// interact with the wayland socket (such as `dispatch()` or `prepare_read()`). These may
|
||||
/// interfere with the proper waking up of this event source in the event loop.
|
||||
pub fn queue(&mut self) -> &mut EventQueue<D> {
|
||||
&mut self.queue
|
||||
}
|
||||
|
||||
/// Insert this source into the given event loop.
|
||||
///
|
||||
/// This adapter will pass the event loop's shared data as the `D` type for the event loop.
|
||||
pub fn insert(self, handle: LoopHandle<D>) -> Result<RegistrationToken, InsertError<Self>>
|
||||
where
|
||||
D: 'static,
|
||||
{
|
||||
handle.insert_source(self, |_, queue, data| queue.dispatch_pending(data))
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> EventSource for WaylandSource<D> {
|
||||
type Event = ();
|
||||
|
||||
/// The underlying event queue.
|
||||
///
|
||||
/// You should call [`EventQueue::dispatch_pending`] inside your callback using this queue.
|
||||
type Metadata = EventQueue<D>;
|
||||
type Ret = Result<usize, DispatchError>;
|
||||
type Error = calloop::Error;
|
||||
|
||||
fn process_events<F>(
|
||||
&mut self,
|
||||
readiness: Readiness,
|
||||
token: Token,
|
||||
mut callback: F,
|
||||
) -> Result<PostAction, Self::Error>
|
||||
where
|
||||
F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret,
|
||||
{
|
||||
let queue = &mut self.queue;
|
||||
let read_guard = &mut self.read_guard;
|
||||
|
||||
let action = self.fd.process_events(readiness, token, |_, _| {
|
||||
// 1. read events from the socket if any are available
|
||||
if let Some(guard) = read_guard.take() {
|
||||
// might be None if some other thread read events before us, concurrently
|
||||
if let Err(WaylandError::Io(err)) = guard.read() {
|
||||
if err.kind() != io::ErrorKind::WouldBlock {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. dispatch any pending events in the queue
|
||||
// This is done to ensure we are not waiting for messages that are already in the buffer.
|
||||
Self::loop_callback_pending(queue, &mut callback)?;
|
||||
*read_guard = Some(Self::prepare_read(queue)?);
|
||||
|
||||
// 3. Once dispatching is finished, flush the responses to the compositor
|
||||
if let Err(WaylandError::Io(e)) = queue.flush() {
|
||||
if e.kind() != io::ErrorKind::WouldBlock {
|
||||
// in case of error, forward it and fast-exit
|
||||
return Err(e);
|
||||
}
|
||||
// WouldBlock error means the compositor could not process all our messages
|
||||
// quickly. Either it is slowed down or we are a spammer.
|
||||
// Should not really happen, if it does we do nothing and will flush again later
|
||||
}
|
||||
|
||||
Ok(PostAction::Continue)
|
||||
})?;
|
||||
|
||||
Ok(action)
|
||||
}
|
||||
|
||||
fn register(
|
||||
&mut self,
|
||||
poll: &mut Poll,
|
||||
token_factory: &mut TokenFactory,
|
||||
) -> calloop::Result<()> {
|
||||
self.fd.register(poll, token_factory)
|
||||
}
|
||||
|
||||
fn reregister(
|
||||
&mut self,
|
||||
poll: &mut Poll,
|
||||
token_factory: &mut TokenFactory,
|
||||
) -> calloop::Result<()> {
|
||||
self.fd.reregister(poll, token_factory)
|
||||
}
|
||||
|
||||
fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> {
|
||||
self.fd.unregister(poll)
|
||||
}
|
||||
|
||||
fn pre_run<F>(&mut self, mut callback: F) -> calloop::Result<()>
|
||||
where
|
||||
F: FnMut((), &mut Self::Metadata) -> Self::Ret,
|
||||
{
|
||||
debug_assert!(self.read_guard.is_none());
|
||||
|
||||
// flush the display before starting to poll
|
||||
if let Err(WaylandError::Io(err)) = self.queue.flush() {
|
||||
if err.kind() != io::ErrorKind::WouldBlock {
|
||||
// in case of error, don't prepare a read, if the error is persistent, it'll trigger in other
|
||||
// wayland methods anyway
|
||||
log::error!("Error trying to flush the wayland display: {}", err);
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
|
||||
// ensure we are not waiting for messages that are already in the buffer.
|
||||
Self::loop_callback_pending(&mut self.queue, &mut callback)?;
|
||||
self.read_guard = Some(Self::prepare_read(&mut self.queue)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_run<F>(&mut self, _: F) -> calloop::Result<()>
|
||||
where
|
||||
F: FnMut((), &mut Self::Metadata) -> Self::Ret,
|
||||
{
|
||||
// Drop implementation of ReadEventsGuard will do cleanup
|
||||
self.read_guard.take();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> WaylandSource<D> {
|
||||
/// Loop over the callback until all pending messages have been dispatched.
|
||||
fn loop_callback_pending<F>(queue: &mut EventQueue<D>, callback: &mut F) -> io::Result<()>
|
||||
where
|
||||
F: FnMut((), &mut EventQueue<D>) -> Result<usize, DispatchError>,
|
||||
{
|
||||
// Loop on the callback until no pending events are left.
|
||||
loop {
|
||||
match callback((), queue) {
|
||||
// No more pending events.
|
||||
Ok(0) => break Ok(()),
|
||||
|
||||
Ok(_) => continue,
|
||||
|
||||
Err(DispatchError::Backend(WaylandError::Io(err))) => {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Err(DispatchError::Backend(WaylandError::Protocol(err))) => {
|
||||
log::error!("Protocol error received on display: {}", err);
|
||||
|
||||
break Err(Errno::EPROTO.into());
|
||||
}
|
||||
|
||||
Err(DispatchError::BadMessage { msg, interface }) => {
|
||||
log::error!(
|
||||
"Bad message on interface \"{}\": (opcode: {}, args: {:?})",
|
||||
interface,
|
||||
msg.opcode,
|
||||
msg.args,
|
||||
);
|
||||
|
||||
break Err(Errno::EPROTO.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_read(queue: &mut EventQueue<D>) -> io::Result<ReadEventsGuard> {
|
||||
queue.prepare_read().map_err(|err| match err {
|
||||
WaylandError::Io(err) => err,
|
||||
|
||||
WaylandError::Protocol(err) => {
|
||||
log::error!("Protocol error received on display: {}", err);
|
||||
Errno::EPROTO.into()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue