diff --git a/Cargo.toml b/Cargo.toml
index 2925af79..36c0f111 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 00000000..f2a64530
--- /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.
+
+
diff --git a/examples/gtklauncher/application_row/application_row.ui b/examples/gtklauncher/application_row/application_row.ui
new file mode 100644
index 00000000..a70d7958
--- /dev/null
+++ b/examples/gtklauncher/application_row/application_row.ui
@@ -0,0 +1,53 @@
+
+
+
+ horizontal
+ 12
+ 4
+ 4
+ true
+
+
+
+
+
+
+
+
+ end
+ false
+
+
+
+
+
diff --git a/examples/gtklauncher/application_row/imp.rs b/examples/gtklauncher/application_row/imp.rs
new file mode 100644
index 00000000..f27eb54e
--- /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 00000000..8054c928
--- /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 00000000..584e1897
--- /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 00000000..8ffcefa1
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 00000000..0c72f8a3
--- /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 00000000..901181cf
--- /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 00000000..b5a530cb
--- /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 00000000..9ff6532d
--- /dev/null
+++ b/examples/gtklauncher/window/window.ui
@@ -0,0 +1,32 @@
+
+
+
+ 600
+ Gtk Pop Launcher
+ false
+
+
+ vertical
+ 12
+ 12
+ 12
+ 12
+
+
+ 12
+
+
+
+
+ never
+ 500
+ true
+
+
+
+
+
+
+
+
+