Add a popover menu to switch languages
This commit is contained in:
@ -36,4 +36,5 @@ pub enum Action {
|
||||
/// The key events this symbol submits when submitting text is not possible
|
||||
keys: Vec<KeySym>,
|
||||
},
|
||||
ShowPreferences,
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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,
|
||||
|
||||
159
src/layout.rs
159
src/layout.rs
@ -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)]
|
||||
|
||||
@ -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
9
src/popover.h
Normal 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
158
src/popover.rs
Normal 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(>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 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();
|
||||
}
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user