cosmic-applets/applets/cosmic-app-list/src/main.rs

282 lines
13 KiB
Rust

// 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 std::collections::BTreeMap;
use utils::{block_on, AppListEvent, BoxedWindowList, DEST, PATH};
use wayland::{Toplevel, ToplevelEvent};
mod apps_container;
mod apps_window;
mod config;
mod dock_item;
mod dock_list;
mod dock_object;
mod dock_popover;
mod localize;
mod utils;
mod wayland;
mod wayland_source;
const ID: &str = "com.system76.CosmicAppList";
static TX: OnceCell<glib::Sender<AppListEvent>> = OnceCell::new();
static WAYLAND_TX: OnceCell<SyncSender<ToplevelEvent>> = 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() {
// Initialize logger
pretty_env_logger::init();
glib::set_application_name("Cosmic Dock App List");
localize();
gio::resources_register_include!("compiled.gresource").unwrap();
let app = gtk4::Application::new(Some(ID), ApplicationFlags::default());
app.connect_activate(|app| {
load_css();
let (tx, rx) = glib::MainContext::channel(glib::Priority::default());
let window = CosmicAppListWindow::new(app);
let wayland_tx = wayland::spawn_toplevels();
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();
rx.attach(None, glib::clone!(@weak window => @default-return glib::prelude::Continue(true), move |event| {
let apps_container = window.apps_container();
let should_apply_changes = match event {
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_name() == 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_name() == 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);
}
}
let _ = tx.send(AppListEvent::Refresh);
false
}
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.app_id) {
v.0.push(elem.clone());
} else {
acc.insert(
elem.app_id.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)| Some(&s.0[0].app_id) == cur_app_info.filename().and_then(|p| p
.file_stem()
.and_then(|s| s.to_str().map(|s| s.to_string()))).as_ref())
{
// 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| Some(&s.app_id) == cur_app_info.filename().and_then(|p| p
.file_stem()
.and_then(|s| s.to_str().map(|s| s.to_string()))).as_ref())
{
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()
.filter_map(|v| DockObject::from_window_list(v).map(|o| o.upcast()))
.collect();
active_app_model.splice(0, model_len, &new_results[..]);
true
}
AppListEvent::WindowList(toplevels) => {
cached_results = toplevels;
true
}
AppListEvent::Remove(top_level) => {
if let Some(i) = cached_results.iter().position(|t| t.toplevel_handle == top_level.toplevel_handle) {
cached_results.swap_remove(i);
}
true
}
AppListEvent::Add(top_level) => {
// sort to make comparison with cache easier
if let Some(i) = cached_results.iter().position(|t| t.toplevel_handle == top_level.toplevel_handle) {
cached_results[i] = top_level;
} else {
cached_results.push(top_level);
}
true
}
};
if should_apply_changes {
// dbg!(&cached_results);
// build active app stacks for each app
let stack_active = cached_results.iter().fold(
BTreeMap::new(),
|mut acc: BTreeMap<String, BoxedWindowList>, elem| {
if let Some(v) = acc.get_mut(&elem.app_id) {
v.0.push(elem.clone());
} else {
acc.insert(
elem.app_id.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)| Some(&s.0[0].app_id) == cur_app_info.filename().and_then(|p| p
.file_stem()
.and_then(|s| s.to_str().map(|s| s.to_string()))).as_ref())
{
// 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| Some(&s.app_id) == cur_app_info.filename().and_then(|p| p
.file_stem()
.and_then(|s| s.to_str().map(|s| s.to_string()))).as_ref())
{
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()
.filter_map(|v| DockObject::from_window_list(v).map(|o| o.upcast()))
.collect();
active_app_model.splice(0, model_len, &new_results[..]);
}
glib::prelude::Continue(true)
}));
window.show();
});
app.run();
}