Add a popover menu to switch languages

This commit is contained in:
Dorota Czaplejewicz
2019-09-13 14:45:14 +00:00
parent dcd4dbf931
commit 47c4119ab7
14 changed files with 623 additions and 34 deletions

View File

@ -36,4 +36,5 @@ pub enum Action {
/// The key events this symbol submits when submitting text is not possible
keys: Vec<KeySym>,
},
ShowPreferences,
}

View File

@ -509,10 +509,7 @@ fn create_action(
&view_names
),
},
Some(Action::ShowPrefs) => ::action::Action::Submit {
text: None,
keys: Vec::new(),
},
Some(Action::ShowPrefs) => ::action::Action::ShowPreferences,
None => ::action::Action::Submit {
text: None,
keys: keysyms.into_iter().map(::action::KeySym).collect(),

View File

@ -62,7 +62,10 @@ const char *squeek_layout_get_keymap(const struct squeek_layout*);
enum squeek_arrangement_kind squeek_layout_get_kind(const struct squeek_layout *);
void squeek_layout_free(struct squeek_layout*);
void squeek_layout_release(struct squeek_layout *layout, struct zwp_virtual_keyboard_v1 *virtual_keyboard, uint32_t timestamp, EekGtkKeyboard *ui_keyboard);
void squeek_layout_release(struct squeek_layout *layout, struct zwp_virtual_keyboard_v1 *virtual_keyboard,
struct transformation widget_to_layout,
uint32_t timestamp,
EekGtkKeyboard *ui_keyboard);
void squeek_layout_release_all_only(struct squeek_layout *layout, struct zwp_virtual_keyboard_v1 *virtual_keyboard, uint32_t timestamp);
void squeek_layout_depress(struct squeek_layout *layout, struct zwp_virtual_keyboard_v1 *virtual_keyboard,
double x_widget, double y_widget,

View File

@ -37,6 +37,7 @@ pub mod c {
use std::ffi::CStr;
use std::os::raw::{ c_char, c_void };
use std::ptr;
use gtk_sys;
// The following defined in C
@ -45,7 +46,7 @@ pub mod c {
#[repr(transparent)]
#[derive(Copy, Clone)]
pub struct EekGtkKeyboard(*const c_void);
pub struct EekGtkKeyboard(pub *const gtk_sys::GtkWidget);
/// Defined in eek-types.h
#[repr(C)]
@ -245,7 +246,7 @@ pub mod c {
origin_y: f64,
scale: f64,
}
impl Transformation {
fn forward(&self, p: Point) -> Point {
Point {
@ -253,6 +254,25 @@ pub mod c {
y: (p.y - self.origin_y) / self.scale,
}
}
fn reverse(&self, p: Point) -> Point {
Point {
x: p.x * self.scale + self.origin_x,
y: p.y * self.scale + self.origin_y,
}
}
pub fn reverse_bounds(&self, b: Bounds) -> Bounds {
let start = self.reverse(Point { x: b.x, y: b.y });
let end = self.reverse(Point {
x: b.x + b.width,
y: b.y + b.height,
});
Bounds {
x: start.x,
y: start.y,
width: end.x - start.x,
height: end.y - start.y,
}
}
}
// This is constructed only in C, no need for warnings
@ -319,28 +339,30 @@ pub mod c {
}
}
/// Release pointer in the specified position
#[no_mangle]
pub extern "C"
fn squeek_layout_release(
layout: *mut Layout,
virtual_keyboard: ZwpVirtualKeyboardV1, // TODO: receive a reference to the backend
widget_to_layout: Transformation,
time: u32,
ui_keyboard: EekGtkKeyboard,
) {
let time = Timestamp(time);
let layout = unsafe { &mut *layout };
let virtual_keyboard = VirtualKeyboard(virtual_keyboard);
// The list must be copied,
// because it will be mutated in the loop
for key in layout.pressed_keys.clone() {
let key: &Rc<RefCell<KeyState>> = key.borrow();
layout.release_key(
ui::release_key(
layout,
&virtual_keyboard,
&mut key.clone(),
Timestamp(time)
);
let view = layout.get_current_view();
::layout::procedures::release_ui_buttons(
&view, key, ui_keyboard,
&widget_to_layout,
time,
ui_keyboard,
key
);
}
}
@ -418,6 +440,7 @@ pub mod c {
time: u32,
ui_keyboard: EekGtkKeyboard,
) {
let time = Timestamp(time);
let layout = unsafe { &mut *layout };
let virtual_keyboard = VirtualKeyboard(virtual_keyboard);
@ -442,36 +465,30 @@ pub mod c {
if Rc::ptr_eq(&state, &wrapped_key.0) {
found = true;
} else {
layout.release_key(
ui::release_key(
layout,
&virtual_keyboard,
&mut key.clone(),
Timestamp(time),
);
let view = layout.get_current_view();
::layout::procedures::release_ui_buttons(
&view, key, ui_keyboard,
&widget_to_layout,
time,
ui_keyboard,
key,
);
}
}
if !found {
layout.press_key(
&virtual_keyboard,
&mut state,
Timestamp(time),
);
layout.press_key(&virtual_keyboard, &mut state, time);
unsafe { eek_gtk_on_button_pressed(c_place, ui_keyboard) };
}
} else {
for wrapped_key in pressed {
let key: &Rc<RefCell<KeyState>> = wrapped_key.borrow();
layout.release_key(
ui::release_key(
layout,
&virtual_keyboard,
&mut key.clone(),
Timestamp(time),
);
let view = layout.get_current_view();
::layout::procedures::release_ui_buttons(
&view, key, ui_keyboard,
&widget_to_layout,
time,
ui_keyboard,
key,
);
}
}
@ -503,6 +520,28 @@ pub mod c {
}
}
}
#[cfg(test)]
mod test {
use super::*;
fn near(a: f64, b: f64) -> bool {
(a - b).abs() < ((a + b) * 0.001f64).abs()
}
#[test]
fn transform_back() {
let transform = Transformation {
origin_x: 10f64,
origin_y: 11f64,
scale: 12f64,
};
let point = Point { x: 1f64, y: 1f64 };
let transformed = transform.reverse(transform.forward(point.clone()));
assert!(near(point.x, transformed.x));
assert!(near(point.y, transformed.y));
}
}
}
}
@ -717,6 +756,12 @@ pub struct Layout {
pub keymap_str: CString,
// Changeable state
// a Vec would be enough, but who cares, this will be small & fast enough
// TODO: turn those into per-input point *_buttons to track dragging.
// The renderer doesn't need the list of pressed keys any more,
// because it needs to iterate
// through all buttons of the current view anyway.
// When the list tracks actual location,
// it becomes possible to place popovers and other UI accurately.
pub pressed_keys: HashSet<::util::Pointer<RefCell<KeyState>>>,
pub locked_keys: HashSet<::util::Pointer<RefCell<KeyState>>>,
}
@ -949,6 +994,64 @@ mod procedures {
);
}
}
pub fn get_button_bounds(
view: &View,
row: &Row,
button: &Button
) -> Option<c::Bounds> {
match &row.bounds {
Some(row) => Some(c::Bounds {
x: view.bounds.x + row.x + button.bounds.x,
y: view.bounds.y + row.y + button.bounds.y,
width: button.bounds.width,
height: button.bounds.height,
}),
_ => None,
}
}
}
/// Top level UI procedures
mod ui {
use super::*;
// TODO: turn into release_button
pub fn release_key(
layout: &mut Layout,
virtual_keyboard: &VirtualKeyboard,
widget_to_layout: &c::procedures::Transformation,
time: Timestamp,
ui_keyboard: c::EekGtkKeyboard,
key: &Rc<RefCell<KeyState>>,
) {
layout.release_key(virtual_keyboard, &mut key.clone(), time);
let view = layout.get_current_view();
let action = RefCell::borrow(key).action.clone();
if let Action::ShowPreferences = action {
let paths = ::layout::procedures::find_key_paths(
view, key
);
// getting first item will cause mispositioning
// with more than one button with the same key
// on the keyboard
if let Some((row, button)) = paths.get(0) {
let bounds = ::layout::procedures::get_button_bounds(
view, row, button
).unwrap_or_else(|| {
eprintln!("BUG: Clicked button has no position?");
c::Bounds { x: 0f64, y: 0f64, width: 0f64, height: 0f64 }
});
::popover::show(
ui_keyboard,
widget_to_layout.reverse_bounds(bounds)
);
}
}
procedures::release_ui_buttons(view, key, ui_keyboard);
}
}
#[cfg(test)]

View File

@ -1,5 +1,10 @@
#[macro_use]
extern crate bitflags;
extern crate gio;
extern crate glib;
extern crate glib_sys;
extern crate gtk;
extern crate gtk_sys;
#[allow(unused_imports)]
#[macro_use] // only for tests
extern crate maplit;
@ -13,6 +18,7 @@ pub mod imservice;
mod keyboard;
mod layout;
mod outputs;
mod popover;
mod resources;
mod submission;
mod util;

9
src/popover.h Normal file
View File

@ -0,0 +1,9 @@
#ifndef POPOVER_H__
#define POPOVER_H__
#include <gtk/gtk.h>
#include "eek/eek-keyboard.h"
void squeek_popover_show(GtkWidget*, struct button_place);
#endif

158
src/popover.rs Normal file
View File

@ -0,0 +1,158 @@
/*! The layout chooser popover */
use gio;
use gtk;
use ::layout::c::EekGtkKeyboard;
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_sys;
use glib::translate::FromGlibPtrFull;
use glib::translate::ToGlibPtr;
/// Unpacks tuple & array variants
fn get_items(items: glib::Variant) -> Vec<glib::Variant> {
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::<String>().unwrap(),
v[1].get::<String>().unwrap(),
)
})
.collect()
}
}
fn make_menu_builder(inputs: Vec<&str>) -> gtk::Builder {
let mut xml: Vec<u8> = Vec::new();
writeln!(
xml,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<interface>
<menu id=\"app-menu\">
<section>"
).unwrap();
for input in inputs {
writeln!(
xml,
"
<item>
<attribute name=\"label\" translatable=\"yes\">{}</attribute>
<attribute name=\"action\">layout</attribute>
<attribute name=\"target\">{0}</attribute>
</item>",
input,
).unwrap();
}
writeln!(
xml,
"
</section>
</menu>
</interface>"
).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 inputs = variants::get_tuples(inputs).into_iter();
for (index, (ikind, iname)) in inputs.enumerate() {
if (&ikind, &iname) == (&kind, &name) {
settings.set_uint("current", index as u32);
}
}
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 current = settings.get_uint("current") as usize;
let inputs = variants::get_tuples(inputs);
let input_names: Vec<&str> = inputs.iter()
.map(|(_kind, name)| name.as_str())
.collect();
let builder = make_menu_builder(input_names.clone());
// 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(&gtk::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 initial_state = input_names[current].to_variant();
let layout_action = gio::SimpleAction::new_stateful(
"layout",
Some(initial_state.type_()),
&initial_state,
);
let action_group = gio::SimpleActionGroup::new();
action_group.add_action(&layout_action);
menu.insert_action_group("popup", Some(&action_group));
menu.bind_model(Some(&model), Some("popup"));
menu.connect_closed(move |_menu| {
let state = match layout_action.get_state() {
Some(v) => {
let s = v.get::<String>().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();
}

View File

@ -23,6 +23,7 @@ pub mod c {
}
}
#[derive(Clone, Copy)]
pub struct Timestamp(pub u32);
/// Layout-independent backend. TODO: Have one instance per program or seat