From 1fa7b8243a0406242ebab6fa51b75e17b0990ab7 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 21 Jan 2022 14:43:01 -0500 Subject: [PATCH] app library popover for addign app group --- Cargo.toml | 2 +- examples/app_library/app_group/imp.rs | 143 +++------------- examples/app_library/app_group/mod.rs | 73 ++++++-- examples/app_library/grid_item/imp.rs | 41 ++++- examples/app_library/grid_item/mod.rs | 77 +++++++-- examples/app_library/group_grid/mod.rs | 207 +++++++++-------------- examples/app_library/window/mod.rs | 33 ++-- examples/app_library/window_inner/mod.rs | 8 + 8 files changed, 282 insertions(+), 302 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6d33bdd6..7982f275 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ glib-sys = "0.15.1" relm4-macros = { git = "https://github.com/AaronErhardt/Relm4" } pop-launcher-service = { git = "https://github.com/wash2/launcher.git" } pop-launcher = { git = "https://github.com/wash2/launcher.git" } -serde = "1.0.133" +serde = "1.0.134" serde_json = "1.0.75" tokio = { version = "1.15.0", features = ["sync"] } futures = "0.3.19" diff --git a/examples/app_library/app_group/imp.rs b/examples/app_library/app_group/imp.rs index cf64cbcc..b982a290 100644 --- a/examples/app_library/app_group/imp.rs +++ b/examples/app_library/app_group/imp.rs @@ -1,22 +1,19 @@ use std::cell::RefCell; use std::rc::Rc; -use gdk4::glib::ParamSpecBoolean; -use gdk4::glib::ParamSpecString; -use gdk4::glib::ParamSpecUInt; -use gdk4::glib::ParamSpecVariant; -use glib::{FromVariant, ParamFlags, ParamSpec, ToVariant, Value, Variant, VariantTy}; +use gdk4::glib::ParamSpecBoxed; +use glib::{ParamFlags, ParamSpec, Value}; use gtk4::glib; use gtk4::prelude::*; use gtk4::subclass::prelude::*; use once_cell::sync::Lazy; -use super::AppGroupData; +use super::BoxedAppGroupType; // Object holding the state #[derive(Default)] pub struct AppGroup { - pub data: Rc>, + pub inner: Rc>, } // The central trait for subclassing a GObject @@ -31,123 +28,26 @@ impl ObjectSubclass for AppGroup { impl ObjectImpl for AppGroup { fn properties() -> &'static [ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - ParamSpecUInt::new( - // Name - "id", - // Nickname - "id", - // Short description - "Name of the application group", - 0, - u32::MAX, - // Default value - 0, - // The property can be read and written to - ParamFlags::READWRITE, - ), - ParamSpecString::new( - // Name - "name", - // Nickname - "name", - // Short description - "Name of the application group", - // Default value - Some(""), - // The property can be read and written to - ParamFlags::READWRITE, - ), - ParamSpecBoolean::new( - // Name - "mutable", - // Nickname - "mutable", - // Short description - "Mutability of the application group", - // Default value - false, - // The property can be read and written to - ParamFlags::READWRITE, - ), - ParamSpecString::new( - // Name - "icon", - // Nickname - "icon", - // Short description - "Icon of application Group", - // Default value - Some("folder"), - // The property can be read and written to - ParamFlags::READWRITE, - ), - ParamSpecString::new( - // Name - "category", - // Nickname - "category", - // Short description - "Category of application Group", - // Default value - None, - // The property can be read and written to - ParamFlags::READWRITE, - ), - ParamSpecVariant::new( - // Name - "appnames", - // Nickname - "appnames", - // Short description - "Names of applications in the App Group", - VariantTy::new("as").expect("Oops invalid string for VariantTy tuple."), - // Default value - None, - // The property can be read and written to - ParamFlags::READWRITE, - ), - ] + vec![ParamSpecBoxed::new( + // Name + "inner", + // Nickname + "inner", + // Short description + "inner", + BoxedAppGroupType::static_type(), + // The property can be read and written to + ParamFlags::READWRITE, + )] }); PROPERTIES.as_ref() } fn set_property(&self, _obj: &Self::Type, _id: usize, value: &Value, pspec: &ParamSpec) { match pspec.name() { - "id" => { - let id = value.get().expect("The value needs to be of type `u32`."); - self.data.borrow_mut().id = id; - } - "name" => { - let name = value - .get() - .expect("The value needs to be of type `String`."); - self.data.borrow_mut().name = name; - } - "icon" => { - let icon = value.get().expect("The icon needs to be of type `String`"); - self.data.borrow_mut().icon = icon; - } - "category" => { - let category = value - .get() - .expect("The category needs to be of type `String`"); - self.data.borrow_mut().category = category; - } - "mutable" => { - let mutable = value - .get() - .expect("The mutable property needs to be of type `bool`"); - self.data.borrow_mut().mutable = mutable; - } - "appnames" => { - let appnames = >::from_variant( - &value - .get::() - .expect("The icon needs to be a Variant"), - ) - .expect("The icon variant needs to be a Vec"); - self.data.borrow_mut().app_names = appnames; + "inner" => { + let inner = value.get().expect("The value needs to be of type `u32`."); + self.inner.replace(inner); } _ => unimplemented!(), } @@ -155,12 +55,7 @@ impl ObjectImpl for AppGroup { fn property(&self, _obj: &Self::Type, _id: usize, pspec: &ParamSpec) -> Value { match pspec.name() { - "id" => self.data.borrow().id.to_value(), - "name" => self.data.borrow().name.to_value(), - "icon" => self.data.borrow().icon.to_value(), - "mutable" => self.data.borrow().mutable.to_value(), - "category" => self.data.borrow().category.to_value(), - "appnames" => self.data.borrow().app_names.to_variant().to_value(), + "inner" => self.inner.borrow().to_value(), _ => unimplemented!(), } } diff --git a/examples/app_library/app_group/mod.rs b/examples/app_library/app_group/mod.rs index 5ce32250..f167c8ca 100644 --- a/examples/app_library/app_group/mod.rs +++ b/examples/app_library/app_group/mod.rs @@ -1,6 +1,4 @@ use glib::Object; -use glib::ObjectExt; -use glib::ToVariant; use gtk4::glib; use gtk4::subclass::prelude::*; use serde::{Deserialize, Serialize}; @@ -12,22 +10,68 @@ glib::wrapper! { } impl AppGroup { - pub fn new(data: AppGroupData) -> Self { - let self_: Self = Object::new(&[ - ("id", &data.id), - ("name", &data.name), - ("mutable", &data.mutable), - ("icon", &data.icon), - ("category", &data.category), - ]) - .expect("Failed to create `ApplicationObject`."); - self_.set_property("appnames", data.app_names.to_variant()); + pub fn new(data: BoxedAppGroupType) -> Self { + let self_: Self = + Object::new(&[("inner", &data)]).expect("Failed to create `ApplicationObject`."); self_ } - pub fn group_data(&self) -> AppGroupData { + pub fn popup(&self) { let imp = imp::AppGroup::from_instance(self); - imp.data.borrow().clone() + let inner = imp.inner.borrow().clone(); + match inner { + BoxedAppGroupType::Group(d) => { + // d.popup = true; + imp.inner.replace(BoxedAppGroupType::Group(d)); + } + BoxedAppGroupType::NewGroup(_) => { + imp.inner.replace(BoxedAppGroupType::NewGroup(true)); + } + }; + } + + pub fn popdown(&self) { + let imp = imp::AppGroup::from_instance(self); + let inner = imp.inner.borrow().clone(); + match inner { + BoxedAppGroupType::Group(d) => { + // d.popup = false; + imp.inner.replace(BoxedAppGroupType::Group(d)); + } + BoxedAppGroupType::NewGroup(_) => { + imp.inner.replace(BoxedAppGroupType::NewGroup(false)); + } + }; + } + + pub fn is_popup_active(&self) -> bool { + let imp = imp::AppGroup::from_instance(self); + match imp.inner.borrow().clone() { + BoxedAppGroupType::Group(_d) => false, + BoxedAppGroupType::NewGroup(is_active) => is_active, + } + } + + pub fn group_data(&self) -> Option { + let imp = imp::AppGroup::from_instance(self); + let inner = imp.inner.borrow().clone(); + match inner { + BoxedAppGroupType::Group(d) => Some(d), + _ => None, + } + } +} + +#[derive(Serialize, Deserialize, Clone, glib::Boxed)] +#[boxed_type(name = "BoxedAppGroupType")] +pub enum BoxedAppGroupType { + Group(AppGroupData), + NewGroup(bool), +} + +impl Default for BoxedAppGroupType { + fn default() -> Self { + Self::NewGroup(false) } } @@ -40,4 +84,5 @@ pub struct AppGroupData { pub mutable: bool, pub app_names: Vec, pub category: String, + // pub popup: bool, } diff --git a/examples/app_library/grid_item/imp.rs b/examples/app_library/grid_item/imp.rs index c3b7ae60..f274473d 100644 --- a/examples/app_library/grid_item/imp.rs +++ b/examples/app_library/grid_item/imp.rs @@ -1,15 +1,18 @@ +use glib::subclass::Signal; +use gtk4::subclass::prelude::*; +use once_cell::sync::Lazy; use std::cell::Cell; use std::cell::RefCell; use std::rc::Rc; -use gtk4::glib; -use gtk4::subclass::prelude::*; +use gtk4::{glib, prelude::*, Popover}; #[derive(Debug, Default)] pub struct GridItem { - pub name: Rc>, - pub image: Rc>, - pub index: Cell, + pub(super) name: Rc>, + pub(super) image: Rc>, + pub(super) index: Cell, + pub(super) popover: Rc>>, } #[glib::object_subclass] @@ -19,7 +22,33 @@ impl ObjectSubclass for GridItem { type ParentType = gtk4::Box; } -impl ObjectImpl for GridItem {} +impl ObjectImpl for GridItem { + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = Lazy::new(|| { + vec![ + Signal::builder( + // Signal name + "new-group", + // Types of the values which will be sent to the signal handler + &[String::static_type().into()], + // Type of the value the signal handler sends back + <()>::static_type().into(), + ) + .build(), + Signal::builder( + // Signal name + "popover-closed", + // Types of the values which will be sent to the signal handler + &[], + // Type of the value the signal handler sends back + <()>::static_type().into(), + ) + .build(), + ] + }); + SIGNALS.as_ref() + } +} impl WidgetImpl for GridItem {} diff --git a/examples/app_library/grid_item/mod.rs b/examples/app_library/grid_item/mod.rs index f2209db4..facb7cf5 100644 --- a/examples/app_library/grid_item/mod.rs +++ b/examples/app_library/grid_item/mod.rs @@ -8,6 +8,7 @@ use gtk4::prelude::*; use gtk4::subclass::prelude::*; use gtk4::traits::WidgetExt; use gtk4::Align; +use gtk4::Button; use gtk4::DragSource; use gtk4::IconTheme; use gtk4::Image; @@ -16,6 +17,7 @@ use gtk4::Orientation; use gtk4::{gio, glib}; use crate::app_group::AppGroup; +use crate::app_group::BoxedAppGroupType; mod imp; @@ -66,7 +68,6 @@ impl GridItem { imp.name.replace(name); imp.image.replace(image); - self_ } @@ -108,18 +109,74 @@ impl GridItem { } pub fn set_group_info(&self, app_group: AppGroup) { - let self_ = imp::GridItem::from_instance(self); - self_ - .name - .borrow() - .set_text(&app_group.property::("name")); - self_ - .image - .borrow() - .set_from_icon_name(Some(&app_group.property::("icon"))); + // if data type set name and icon to values in data + let imp = imp::GridItem::from_instance(self); + match app_group.property::("inner") { + BoxedAppGroupType::Group(data) => { + imp.name.borrow().set_text(&data.name); + imp.image.borrow().set_from_icon_name(Some(&data.icon)); + } + BoxedAppGroupType::NewGroup(popover_active) => { + // else must be add group + imp.name.borrow().set_text("New Group"); + imp.image.borrow().set_from_icon_name(Some("folder-new")); + + let popover_menu = gtk4::Box::builder() + .spacing(12) + .hexpand(true) + .orientation(gtk4::Orientation::Vertical) + .margin_top(12) + .margin_bottom(12) + .margin_end(12) + .margin_start(12) + .build(); + + // build menu + let dialog_entry = gtk4::Entry::new(); + let label = cascade! { + Label::new(Some("Name")); + ..set_justify(gtk4::Justification::Left); + ..set_xalign(0.0); + }; + popover_menu.append(&label); + popover_menu.append(&dialog_entry); + let btn_container = cascade! { + gtk4::Box::new(Orientation::Horizontal, 8); + }; + let ok_btn = cascade! { + Button::with_label("Ok"); + }; + let cancel_btn = cascade! { + Button::with_label("Cancel"); + }; + btn_container.append(&ok_btn); + btn_container.append(&cancel_btn); + popover_menu.append(&btn_container); + let popover = cascade! { + gtk4::Popover::new(); + ..set_autohide(true); + ..set_child(Some(&popover_menu)); + }; + self.append(&popover); + popover.connect_closed(glib::clone!(@weak self as self_ => move |_| { + self_.emit_by_name::<()>("popover-closed", &[]); + })); + imp.popover.replace(Some(popover)); + if popover_active { + self.popup(); + } + } + } } pub fn set_index(&self, index: u32) { imp::GridItem::from_instance(self).index.set(index); } + + pub fn popup(&self) { + let imp = imp::GridItem::from_instance(self); + if let Some(popover) = imp.popover.borrow().as_ref() { + popover.popup(); + } + } } diff --git a/examples/app_library/group_grid/mod.rs b/examples/app_library/group_grid/mod.rs index 3acece30..12b31c25 100644 --- a/examples/app_library/group_grid/mod.rs +++ b/examples/app_library/group_grid/mod.rs @@ -1,15 +1,11 @@ use cascade::cascade; use glib::Object; -use glib::{FromVariant, Variant}; use gtk4::prelude::*; use gtk4::subclass::prelude::*; -use gtk4::{ - gio, glib, Dialog, Entry, GridView, Label, PolicyType, ScrolledWindow, SignalListItemFactory, -}; +use gtk4::{gio, glib, GridView, PolicyType, ScrolledWindow, SignalListItemFactory}; use std::fs::File; -use crate::app_group::AppGroup; -use crate::app_group::AppGroupData; +use crate::app_group::{AppGroup, AppGroupData, BoxedAppGroupType}; use crate::grid_item::GridItem; use crate::utils::data_path; use crate::utils::set_group_scroll_policy; @@ -71,30 +67,30 @@ impl GroupGrid { .set(group_model.clone()) .expect("Could not set group model"); vec![ - AppGroup::new(AppGroupData { + AppGroup::new(BoxedAppGroupType::Group(AppGroupData { id: 0, name: "Library Home".to_string(), icon: "user-home".to_string(), mutable: false, app_names: Vec::new(), category: "".to_string(), - }), - AppGroup::new(AppGroupData { + })), + AppGroup::new(BoxedAppGroupType::Group(AppGroupData { id: 0, name: "System".to_string(), icon: "folder".to_string(), mutable: false, app_names: Vec::new(), category: "System".to_string(), - }), - AppGroup::new(AppGroupData { + })), + AppGroup::new(BoxedAppGroupType::Group(AppGroupData { id: 0, name: "Utilities".to_string(), icon: "folder".to_string(), mutable: false, app_names: Vec::new(), category: "Utility".to_string(), - }), + })), // Example of group with app name // AppGroup::new(AppGroupData { // id: 0, @@ -104,14 +100,7 @@ impl GroupGrid { // app_names: vec!["Firefox Web Browser".to_string()], // category: "".to_string(), // }), - AppGroup::new(AppGroupData { - id: 0, - name: "New Group".to_string(), - icon: "folder-new".to_string(), - mutable: true, - app_names: vec![], - category: "".to_string(), - }), + AppGroup::new(BoxedAppGroupType::NewGroup(false)), ] .iter() .for_each(|group| { @@ -133,11 +122,7 @@ impl GroupGrid { fn setup_callbacks(&self) { let imp = imp::GroupGrid::from_instance(self); let group_grid_view = &imp.group_grid_view.get().unwrap(); - let group_selection_model = group_grid_view - .model() - .expect("List view missing selection model") - .downcast::() - .expect("could not downcast listview model to single selection model"); + let scroll_window = &imp.group_scroll_window.get().unwrap(); // dynamically set scroll method self.group_model().connect_items_changed( @@ -149,124 +134,88 @@ impl GroupGrid { let self_clone = self.clone(); group_grid_view.connect_activate(move |group_grid_view, i| { // on activation change the group filter model to use the app names, and category - // let window = group_grid_view.root().unwrap().downcast::().unwrap(); println!("grid view activated. {}", i); - let group_model = group_grid_view.model().unwrap().downcast::().unwrap() + let group_model = group_grid_view + .model() + .unwrap() + .downcast::() + .unwrap() .model() .downcast::() .expect("could not downcast app group view selection model to list store model"); - - // if last item in the model, don't change filter, instead show dialog for adding new group! - if i == group_model.n_items() - 1 { - let dialog_entry = Entry::new(); - let label = Label::new(Some("Name")); - label.set_justify(gtk4::Justification::Left); - label.set_xalign(0.0); - let vbox = gtk4::Box::builder() - .spacing(12) - .hexpand(true) - .orientation(gtk4::Orientation::Vertical) - .margin_top(12) - .margin_bottom(12) - .margin_end(12) - .margin_start(12) - .build(); - vbox.append(&label); - vbox.append(&dialog_entry); - - let dialog = Dialog::builder() - .modal(true) - .resizable(false) - .use_header_bar(true.into()) - .destroy_with_parent(true) - // .transient_for(&window) - .title("New App Group") - .child(&vbox) - .build(); - // let app = window - // .application() - // .expect("could not get application from window"); - - // dialog.set_application(Some(&app)); - dialog.add_buttons(&[ - ("Apply", gtk4::ResponseType::Apply), - ("Cancel", gtk4::ResponseType::Cancel), - ]); - - dialog.connect_response( - glib::clone!(@weak dialog_entry, @weak group_selection_model, @weak group_model => move |dialog, response_type| { - println!("dialog should be closing..."); - let name = dialog_entry.text().to_string(); - if response_type == gtk4::ResponseType::Apply && name != "" { - let new_app_group = AppGroup::new(AppGroupData { - id: 0, - name: name, - icon: "folder".to_string(), - mutable: true, - app_names: vec![], - category: "".to_string(), - }); - group_model.insert(group_model.n_items() - 1, &new_app_group); - group_selection_model.set_selected(i - 1); - } else { - group_selection_model.set_selected(0); - } - dialog.emit_close(); - }), - ); - // dialog.connect_is_active_notify(move |win| { - // let app = win - // .application() - // .expect("could not get application from window"); - // let active_window = app - // .active_window() - // .expect("no active window available, closing app library."); - // dbg!(&active_window); - // if win == &active_window && !win.is_active() { - // println!("no focus"); - // // close top level window - // window.close(); - // } - // }); - dialog.show(); - return; - }; // update the application filter - let app_info = group_model + if let Some(data) = group_model .item(i) .unwrap() .downcast::() - .unwrap(); - let category = - app_info.property::("category").to_lowercase(); + .unwrap() + .group_data() + { + let category = data.category.to_lowercase(); - let app_names = - >::from_variant(&app_info.property::("appnames")).unwrap_or_default(); - dbg!(&app_names); - let new_filter: gtk4::CustomFilter = gtk4::CustomFilter::new(move |obj| { - let app = obj - .downcast_ref::() - .expect("The Object needs to be of type AppInfo"); - if app_names.len() > 0 { - return app_names.contains(&String::from(app.name().as_str())); - } - match app.categories() { - Some(categories) => categories.to_string().to_lowercase().contains(&category), - None => false, - } - }); - self_clone - .emit_by_name::<()>("group-changed", &[&new_filter]); + let new_filter: gtk4::CustomFilter = gtk4::CustomFilter::new(move |obj| { + let app = obj + .downcast_ref::() + .expect("The Object needs to be of type AppInfo"); + if data.app_names.len() > 0 { + return data.app_names.contains(&String::from(app.name().as_str())); + } + match app.categories() { + Some(categories) => { + categories.to_string().to_lowercase().contains(&category) + } + None => false, + } + }); + self_clone.emit_by_name::<()>("group-changed", &[&new_filter]); + } else { + // don't change filter, instead show dialog for adding new group! + let item = group_model.item(i).unwrap().downcast::().unwrap(); + item.popup(); + group_model.items_changed(i, 0, 0); + } }); } + pub fn is_popup_active(&self) -> bool { + let model = self.group_model(); + for i in 0..model.n_items() { + let item = model.item(i).unwrap().downcast::().unwrap(); + if item.is_popup_active() { + return true; + } + } + return false; + } + fn setup_factory(&self) { let imp = imp::GroupGrid::from_instance(&self); let group_factory = SignalListItemFactory::new(); - group_factory.connect_setup(move |_factory, item| { - let row = GridItem::new(); - item.set_child(Some(&row)); - }); + group_factory.connect_setup(glib::clone!(@weak self as self_ => move |_factory, item| { + let obj = GridItem::new(); + item.set_child(Some(&obj)); + obj + .connect_local("new-group", false, glib::clone!(@weak self_ => @default-return None, move |args| { + let m = self_.group_model(); + match args[1].get::() { + Ok(_name) => { + // m.items_changed(m.n_items() - 2, 0, 1); + todo!(); + } + _ => unimplemented!(), + }; + None + })); + obj + .connect_local("popover-closed", false, glib::clone!(@weak self_ => @default-return None, move |_| { + let m = self_.group_model(); + let group = m.item(m.n_items() - 1).unwrap().downcast::().unwrap(); + glib::idle_add_local_once(move || { + group.popdown(); + }); + None + })); + })); // the bind stage is used for "binding" the data to the created widgets on the "setup" stage group_factory.connect_bind(move |_factory, grid_item| { @@ -290,7 +239,7 @@ impl GroupGrid { let app_group_objects: Vec = backup_data .into_iter() - .map(|data| AppGroup::new(data).upcast::()) + .map(|data| AppGroup::new(BoxedAppGroupType::Group(data)).upcast::()) .collect(); let scroll_window = &imp::GroupGrid::from_instance(self) .group_scroll_window diff --git a/examples/app_library/window/mod.rs b/examples/app_library/window/mod.rs index 5eaa38fe..e252334a 100644 --- a/examples/app_library/window/mod.rs +++ b/examples/app_library/window/mod.rs @@ -153,25 +153,22 @@ impl AppLibraryWindow { println!("failed to get X11 window"); } }); - window.connect_is_active_notify(move |win| { - if !win.is_active() { + + let imp = imp::AppLibraryWindow::from_instance(&self); + let inner = imp.inner.get().unwrap(); + window.connect_is_active_notify(glib::clone!(@weak inner => move |win| { + let app = win + .application() + .expect("could not get application from window"); + let active_window = app + .active_window() + .expect("no active window available, closing app library."); + dbg!(&active_window); + dbg!(!inner.is_popup_active()); + if win == &active_window && !win.is_active() && !inner.is_popup_active() { + dbg!(win); win.close(); } - }); - // let focus_ctrl = gtk4::EventControllerFocus::builder() - // .propagation_phase(gtk4::PropagationPhase::Capture) - // .propagation_limit(gtk4::PropagationLimit::None) - // .build(); - // focus_ctrl.connect_leave(move |_| { - // println!("Exiting"); - // }); - // focus_ctrl.connect_enter(move |_| { - // println!("hewwo"); - // }); - // focus_ctrl.connect_contains_focus_notify(move |_| { - // println!("hewwo"); - // }); - - // window.add_controller(&focus_ctrl); + })); } } diff --git a/examples/app_library/window_inner/mod.rs b/examples/app_library/window_inner/mod.rs index 2972aef8..cc414237 100644 --- a/examples/app_library/window_inner/mod.rs +++ b/examples/app_library/window_inner/mod.rs @@ -69,6 +69,14 @@ impl AppLibraryWindowInner { imp.group_grid.get() } + pub fn is_popup_active(&self) -> bool { + if let Some(group_grid) = self.group_grid() { + group_grid.is_popup_active() + } else { + false + } + } + fn setup_callbacks(&self) { // Get state let imp = imp::AppLibraryWindowInner::from_instance(self);