diff --git a/Cargo.toml b/Cargo.toml index aebdb0d..a0e96f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,4 @@ 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/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..da62161 --- /dev/null +++ b/examples/gtklauncher/main.rs @@ -0,0 +1,289 @@ +use gtk4 as gtk; +mod application_row; +use application_row::ApplicationRow; +use gtk::gdk::Display; +use gtk::prelude::*; +use gtk::{gio, glib}; +use libcosmic::x; +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, + ); + 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..."); + }); + 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/.#window.ui b/examples/gtklauncher/window/.#window.ui new file mode 120000 index 0000000..d0bd968 --- /dev/null +++ b/examples/gtklauncher/window/.#window.ui @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/gtklauncher/window/imp.rs @@ -0,0 +1 @@ + diff --git a/examples/gtklauncher/window/mod.rs b/examples/gtklauncher/window/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/gtklauncher/window/mod.rs @@ -0,0 +1 @@ + diff --git a/examples/gtklauncher/window/window.ui b/examples/gtklauncher/window/window.ui new file mode 100644 index 0000000..d061a7f --- /dev/null +++ b/examples/gtklauncher/window/window.ui @@ -0,0 +1,30 @@ + + + + \ No newline at end of file diff --git a/examples/launcher/launcher_row/imp.rs b/examples/launcher/launcher_row/imp.rs new file mode 100644 index 0000000..e69de29 diff --git a/examples/launcher/launcher_row/launcher_row.ui b/examples/launcher/launcher_row/launcher_row.ui new file mode 100644 index 0000000..8e3a569 --- /dev/null +++ b/examples/launcher/launcher_row/launcher_row.ui @@ -0,0 +1,40 @@ +?xml version="1.0" encoding="UTF-8"?> + + + \ No newline at end of file diff --git a/examples/launcher/launcher_row/mod.rs b/examples/launcher/launcher_row/mod.rs new file mode 100644 index 0000000..e69de29