/*! The layout chooser popover */ use gio; use gtk; use std::ffi::CString; use std::cmp::Ordering; use crate::actors; use crate::layout::c::{ Bounds, EekGtkKeyboard }; use crate::locale::{ OwnedTranslation, compare_current_locale }; use crate::logging; use crate::receiver; use crate::resources; use crate::state; // Traits use gio::prelude::ActionMapExt; use gio::prelude::SettingsExt; use glib::translate::FromGlibPtrNone; use glib::variant::ToVariant; use gtk::prelude::*; use crate::logging::Warn; mod c { use std::os::raw::c_char; extern "C" { pub fn popover_open_settings_panel(panel: *const c_char); } } mod variants { use glib; use glib::Variant; use glib_sys; use std::os::raw::c_char; use glib::ToVariant; use glib::translate::FromGlibPtrFull; use glib::translate::FromGlibPtrNone; use glib::translate::ToGlibPtr; /// Unpacks tuple & array variants fn get_items(items: glib::Variant) -> Vec { let variant_naked = items.to_glib_none().0; let count = unsafe { glib_sys::g_variant_n_children(variant_naked) }; (0..count).map(|index| unsafe { glib::Variant::from_glib_full( glib_sys::g_variant_get_child_value(variant_naked, index) ) } ).collect() } /// Unpacks "a(ss)" variants pub fn get_tuples(items: glib::Variant) -> Vec<(String, String)> { get_items(items) .into_iter() .map(get_items) .map(|v| { ( v[0].get::().unwrap(), v[1].get::().unwrap(), ) }) .collect() } /// "a(ss)" variant /// Rust doesn't allow implementing existing traits for existing types pub struct ArrayPairString(pub Vec<(String, String)>); impl ToVariant for ArrayPairString { fn to_variant(&self) -> Variant { let tspec = "a(ss)".to_glib_none(); let builder = unsafe { let vtype = glib_sys::g_variant_type_checked_(tspec.0); glib_sys::g_variant_builder_new(vtype) }; let ispec = "(ss)".to_glib_none(); for (a, b) in &self.0 { let a = a.to_glib_none(); let b = b.to_glib_none(); // string pointers are weak references // and will get silently invalidated // as soon as the source is out of scope { let a: *const c_char = a.0; let b: *const c_char = b.0; unsafe { glib_sys::g_variant_builder_add( builder, ispec.0, a, b ); } } } unsafe { let ret = glib_sys::g_variant_builder_end(builder); glib_sys::g_variant_builder_unref(builder); glib::Variant::from_glib_none(ret) } } } } fn get_settings(schema_name: &str) -> Option { let mut error_handler = logging::Print{}; let ss = gio::SettingsSchemaSource::default(); ss.or_warn( &mut error_handler, logging::Problem::Surprise, "No gsettings schemas installed.", ) .and_then(|sss| sss.lookup(schema_name, true) .or_warn( &mut error_handler, logging::Problem::Surprise, &format!("Gsettings schema {} not installed", schema_name), ) ) .map(|_sschema| gio::Settings::new(schema_name)) } fn set_layout(kind: &str, name: &str) { let settings = get_settings("org.gnome.desktop.input-sources"); if let Some(settings) = settings { let kind = String::from(kind); let name = String::from(name); let inputs = settings.value("sources"); let current = (kind.clone(), name.clone()); let inputs = variants::get_tuples(inputs).into_iter() .filter(|t| t != ¤t); let inputs = vec![(kind, name)].into_iter() .chain(inputs).collect(); let _ = settings.set_value( "sources", &variants::ArrayPairString(inputs).to_variant(), ); settings.apply(); } } /// A reference to what the user wants to see #[derive(PartialEq, Clone, Debug)] pub enum LayoutId { /// Affects the layout in system settings System { kind: String, name: String, }, /// Only affects what this input method presents Local(String), } impl LayoutId { fn get_name(&self) -> &str { match &self { LayoutId::System { kind: _, name } => name.as_str(), LayoutId::Local(name) => name.as_str(), } } } fn set_visible_layout( layout_id: &LayoutId, ) { match layout_id { LayoutId::System { kind, name } => { set_layout(kind, name); }, _ => {}, } } /// Takes into account first any overlays, then system layouts from the list fn get_current_layout( popover: &actors::popover::State, system_layouts: &Vec, ) -> Option { match &popover.overlay { Some(name) => Some(LayoutId::Local(name.into())), None => system_layouts.get(0).map(LayoutId::clone), } } /// Translates all provided layout names according to current locale, /// for the purpose of display (i.e. errors will be caught and reported) fn translate_layout_names(layouts: &Vec) -> Vec { // `XkbInfo` being temporary means that its return values must be // copied, forcing the use of `OwnedTranslation`. enum Status { /// xkb names should get all translated here Translated(OwnedTranslation), /// Builtin names need builtin translations Remaining(String), } // Attempt to take all xkb names from gnome-desktop's xkb info. let xkb_translator = crate::locale::XkbInfo::new(); let translated_names = layouts.iter() .map(|id| match id { LayoutId::System { name, kind: _ } => { xkb_translator.get_display_name(name) .map(|s| Status::Translated(OwnedTranslation(s))) .or_print( logging::Problem::Surprise, &format!("No display name for xkb layout {}", name), ).unwrap_or_else(|| Status::Remaining(name.clone())) }, LayoutId::Local (_) => unreachable!(), }); translated_names .map(|status| match status { Status::Remaining(name) => OwnedTranslation(name), Status::Translated(t) => t, }) .collect() } pub fn show( window: EekGtkKeyboard, position: Bounds, popover: &actors::popover::State, app_state: receiver::State, ) { unsafe { gtk::set_initialized() }; let window = unsafe { gtk::Widget::from_glib_none(window.0) }; let overlay_layouts = resources::get_overlays().into_iter() .map(|name| LayoutId::Local(name.to_string())); let settings = get_settings("org.gnome.desktop.input-sources"); let inputs = settings .map(|settings| { let inputs = settings.value("sources"); variants::get_tuples(inputs) }) .unwrap_or_else(|| Vec::new()); let system_layouts: Vec = inputs.into_iter() .map(|(kind, name)| LayoutId::System { kind, name }) .collect(); let all_layouts: Vec = system_layouts.clone() .into_iter() .chain(overlay_layouts) .collect(); let translated_names = translate_layout_names(&system_layouts); // sorted collection of language layouts let mut human_names: Vec<(OwnedTranslation, LayoutId)> = translated_names .into_iter() .zip(system_layouts.clone().into_iter()) .collect(); human_names.sort_unstable_by(|(tr_a, layout_a), (tr_b, layout_b)| { // Sort first by layout then name match (layout_a, layout_b) { (LayoutId::Local(_), LayoutId::System { .. }) => Ordering::Greater, (LayoutId::System { .. }, LayoutId::Local(_)) => Ordering::Less, _ => compare_current_locale(&tr_a.0, &tr_b.0) } }); let model: gio::Menu = { { let builder = gtk::Builder::from_resource("/sm/puri/squeekboard/popover.ui"); builder.object("app-menu").unwrap() } }; for (tr, l) in human_names.iter().rev() { let detailed_action = format!("layout::{}", l.get_name()); let item = gio::MenuItem::new(Some(&tr.0), Some(detailed_action.as_str())); model.prepend_item (&item); } let menu = gtk::Popover::from_model(Some(&window), &model); menu.set_pointing_to(>k::Rectangle::new ( position.x.ceil() as i32, position.y.ceil() as i32, position.width.floor() as i32, position.width.floor() as i32, )); menu.set_constrain_to(gtk::PopoverConstraint::None); let action_group = gio::SimpleActionGroup::new(); if let Some(current_layout) = get_current_layout(popover, &system_layouts) { let current_layout_name = all_layouts.iter() .find( |l| l.get_name() == current_layout.get_name() ).unwrap() .get_name(); log_print!(logging::Level::Debug, "Current Layout {}", current_layout_name); let layout_action = gio::SimpleAction::new_stateful( "layout", Some(current_layout_name.to_variant().type_()), ¤t_layout_name.to_variant() ); let menu_inner = menu.clone(); layout_action.connect_change_state(move |_action, state| { match state { Some(v) => { log_print!(logging::Level::Debug, "Selected layout {}", v); v.get::() .or_print( logging::Problem::Bug, &format!("Variant is not string: {:?}", v) ) .map(|state| { let layout = all_layouts.iter() .find( |choices| state == choices.get_name() ).unwrap(); app_state .send(state::Event::OverlayChanged(layout.clone())) .or_print( logging::Problem::Bug, &format!("Can't send to state"), ); set_visible_layout(layout) }); }, None => log_print!( logging::Level::Debug, "No variant selected", ), }; menu_inner.popdown(); }); action_group.add_action(&layout_action); }; let settings_action = gio::SimpleAction::new("settings", None); settings_action.set_enabled(popover.settings_active); settings_action.connect_activate(move |_, _| { let s = CString::new("region").unwrap(); unsafe { c::popover_open_settings_panel(s.as_ptr()) }; }); action_group.add_action(&settings_action); menu.insert_action_group("popup", Some(&action_group)); menu.bind_model(Some(&model), Some("popup")); glib::idle_add_local(move || { menu.popup(); glib::ControlFlow::Break }); }