From 16fbedecebe94fd70a187532585227d111cd08f1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 29 Nov 2021 14:07:13 -0500 Subject: [PATCH] refactor: move window to its own module --- Cargo.toml | 1 - examples/gtklauncher/main.rs | 270 +----------------------- examples/gtklauncher/window/.#window.ui | 1 - examples/gtklauncher/window/imp.rs | 55 +++++ examples/gtklauncher/window/mod.rs | 248 ++++++++++++++++++++++ examples/gtklauncher/window/window.ui | 16 +- 6 files changed, 321 insertions(+), 270 deletions(-) delete mode 120000 examples/gtklauncher/window/.#window.ui diff --git a/Cargo.toml b/Cargo.toml index a0e96f03..aebdb0d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,3 @@ x11 = { version = "2", features = ["xlib"] } pop-launcher = "=1.0.3" serde_json = "1.0.70" once_cell = "1.8.0" -wnck-sys = "0.1.0" diff --git a/examples/gtklauncher/main.rs b/examples/gtklauncher/main.rs index da621613..9dfc8173 100644 --- a/examples/gtklauncher/main.rs +++ b/examples/gtklauncher/main.rs @@ -1,3 +1,5 @@ +mod window; + use gtk4 as gtk; mod application_row; use application_row::ApplicationRow; @@ -5,6 +7,9 @@ use gtk::gdk::Display; use gtk::prelude::*; use gtk::{gio, glib}; use libcosmic::x; + +use window::Window; + fn main() { let application = gtk::Application::new( Some("com.github.gtk-rs.examples.apps_launcher"), @@ -19,271 +24,14 @@ fn main() { &provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, ); - build_ui(app); + new_build_ui(app); }); application.run(); } -fn build_ui(app: >k::Application) { - let window = gtk::ApplicationWindow::builder() - .decorated(false) - .default_width(600) - .default_height(600) - .application(app) - .title("ListView: Applications Launcher") - .build(); - - let model = gio::ListStore::new(gio::AppInfo::static_type()); - gio::AppInfo::all().iter().for_each(|app_info| { - model.append(app_info); - }); - let window_model = gtk::Window::list_toplevels(); - dbg!(window_model.clone()); - for i in window_model { - dbg!(i); - } - // TODO window ui file and custom class - // TODO application search entry ui file and custom class - // TODO list open windows in application search entries - // TODO list aliases in application search entries - let factory = gtk::SignalListItemFactory::new(); - // the "setup" stage is used for creating the widgets - factory.connect_setup(move |_factory, item| { - let row = ApplicationRow::new(); - 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 - .item() - .unwrap() - .downcast::() - .unwrap(); - println!("position: {}", &list_item.position()); - println!("{}", app_info.name()); - // println!("{}", app_info.description()); - - let child = list_item - .child() - .unwrap() - .downcast::() - .unwrap(); - child.set_app_info(&app_info); - if list_item.position() < 9 { - child.set_shortcut(list_item.position() + 1); - } - }); - - // 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 selection_model = gtk::SingleSelection::new(Some(&slice_model)); - - let list_view = gtk::ListView::new(Some(&selection_model), Some(&factory)); - let action_launch = gio::SimpleAction::new("launch", Some(&i32::static_variant_type())); - action_launch.connect_activate(glib::clone!(@weak list_view => move |_action, parameter| { - // Get parameter - let parameter = parameter - .expect("Could not get parameter.") - .get::() - .expect("The variant needs to be of type `i32`."); - println!("{}", parameter); - let model = list_view.model().unwrap(); - let app_info = model - .item(u32::try_from(parameter).unwrap()); - - if app_info.is_none() {return} - let app_info = app_info - .unwrap() - .downcast::() - .unwrap(); - - 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(); - } - - })); - window.add_action(&action_launch); - for i in 1..10 { - let action_launchi = gio::SimpleAction::new(&format!("launch{}", i), None); - app.set_accels_for_action(&format!("win.launch{}", i), &[&format!("{}", i)]); - window.add_action(&action_launchi); - action_launchi.connect_activate( - glib::clone!(@weak action_launch, @weak list_view => move |_action, _parameter| { - let model = list_view.model().unwrap(); - let app_info = model - .item(i-1); - - println!("launching item {}", i); - if app_info.is_none() {return} - let app_info = app_info - .unwrap() - .downcast::() - .unwrap(); - println!("starting {}", app_info.name()); - 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(); - } - }), - ); - } - - // Launch the application when an item of the list is activated - list_view.connect_activate(move |list_view, position| { - let model = list_view.model().unwrap(); - let app_info = model - .item(position) - .unwrap() - .downcast::() - .unwrap(); - - 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(); - } - }); - - let scrolled_window = gtk::ScrolledWindow::builder() - .hscrollbar_policy(gtk::PolicyType::Never) // Disable horizontal scrolling - .min_content_width(360) - .vexpand(true) - .child(&list_view) - .build(); - - let launcher_box = gtk::Box::builder() - .margin_top(12) - .margin_bottom(12) - .margin_start(12) - .margin_end(12) - //.valign(gtk::Align::Center) - //.halign(gtk::Align::Center) - //.spacing(12) - .orientation(gtk::Orientation::Vertical) - .build(); - let search_input = gtk::Entry::builder() - //.margin_top(12) - .margin_bottom(12) - //.margin_start(12) - //.margin_end(12) - //.valign(gtk::Align::Center) - //.halign(gtk::Align::Center) - .build(); - // Filter model whenever the value of the key "filter" changes - search_input.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(); - } - - 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() - } - }); - - filter_model.set_filter(Some(new_filter).as_ref()); - sorted_model.set_sorter(Some(new_sorter).as_ref()); - }), - ); - // 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"); - } - }); - - launcher_box.append(&search_input); - launcher_box.append(&scrolled_window); - window.set_child(Some(&launcher_box)); - // Add action "quit" to `window` taking no parameter - app.set_accels_for_action("win.quit", &["W", "Escape"]); - 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(); - } - println!("active or not lets find out..."); - }); +fn new_build_ui(app: >k::Application) { + // Create a new custom window and show it + let window = Window::new(app); window.show(); } diff --git a/examples/gtklauncher/window/.#window.ui b/examples/gtklauncher/window/.#window.ui deleted file mode 120000 index d0bd9687..00000000 --- a/examples/gtklauncher/window/.#window.ui +++ /dev/null @@ -1 +0,0 @@ -wash@pop-os.22535:1638129315 \ No newline at end of file diff --git a/examples/gtklauncher/window/imp.rs b/examples/gtklauncher/window/imp.rs index 8b137891..901181cf 100644 --- a/examples/gtklauncher/window/imp.rs +++ b/examples/gtklauncher/window/imp.rs @@ -1 +1,56 @@ +use gtk4 as gtk; +use glib::subclass::InitializingObject; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::{gio, glib}; +use gtk::{CompositeTemplate, Entry, ListView}; +use once_cell::sync::OnceCell; + +// Object holding the state +#[derive(CompositeTemplate, Default)] +#[template(file = "window.ui")] +pub struct Window { + #[template_child] + pub entry: TemplateChild, + #[template_child] + pub list_view: TemplateChild, + pub model: OnceCell, +} + +// The central trait for subclassing a GObject +#[glib::object_subclass] +impl ObjectSubclass for Window { + // `NAME` needs to match `class` attribute of template + const NAME: &'static str = "LauncherWindow"; + type Type = super::Window; + type ParentType = gtk::ApplicationWindow; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } +} +// Trait shared by all GObjects +impl ObjectImpl for Window { + fn constructed(&self, obj: &Self::Type) { + // Call "constructed" on parent + self.parent_constructed(obj); + + // Setup + obj.setup_model(); + obj.setup_callbacks(); + obj.setup_factory(); + } +} +// Trait shared by all widgets +impl WidgetImpl for Window {} + +// Trait shared by all windows +impl WindowImpl for Window {} + +// Trait shared by all application +impl ApplicationWindowImpl for Window {} diff --git a/examples/gtklauncher/window/mod.rs b/examples/gtklauncher/window/mod.rs index 8b137891..a3b5441c 100644 --- a/examples/gtklauncher/window/mod.rs +++ b/examples/gtklauncher/window/mod.rs @@ -1 +1,249 @@ +mod imp; +use gtk4 as gtk; +use crate::application_row::ApplicationRow; +use glib::Object; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::{gio, glib}; +use gtk::{Application, SignalListItemFactory}; + +use libcosmic::x; + +glib::wrapper! { + pub struct Window(ObjectSubclass) + @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget, + @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, + gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; +} + +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`.") + } + + 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"); + + // 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 selection_model = gtk::SingleSelection::new(Some(&slice_model)); + + // Wrap model with selection and pass it to the list view + imp.list_view.set_model(Some(&selection_model)); + } + + fn setup_callbacks(&self) { + // Get state + 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| { + 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)) { + + 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") + } + println!("{}", i-1); + })); + } + + // Launch the application when an item of the list is activated + list_view.connect_activate(move |list_view, position| { + let model = list_view.model().unwrap(); + let app_info = model + .item(position) + .unwrap() + .downcast::() + .unwrap(); + + 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(); + } + }); + + 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(); + } + + 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() + } + }); + + filter_model.set_filter(Some(new_filter).as_ref()); + sorted_model.set_sorter(Some(new_sorter).as_ref()); + }), + ); + + 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(); + })); + self.add_action(&action_quit); + + window.connect_is_active_notify(|win| { + if !win.is_active() { + win.close(); + } + println!("active or not lets find out..."); + }); + } + + fn setup_factory(&self) { + let factory = SignalListItemFactory::new(); + factory.connect_setup(move |_factory, item| { + let row = ApplicationRow::new(); + 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 + .item() + .unwrap() + .downcast::() + .unwrap(); + println!("position: {}", &list_item.position()); + println!("{}", app_info.name()); + // println!("{}", app_info.description()); + + let child = list_item + .child() + .unwrap() + .downcast::() + .unwrap(); + child.set_app_info(&app_info); + if list_item.position() < 9 { + child.set_shortcut(list_item.position() + 1); + } + }); + // Set the factory of the list view + let imp = imp::Window::from_instance(self); + imp.list_view.set_factory(Some(&factory)); + } +} diff --git a/examples/gtklauncher/window/window.ui b/examples/gtklauncher/window/window.ui index d061a7fa..9ff6532d 100644 --- a/examples/gtklauncher/window/window.ui +++ b/examples/gtklauncher/window/window.ui @@ -1,8 +1,9 @@ -