From fba59c5290a80b6c211444f5e2e5eebb9e928d47 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 1 Dec 2021 09:07:26 -0500 Subject: [PATCH] refactor: use template for window --- examples/launcher/main.rs | 269 +++++++++-------------------- examples/launcher/style.css | 0 examples/launcher/window/mod.rs | 202 ++++++++-------------- examples/launcher/window/window.ui | 1 + 4 files changed, 150 insertions(+), 322 deletions(-) create mode 100644 examples/launcher/style.css diff --git a/examples/launcher/main.rs b/examples/launcher/main.rs index e5994b1b..46507553 100644 --- a/examples/launcher/main.rs +++ b/examples/launcher/main.rs @@ -1,22 +1,28 @@ mod application_object; mod application_row; -use gio::DesktopAppInfo; -use gtk4 as gtk; -use gtk4::SliceListModel; +mod window; + +use gdk4::Display; +use gio::DesktopAppInfo; +use gtk::Application; +use gtk4 as gtk; +use gtk4::CssProvider; +use gtk4::StyleContext; +use once_cell::sync::Lazy; +use std::sync::Mutex; -use cascade::cascade; use gtk::gio; +use gtk::glib; use gtk::prelude::*; -use gtk::{glib, ListView, SignalListItemFactory, SingleSelection}; -use libcosmic::x; use pop_launcher_service::IpcClient; use postage::mpsc::Sender; use postage::prelude::*; use self::application_object::ApplicationObject; -use self::application_row::ApplicationRow; +use self::window::Window; const NUM_LAUNCHER_ITEMS: u8 = 10; +static TX: Lazy>>> = Lazy::new(|| Mutex::new(None)); fn icon_source(icon: >k::Image, source: &Option) { match source { @@ -32,7 +38,7 @@ fn icon_source(icon: >k::Image, source: &Option) { } } -enum Event { +pub enum Event { Response(pop_launcher::Response), Search(String), Activate(u32), @@ -53,200 +59,85 @@ fn spawn_launcher(mut tx: Sender) -> IpcClient { launcher } +fn setup_shortcuts(app: &Application) { + //quit shortcut + app.set_accels_for_action("win.quit", &["W", "Escape"]); + //launch shortcuts + for i in 1..NUM_LAUNCHER_ITEMS { + app.set_accels_for_action(&format!("win.launch{}", i), &[&format!("{}", i)]); + } +} + +fn load_css() { + // Load the css file and add it to the provider + let provider = CssProvider::new(); + provider.load_from_data(include_bytes!("style.css")); + + // Add the provider to the default screen + StyleContext::add_provider_for_display( + &Display::default().expect("Error initializing GTK CSS provider."), + &provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); +} + fn main() { let app = gtk::Application::builder() .application_id("com.system76.Launcher") .build(); + app.connect_startup(|app| { + setup_shortcuts(app); + load_css() + }); app.connect_activate(move |app| { let (tx, mut rx) = postage::mpsc::channel(1); let mut launcher = spawn_launcher(tx.clone()); - - //quit shortcut - app.set_accels_for_action("win.quit", &["W", "Escape"]); - //launch shortcuts - for i in 1..10 { - app.set_accels_for_action(&format!("win.launch{}", i), &[&format!("{}", i)]); - } - - let window = gtk::ApplicationWindow::builder() - .application(app) - .decorated(false) - .default_width(600) - .default_height(440) - .title("Launcher") - .resizable(false) - .build(); - - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 16); - vbox.set_margin_start(16); - vbox.set_margin_end(16); - vbox.set_margin_top(16); - vbox.set_margin_bottom(16); - window.set_child(Some(&vbox)); - - let search = gtk::Entry::new(); - search.set_placeholder_text(Some(" Type to search apps, or type '?' for more options.")); - vbox.append(&search); - - let model = gio::ListStore::new(ApplicationObject::static_type()); - let factory = SignalListItemFactory::new(); - factory.connect_setup(move |_, list_item| { - let row = ApplicationRow::new(); - list_item.set_child(Some(&row)) - }); - factory.connect_bind(move |_, list_item| { - let application_object = list_item - .item() - .expect("The item has to exist.") - .downcast::() - .expect("The item has to be an `ApplicationObject`"); - let row = list_item - .child() - .expect("The list item child needs to exist.") - .downcast::() - .expect("The list item type needs to be `ApplicationRow`"); - if list_item.position() < 9 { - row.set_shortcut(list_item.position() + 1); - } - - row.set_app_info(application_object); - }); - let slice_model = SliceListModel::new(Some(&model), 0, NUM_LAUNCHER_ITEMS.into()); - let selection_model = SingleSelection::new(Some(&slice_model)); - let list_view = ListView::new(Some(&selection_model), Some(&factory)); - let scroll = cascade! { - gtk::ScrolledWindow::new(); - ..set_min_content_height(400); - ..set_max_content_height(700); - ..set_propagate_natural_height(true); - ..set_vexpand(true); - ..set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic); - }; - scroll.set_child(Some(&list_view)); - vbox.append(&scroll); - - for i in 1..10 { - let action_launchi = gio::SimpleAction::new(&format!("launch{}", i), None); - window.add_action(&action_launchi); - action_launchi.connect_activate( - glib::clone!(@weak list_view, @strong tx => move |_action, _parameter| { - println!("acitvating... {}", i); - let model = list_view.model().unwrap(); - let app_info = model.item(i - 1); - if app_info.is_none() { - println!("oops no app for this row..."); - return; - } - if let Ok(id)= app_info.unwrap().property("id") { - let id = id.get::().expect("App ID must be u32"); - let mut tx = tx.clone(); - - glib::MainContext::default().spawn_local(async move { - let _ = tx.send(Event::Activate(id)).await; - }); - } - }), - ); - } - - list_view.connect_activate(glib::clone!(@strong tx => move |list_view, i| { - println!("acitvating... {}", i + 1); - let model = list_view.model().unwrap(); - let app_info = model.item(i); - if app_info.is_none() { - println!("oops no app for this row..."); - return; - } - if let Ok(id)= app_info.unwrap().property("id") { - let id = id.get::().expect("App ID must be u32"); - let mut tx = tx.clone(); - - glib::MainContext::default().spawn_local(async move { - let _ = tx.send(Event::Activate(id)).await; - }); - } - })); - { - let search_changed = glib::clone!(@strong tx => move |search: >k::Entry| { - let search = search.text().to_string(); - - let mut tx = tx.clone(); - glib::MainContext::default().spawn_local(async move { - let _ = tx.send(Event::Search(search)).await; - }); - }); - - search_changed(&search); - search.connect_changed(search_changed); + let mut global_tx = TX.lock().unwrap(); + *global_tx = Some(tx.clone()); } - // Setting the window to dialog type must happen between realize and show. Dialog windows - // show up centered on the display with the cursor, so we do not have to set position - window.connect_realize(move |window| { - if let Some((display, surface)) = x::get_window_x11(window) { - unsafe { - x::change_property( - &display, - &surface, - "_NET_WM_WINDOW_TYPE", - x::PropMode::Replace, - &[x::Atom::new(&display, "_NET_WM_WINDOW_TYPE_DIALOG").unwrap()], - ); - } - } else { - println!("failed to get X11 window"); - } - }); - let action_quit = gio::SimpleAction::new("quit", None); - action_quit.connect_activate(glib::clone!(@weak window => move |_, _| { - window.close(); - })); - window.add_action(&action_quit); - - window.connect_is_active_notify(|win| { - if !win.is_active() { - win.close(); - } - }); - + let window = Window::new(app); + let wclone = window.clone(); window.show(); - glib::MainContext::default().spawn_local(async move { - while let Some(event) = rx.recv().await { - match event { - Event::Search(search) => { - let _ = launcher.send(pop_launcher::Request::Search(search)).await; - } - Event::Activate(index) => { - let _ = launcher.send(pop_launcher::Request::Activate(index)).await; - } - Event::Response(event) => { - if let pop_launcher::Response::Update(results) = event { - let model_len = model.n_items(); - dbg!(&results); - let new_results: Vec = results - [0..std::cmp::min(results.len(), NUM_LAUNCHER_ITEMS.into())] - .iter() - .map(|result| ApplicationObject::new(result).upcast()) - .collect(); - model.splice(0, model_len, &new_results[..]); - } else if let pop_launcher::Response::DesktopEntry { - path, - gpu_preference: _gpu_preference, // TODO use GPU preference when launching app - } = event - { - let app_info = - DesktopAppInfo::new(&path.file_name().expect("desktop entry path needs to be a valid filename").to_string_lossy()) - .expect("failed to create a Desktop App info for launching the application."); - app_info - .launch(&[], Some(&window.display().app_launch_context().clone())).expect("failed to launch the application."); - } - } - } - } - }) + glib::MainContext::default().spawn_local(async move { + while let Some(event) = rx.recv().await { + match event { + Event::Search(search) => { + let _ = launcher.send(pop_launcher::Request::Search(search)).await; + } + Event::Activate(index) => { + let _ = launcher.send(pop_launcher::Request::Activate(index)).await; + } + + Event::Response(event) => { + if let pop_launcher::Response::Update(results) = event { + let model = window.model(); + let model_len = model.n_items(); + dbg!(&results); + let new_results: Vec = results + [0..std::cmp::min(results.len(), NUM_LAUNCHER_ITEMS.into())] + .iter() + .map(|result| ApplicationObject::new(result).upcast()) + .collect(); + model.splice(0, model_len, &new_results[..]); + } else if let pop_launcher::Response::DesktopEntry { + path, + gpu_preference: _gpu_preference, // TODO use GPU preference when launching app + } = event + { + let app_info = + DesktopAppInfo::new(&path.file_name().expect("desktop entry path needs to be a valid filename").to_string_lossy()) + .expect("failed to create a Desktop App info for launching the application."); + app_info + .launch(&[], Some(&wclone.display().app_launch_context().clone())).expect("failed to launch the application."); + } + } + } + } + }) }); app.run(); diff --git a/examples/launcher/style.css b/examples/launcher/style.css new file mode 100644 index 00000000..e69de29b diff --git a/examples/launcher/window/mod.rs b/examples/launcher/window/mod.rs index b5a530cb..97d8f7b6 100644 --- a/examples/launcher/window/mod.rs +++ b/examples/launcher/window/mod.rs @@ -1,5 +1,8 @@ mod imp; +use crate::ApplicationObject; +use crate::TX; use gtk4 as gtk; +use postage::prelude::Sink; use crate::application_row::ApplicationRow; use glib::Object; @@ -17,51 +20,29 @@ glib::wrapper! { gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; } +const NUM_LAUNCHER_ITEMS: u8 = 9; + impl Window { pub fn new(app: &Application) -> Self { - //quit shortcut - app.set_accels_for_action("win.quit", &["W", "Escape"]); - //launch shortcuts - for i in 1..10 { - app.set_accels_for_action(&format!("win.launch{}", i), &[&format!("{}", i)]); - } - Object::new(&[("application", app)]).expect("Failed to create `Window`.") + let self_: Self = Object::new(&[("application", app)]).expect("Failed to create `Window`."); + self_ } - fn model(&self) -> &gio::ListStore { + pub fn model(&self) -> &gio::ListStore { // Get state let imp = imp::Window::from_instance(self); imp.model.get().expect("Could not get model") } fn setup_model(&self) { - // Create new model - let model = gio::ListStore::new(gio::AppInfo::static_type()); - gio::AppInfo::all().iter().for_each(|app_info| { - model.append(app_info); - }); - // Get state and set model let imp = imp::Window::from_instance(self); - imp.model.set(model.clone()).expect("Could not set model"); + let model = gio::ListStore::new(ApplicationObject::static_type()); - // A sorter used to sort AppInfo in the model by their name - let sorter = gtk::CustomSorter::new(move |obj1, obj2| { - let app_info1 = obj1.downcast_ref::().unwrap(); - let app_info2 = obj2.downcast_ref::().unwrap(); - - app_info1 - .name() - .to_lowercase() - .cmp(&app_info2.name().to_lowercase()) - .into() - }); - let filter = gtk::CustomFilter::new(|_obj| true); - let filter_model = gtk::FilterListModel::new(Some(&model), Some(filter).as_ref()); - let sorted_model = gtk::SortListModel::new(Some(&filter_model), Some(&sorter)); - let slice_model = gtk::SliceListModel::new(Some(&sorted_model), 0, 9); + let slice_model = gtk::SliceListModel::new(Some(&model), 0, NUM_LAUNCHER_ITEMS.into()); let selection_model = gtk::SingleSelection::new(Some(&slice_model)); + imp.model.set(model).expect("Could not set model"); // Wrap model with selection and pass it to the list view imp.list_view.set_model(Some(&selection_model)); } @@ -71,118 +52,75 @@ impl Window { let imp = imp::Window::from_instance(self); let window = self.clone().upcast::(); let list_view = &imp.list_view; - let sorted_model = list_view - .model() - .expect("List view missing selection model") - .downcast::() - .expect("could not downcast listview model to single selection model") - .model() - .downcast::() - .expect("could not downcast single selection model to slice list model.") - .model() - .expect("sorted list model is missing from slice list model") - .downcast::() - .expect("sorted list model could not be downcast"); - let filter_model = sorted_model - .model() - .expect("missing model for sort list model.") - .downcast::() - .expect("could not downcast sort list model to filter list model"); - let entry = &imp.entry; let lv = list_view.get(); for i in 1..10 { let action_launchi = gio::SimpleAction::new(&format!("launch{}", i), None); self.add_action(&action_launchi); - let context = list_view.display().app_launch_context().clone(); - let parent_window = list_view.root().unwrap().downcast::().unwrap(); action_launchi.connect_activate(glib::clone!(@weak lv => move |_action, _parameter| { + println!("acitvating... {}", i); let model = lv.model().unwrap(); let app_info = model.item(i - 1); if app_info.is_none() { println!("oops no app for this row..."); return; } - let app_info = app_info.unwrap().downcast::().unwrap(); - if let Err(err) = app_info.launch(&[], Some(&context)) { + if let Ok(id)= app_info.unwrap().property("id") { + let id = id.get::().expect("App ID must be u32"); - gtk::MessageDialog::builder() - .text(&format!("Failed to start {}", app_info.name())) - .secondary_text(&err.to_string()) - .message_type(gtk::MessageType::Error) - .modal(true) - .transient_for(&parent_window) - .build() - .show(); - - println!("oops launch failed") + glib::MainContext::default().spawn_local(async move { + if let Ok(tx) = TX.lock() { + if let Some(tx) = &*tx { + let _ = tx.clone().send(crate::Event::Activate(id)).await; + }} + }); } - println!("{}", i-1); })); } - - // Launch the application when an item of the list is activated - list_view.connect_activate(move |list_view, position| { + list_view.connect_activate(move |list_view, i| { + println!("acitvating... {}", i + 1); let model = list_view.model().unwrap(); - let app_info = model - .item(position) - .unwrap() - .downcast::() - .unwrap(); + let app_info = model.item(i); + if app_info.is_none() { + println!("oops no app for this row..."); + return; + } + if let Ok(id) = app_info.unwrap().property("id") { + let id = id.get::().expect("App ID must be u32"); - let context = list_view.display().app_launch_context(); - if let Err(err) = app_info.launch(&[], Some(&context)) { - let parent_window = list_view.root().unwrap().downcast::().unwrap(); - - gtk::MessageDialog::builder() - .text(&format!("Failed to start {}", app_info.name())) - .secondary_text(&err.to_string()) - .message_type(gtk::MessageType::Error) - .modal(true) - .transient_for(&parent_window) - .build() - .show(); + glib::MainContext::default().spawn_local(async move { + if let Ok(tx) = TX.lock() { + if let Some(tx) = &*tx { + let _ = tx.clone().send(crate::Event::Activate(id)).await; + } + } + }); } }); - entry.connect_changed( - glib::clone!(@weak filter_model, @weak sorted_model => move |search: >k::Entry| { - let search_text = search.text().to_string().to_lowercase(); - let new_filter: gtk::CustomFilter = gtk::CustomFilter::new(move |obj| { - let search_res = obj.downcast_ref::() - .expect("The Object needs to be of type AppInfo"); - search_res.name().to_string().to_lowercase().contains(&search_text) - }); - let search_text = search.text().to_string().to_lowercase(); - let new_sorter: gtk::CustomSorter = gtk::CustomSorter::new(move |obj1, obj2| { - let app_info1 = obj1.downcast_ref::().unwrap(); - let app_info2 = obj2.downcast_ref::().unwrap(); - if search_text == "" { - return app_info1 - .name() - .to_lowercase() - .cmp(&app_info2.name().to_lowercase()) - .into(); - } + entry.connect_changed(move |search: >k::Entry| { + let search = search.text().to_string(); - let i_1 = app_info1.name().to_lowercase().find(&search_text); - let i_2 = app_info2.name().to_lowercase().find(&search_text); - match (i_1, i_2) { - (Some(i_1), Some(i_2)) => i_1.cmp(&i_2).into(), - (Some(_), None) => std::cmp::Ordering::Less.into(), - (None, Some(_)) => std::cmp::Ordering::Greater.into(), - _ => app_info1 - .name() - .to_lowercase() - .cmp(&app_info2.name().to_lowercase()) - .into() + glib::MainContext::default().spawn_local(async move { + if let Ok(tx) = TX.lock() { + if let Some(tx) = &*tx { + let _ = tx.clone().send(crate::Event::Search(search)).await; } - }); + } + }); + }); - filter_model.set_filter(Some(new_filter).as_ref()); - sorted_model.set_sorter(Some(new_sorter).as_ref()); - }), - ); + entry.connect_realize(move |search: >k::Entry| { + let search = search.text().to_string(); + + glib::MainContext::default().spawn_local(async move { + if let Ok(tx) = TX.lock() { + if let Some(tx) = &*tx { + let _ = tx.clone().send(crate::Event::Search(search)).await; + } + } + }); + }); window.connect_realize(move |window| { if let Some((display, surface)) = x::get_window_x11(window) { @@ -215,28 +153,26 @@ impl Window { fn setup_factory(&self) { let factory = SignalListItemFactory::new(); - factory.connect_setup(move |_factory, item| { + factory.connect_setup(move |_, list_item| { let row = ApplicationRow::new(); - item.set_child(Some(&row)); + list_item.set_child(Some(&row)) }); - - // the bind stage is used for "binding" the data to the created widgets on the "setup" stage - factory.connect_bind(move |_factory, list_item| { - let app_info = list_item + factory.connect_bind(move |_, list_item| { + let application_object = list_item .item() - .unwrap() - .downcast::() - .unwrap(); - - let child = list_item + .expect("The item has to exist.") + .downcast::() + .expect("The item has to be an `ApplicationObject`"); + let row = list_item .child() - .unwrap() + .expect("The list item child needs to exist.") .downcast::() - .unwrap(); - child.set_app_info(&app_info); + .expect("The list item type needs to be `ApplicationRow`"); if list_item.position() < 9 { - child.set_shortcut(list_item.position() + 1); + row.set_shortcut(list_item.position() + 1); } + + row.set_app_info(application_object); }); // Set the factory of the list view let imp = imp::Window::from_instance(self); diff --git a/examples/launcher/window/window.ui b/examples/launcher/window/window.ui index 9ff6532d..516e1f09 100644 --- a/examples/launcher/window/window.ui +++ b/examples/launcher/window/window.ui @@ -4,6 +4,7 @@ 600 Gtk Pop Launcher false + false vertical