Compare commits

..

24 Commits

Author SHA1 Message Date
523caa99c5 build: Avoid MaybeUninit on older Debian 2020-10-03 13:11:17 +00:00
20c44476a3 Merge branch 'master' into x11kb 2020-10-03 12:34:50 +00:00
e6c136918c keymaps: Use multiple key maps, each within the limit of what Xorg can accept.
Key maps are switched on key press whenever needed.
2020-10-03 12:22:01 +00:00
24adba44be Merge branch 'leak-fixes' into 'master'
Fix 2 leaks

Closes #148

See merge request Librem5/squeekboard!386
2020-10-03 07:38:52 +00:00
5e43a31051 Fix leak endlessly adding a resource path to the default theme 2020-10-03 16:23:13 +10:00
40850267d4 Fix leak in level_keyboard_new
xkb_keymap_get_as_string requires that the string it returns is freed by
the caller.
2020-10-03 16:07:36 +10:00
c686cf7e81 syntax: Let older rustc understand symbolmap's lifetime 2020-10-01 14:12:15 +00:00
ec5570a547 Merge branch 'keyboard-layout-belgian' into 'master'
proposal for belgian layout (copy of fr)

See merge request Librem5/squeekboard!382
2020-10-01 13:21:49 +00:00
Al
72bd265065 alphabetical order for src/resources.rs tests/meson.build 2020-10-01 14:54:22 +02:00
Al
4357052fe7 proposal for belgian layout (copy of fr) 2020-09-30 15:42:31 +02:00
2959d27ea3 tests: Check for missing return in builtin layouts except emoji 2020-09-28 20:37:34 +00:00
88d3a45083 keymap: Concentrate special handling of BackSpace, which is implicit in Erase action 2020-09-28 20:37:34 +00:00
edc330d683 data: Restore testability of action->keysym conversion 2020-09-28 20:37:31 +00:00
44e06bc0dc keymap: Generate from symbol map, not layout 2020-09-28 17:52:00 +00:00
8e2e8b0f5f vkeyboard: Use a generic slice instead of a vector 2020-09-28 17:36:59 +00:00
4b825c26a6 Merge branch 'docs-fixes' into 'master'
Expand the development documentation in the readme

Closes #227

See merge request Librem5/squeekboard!377
2020-09-26 09:00:51 +00:00
94bfa92c12 Expand the development documentation in the readme
Fixes #227
2020-09-26 00:34:09 +10:00
50fb124b26 Merge branch 'hacking-spelling-fixes' into 'master'
Fix spelling mistakes in doc/hacking.md

Closes #217

See merge request Librem5/squeekboard!378
2020-09-25 12:47:17 +00:00
7aa004ceff Fix spelling mistakes in doc/hacking.md
Fixes #217
2020-09-25 21:44:27 +10:00
fea4ea7392 keymap: Make acceptable by Xwayland
Xwayland is more strict about accepting key maps than Wayland, and it also fails silently. Instead of fixing the other parts of the stack to reshape accepted Wayland key maps into acceptable Xorg key maps, this patch makes Squeekboard serve the maximum compatibility version in the first place.

Compatibility not actually guaranteed, that's purely observational.

Layouts with many characters (above 240) may lose some characters. This is again due to Xwayland accepting stricter layouts than Wayland.
2020-09-25 09:12:01 +00:00
60056dcf26 Merge branch 'honor-a11y-setting' into 'master'
Honor org.gnome.desktop.a11y.applications screen-keyboard-enabled

Closes #222

See merge request Librem5/squeekboard!370
2020-09-24 06:49:13 +00:00
5580853f31 Merge branch 'depr' into 'master'
rust: Fix deprecation warnings

See merge request Librem5/squeekboard!374
2020-09-21 17:01:51 +00:00
d93e9c2b11 rust: Fix deprecation warnings 2020-09-21 10:57:01 +00:00
4ccf11f4fd server-context-service: Don't show keyboard when disabled
If the corresponding a11y settings is disbaled don't unfold
the keyboad at all.

This helps e.g. running the same session on laptops or when
an external keyboard is attached.

Closes: #222
2020-09-14 11:34:17 +02:00
29 changed files with 746 additions and 310 deletions

View File

@ -19,6 +19,7 @@ path = "@path@/examples/test_layout.rs"
[features] [features]
gio_v0_5 = [] gio_v0_5 = []
gtk_v0_5 = [] gtk_v0_5 = []
rustc_less_1_36 = []
# Dependencies which don't change based on build flags # Dependencies which don't change based on build flags
[dependencies.cairo-sys-rs] [dependencies.cairo-sys-rs]

View File

@ -30,29 +30,42 @@ Building
### Dependencies ### Dependencies
See `.gitlab-ci.yml`. See `.gitlab-ci.yml` or run `apt-get build-dep .`
### Build from git repo ### Build from git repo
``` ```bash
$ git clone https://source.puri.sm/Librem5/squeekboard.git $ git clone https://source.puri.sm/Librem5/squeekboard.git
$ cd squeekboard $ cd squeekboard
$ mkdir ../build $ mkdir _build
$ meson ../build/ $ meson _build/
$ cd ../build $ cd _build
$ ninja test $ ninja
$ ninja install
``` ```
To run tests use `ninja test`. To install squeekboard run `ninja install`.
Running Running
------- -------
``` ```bash
$ phoc # if no compatible Wayland compositor is running yet $ phoc # if no compatible Wayland compositor is running yet
$ cd ../build/ $ cd ../build/
$ src/squeekboard $ src/squeekboard
``` ```
Squeekboard honors the gnome "screen-keyboard-enabled" setting. Either enable this through gnome-settings under accessibility or run:
```bash
$ gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true
```
To make the keyboard show you can use either an application that does so automatically, like a text editor or `python3 ./tests/entry.py`, or you can manually trigger it with:
```bash
busctl call --user sm.puri.OSK0 /sm/puri/OSK0 sm.puri.OSK0 SetVisible b true
```
Developing Developing
---------- ----------

89
data/keyboards/be.yaml Normal file
View File

@ -0,0 +1,89 @@
---
outlines:
default: { width: 35.33, height: 52 }
altline: { width: 52.67, height: 52 }
wide: { width: 59, height: 52 }
spaceline: { width: 140, height: 52 }
special: { width: 44, height: 52 }
views:
base:
- "a z e r t y u i o p"
- "q s d f g h j k l m"
- "Shift_L w x c v b n . BackSpace"
- "show_numbers preferences space show_eschars Return"
upper:
- "A Z E R T Y U I O P"
- "Q S D F G H J K L M"
- "Shift_L W X C V B N , BackSpace"
- "show_numbers preferences space show_eschars Return"
numbers:
- "1 2 3 4 5 6 7 8 9 0"
- "@ # € % & - _ + ( )"
- "show_symbols , \" ' colon ; ! ? BackSpace"
- "show_letters preferences space show_eschars Return"
symbols:
- "~ ` | · √ π τ ÷ × ¶"
- "© ® £ $ ¥ ^ ° * { }"
- "show_numbers_from_symbols \\ / < > = [ ] BackSpace"
- "show_letters preferences space show_eschars Return"
eschars:
- "à â ç é è ê î ô ù û"
- "À Â Ç É È Ê Î Ô Ù Û"
- "show_numbers_from_symbols æ œ ä ë ï ö ü BackSpace"
- "show_letters preferences space show_eschars Return"
buttons:
Shift_L:
action:
locking:
lock_view: "upper"
unlock_view: "base"
outline: "altline"
icon: "key-shift"
BackSpace:
outline: "altline"
icon: "edit-clear-symbolic"
action: erase
preferences:
action: "show_prefs"
outline: "special"
icon: "keyboard-mode-symbolic"
show_numbers:
action:
set_view: "numbers"
outline: "wide"
label: "123"
show_numbers_from_symbols:
action:
set_view: "numbers"
outline: "altline"
label: "123"
show_letters:
action:
set_view: "base"
outline: "wide"
label: "abc"
show_symbols:
action:
set_view: "symbols"
outline: "altline"
label: "*/="
show_eschars:
action:
locking:
lock_view: "eschars"
unlock_view: "base"
outline: "altline"
label: "âÂ"
space:
outline: "spaceline"
text: " "
Return:
outline: "wide"
icon: "key-enter"
keysym: "Return"
colon:
text: ":"
"\"":
keysym: "quotedbl"

6
debian/changelog vendored
View File

@ -1,9 +1,3 @@
squeekboard (1.9.3.0pureos0.1) byzantium; urgency=medium
* Upload to byzantium
-- Guido Günther <agx@sigxcpu.org> Tue, 22 Sep 2020 12:42:41 +0200
squeekboard (1.9.3) amber-phone; urgency=medium squeekboard (1.9.3) amber-phone; urgency=medium
[ Björn Tantau ] [ Björn Tantau ]

7
debian/gbp.conf vendored
View File

@ -1,7 +0,0 @@
[DEFAULT]
debian-branch = pureos/byzantium
debian-tag = pureos/%(version)s
debian-tag-msg = %(pkg)s %(version)s
[tag]
sign-tags = true

View File

@ -13,16 +13,16 @@ The overarching principle of *squeekboard* is to empower users.
Software is primarily meant to solve problems of its users. Often in the quest to make software better, a hard distinction is made between the developer, who becomes the creator, and the user, who takes the role of the consumer, without direct influence on the software they use. Software is primarily meant to solve problems of its users. Often in the quest to make software better, a hard distinction is made between the developer, who becomes the creator, and the user, who takes the role of the consumer, without direct influence on the software they use.
This project aims to give users the power to make the software work for them by blurring the lines between users and developers. This project aims to give users the power to make the software work for them by blurring the lines between users and developers.
Nonwithstanding its current state, *squeekboard* must be structured in a way that provides users a gradual way to gain more experience and power to adjust it. It must be easy, in order of importance: Notwithstanding its current state, *squeekboard* must be structured in a way that provides users a gradual way to gain more experience and power to adjust it. It must be easy, in order of importance:
- to use the software, - to use the software,
- to modify its resources, - to modify its resources,
- to change its behaviour, - to change its behavior,
- to contribute upstream. - to contribute upstream.
To give an idea of what it means in practice, those are some examples of what has been important for *squeekboard* so far: To give an idea of what it means in practice, those are some examples of what has been important for *squeekboard* so far:
- being quick and useable, - being quick and usable,
- allowing local overrides of resources and config, - allowing local overrides of resources and config,
- storing resources and config as editable, standard files, - storing resources and config as editable, standard files,
- having complete, up to date documentation of interfaces, - having complete, up to date documentation of interfaces,
@ -33,7 +33,7 @@ To give an idea of what it means in practice, those are some examples of what ha
- having code that is [simple and obvious](https://www.python.org/dev/peps/pep-0020/), - having code that is [simple and obvious](https://www.python.org/dev/peps/pep-0020/),
- having an easy process of testing and accepting contributions. - having an easy process of testing and accepting contributions.
You may notice that they are ordered roughly from "user-focused" to "maintainer-focused". While good properties are desired, sometimes they conflict, and maintainers should give additional weight to those benefitting the user compared to those benefitting regular contributors. You may notice that they are ordered roughly from "user-focused" to "maintainer-focused". While good properties are desired, sometimes they conflict, and maintainers should give additional weight to those benefiting the user compared to those benefiting regular contributors.
Sending patches Sending patches
--------------- ---------------
@ -43,7 +43,7 @@ By submitting a change to this project, you agree to license it under the [GPL l
Development environment Development environment
----------------------- -----------------------
*Squeekboard* is regularly built and tested on [the develpment environment](https://developer.puri.sm/Librem5/Development_Environment.html). *Squeekboard* is regularly built and tested on [the development environment](https://developer.puri.sm/Librem5/Development_Environment.html).
Recent Fedora releases are likely to be tested as well. Recent Fedora releases are likely to be tested as well.
@ -162,7 +162,7 @@ Maintenance
Squeekboard uses Rust & Cargo for some of its dependencies. Squeekboard uses Rust & Cargo for some of its dependencies.
Use the `cargo.sh` script for maintaining the Cargo part of the build. The script takes the usual Cargo commands, after the first 2 positionsl arguments: source directory, and output artifact. So, `cargo test` becomes: Use the `cargo.sh` script for maintaining the Cargo part of the build. The script takes the usual Cargo commands, after the first 2 positional arguments: source directory, and output artifact. So, `cargo test` becomes:
``` ```
cd build_dir cd build_dir

View File

@ -360,6 +360,10 @@ eek_gtk_keyboard_init (EekGtkKeyboard *self)
priv->event = lfb_event_new ("button-pressed"); priv->event = lfb_event_new ("button-pressed");
else else
g_warning ("Failed to init libfeedback: %s", err->message); g_warning ("Failed to init libfeedback: %s", err->message);
GtkIconTheme *theme = gtk_icon_theme_get_default ();
gtk_icon_theme_add_resource_path (theme, "/sm/puri/squeekboard/icons");
} }
static void static void

View File

@ -31,30 +31,19 @@
#include "eek-keyboard.h" #include "eek-keyboard.h"
void level_keyboard_free(LevelKeyboard *self) { /// External linkage for Rust.
xkb_keymap_unref(self->keymap); /// Don't call multiple times on the same copy, just in Drop.
close(self->keymap_fd); void eek_key_map_deinit(struct KeyMap *self) {
squeek_layout_free(self->layout); close(self->fd);
g_free(self);
} }
LevelKeyboard* /// External linkage for Rust.
level_keyboard_new (struct squeek_layout *layout) struct KeyMap eek_key_map_from_str(char *keymap_str) {
{
LevelKeyboard *keyboard = g_new0(LevelKeyboard, 1);
if (!keyboard) {
g_error("Failed to create a keyboard");
}
keyboard->layout = layout;
struct xkb_context *context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); struct xkb_context *context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if (!context) { if (!context) {
g_error("No context created"); g_error("No context created");
} }
const gchar *keymap_str = squeek_layout_get_keymap(keyboard->layout);
struct xkb_keymap *keymap = xkb_keymap_new_from_string(context, keymap_str, struct xkb_keymap *keymap = xkb_keymap_new_from_string(context, keymap_str,
XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
@ -62,10 +51,9 @@ level_keyboard_new (struct squeek_layout *layout)
g_error("Bad keymap:\n%s", keymap_str); g_error("Bad keymap:\n%s", keymap_str);
xkb_context_unref(context); xkb_context_unref(context);
keyboard->keymap = keymap;
keymap_str = xkb_keymap_get_as_string(keymap, XKB_KEYMAP_FORMAT_TEXT_V1); char *xkb_keymap_str = xkb_keymap_get_as_string(keymap, XKB_KEYMAP_FORMAT_TEXT_V1);
keyboard->keymap_len = strlen(keymap_str) + 1; size_t keymap_len = strlen(xkb_keymap_str) + 1;
g_autofree char *path = strdup("/eek_keymap-XXXXXX"); g_autofree char *path = strdup("/eek_keymap-XXXXXX");
char *r = &path[strlen(path) - 6]; char *r = &path[strlen(path) - 6];
@ -79,17 +67,39 @@ level_keyboard_new (struct squeek_layout *layout)
if (keymap_fd < 0) { if (keymap_fd < 0) {
g_error("Failed to set up keymap fd"); g_error("Failed to set up keymap fd");
} }
keyboard->keymap_fd = keymap_fd;
shm_unlink(path); shm_unlink(path);
if (ftruncate(keymap_fd, (off_t)keyboard->keymap_len)) { if (ftruncate(keymap_fd, (off_t)keymap_len)) {
g_error("Failed to increase keymap fd size"); g_error("Failed to increase keymap fd size");
} }
char *ptr = mmap(NULL, keyboard->keymap_len, PROT_WRITE, MAP_SHARED, char *ptr = mmap(NULL, keymap_len, PROT_WRITE, MAP_SHARED,
keymap_fd, 0); keymap_fd, 0);
if ((void*)ptr == (void*)-1) { if ((void*)ptr == (void*)-1) {
g_error("Failed to set up mmap"); g_error("Failed to set up mmap");
} }
strncpy(ptr, keymap_str, keyboard->keymap_len); strncpy(ptr, xkb_keymap_str, keymap_len);
munmap(ptr, keyboard->keymap_len); munmap(ptr, keymap_len);
free(xkb_keymap_str);
xkb_keymap_unref(keymap);
struct KeyMap km = {
.fd = keymap_fd,
.fd_len = keymap_len,
};
return km;
}
void level_keyboard_free(LevelKeyboard *self) {
squeek_layout_free(self->layout);
g_free(self);
}
LevelKeyboard*
level_keyboard_new (struct squeek_layout *layout)
{
LevelKeyboard *keyboard = g_new0(LevelKeyboard, 1);
if (!keyboard) {
g_error("Failed to create a keyboard");
}
keyboard->layout = layout;
return keyboard; return keyboard;
} }

View File

@ -35,16 +35,18 @@ G_BEGIN_DECLS
/// Keyboard state holder /// Keyboard state holder
struct _LevelKeyboard { struct _LevelKeyboard {
struct squeek_layout *layout; // owned struct squeek_layout *layout; // owned
struct xkb_keymap *keymap; // owned // FIXME: This no longer needs to exist, keymap was folded into layout.
int keymap_fd; // keymap formatted as XKB string
size_t keymap_len; // length of the data inside keymap_fd
guint id; // as a key to layout choices guint id; // as a key to layout choices
}; };
typedef struct _LevelKeyboard LevelKeyboard; typedef struct _LevelKeyboard LevelKeyboard;
gchar * eek_keyboard_get_keymap /// Keymap container for Rust interoperability.
(LevelKeyboard *keyboard); struct KeyMap {
uint32_t fd; // keymap formatted as XKB string
size_t fd_len; // length of the data inside keymap_fd
};
gchar *eek_keyboard_get_keymap(LevelKeyboard *keyboard);
LevelKeyboard* LevelKeyboard*
level_keyboard_new (struct squeek_layout *layout); level_keyboard_new (struct squeek_layout *layout);

View File

@ -265,10 +265,6 @@ renderer_init (EekRenderer *self)
self->allocation_height = 0.0; self->allocation_height = 0.0;
self->scale_factor = 1; self->scale_factor = 1;
GtkIconTheme *theme = gtk_icon_theme_get_default ();
gtk_icon_theme_add_resource_path (theme, "/sm/puri/squeekboard/icons");
self->css_provider = squeek_load_style(); self->css_provider = squeek_load_style();
} }

View File

@ -159,7 +159,7 @@ eekboard_context_service_use_layout(EekboardContextService *context, struct sque
// Update the keymap if necessary. // Update the keymap if necessary.
// TODO: Update submission on change event // TODO: Update submission on change event
if (context->submission) { if (context->submission) {
submission_set_keyboard(context->submission, keyboard, timestamp); submission_use_layout(context->submission, keyboard->layout, timestamp);
} }
// Update UI // Update UI
@ -345,7 +345,7 @@ void eekboard_context_service_set_submission(EekboardContextService *context, st
context->submission = submission; context->submission = submission;
if (context->submission) { if (context->submission) {
uint32_t time = gdk_event_get_time(NULL); uint32_t time = gdk_event_get_time(NULL);
submission_set_keyboard(context->submission, context->keyboard, time); submission_use_layout(context->submission, context->keyboard->layout, time);
} }
} }

View File

@ -5,6 +5,7 @@ use std::env;
fn main() -> () { fn main() -> () {
check_builtin_layout( check_builtin_layout(
env::args().nth(1).expect("No argument given").as_str() env::args().nth(1).expect("No argument given").as_str(),
env::args().nth(2).map(|s| s == "allow_missing_return").unwrap_or(false),
); );
} }

View File

@ -83,7 +83,7 @@ cargo_toml_base = configure_file(
cargo_deps = files('Cargo.deps') cargo_deps = files('Cargo.deps')
if get_option('legacy') == true if get_option('legacy') == true
cargo_build_flags += ['--features', 'gtk_v0_5,gio_v0_5'] cargo_build_flags += ['--features', 'gtk_v0_5,gio_v0_5,rustc_less_1_36']
cargo_deps = files('Cargo.deps.legacy') cargo_deps = files('Cargo.deps.legacy')
endif endif

View File

@ -18,7 +18,7 @@ use xkbcommon::xkb;
use ::action; use ::action;
use ::keyboard::{ use ::keyboard::{
KeyState, PressType, KeyState, PressType,
generate_keymap, generate_keycodes, FormattingError generate_keymaps, generate_keycodes, KeyCode, FormattingError
}; };
use ::layout; use ::layout;
use ::layout::ArrangementKind; use ::layout::ArrangementKind;
@ -382,56 +382,45 @@ impl Layout {
) )
)}).collect(); )}).collect();
let keymap: HashMap<String, u32> = generate_keycodes( let symbolmap: HashMap<String, KeyCode> = generate_keycodes(
button_actions.iter() extract_symbol_names(&button_actions)
.filter_map(|(_name, action)| {
match action {
::action::Action::Submit {
text: _, keys,
} => Some(keys),
_ => None,
}
})
.flatten()
.map(|named_keysym| named_keysym.0.as_str())
); );
let button_states = button_actions.into_iter().map(|(name, action)| {
let keycodes = match &action {
::action::Action::Submit { text: _, keys } => {
keys.iter().map(|named_keycode| {
*keymap.get(named_keycode.0.as_str())
.expect(
format!(
"keycode {} in key {} missing from keymap",
named_keycode.0,
name
).as_str()
)
}).collect()
},
action::Action::Erase => vec![
*keymap.get("BackSpace")
.expect(&format!("BackSpace missing from keymap")),
],
_ => Vec::new(),
};
(
name.into(),
KeyState {
pressed: PressType::Released,
keycodes,
action,
}
)
});
let button_states = HashMap::<String, KeyState>::from_iter( let button_states = HashMap::<String, KeyState>::from_iter(
button_states button_actions.into_iter().map(|(name, action)| {
let keycodes = match &action {
::action::Action::Submit { text: _, keys } => {
keys.iter().map(|named_keysym| {
symbolmap.get(named_keysym.0.as_str())
.expect(
format!(
"keysym {} in key {} missing from symbol map",
named_keysym.0,
name
).as_str()
)
.clone()
}).collect()
},
action::Action::Erase => vec![
symbolmap.get("BackSpace")
.expect(&format!("BackSpace missing from symbol map"))
.clone(),
],
_ => Vec::new(),
};
(
name.into(),
KeyState {
pressed: PressType::Released,
keycodes,
action,
}
)
})
); );
// TODO: generate from symbols let keymaps = match generate_keymaps(symbolmap) {
let keymap_str = match generate_keymap(&button_states) {
Err(e) => { return (Err(e), warning_handler) }, Err(e) => { return (Err(e), warning_handler) },
Ok(v) => v, Ok(v) => v,
}; };
@ -495,10 +484,10 @@ impl Layout {
( (
Ok(::layout::LayoutData { Ok(::layout::LayoutData {
views: views, views: views,
keymap_str: { keymaps: keymaps.into_iter().map(|keymap_str|
CString::new(keymap_str) CString::new(keymap_str)
.expect("Invalid keymap string generated") .expect("Invalid keymap string generated")
}, ).collect(),
// FIXME: use a dedicated field // FIXME: use a dedicated field
margins: layout::Margins { margins: layout::Margins {
top: self.margins.top, top: self.margins.top,
@ -734,11 +723,27 @@ fn create_button<H: logging::Handler>(
} }
} }
fn extract_symbol_names<'a>(actions: &'a [(&str, action::Action)])
-> impl Iterator<Item=String> + 'a
{
actions.iter()
.filter_map(|(_name, act)| {
match act {
action::Action::Submit {
text: _, keys,
} => Some(keys.clone()),
action::Action::Erase => Some(vec!(action::KeySym("BackSpace".into()))),
_ => None,
}
})
.flatten()
.map(|named_keysym| named_keysym.0)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::error::Error as ErrorTrait;
use ::logging::ProblemPanic; use ::logging::ProblemPanic;
const THIS_FILE: &str = file!(); const THIS_FILE: &str = file!();
@ -786,7 +791,8 @@ mod tests {
Err(e) => { Err(e) => {
let mut handled = false; let mut handled = false;
if let Error::Yaml(ye) = &e { if let Error::Yaml(ye) = &e {
handled = ye.description() == "missing field `views`"; handled = ye.to_string()
.starts_with("missing field `views`");
}; };
if !handled { if !handled {
println!("Unexpected error {:?}", e); println!("Unexpected error {:?}", e);
@ -804,7 +810,7 @@ mod tests {
Err(e) => { Err(e) => {
let mut handled = false; let mut handled = false;
if let Error::Yaml(ye) = &e { if let Error::Yaml(ye) = &e {
handled = ye.description() handled = ye.to_string()
.starts_with("unknown field `bad_field`"); .starts_with("unknown field `bad_field`");
}; };
if !handled { if !handled {
@ -862,6 +868,23 @@ mod tests {
); );
} }
/// Test if erase yields a useable keycode
#[test]
fn test_layout_erase() {
let out = Layout::from_file(path_from_root("tests/layout_erase.yaml"))
.unwrap()
.build(ProblemPanic).0
.unwrap();
assert_eq!(
out.views["base"].1
.get_rows()[0].1
.buttons[0].1
.state.borrow()
.keycodes.len(),
1
);
}
#[test] #[test]
fn parsing_fallback() { fn parsing_fallback() {
assert!(Layout::from_resource(FALLBACK_LAYOUT_NAME) assert!(Layout::from_resource(FALLBACK_LAYOUT_NAME)
@ -939,4 +962,35 @@ mod tests {
} }
); );
} }
#[test]
fn test_extract_symbols() {
let actions = [(
"ac",
action::Action::Submit {
text: None,
keys: vec![
action::KeySym("a".into()),
action::KeySym("c".into()),
],
},
)];
assert_eq!(
extract_symbol_names(&actions[..]).collect::<Vec<_>>(),
vec!["a", "c"],
);
}
#[test]
fn test_extract_symbols_erase() {
let actions = [(
"Erase",
action::Action::Erase,
)];
assert_eq!(
extract_symbol_names(&actions[..]).collect::<Vec<_>>(),
vec!["BackSpace"],
);
}
} }

View File

@ -5,11 +5,13 @@ use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::io; use std::io;
use std::mem;
use std::ptr;
use std::rc::Rc; use std::rc::Rc;
use std::string::FromUtf8Error; use std::string::FromUtf8Error;
use ::action::Action; use ::action::Action;
use ::logging; use ::util;
// Traits // Traits
use std::io::Write; use std::io::Write;
@ -21,7 +23,12 @@ pub enum PressType {
Pressed = 1, Pressed = 1,
} }
pub type KeyCode = u32; /// The extended, unambiguous layout-keycode
#[derive(Debug, Clone)]
pub struct KeyCode {
pub code: u32,
pub keymap_idx: usize,
}
bitflags!{ bitflags!{
/// Map to `virtual_keyboard.modifiers` modifiers values /// Map to `virtual_keyboard.modifiers` modifiers values
@ -80,10 +87,10 @@ impl KeyState {
} }
/// Sorts an iterator by converting it to a Vector and back /// Sorts an iterator by converting it to a Vector and back
fn sorted<'a, I: Iterator<Item=&'a str>>( fn sorted<'a, I: Iterator<Item=String>>(
iter: I iter: I
) -> impl Iterator<Item=&'a str> { ) -> impl Iterator<Item=String> {
let mut v: Vec<&'a str> = iter.collect(); let mut v: Vec<String> = iter.collect();
v.sort(); v.sort();
v.into_iter() v.into_iter()
} }
@ -91,15 +98,17 @@ fn sorted<'a, I: Iterator<Item=&'a str>>(
/// Generates a mapping where each key gets a keycode, starting from ~~8~~ /// Generates a mapping where each key gets a keycode, starting from ~~8~~
/// HACK: starting from 9, because 8 results in keycode 0, /// HACK: starting from 9, because 8 results in keycode 0,
/// which the compositor likes to discard /// which the compositor likes to discard
pub fn generate_keycodes<'a, C: IntoIterator<Item=&'a str>>( pub fn generate_keycodes<'a, C: IntoIterator<Item=String>>(
key_names: C key_names: C,
) -> HashMap<String, u32> { ) -> HashMap<String, KeyCode> {
let special_keysyms = ["BackSpace", "Return"].iter().map(|&s| s);
HashMap::from_iter( HashMap::from_iter(
// sort to remove a source of indeterminism in keycode assignment // Sort to remove a source of indeterminism in keycode assignment.
sorted(key_names.into_iter().chain(special_keysyms)) sorted(key_names.into_iter())
.map(|name| String::from(name)) .zip(util::cycle_count(9..255))
.zip(9..) .map(|(name, (code, keymap_idx))| (
String::from(name),
KeyCode { code, keymap_idx },
))
) )
} }
@ -124,12 +133,54 @@ impl From<io::Error> for FormattingError {
} }
} }
/// Index is the key code, String is the occupant.
/// Starts all empty.
/// https://gitlab.freedesktop.org/xorg/xserver/-/issues/260
type SingleKeyMap = [Option<String>; 256];
fn single_key_map_new() -> SingleKeyMap {
// Why can't we just initialize arrays without tricks -_- ?
unsafe {
// Inspired by
// https://www.reddit.com/r/rust/comments/5n7bh1/how_to_create_an_array_of_a_type_with_clone_but/
#[cfg(feature = "rustc_less_1_36")]
let mut array: SingleKeyMap = mem::uninitialized();
#[cfg(not(feature = "rustc_less_1_36"))]
let mut array: SingleKeyMap = mem::MaybeUninit::uninit().assume_init();
for element in array.iter_mut() {
ptr::write(element, None);
}
array
}
}
pub fn generate_keymaps(symbolmap: HashMap::<String, KeyCode>)
-> Result<Vec<String>, FormattingError>
{
let mut bins: Vec<SingleKeyMap> = Vec::new();
for (name, KeyCode { code, keymap_idx }) in symbolmap.into_iter() {
if keymap_idx >= bins.len() {
bins.resize_with(
keymap_idx + 1,
|| single_key_map_new(),
);
}
bins[keymap_idx][code as usize] = Some(name);
}
let mut out = Vec::new();
for bin in bins {
out.push(generate_keymap(&bin)?);
}
Ok(out)
}
/// Generates a de-facto single level keymap. /// Generates a de-facto single level keymap.
// TODO: don't rely on keys and their order, /// Key codes must not repeat and must remain between 9 and 255.
// but rather on what keysyms and keycodes are in use. fn generate_keymap(
// Iterating actions makes it hard to deduplicate keysyms. symbolmap: &SingleKeyMap,
pub fn generate_keymap(
keystates: &HashMap::<String, KeyState>
) -> Result<String, FormattingError> { ) -> Result<String, FormattingError> {
let mut buf: Vec<u8> = Vec::new(); let mut buf: Vec<u8> = Vec::new();
writeln!( writeln!(
@ -140,86 +191,80 @@ pub fn generate_keymap(
minimum = 8; minimum = 8;
maximum = 255;" maximum = 255;"
)?; )?;
let pairs: Vec<(&String, usize)> = symbolmap.iter()
// Attach a key code to each cell.
.enumerate()
// Get rid of empty keycodes.
.filter_map(|(code, name)| name.as_ref().map(|n| (n, code)))
.collect();
for (name, state) in keystates.iter() { // Xorg can only consume up to 255 keys, so this may not work in Xwayland.
match &state.action { // Two possible solutions:
Action::Submit { text: _, keys } => { // - use levels to cram multiple characters into one key
if let 0 = keys.len() { // - swap layouts on key presses
log_print!( for (_name, keycode) in &pairs {
logging::Level::Warning, write!(
"Key {} has no keysyms", name, buf,
); "
}; <I{}> = {0};",
for (named_keysym, keycode) in keys.iter().zip(&state.keycodes) { keycode,
write!( )?;
buf,
"
<{}> = {};",
named_keysym.0,
keycode,
)?;
}
},
Action::Erase => {
let mut keycodes = state.keycodes.iter();
write!(
buf,
"
<BackSpace> = {};",
keycodes.next().expect("Erase key has no keycode"),
)?;
if let Some(_) = keycodes.next() {
log_print!(
logging::Level::Bug,
"Erase key has multiple keycodes",
);
}
},
_ => {},
}
} }
writeln!( writeln!(
buf, buf,
" "
indicator 1 = \"Caps Lock\"; // Xwayland won't accept without it.
}}; }};
xkb_symbols \"squeekboard\" {{ xkb_symbols \"squeekboard\" {{
"
name[Group1] = \"Letters\";
name[Group2] = \"Numbers/Symbols\";
key <BackSpace> {{ [ BackSpace ] }};"
)?; )?;
for (_name, state) in keystates.iter() { for (name, keycode) in pairs {
if let Action::Submit { text: _, keys } = &state.action { write!(
for keysym in keys.iter() { buf,
write!( "
buf, key <I{}> {{ [ {} ] }};",
" keycode,
key <{}> {{ [ {0} ] }};", name,
keysym.0, )?;
)?;
}
}
} }
writeln!( writeln!(
buf, buf,
" "
}}; }};
xkb_types \"squeekboard\" {{ xkb_types \"squeekboard\" {{
virtual_modifiers Squeekboard; // No modifiers! Needed for Xorg for some reason.
// Those names are needed for Xwayland.
type \"ONE_LEVEL\" {{
modifiers= none;
level_name[Level1]= \"Any\";
}};
type \"TWO_LEVEL\" {{
level_name[Level1]= \"Base\";
}};
type \"ALPHABETIC\" {{
level_name[Level1]= \"Base\";
}};
type \"KEYPAD\" {{
level_name[Level1]= \"Base\";
}};
type \"SHIFT+ALT\" {{
level_name[Level1]= \"Base\";
}};
type \"TWO_LEVEL\" {{
modifiers = Shift;
map[Shift] = Level2;
level_name[Level1] = \"Base\";
level_name[Level2] = \"Shift\";
}};
}}; }};
xkb_compatibility \"squeekboard\" {{ xkb_compatibility \"squeekboard\" {{
// Needed for Xwayland again.
interpret Any+AnyOf(all) {{
action= SetMods(modifiers=modMapMods,clearLocks);
}};
}}; }};
}};" }};"
)?; )?;
@ -234,22 +279,15 @@ mod tests {
use xkbcommon::xkb; use xkbcommon::xkb;
use ::action::KeySym;
#[test] #[test]
fn test_keymap_multi() { fn test_keymap_single_resolve() {
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); let mut key_map = single_key_map_new();
key_map[9] = Some("a".into());
key_map[10] = Some("c".into());
let keymap_str = generate_keymap(&hashmap!{ let keymap_str = generate_keymap(&key_map).unwrap();
"ac".into() => KeyState {
action: Action::Submit { let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
text: None,
keys: vec!(KeySym("a".into()), KeySym("c".into())),
},
keycodes: vec!(9, 10),
pressed: PressType::Released,
},
}).unwrap();
let keymap = xkb::Keymap::new_from_string( let keymap = xkb::Keymap::new_from_string(
&context, &context,
@ -263,4 +301,36 @@ mod tests {
assert_eq!(state.key_get_one_sym(9), xkb::KEY_a); assert_eq!(state.key_get_one_sym(9), xkb::KEY_a);
assert_eq!(state.key_get_one_sym(10), xkb::KEY_c); assert_eq!(state.key_get_one_sym(10), xkb::KEY_c);
} }
#[test]
fn test_keymap_second_resolve() {
let keymaps = generate_keymaps(hashmap!(
"a".into() => KeyCode { keymap_idx: 1, code: 9 },
)).unwrap();
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
let keymap = xkb::Keymap::new_from_string(
&context,
keymaps[1].clone(), // this index is part of the test
xkb::KEYMAP_FORMAT_TEXT_V1,
xkb::KEYMAP_COMPILE_NO_FLAGS,
).expect("Failed to create keymap");
let state = xkb::State::new(&keymap);
assert_eq!(state.key_get_one_sym(9), xkb::KEY_a);
}
#[test]
fn test_symbolmap_overflow() {
// The 257th key (U1101) is interesting.
// Use Unicode encoding for being able to use in xkb keymaps.
let keynames = (0..258).map(|num| format!("U{:04X}", 0x1000 + num));
let keycodes = generate_keycodes(keynames);
// test now
let code = keycodes.get("U1101").expect("Did not find the tested keysym");
assert_eq!(code.keymap_idx, 1);
}
} }

View File

@ -39,7 +39,6 @@ struct transformation squeek_layout_calculate_transformation(
double allocation_width, double allocation_size); double allocation_width, double allocation_size);
struct squeek_layout *squeek_load_layout(const char *name, uint32_t type); struct squeek_layout *squeek_load_layout(const char *name, uint32_t type);
const char *squeek_layout_get_keymap(const struct squeek_layout*);
enum squeek_arrangement_kind squeek_layout_get_kind(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_free(struct squeek_layout*);

View File

@ -236,13 +236,6 @@ pub mod c {
height: allocation_height, height: allocation_height,
}) })
} }
#[no_mangle]
pub extern "C"
fn squeek_layout_get_keymap(layout: *const Layout) -> *const c_char {
let layout = unsafe { &*layout };
layout.keymap_str.as_ptr()
}
#[no_mangle] #[no_mangle]
pub extern "C" pub extern "C"
@ -632,8 +625,8 @@ pub struct Layout {
pub views: HashMap<String, (c::Point, View)>, pub views: HashMap<String, (c::Point, View)>,
// Non-UI stuff // Non-UI stuff
/// xkb keymap applicable to the contained keys. Unchangeable /// xkb keymaps applicable to the contained keys. Unchangeable
pub keymap_str: CString, pub keymaps: Vec<CString>,
// Changeable state // Changeable state
// a Vec would be enough, but who cares, this will be small & fast enough // 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. // TODO: turn those into per-input point *_buttons to track dragging.
@ -649,7 +642,7 @@ pub struct Layout {
pub struct LayoutData { pub struct LayoutData {
/// Point is the offset within layout /// Point is the offset within layout
pub views: HashMap<String, (c::Point, View)>, pub views: HashMap<String, (c::Point, View)>,
pub keymap_str: CString, pub keymaps: Vec<CString>,
pub margins: Margins, pub margins: Margins,
} }
@ -672,7 +665,7 @@ impl Layout {
kind, kind,
current_view: "base".to_owned(), current_view: "base".to_owned(),
views: data.views, views: data.views,
keymap_str: data.keymap_str, keymaps: data.keymaps,
pressed_keys: HashSet::new(), pressed_keys: HashSet::new(),
margins: data.margins, margins: data.margins,
} }
@ -1135,7 +1128,7 @@ mod test {
]); ]);
let layout = Layout { let layout = Layout {
current_view: String::new(), current_view: String::new(),
keymap_str: CString::new("").unwrap(), keymaps: Vec::new(),
kind: ArrangementKind::Base, kind: ArrangementKind::Base,
pressed_keys: HashSet::new(), pressed_keys: HashSet::new(),
// Lots of bottom margin // Lots of bottom margin

View File

@ -31,22 +31,16 @@ pub enum Error {
impl ::std::fmt::Display for Error { impl ::std::fmt::Display for Error {
fn fmt(&self, out: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { fn fmt(&self, out: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
use ::std::error::Error; out.write_str(match self {
out.write_str(self.description())
}
}
impl ::std::error::Error for Error {
fn description(&self) -> &str {
match self {
&Error::NotWellFormed => "Language tag is not well-formed.", &Error::NotWellFormed => "Language tag is not well-formed.",
// this is exception: here we do want exhaustive match so we don't publish version with // this is exception: here we do want exhaustive match so we don't publish version with
// missing descriptions by mistake. // missing descriptions by mistake.
&Error::__NonExhaustive => panic!("Placeholder error must not be instantiated!"), &Error::__NonExhaustive => panic!("Placeholder error must not be instantiated!"),
} })
} }
} }
/// Convenience Result alias. /// Convenience Result alias.
type Result<T> = ::std::result::Result<T, Error>; type Result<T> = ::std::result::Result<T, Error>;

View File

@ -17,6 +17,7 @@ const KEYBOARDS: &[(*const str, *const str)] = &[
("us_wide", include_str!("../data/keyboards/us_wide.yaml")), ("us_wide", include_str!("../data/keyboards/us_wide.yaml")),
("br", include_str!("../data/keyboards/br.yaml")), ("br", include_str!("../data/keyboards/br.yaml")),
("de", include_str!("../data/keyboards/de.yaml")), ("de", include_str!("../data/keyboards/de.yaml")),
("be", include_str!("../data/keyboards/be.yaml")),
("de_wide", include_str!("../data/keyboards/de_wide.yaml")), ("de_wide", include_str!("../data/keyboards/de_wide.yaml")),
("dk", include_str!("../data/keyboards/dk.yaml")), ("dk", include_str!("../data/keyboards/dk.yaml")),
("es", include_str!("../data/keyboards/es.yaml")), ("es", include_str!("../data/keyboards/es.yaml")),

View File

@ -31,6 +31,7 @@
enum { enum {
PROP_0, PROP_0,
PROP_VISIBLE, PROP_VISIBLE,
PROP_ENABLED,
PROP_LAST PROP_LAST
}; };
@ -44,6 +45,7 @@ struct _ServerContextService {
struct ui_manager *manager; // unowned struct ui_manager *manager; // unowned
gboolean visible; gboolean visible;
gboolean enabled;
PhoshLayerSurface *window; PhoshLayerSurface *window;
GtkWidget *widget; // nullable GtkWidget *widget; // nullable
guint hiding; guint hiding;
@ -208,6 +210,9 @@ on_hide (ServerContextService *self)
static void static void
server_context_service_real_show_keyboard (ServerContextService *self) server_context_service_real_show_keyboard (ServerContextService *self)
{ {
if (!self->enabled)
return;
if (self->hiding) { if (self->hiding) {
g_source_remove (self->hiding); g_source_remove (self->hiding);
self->hiding = 0; self->hiding = 0;
@ -263,7 +268,9 @@ server_context_service_set_property (GObject *object,
case PROP_VISIBLE: case PROP_VISIBLE:
self->visible = g_value_get_boolean (value); self->visible = g_value_get_boolean (value);
break; break;
case PROP_ENABLED:
server_context_service_set_enabled (self, g_value_get_boolean (value));
break;
default: default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break; break;
@ -319,11 +326,43 @@ server_context_service_class_init (ServerContextServiceClass *klass)
g_object_class_install_property (gobject_class, g_object_class_install_property (gobject_class,
PROP_VISIBLE, PROP_VISIBLE,
pspec); pspec);
/**
* ServerContextServie:keyboard:
*
* Does the user want the keyboard to show up automatically?
*/
pspec =
g_param_spec_boolean ("enabled",
"Enabled",
"Whether the keyboard is enabled",
TRUE,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
g_object_class_install_property (gobject_class,
PROP_ENABLED,
pspec);
} }
static void static void
server_context_service_init (ServerContextService *self) { server_context_service_init (ServerContextService *self) {
(void)self; const char *schema_name = "org.gnome.desktop.a11y.applications";
GSettingsSchemaSource *ssrc = g_settings_schema_source_get_default();
g_autoptr(GSettingsSchema) schema = NULL;
self->enabled = TRUE;
if (!ssrc) {
g_warning("No gsettings schemas installed.");
return;
}
schema = g_settings_schema_source_lookup(ssrc, schema_name, TRUE);
if (schema) {
g_autoptr(GSettings) settings = g_settings_new (schema_name);
g_settings_bind (settings, "screen-keyboard-enabled",
self, "enabled", G_SETTINGS_BIND_GET);
} else {
g_warning("Gsettings schema %s is not installed on the system. "
"Enabling by default.", schema_name);
}
} }
ServerContextService * ServerContextService *
@ -336,3 +375,18 @@ server_context_service_new (EekboardContextService *self, struct submission *sub
ui->manager = uiman; ui->manager = uiman;
return ui; return ui;
} }
void
server_context_service_set_enabled (ServerContextService *self, gboolean enabled)
{
g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (self));
if (enabled == self->enabled)
return;
self->enabled = enabled;
if (self->enabled)
server_context_service_show_keyboard (self);
else
server_context_service_hide_keyboard (self);
}

View File

@ -33,6 +33,7 @@ ServerContextService *server_context_service_new(EekboardContextService *self, s
enum squeek_arrangement_kind server_context_service_get_layout_type(ServerContextService *); enum squeek_arrangement_kind server_context_service_get_layout_type(ServerContextService *);
void server_context_service_show_keyboard (ServerContextService *self); void server_context_service_show_keyboard (ServerContextService *self);
void server_context_service_hide_keyboard (ServerContextService *self); void server_context_service_hide_keyboard (ServerContextService *self);
void server_context_service_set_enabled (ServerContextService *self, gboolean enabled);
G_END_DECLS G_END_DECLS
#endif /* SERVER_CONTEXT_SERVICE_H */ #endif /* SERVER_CONTEXT_SERVICE_H */

View File

@ -6,6 +6,7 @@
#include "eek/eek-types.h" #include "eek/eek-types.h"
struct submission; struct submission;
struct squeek_layout;
struct submission* get_submission(struct zwp_input_method_manager_v2 *immanager, struct submission* get_submission(struct zwp_input_method_manager_v2 *immanager,
struct zwp_virtual_keyboard_manager_v1 *vkmanager, struct zwp_virtual_keyboard_manager_v1 *vkmanager,
@ -15,5 +16,5 @@ struct submission* get_submission(struct zwp_input_method_manager_v2 *immanager,
// Defined in Rust // Defined in Rust
struct submission* submission_new(struct zwp_input_method_v2 *im, struct zwp_virtual_keyboard_v1 *vk, EekboardContextService *state); struct submission* submission_new(struct zwp_input_method_v2 *im, struct zwp_virtual_keyboard_v1 *vk, EekboardContextService *state);
void submission_set_ui(struct submission *self, ServerContextService *ui_context); void submission_set_ui(struct submission *self, ServerContextService *ui_context);
void submission_set_keyboard(struct submission *self, LevelKeyboard *keyboard, uint32_t time); void submission_use_layout(struct submission *self, struct squeek_layout *layout, uint32_t time);
#endif #endif

View File

@ -23,8 +23,9 @@ use ::action::Modifier;
use ::imservice; use ::imservice;
use ::imservice::IMService; use ::imservice::IMService;
use ::keyboard::{ KeyCode, KeyStateId, Modifiers, PressType }; use ::keyboard::{ KeyCode, KeyStateId, Modifiers, PressType };
use ::layout::c::LevelKeyboard; use ::layout;
use ::util::vec_remove; use ::util::vec_remove;
use ::vkeyboard;
use ::vkeyboard::VirtualKeyboard; use ::vkeyboard::VirtualKeyboard;
// traits // traits
@ -68,6 +69,8 @@ pub mod c {
modifiers_active: Vec::new(), modifiers_active: Vec::new(),
virtual_keyboard: VirtualKeyboard(vk), virtual_keyboard: VirtualKeyboard(vk),
pressed: Vec::new(), pressed: Vec::new(),
keymap_fds: Vec::new(),
keymap_idx: None,
} }
)) ))
} }
@ -91,16 +94,17 @@ pub mod c {
#[no_mangle] #[no_mangle]
pub extern "C" pub extern "C"
fn submission_set_keyboard( fn submission_use_layout(
submission: *mut Submission, submission: *mut Submission,
keyboard: LevelKeyboard, layout: *const layout::Layout,
time: u32, time: u32,
) { ) {
if submission.is_null() { if submission.is_null() {
panic!("Null submission pointer"); panic!("Null submission pointer");
} }
let submission: &mut Submission = unsafe { &mut *submission }; let submission: &mut Submission = unsafe { &mut *submission };
submission.update_keymap(keyboard, Timestamp(time)); let layout = unsafe { &*layout };
submission.use_layout(layout, Timestamp(time));
} }
} }
@ -119,6 +123,8 @@ pub struct Submission {
virtual_keyboard: VirtualKeyboard, virtual_keyboard: VirtualKeyboard,
modifiers_active: Vec<(KeyStateId, Modifier)>, modifiers_active: Vec<(KeyStateId, Modifier)>,
pressed: Vec<(KeyStateId, SubmittedAction)>, pressed: Vec<(KeyStateId, SubmittedAction)>,
keymap_fds: Vec<vkeyboard::c::KeyMap>,
keymap_idx: Option<usize>,
} }
pub enum SubmitData<'a> { pub enum SubmitData<'a> {
@ -177,11 +183,34 @@ impl Submission {
let submit_action = match was_committed_as_text { let submit_action = match was_committed_as_text {
true => SubmittedAction::IMService, true => SubmittedAction::IMService,
false => { false => {
self.virtual_keyboard.switch( let keycodes_count = keycodes.len();
keycodes, for keycode in keycodes.iter() {
PressType::Pressed, self.select_keymap(keycode.keymap_idx, time);
time, let keycode = keycode.code;
); match keycodes_count {
// Pressing a key made out of a single keycode is simple:
// press on press, release on release.
1 => self.virtual_keyboard.switch(
keycode,
PressType::Pressed,
time,
),
// A key made of multiple keycodes
// has to submit them one after the other.
_ => {
self.virtual_keyboard.switch(
keycode.clone(),
PressType::Pressed,
time,
);
self.virtual_keyboard.switch(
keycode.clone(),
PressType::Released,
time,
);
},
};
}
SubmittedAction::VirtualKeyboard(keycodes.clone()) SubmittedAction::VirtualKeyboard(keycodes.clone())
}, },
}; };
@ -199,11 +228,21 @@ impl Submission {
// no matter if the imservice got activated, // no matter if the imservice got activated,
// keys must be released // keys must be released
SubmittedAction::VirtualKeyboard(keycodes) => { SubmittedAction::VirtualKeyboard(keycodes) => {
self.virtual_keyboard.switch( let keycodes_count = keycodes.len();
&keycodes, match keycodes_count {
PressType::Released, 1 => {
time, let keycode = &keycodes[0];
) self.select_keymap(keycode.keymap_idx, time);
self.virtual_keyboard.switch(
keycode.code,
PressType::Released,
time,
);
},
// Design choice here: submit multiple all at press time
// and do nothing at release time.
_ => {},
};
}, },
} }
}; };
@ -274,6 +313,7 @@ impl Submission {
} }
} }
/// Changes keymap and clears pressed keys and modifiers. /// Changes keymap and clears pressed keys and modifiers.
/// ///
/// It's not obvious if clearing is the right thing to do, /// It's not obvious if clearing is the right thing to do,
@ -283,9 +323,28 @@ impl Submission {
/// Alternatively, modifiers could be restored on the new keymap. /// Alternatively, modifiers could be restored on the new keymap.
/// That approach might be difficult /// That approach might be difficult
/// due to modifiers meaning different things in different keymaps. /// due to modifiers meaning different things in different keymaps.
pub fn update_keymap(&mut self, keyboard: LevelKeyboard, time: Timestamp) { fn select_keymap(&mut self, idx: usize, time: Timestamp) {
self.clear_all_modifiers(); if self.keymap_idx != Some(idx) {
self.release_all_virtual_keys(time); self.keymap_idx = Some(idx);
self.virtual_keyboard.update_keymap(keyboard); self.clear_all_modifiers();
self.release_all_virtual_keys(time);
let keymap = &self.keymap_fds[idx];
self.virtual_keyboard.update_keymap(keymap);
}
}
pub fn use_layout(&mut self, layout: &layout::Layout, time: Timestamp) {
self.keymap_fds = layout.keymaps.iter()
.map(|keymap_str| vkeyboard::c::KeyMap::from_cstr(
keymap_str.as_c_str()
))
.collect();
self.keymap_idx = None;
// This can probably be eliminated,
// because key presses can trigger an update anyway.
// However, self.keymap_idx needs to become Option<>
// in order to force update on new layouts.
self.select_keymap(0, time);
} }
} }

View File

@ -26,49 +26,104 @@ impl CountAndPrint {
} }
} }
pub fn check_builtin_layout(name: &str) { pub fn check_builtin_layout(name: &str, missing_return: bool) {
check_layout(Layout::from_resource(name).expect("Invalid layout data")) check_layout(
Layout::from_resource(name).expect("Invalid layout data"),
missing_return,
)
} }
pub fn check_layout_file(path: &str) { pub fn check_layout_file(path: &str) {
check_layout(Layout::from_file(path.into()).expect("Invalid layout file")) check_layout(
Layout::from_file(path.into()).expect("Invalid layout file"),
false,
)
} }
fn check_layout(layout: Layout) { fn check_sym_in_keymap(state: &xkb::State, sym_name: &str) -> bool {
let sym = xkb::keysym_from_name(sym_name, xkb::KEYSYM_NO_FLAGS);
if sym == xkb::KEY_NoSymbol {
panic!(format!("Entered invalid keysym: {}", sym_name));
}
let map = state.get_keymap();
let range = map.min_keycode()..=map.max_keycode();
range.flat_map(|code| state.key_get_syms(code))
.find(|s| **s == sym)
.is_some()
}
fn check_sym_presence(
states: &[xkb::State],
sym_name: &str,
handler: &mut dyn logging::Handler,
) {
let found = states.iter()
.position(|state| {
check_sym_in_keymap(&state, sym_name)
});
if let None = found {
handler.handle(
logging::Level::Surprise,
&format!("There's no way to input the keysym {} on this layout", sym_name),
)
}
}
fn check_layout(layout: Layout, allow_missing_return: bool) {
let handler = CountAndPrint::new(); let handler = CountAndPrint::new();
let (layout, handler) = layout.build(handler); let (layout, mut handler) = layout.build(handler);
if handler.0 > 0 { if handler.0 > 0 {
println!("{} problems while parsing layout", handler.0) println!("{} problems while parsing layout", handler.0)
} }
let layout = layout.expect("layout broken"); let layout = layout.expect("layout broken");
let xkb_states: Vec<xkb::State> = layout.keymaps.iter()
.map(|keymap_str| {
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
let keymap_str = keymap_str
.clone()
.into_string().expect("Failed to decode keymap string");
let keymap = xkb::Keymap::new_from_string(
&context,
keymap_str.clone(),
xkb::KEYMAP_FORMAT_TEXT_V1,
xkb::KEYMAP_COMPILE_NO_FLAGS,
).expect("Failed to create keymap");
xkb::State::new(&keymap)
})
.collect();
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); check_sym_presence(&xkb_states, "BackSpace", &mut handler);
let mut printer = logging::Print;
let keymap_str = layout.keymap_str check_sym_presence(
.clone() &xkb_states,
.into_string().expect("Failed to decode keymap string"); "Return",
if allow_missing_return { &mut printer }
let keymap = xkb::Keymap::new_from_string( else { &mut handler },
&context, );
keymap_str.clone(),
xkb::KEYMAP_FORMAT_TEXT_V1,
xkb::KEYMAP_COMPILE_NO_FLAGS,
).expect("Failed to create keymap");
let state = xkb::State::new(&keymap);
// "Press" each button with keysyms // "Press" each button with keysyms
for (_pos, view) in layout.views.values() { for (_pos, view) in layout.views.values() {
for (_y, row) in &view.get_rows() { for (_y, row) in &view.get_rows() {
for (_x, button) in &row.buttons { for (_x, button) in &row.buttons {
let keystate = button.state.borrow(); let keystate = button.state.borrow();
for keycode in &keystate.keycodes { for keycode in &keystate.keycodes {
match state.key_get_one_sym(*keycode) { match xkb_states[keycode.keymap_idx].key_get_one_sym(keycode.code) {
xkb::KEY_NoSymbol => { xkb::KEY_NoSymbol => {
eprintln!("{}", keymap_str); eprintln!(
panic!("Keysym {} on key {:?} can't be resolved", keycode, button.name); "keymap {}: {}",
keycode.keymap_idx,
layout.keymaps[keycode.keymap_idx].to_str().unwrap(),
);
panic!(
"Keysym for code {:?} on key {} ({:?}) can't be resolved",
keycode,
button.name.to_string_lossy(),
button.name,
);
}, },
_ => {}, _ => {},
} }

View File

@ -203,6 +203,23 @@ pub fn vec_remove<T, F: FnMut(&T) -> bool>(v: &mut Vec<T>, pred: F) -> Option<T>
idx.map(|idx| v.remove(idx)) idx.map(|idx| v.remove(idx))
} }
/// Repeats all the items of the iterator forever,
/// but returns the cycle number alongside.
/// Inefficient due to all the vectors, but doesn't have to be fast.
pub fn cycle_count<T, I: Clone + Iterator<Item=T>>(iter: I)
-> impl Iterator<Item=(T, usize)>
{
let numbered_copies = vec![iter].into_iter()
.cycle()
.enumerate();
numbered_copies.flat_map(|(idx, cycle)|
// Pair each element from the cycle with a copy of the index.
cycle.zip(
vec![idx].into_iter().cycle() // Repeat the index forever.
)
)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -217,4 +234,12 @@ mod tests {
assert_eq!(s.insert(Pointer(Rc::new(2u32))), true); assert_eq!(s.insert(Pointer(Rc::new(2u32))), true);
assert_eq!(s.remove(&Pointer(first)), true); assert_eq!(s.remove(&Pointer(first)), true);
} }
#[test]
fn check_count() {
assert_eq!(
cycle_count(5..8).take(7).collect::<Vec<_>>(),
vec![(5, 0), (6, 0), (7, 0), (5, 1), (6, 1), (7, 1), (5, 2)]
);
}
} }

View File

@ -1,18 +1,42 @@
/*! Managing the events belonging to virtual-keyboard interface. */ /*! Managing the events belonging to virtual-keyboard interface. */
use ::keyboard::{ KeyCode, Modifiers, PressType }; use ::keyboard::{ Modifiers, PressType };
use ::layout::c::LevelKeyboard;
use ::submission::Timestamp; use ::submission::Timestamp;
/// Standard xkb keycode
type KeyCode = u32;
/// Gathers stuff defined in C or called by C /// Gathers stuff defined in C or called by C
pub mod c { pub mod c {
use super::*; use std::ffi::CStr;
use std::os::raw::c_void; use std::os::raw::{ c_char, c_void };
#[repr(transparent)] #[repr(transparent)]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct ZwpVirtualKeyboardV1(*const c_void); pub struct ZwpVirtualKeyboardV1(*const c_void);
#[repr(C)]
pub struct KeyMap {
fd: u32,
fd_len: usize,
}
impl KeyMap {
pub fn from_cstr(s: &CStr) -> KeyMap {
unsafe {
eek_key_map_from_str(s.as_ptr())
}
}
}
impl Drop for KeyMap {
fn drop(&mut self) {
unsafe {
eek_key_map_deinit(self as *mut KeyMap);
}
}
}
#[no_mangle] #[no_mangle]
extern "C" { extern "C" {
pub fn eek_virtual_keyboard_v1_key( pub fn eek_virtual_keyboard_v1_key(
@ -24,13 +48,16 @@ pub mod c {
pub fn eek_virtual_keyboard_update_keymap( pub fn eek_virtual_keyboard_update_keymap(
virtual_keyboard: ZwpVirtualKeyboardV1, virtual_keyboard: ZwpVirtualKeyboardV1,
keyboard: LevelKeyboard, keymap: *const KeyMap,
); );
pub fn eek_virtual_keyboard_set_modifiers( pub fn eek_virtual_keyboard_set_modifiers(
virtual_keyboard: ZwpVirtualKeyboardV1, virtual_keyboard: ZwpVirtualKeyboardV1,
modifiers: u32, modifiers: u32,
); );
pub fn eek_key_map_from_str(keymap_str: *const c_char) -> KeyMap;
pub fn eek_key_map_deinit(keymap: *mut KeyMap);
} }
} }
@ -41,35 +68,15 @@ impl VirtualKeyboard {
// TODO: error out if keymap not set // TODO: error out if keymap not set
pub fn switch( pub fn switch(
&self, &self,
keycodes: &Vec<KeyCode>, keycode: KeyCode,
action: PressType, action: PressType,
timestamp: Timestamp, timestamp: Timestamp,
) { ) {
let keycodes_count = keycodes.len(); let keycode = keycode - 8;
for keycode in keycodes.iter() { unsafe {
let keycode = keycode - 8; c::eek_virtual_keyboard_v1_key(
match (action, keycodes_count) { self.0, timestamp.0, keycode, action.clone() as u32
// Pressing a key made out of a single keycode is simple: );
// press on press, release on release.
(_, 1) => unsafe {
c::eek_virtual_keyboard_v1_key(
self.0, timestamp.0, keycode, action.clone() as u32
);
},
// A key made of multiple keycodes
// has to submit them one after the other
(PressType::Pressed, _) => unsafe {
c::eek_virtual_keyboard_v1_key(
self.0, timestamp.0, keycode, PressType::Pressed as u32
);
c::eek_virtual_keyboard_v1_key(
self.0, timestamp.0, keycode, PressType::Released as u32
);
},
// Design choice here: submit multiple all at press time
// and do nothing at release time
(PressType::Released, _) => {},
}
} }
} }
@ -80,9 +87,12 @@ impl VirtualKeyboard {
} }
} }
pub fn update_keymap(&self, keyboard: LevelKeyboard) { pub fn update_keymap(&self, keymap: &c::KeyMap) {
unsafe { unsafe {
c::eek_virtual_keyboard_update_keymap(self.0, keyboard); c::eek_virtual_keyboard_update_keymap(
self.0,
keymap as *const c::KeyMap,
);
} }
} }
} }

View File

@ -14,10 +14,10 @@ eek_virtual_keyboard_v1_key(struct zwp_virtual_keyboard_v1 *zwp_virtual_keyboard
} }
void eek_virtual_keyboard_update_keymap(struct zwp_virtual_keyboard_v1 *zwp_virtual_keyboard_v1, const LevelKeyboard *keyboard) { void eek_virtual_keyboard_update_keymap(struct zwp_virtual_keyboard_v1 *zwp_virtual_keyboard_v1, struct KeyMap *keymap) {
zwp_virtual_keyboard_v1_keymap(zwp_virtual_keyboard_v1, zwp_virtual_keyboard_v1_keymap(zwp_virtual_keyboard_v1,
WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1, WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1,
keyboard->keymap_fd, keyboard->keymap_len); keymap->fd, keymap->fd_len);
} }
void void

10
tests/layout_erase.yaml Normal file
View File

@ -0,0 +1,10 @@
---
# Erase only
views:
base:
- "BackSpace"
outlines:
default: { width: 0, height: 0 }
buttons:
BackSpace:
action: erase

View File

@ -50,6 +50,7 @@ endforeach
foreach layout : [ foreach layout : [
'us', 'us_wide', 'us', 'us_wide',
'br', 'br',
'be',
'de', 'de_wide', 'de', 'de_wide',
'dk', 'dk',
'es', 'es',
@ -68,11 +69,17 @@ foreach layout : [
'emoji', 'emoji',
] ]
extra = []
if layout == 'emoji'
extra += ['allow_missing_return']
endif
test( test(
'test_layout_' + layout, 'test_layout_' + layout,
cargo_script, cargo_script,
args: ['run'] + cargo_build_flags args: ['run'] + cargo_build_flags
+ [ '--example', 'test_layout', '--', layout], + ['--example', 'test_layout', '--', layout]
+ extra,
workdir: meson.build_root(), workdir: meson.build_root(),
) )
endforeach endforeach