diff --git a/Cargo.toml b/Cargo.toml index 2925af7..36c0f11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,17 +10,7 @@ gdk4-x11 = "0.3.0" gio = "0.14.8" gtk4 = "0.3.1" x11 = { version = "2", features = ["xlib"] } - # examples/launcher -#pop-launcher = "1.0.3" -pop-launcher = { git = "https://github.com/pop-os/launcher", branch = "master" } -serde_json = "1.0.72" -pop-launcher-service = { git = "https://github.com/pop-os/launcher", branch = "master" } -postage = "0.4.1" -futures = "0.3.18" -glib = "0.14.8" -# examples/gtklauncher +pop-launcher = "1.0.3" +serde_json = "1.0.70" once_cell = "1.8.0" -xdg = "2.4.0" -serde = "1.0.130" -x11rb = "0.9.0" diff --git a/examples/gtklauncher/README.md b/examples/gtklauncher/README.md new file mode 100644 index 0000000..f2a6453 --- /dev/null +++ b/examples/gtklauncher/README.md @@ -0,0 +1,5 @@ +# ListView: Applications Launcher + +This example shows how to create a `gtk::ListView` and fill it with applications data from `gio::AppInfo` with the possibility to open an application when an item of the list is activated. + +![Screenshot](screenshot.png) diff --git a/examples/gtklauncher/application_row/application_row.ui b/examples/gtklauncher/application_row/application_row.ui new file mode 100644 index 0000000..a70d795 --- /dev/null +++ b/examples/gtklauncher/application_row/application_row.ui @@ -0,0 +1,53 @@ + + + + diff --git a/examples/gtklauncher/application_row/imp.rs b/examples/gtklauncher/application_row/imp.rs new file mode 100644 index 0000000..f27eb54 --- /dev/null +++ b/examples/gtklauncher/application_row/imp.rs @@ -0,0 +1,38 @@ +use gtk::glib; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk4 as gtk; + +use gtk::CompositeTemplate; + +#[derive(Debug, Default, CompositeTemplate)] +#[template(file = "application_row.ui")] +pub struct ApplicationRow { + #[template_child] + pub name: TemplateChild, + #[template_child] + pub description: TemplateChild, + #[template_child] + pub shortcut: TemplateChild, + #[template_child] + pub image: TemplateChild, +} + +#[glib::object_subclass] +impl ObjectSubclass for ApplicationRow { + const NAME: &'static str = "ApplicationRow"; + type Type = super::ApplicationRow; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } +} + +impl ObjectImpl for ApplicationRow {} +impl WidgetImpl for ApplicationRow {} +impl BoxImpl for ApplicationRow {} diff --git a/examples/gtklauncher/application_row/mod.rs b/examples/gtklauncher/application_row/mod.rs new file mode 100644 index 0000000..8054c92 --- /dev/null +++ b/examples/gtklauncher/application_row/mod.rs @@ -0,0 +1,39 @@ +use gtk4 as gtk; +mod imp; + +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::{gio, glib}; + +glib::wrapper! { + pub struct ApplicationRow(ObjectSubclass) + @extends gtk::Widget, gtk::Box; +} + +impl Default for ApplicationRow { + fn default() -> Self { + Self::new() + } +} + +impl ApplicationRow { + pub fn new() -> Self { + glib::Object::new(&[]).expect("Failed to create ApplicationRow") + } + + pub fn set_app_info(&self, app_info: &gio::AppInfo) { + let self_ = imp::ApplicationRow::from_instance(self); + self_.name.set_text(&app_info.name()); + if let Some(desc) = app_info.description() { + self_.description.set_text(&desc); + } + if let Some(icon) = app_info.icon() { + self_.image.set_from_gicon(&icon); + } + } + + pub fn set_shortcut(&self, indx: u32) { + let self_ = imp::ApplicationRow::from_instance(self); + self_.shortcut.set_text(&format!("Ctrl + {}", indx)); + } +} diff --git a/examples/gtklauncher/main.rs b/examples/gtklauncher/main.rs new file mode 100644 index 0000000..584e189 --- /dev/null +++ b/examples/gtklauncher/main.rs @@ -0,0 +1,34 @@ +mod application_row; +mod window; + +use gtk::gdk::Display; +use gtk::prelude::*; +use gtk4 as gtk; + +use window::Window; + +fn main() { + let application = gtk::Application::new( + Some("com.github.gtk-rs.examples.apps_launcher"), + Default::default(), + ); + + application.connect_activate(|app| { + let provider = gtk::CssProvider::new(); + provider.load_from_data(include_bytes!("style.css")); + gtk::StyleContext::add_provider_for_display( + &Display::default().expect("Error initializing gtk css provider."), + &provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + new_build_ui(app); + }); + + application.run(); +} + +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/screenshot.png b/examples/gtklauncher/screenshot.png new file mode 100644 index 0000000..8ffcefa Binary files /dev/null and b/examples/gtklauncher/screenshot.png differ diff --git a/examples/gtklauncher/style.css b/examples/gtklauncher/style.css new file mode 100644 index 0000000..0c72f8a --- /dev/null +++ b/examples/gtklauncher/style.css @@ -0,0 +1,15 @@ +description { + line-height: 1.5em; + background-image: none; + background-color: red; +} + +row.row1 { + background-image: none; + background-color: black; +} + +shortcut { + background-image: none; + background-color: green; +} diff --git a/examples/gtklauncher/window/imp.rs b/examples/gtklauncher/window/imp.rs new file mode 100644 index 0000000..901181c --- /dev/null +++ b/examples/gtklauncher/window/imp.rs @@ -0,0 +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 new file mode 100644 index 0000000..b5a530c --- /dev/null +++ b/examples/gtklauncher/window/mod.rs @@ -0,0 +1,245 @@ +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(); + } + }); + } + + 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(); + + 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 new file mode 100644 index 0000000..9ff6532 --- /dev/null +++ b/examples/gtklauncher/window/window.ui @@ -0,0 +1,32 @@ + + + +