/*! The layout chooser popover */ use gio; use gtk; use ::layout::c::EekGtkKeyboard; use ::locale::compare_current_locale; use ::locale_config::system_locale; use ::resources; use gio::ActionExt; use gio::ActionMapExt; use gio::SettingsExt; use glib::translate::FromGlibPtrNone; use glib::variant::ToVariant; use gtk::PopoverExt; use gtk::WidgetExt; use std::io::Write; 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::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_full(ret) } } } } fn make_menu_builder(inputs: Vec<(&str, &str)>) -> gtk::Builder { let mut xml: Vec = Vec::new(); writeln!( xml, "
" ).unwrap(); for (input_name, human_name) in inputs { writeln!( xml, " {} layout {} ", human_name, input_name, ).unwrap(); } writeln!( xml, "
" ).unwrap(); gtk::Builder::new_from_string( &String::from_utf8(xml).expect("Bad menu definition") ) } fn set_layout(kind: String, name: String) { let settings = gio::Settings::new("org.gnome.desktop.input-sources"); let inputs = settings.get_value("sources").unwrap(); 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(); settings.set_value( "sources", &variants::ArrayPairString(inputs).to_variant() ); settings.apply(); } pub fn show(window: EekGtkKeyboard, position: ::layout::c::Bounds) { unsafe { gtk::set_initialized() }; let window = unsafe { gtk::Widget::from_glib_none(window.0) }; let settings = gio::Settings::new("org.gnome.desktop.input-sources"); let inputs = settings.get_value("sources").unwrap(); let inputs = variants::get_tuples(inputs); let input_names: Vec<&str> = inputs.iter() .map(|(_kind, name)| name.as_str()) .collect(); let translations = system_locale() .map(|locale| locale.tags_for("messages") .next().unwrap() // guaranteed to exist .as_ref() .to_owned() ) .and_then(|lang| resources::get_layout_names(lang.as_str())); // sorted collection of human and machine names let mut human_names: Vec<(&str, &str)> = match translations { Some(translations) => { input_names.iter() .map(|name| (*name, *translations.get(name).unwrap_or(name))) .collect() }, // display bare codes None => { input_names.iter() .map(|n| (*n, *n)) // turns &&str into &str .collect() } }; human_names.sort_unstable_by(|(_, human_label_a), (_, human_label_b)| { compare_current_locale(human_label_a, human_label_b) }); let builder = make_menu_builder(human_names); // Much more debuggable to populate the model & menu // from a string representation // than add items imperatively let model: gio::MenuModel = builder.get_object("app-menu").unwrap(); let menu = gtk::Popover::new_from_model(Some(&window), &model); menu.set_pointing_to(>k::Rectangle { x: position.x.ceil() as i32, y: position.y.ceil() as i32, width: position.width.floor() as i32, height: position.width.floor() as i32, }); let action = input_names.get(0).map(|current_name| { let current_name = current_name.to_variant(); let layout_action = gio::SimpleAction::new_stateful( "layout", Some(current_name.type_()), ¤t_name, ); let action_group = gio::SimpleActionGroup::new(); action_group.add_action(&layout_action); menu.insert_action_group("popup", Some(&action_group)); layout_action }); menu.bind_model(Some(&model), Some("popup")); menu.connect_closed(move |_menu| { if let Some(layout_action) = &action { let state = match layout_action.get_state() { Some(v) => { let s = v.get::().or_else(|| { eprintln!("Variant is not string: {:?}", v); None }); // FIXME: the `get_state` docs call for unrefing, // but the function is nowhere to be found // glib::Variant::unref(v); s }, None => { eprintln!("No variant selected"); None }, }; set_layout("xkb".into(), state.unwrap_or("us".into())); } }); menu.popup(); }