diff --git a/Cargo.lock b/Cargo.lock index 0e4605ab..53e3bb6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1530,6 +1530,7 @@ dependencies = [ name = "system76-keyboard-configurator-backend" version = "0.1.0" dependencies = [ + "bitflags", "cascade", "futures", "futures-timer", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 92693a9a..09b1585c 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -6,6 +6,7 @@ license = "GPL-3.0-or-later" edition = "2018" [dependencies] +bitflags = "1.3.2" cascade = "1" futures = "0.3.13" futures-timer = "3.0.2" diff --git a/backend/src/board.rs b/backend/src/board.rs index 18d9f632..5acbcd0e 100644 --- a/backend/src/board.rs +++ b/backend/src/board.rs @@ -259,7 +259,12 @@ impl Board { let mut key_leds = HashMap::new(); for key in self.keys().iter() { let scancodes = (0..self.layout().meta.num_layers as usize) - .map(|layer| key.get_scancode(layer).unwrap().1) + .map(|layer| { + key.get_scancode(layer) + .unwrap() + .1 + .map_or_else(String::new, |x| x.to_string()) + }) .collect(); map.insert(key.logical_name.clone(), scancodes); if !key.leds.is_empty() { diff --git a/backend/src/key.rs b/backend/src/key.rs index 3e19e253..42d8081e 100644 --- a/backend/src/key.rs +++ b/backend/src/key.rs @@ -1,7 +1,7 @@ use glib::prelude::*; use std::cell::Cell; -use crate::{Board, Daemon, Hs, PhysicalLayoutKey, Rect, Rgb}; +use crate::{Board, Daemon, Hs, Keycode, PhysicalLayoutKey, Rect, Rgb}; #[derive(Debug)] pub struct Key { @@ -143,17 +143,16 @@ impl Key { Ok(()) } - pub fn get_scancode(&self, layer: usize) -> Option<(u16, String)> { + // TODO: operate on keycode enum + pub fn get_scancode(&self, layer: usize) -> Option<(u16, Option)> { let board = self.board(); let scancode = self.scancodes.get(layer)?.get(); - let scancode_name = match board.layout().scancode_to_name(scancode) { - Some(some) => some, - None => String::new(), - }; + let scancode_name = board.layout().scancode_to_name(scancode); Some((scancode, scancode_name)) } - pub async fn set_scancode(&self, layer: usize, scancode_name: &str) -> Result<(), String> { + // TODO: operate on keycode enum + pub async fn set_scancode(&self, layer: usize, scancode_name: &Keycode) -> Result<(), String> { let board = self.board(); let scancode = board .layout() diff --git a/backend/src/keycode.rs b/backend/src/keycode.rs new file mode 100644 index 00000000..3e22af9c --- /dev/null +++ b/backend/src/keycode.rs @@ -0,0 +1,252 @@ +// has_keycode logic; convert to/from int +// serialize (Display?)/format +// serde: serialize/deserialize as string + +use bitflags::bitflags; + +bitflags! { + pub struct Mods: u16 { + const CTRL = 0x1; + const SHIFT = 0x2; + const ALT = 0x4; + const SUPER = 0x8; + + const RIGHT = 0x10; + + const RIGHT_CTRL = Self::RIGHT.bits | Self::CTRL.bits; + const RIGHT_SHIFT = Self::RIGHT.bits | Self::SHIFT.bits; + const RIGHT_ALT = Self::RIGHT.bits | Self::ALT.bits; + const RIGHT_SUPER = Self::RIGHT.bits | Self::SUPER.bits; + } +} + +impl Default for Mods { + fn default() -> Self { + Self::empty() + } +} + +impl Mods { + // Convert single modifier from name + pub fn from_mod_str(s: &str) -> Option { + match s { + "LEFT_CTRL" => Some(Self::CTRL), + "LEFT_SHIFT" => Some(Self::SHIFT), + "LEFT_ALT" => Some(Self::ALT), + "LEFT_SUPER" => Some(Self::SUPER), + "RIGHT_CTRL" => Some(Self::RIGHT_CTRL), + "RIGHT_SHIFT" => Some(Self::RIGHT_SHIFT), + "RIGHT_ALT" => Some(Self::RIGHT_ALT), + "RIGHT_SUPER" => Some(Self::RIGHT_SUPER), + _ => None, + } + } + + // Convert to single modifier + pub(crate) fn as_mod_str(self) -> Option<&'static str> { + match self { + Self::CTRL => Some("LEFT_CTRL"), + Self::SHIFT => Some("LEFT_SHIFT"), + Self::ALT => Some("LEFT_ALT"), + Self::SUPER => Some("LEFT_SUPER"), + Self::RIGHT_CTRL => Some("RIGHT_CTRL"), + Self::RIGHT_SHIFT => Some("RIGHT_SHIFT"), + Self::RIGHT_ALT => Some("RIGHT_ALT"), + Self::RIGHT_SUPER => Some("RIGHT_SUPER"), + _ => None, + } + } + + pub fn mod_names(self) -> impl Iterator { + [Self::CTRL, Self::SHIFT, Self::ALT, Self::SUPER] + .iter() + .filter_map(move |i| (self & (*i | Self::RIGHT)).as_mod_str()) + } + + pub fn toggle_mod(self, other: Self) -> Self { + let other_key = other & !Self::RIGHT; + if !self.contains(other_key) { + self | other + } else { + let key = self & !other_key; + if key == Self::RIGHT { + Self::empty() + } else { + key + } + } + } +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, glib::Boxed)] +#[boxed_type(name = "S76Keycode")] +pub enum Keycode { + Basic(Mods, String), + MT(Mods, String), + LT(u8, String), +} + +impl Keycode { + pub fn parse(s: &str) -> Option { + let mut tokens = tokenize(s); + match tokens.next()? { + "MT" => parse_mt(tokens), + "LT" => parse_lt(tokens), + keycode => parse_basic(tokenize(s)), + } + } + + pub fn none() -> Self { + Self::Basic(Mods::empty(), "NONE".to_string()) + } + + pub fn is_none(&self) -> bool { + if let Keycode::Basic(mode, keycode) = self { + mode.is_empty() && keycode.as_str() == "NONE" + } else { + false + } + } + + pub fn is_roll_over(&self) -> bool { + if let Keycode::Basic(mode, keycode) = self { + mode.is_empty() && keycode.as_str() == "ROLL_OVER" + } else { + false + } + } +} + +impl std::fmt::Display for Keycode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Basic(mods, scancode_name) => { + let mut has_mod = false; + for mod_name in mods.mod_names() { + if has_mod { + write!(f, " | ")?; + } + write!(f, "{}", mod_name)?; + has_mod = true; + } + if !(scancode_name == "NONE" && has_mod) { + write!(f, "{}", scancode_name)?; + } + } + Self::MT(mods, scancode_name) => { + write!(f, "MT(")?; + let mut has_mod = false; + for mod_name in mods.mod_names() { + if has_mod { + write!(f, " | ")?; + } + write!(f, "{}", mod_name)?; + has_mod = true; + } + write!(f, ", {})", scancode_name)?; + } + Self::LT(layer, scancode_name) => { + write!(f, "LT({}, {})", layer, scancode_name)?; + } + } + Ok(()) + } +} + +const SEPARATORS: &[char] = &[',', '|', '(', ')']; + +// Tokenize into iterator of &str, splitting on whitespace and putting +// separators in their own tokens. +fn tokenize(mut s: &str) -> impl Iterator { + std::iter::from_fn(move || { + s = s.trim_start_matches(' '); + let idx = if SEPARATORS.contains(&s.chars().next()?) { + 1 + } else { + s.find(|c| c == ' ' || SEPARATORS.contains(&c)) + .unwrap_or(s.len()) + }; + let tok = &s[..idx]; + s = &s[idx..]; + Some(tok) + }) +} + +fn parse_mt<'a>(mut tokens: impl Iterator) -> Option { + if tokens.next() != Some("(") { + return None; + } + + let mut mods = Mods::empty(); + loop { + mods |= Mods::from_mod_str(tokens.next()?)?; + match tokens.next()? { + "|" => {} + "," => { + break; + } + _ => { + return None; + } + } + } + + let keycode = tokens.next()?.to_string(); + + if (tokens.next(), tokens.next()) != (Some(")"), None) { + return None; + } + + Some(Keycode::MT(mods, keycode)) +} + +fn parse_lt<'a>(mut tokens: impl Iterator) -> Option { + if tokens.next() != Some("(") { + return None; + } + + let layer = tokens.next()?.parse().ok()?; + + if tokens.next() != Some(",") { + return None; + } + + let keycode = tokens.next()?.to_string(); + + if (tokens.next(), tokens.next()) != (Some(")"), None) { + return None; + } + + Some(Keycode::LT(layer, keycode)) +} + +// XXX limit to basic if there are mods? +fn parse_basic<'a>(mut tokens: impl Iterator) -> Option { + let mut mods = Mods::empty(); + let mut keycode = None; + + loop { + let token = tokens.next()?; + if let Some(mod_) = Mods::from_mod_str(token) { + mods |= mod_; + } else if keycode.is_none() && token.chars().next()?.is_alphanumeric() { + keycode = Some(token.to_string()); + } else { + return None; + } + match tokens.next() { + Some("|") => {} + Some(_) => { + return None; + } + None => { + break; + } + } + } + + Some(Keycode::Basic( + mods, + keycode.unwrap_or_else(|| "NONE".to_string()), + )) +} diff --git a/backend/src/layout/meta.rs b/backend/src/layout/meta.rs index c62bd6fc..f2f08e14 100644 --- a/backend/src/layout/meta.rs +++ b/backend/src/layout/meta.rs @@ -20,9 +20,9 @@ pub struct Meta { pub has_brightness: bool, /// Has LED with color (i.e. not monochrome) pub has_color: bool, - /// Supports mod-tap bindings (assumes QMK mod-tap encoding) + /// Supports mod-tap and other QMK features #[serde(default)] - pub has_mod_tap: bool, + pub is_qmk: bool, #[serde(default)] /// Disable "Invert F Keys" option pub no_fn_f: bool, diff --git a/backend/src/layout/mod.rs b/backend/src/layout/mod.rs index 49920649..5a2c57cf 100644 --- a/backend/src/layout/mod.rs +++ b/backend/src/layout/mod.rs @@ -1,31 +1,19 @@ -use cascade::cascade; -use regex::Regex; +use once_cell::sync::Lazy; use std::{collections::HashMap, fs, path::Path}; mod meta; -use once_cell::sync::Lazy; mod physical_layout; pub use self::meta::Meta; pub(crate) use physical_layout::{PhysicalLayout, PhysicalLayoutKey}; -use crate::KeyMap; +use crate::{KeyMap, Keycode, Mods}; const QK_MOD_TAP: u16 = 0x6000; const QK_MOD_TAP_MAX: u16 = 0x7FFF; - -pub static MOD_TAP_MODS: Lazy> = Lazy::new(|| { - cascade! { - HashMap::new(); - ..insert("LEFT_CTRL", 0x01); - ..insert("LEFT_SHIFT", 0x02); - ..insert("LEFT_ALT", 0x04); - ..insert("LEFT_SUPER", 0x08); - ..insert("RIGHT_CTRL", 0x11); - ..insert("RIGHT_SHIFT", 0x12); - ..insert("RIGHT_ALT", 0x14); - ..insert("RIGHT_SUPER", 0x18); - } -}); +const QK_LAYER_TAP: u16 = 0x4000; +const QK_LAYER_TAP_MAX: u16 = 0x4FFF; +const QK_MODS: u16 = 0x0100; +const QK_MODS_MAX: u16 = 0x1FFF; pub struct Layout { /// Metadata for keyboard @@ -74,6 +62,14 @@ macro_rules! keyboards { // Calls the `keyboards!` macro include!(concat!(env!("OUT_DIR"), "/keyboards.rs")); +pub fn is_qmk_basic(name: &str) -> bool { + static QMK_KEYMAP: Lazy> = + Lazy::new(|| parse_keymap_json(layout_data("system76/launch_1").unwrap().2).0); + QMK_KEYMAP + .get(name) + .map_or(false, |scancode| *scancode <= 0xff) +} + impl Layout { pub fn from_data( meta_json: &str, @@ -143,31 +139,71 @@ impl Layout { } /// Get the scancode number corresponding to a name - pub fn scancode_to_name(&self, scancode: u16) -> Option { - if scancode >= QK_MOD_TAP && scancode <= QK_MOD_TAP_MAX { - let mod_ = (scancode >> 8) & 0x1f; - let kc = scancode & 0xff; - let mod_name = MOD_TAP_MODS.iter().find(|(_, v)| **v == mod_)?.0; - let kc_name = self.scancode_names.get(&kc)?; - Some(format!("MT({}, {})", mod_name, kc_name)) + pub fn scancode_to_name(&self, scancode: u16) -> Option { + if self.meta.is_qmk { + if scancode >= QK_MOD_TAP && scancode <= QK_MOD_TAP_MAX { + let mods = Mods::from_bits((scancode >> 8) & 0x1f)?; + let kc = scancode & 0xff; + let kc_name = self.scancode_names.get(&kc)?; + return Some(Keycode::MT(mods, kc_name.clone())); + } else if scancode >= QK_LAYER_TAP && scancode <= QK_LAYER_TAP_MAX { + let layer = ((scancode >> 8) & 0xf) as u8; + let kc = scancode & 0xff; + let kc_name = self.scancode_names.get(&kc)?; + return Some(Keycode::LT(layer, kc_name.clone())); + } else if scancode >= QK_MODS && scancode <= QK_MODS_MAX { + let mods = Mods::from_bits((scancode >> 8) & 0x1f)?; + let kc = scancode & 0xff; + let kc_name = self.scancode_names.get(&kc)?; + return Some(Keycode::Basic(mods, kc_name.clone())); + } + } + let kc_name = self.scancode_names.get(&scancode)?; + if let Some(mods) = Mods::from_mod_str(kc_name) { + Some(Keycode::Basic(mods, "NONE".to_string())) } else { - self.scancode_names.get(&scancode).cloned() + Some(Keycode::Basic(Mods::empty(), kc_name.clone())) } } /// Get the name corresponding to a scancode number - pub fn scancode_from_name(&self, name: &str) -> Option { - // Check if mod-tap - let mt_re = Regex::new("MT\\(([^()]+), ([^()]+)\\)").unwrap(); - if let Some(captures) = mt_re.captures(name) { - let mod_ = *MOD_TAP_MODS.get(&captures.get(1).unwrap().as_str())?; - let kc = *self.keymap.get(captures.get(2).unwrap().as_str())?; - Some(QK_MOD_TAP | ((mod_ & 0x1f) << 8) | (kc & 0xff)) - } else { - self.keymap.get(name).copied() + pub fn scancode_from_name(&self, name: &Keycode) -> Option { + match name { + Keycode::MT(_, _) | Keycode::LT(_, _) if !self.meta.is_qmk => None, + Keycode::MT(mods, keycode_name) => { + let kc = *self.keymap.get(keycode_name)?; + Some(QK_MOD_TAP | (mods.bits() << 8) | (kc & 0xff)) + } + Keycode::LT(layer, keycode_name) => { + let kc = *self.keymap.get(keycode_name)?; + if *layer < 8 { + Some(QK_LAYER_TAP | (u16::from(*layer) << 8) | (kc & 0xFF)) + } else { + None + } + } + Keycode::Basic(mods, keycode_name) => { + if mods.is_empty() { + return self.keymap.get(keycode_name).copied(); + } else if keycode_name == "NONE" { + if let Some(mod_name) = mods.as_mod_str() { + return self.keymap.get(mod_name).copied(); + } + } + if self.meta.is_qmk { + let kc = *self.keymap.get(keycode_name)?; + Some((mods.bits() << 8) | (kc & 0xff)) + } else { + None + } + } } } + pub fn has_scancode(&self, scancode_name: &str) -> bool { + self.keymap.get(scancode_name).is_some() + } + pub fn f_keys(&self) -> impl Iterator { self.default.map.iter().filter_map(|(k, v)| { if let Some(num) = v[0].strip_prefix('F') { diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 2dabc4b4..7cec25b9 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -21,6 +21,7 @@ mod color; mod daemon; mod deref_cell; mod key; +mod keycode; mod keymap; mod layer; mod layout; @@ -32,6 +33,6 @@ mod rect; use crate::daemon::*; pub use crate::{ - backend::*, benchmark::*, board::*, color::*, deref_cell::*, key::*, keymap::*, layer::*, - layout::*, localize::*, matrix::*, mode::*, nelson::*, rect::*, + backend::*, benchmark::*, board::*, color::*, deref_cell::*, key::*, keycode::*, keymap::*, + layer::*, layout::*, localize::*, matrix::*, mode::*, nelson::*, rect::*, }; diff --git a/i18n/en/system76_keyboard_configurator.ftl b/i18n/en/system76_keyboard_configurator.ftl index a32860f6..d579b785 100644 --- a/i18n/en/system76_keyboard_configurator.ftl +++ b/i18n/en/system76_keyboard_configurator.ftl @@ -86,3 +86,11 @@ test-replace-switch = Replace switch test-spurious-keypress = Spurious keypress untitled-layout = Untitled Layout + +picker-basics = Basics +picker-extras = Extras +picker-tap-hold = Tap-Hold +picker-shift-click = Shift + click to combine modifier(s) or to combine them with a keycode. +tap-hold-step1 = 1. Select action(s) to use when the key is held. +tap-hold-multiple-mod = Shift + click to select multiple modifiers. +tap-hold-step2 = 2. Select an action to use when the key is tapped. diff --git a/layouts/keysym/README.md b/layouts/keysym/README.md new file mode 100644 index 00000000..9c17e9dd --- /dev/null +++ b/layouts/keysym/README.md @@ -0,0 +1,3 @@ +`base.json` should include keys that are displayed the same way regardless of layout, while `en_us.json` has those for the English US layout that may differ in another layout. + +If it turns out that something actually should/shouldn't differ between different layouts, it can be moved between `base.json` and layout specific files. diff --git a/layouts/keysym/base.json b/layouts/keysym/base.json new file mode 100644 index 00000000..b53eb6e5 --- /dev/null +++ b/layouts/keysym/base.json @@ -0,0 +1,103 @@ +{ + "F1": "F1", + "F2": "F2", + "F3": "F3", + "F4": "F4", + "F5": "F5", + "F6": "F6", + "F7": "F7", + "F8": "F8", + "F9": "F9", + "F10": "F10", + "F11": "F11", + "F12": "F12", + "F13": "F13", + "F14": "F14", + "F15": "F15", + "F16": "F16", + "F17": "F17", + "F18": "F18", + "F19": "F19", + "F20": "F20", + "F21": "F21", + "F22": "F22", + "F23": "F23", + "F24": "F24", + "LEFT": "Left", + "UP": "Up", + "DOWN": "Down", + "RIGHT": "Right", + "HOME": "Home", + "PGUP": "PgUp", + "PGDN": "PgDn", + "END": "End", + "RESET": "Reset Firmware", + "ROLL_OVER": "Reuse", + "NONE": "None", + "MUTE": "Mute", + "VOLUME_UP": "Vol Up", + "VOLUME_DOWN": "Vol Down", + "PLAY_PAUSE": "Play Pause", + "MEDIA_NEXT": "Next Track", + "MEDIA_PREV": "Prev Track", + "FAN_TOGGLE": "Fan Toggle", + "DISPLAY_TOGGLE": "Screen Toggle", + "BRIGHTNESS_UP": "Screen Up", + "BRIGHTNESS_DOWN": "Screen Down", + "DISPLAY_MODE": "Screen Mode", + "SUSPEND": "Suspend", + "CAMERA_TOGGLE": "Camera Toggle", + "AIRPLANE_MODE": "Airplane Mode", + "TOUCHPAD": "Touchpad Toggle", + "SYSTEM_POWER": "Power", + "KBD_TOGGLE": "LED On Off", + "KBD_UP": "LED Brighten", + "KBD_DOWN": "LED Darken", + "KBD_BKL": "LED Cycle", + "KBD_COLOR": "LED Color", + "LAYER_ACCESS_1": "Access Layer\u00a01", + "FN": "Access Layer\u00a02", + "LAYER_ACCESS_3": "Access Layer\u00a03", + "LAYER_ACCESS_4": "Access Layer\u00a04", + "LAYER_SWITCH_1": "Switch to\nLayer\u00a01", + "LAYER_SWITCH_2": "Switch to\nLayer\u00a02", + "LAYER_SWITCH_3": "Switch to\nLayer\u00a03", + "LAYER_SWITCH_4": "Switch to\nLayer\u00a04", + "MS_UP": "Mouse cursor up", + "MS_DOWN": "Mouse cursor down", + "MS_LEFT": "Mouse cursor left", + "MS_RIGHT": "Mouse cursor right", + "MS_BTN1": "Press button 1", + "MS_BTN2": "Press button 2", + "MS_BTN3": "Press button 3", + "MS_BTN4": "Press button 4", + "MS_BTN5": "Press button 5", + "MS_BTN6": "Press button 6", + "MS_BTN7": "Press button 7", + "MS_BTN8": "Press button 8", + "MS_WH_UP": "Move wheel up", + "MS_WH_DOWN": "Move wheel down", + "MS_WH_LEFT": "Move wheel left", + "MS_WH_RIGHT": "Move wheel right", + "MS_ACCEL0": "Set speed to 0", + "MS_ACCEL1": "Set speed to 1", + "MS_ACCEL2": "Set speed to 2", + "INT1": "INT 1", + "INT2": "INT 2", + "INT3": "INT 3", + "INT4": "INT 4", + "INT5": "INT 5", + "INT6": "INT 6", + "INT7": "INT 7", + "INT8": "INT 8", + "INT9": "INT 9", + "LANG1": "LANG 1", + "LANG2": "LANG 2", + "LANG3": "LANG 3", + "LANG4": "LANG 4", + "LANG5": "LANG 5", + "LANG6": "LANG 6", + "LANG7": "LANG 7", + "LANG8": "LANG 8", + "LANG9": "LANG 9" +} diff --git a/layouts/keysym/en_us.json b/layouts/keysym/en_us.json new file mode 100644 index 00000000..70c77d9a --- /dev/null +++ b/layouts/keysym/en_us.json @@ -0,0 +1,89 @@ +{ + "A": "A", + "B": "B", + "C": "C", + "D": "D", + "E": "E", + "F": "F", + "G": "G", + "H": "H", + "I": "I", + "J": "J", + "K": "K", + "L": "L", + "M": "M", + "N": "N", + "O": "O", + "P": "P", + "Q": "Q", + "R": "R", + "S": "S", + "T": "T", + "U": "U", + "V": "V", + "W": "W", + "X": "X", + "Y": "Y", + "Z": "Z", + "1": "!\n1", + "2": "@\n2", + "3": "#\n3", + "4": "$\n4", + "5": "%\n5", + "6": "^\n6", + "7": "&\n7", + "8": "*\n8", + "9": "(\n9", + "0": ")\n0", + "LEFT_ALT": "Left Alt", + "LEFT_CTRL": "Left Ctrl", + "LEFT_SHIFT": "Left Shift", + "LEFT_SUPER": "Left Super", + "RIGHT_ALT": "Right Alt", + "RIGHT_CTRL": "Right Ctrl", + "RIGHT_SHIFT": "Right Shift", + "RIGHT_SUPER": "Right Super", + "ENTER": "Enter", + "BKSP": "Bksp", + "DEL": "Del", + "TAB": "Tab", + "SPACE": "Space", + "CAPS": "Caps", + "APP": "Menu", + "ESC": "Esc", + "PRINT_SCREEN": "PrtSc\nSysrq", + "INSERT": "Insert", + "SCROLL_LOCK": "Scroll Lock", + "PAUSE": "Pause\nBreak", + "NUM_LOCK": "Num Lock", + "NUM_7": "7", + "NUM_8": "8", + "NUM_9": "9", + "NUM_MINUS": "-", + "NUM_PLUS": "+", + "NUM_SLASH": "/", + "NUM_4": "4", + "NUM_5": "5", + "NUM_6": "6", + "NUM_ASTERISK": "*", + "NUM_ENTER": "Enter", + "NUM_0": "0", + "NUM_1": "1", + "NUM_2": "2", + "NUM_3": "3", + "NUM_PERIOD": ".", + "TICK": "~\n`", + "QUOTE": "\"\n'", + "SEMICOLON": ":\n;", + "MINUS": "_\n-", + "EQUALS": "+\n=", + "SLASH": "?\n/", + "COMMA": "<\n,", + "PERIOD": ">\n.", + "BACKSLASH": "|\n\\", + "BRACE_OPEN": "{\n[", + "BRACE_CLOSE": "}\n]", + "NONUS_HASH": "Non-US #", + "NONUS_BSLASH": "Non-US \\" + +} diff --git a/layouts/picker.json b/layouts/picker.json deleted file mode 100644 index f790e52c..00000000 --- a/layouts/picker.json +++ /dev/null @@ -1,638 +0,0 @@ -[ - { - "label": "Alphabet keys", - "cols": 9, - "width": 1, - "keys": [ - { - "keysym": "A", - "label": "A" - }, - { - "keysym": "B", - "label": "B" - }, - { - "keysym": "C", - "label": "C" - }, - { - "keysym": "D", - "label": "D" - }, - { - "keysym": "E", - "label": "E" - }, - { - "keysym": "F", - "label": "F" - }, - { - "keysym": "G", - "label": "G" - }, - { - "keysym": "H", - "label": "H" - }, - { - "keysym": "I", - "label": "I" - }, - { - "keysym": "J", - "label": "J" - }, - { - "keysym": "K", - "label": "K" - }, - { - "keysym": "L", - "label": "L" - }, - { - "keysym": "M", - "label": "M" - }, - { - "keysym": "N", - "label": "N" - }, - { - "keysym": "O", - "label": "O" - }, - { - "keysym": "P", - "label": "P" - }, - { - "keysym": "Q", - "label": "Q" - }, - { - "keysym": "R", - "label": "R" - }, - { - "keysym": "S", - "label": "S" - }, - { - "keysym": "T", - "label": "T" - }, - { - "keysym": "U", - "label": "U" - }, - { - "keysym": "V", - "label": "V" - }, - { - "keysym": "W", - "label": "W" - }, - { - "keysym": "X", - "label": "X" - }, - { - "keysym": "Y", - "label": "Y" - }, - { - "keysym": "Z", - "label": "Z" - } - ] - }, - { - "label": "Number keys", - "cols": 4, - "width": 1, - "keys": [ - { - "keysym": "1", - "label": "!\n1" - }, - { - "keysym": "2", - "label": "@\n2" - }, - { - "keysym": "3", - "label": "#\n3" - }, - { - "keysym": "4", - "label": "$\n4" - }, - { - "keysym": "5", - "label": "%\n5" - }, - { - "keysym": "6", - "label": "^\n6" - }, - { - "keysym": "7", - "label": "&\n7" - }, - { - "keysym": "8", - "label": "*\n8" - }, - { - "keysym": "9", - "label": "(\n9" - }, - { - "keysym": "0", - "label": ")\n0" - } - ] - }, - { - "label": "Modifier keys", - "cols": 4, - "width": 1, - "keys": [ - { - "keysym": "LEFT_ALT", - "label": "Left Alt" - }, - { - "keysym": "RIGHT_ALT", - "label": "Right Alt" - }, - { - "keysym": "LEFT_CTRL", - "label": "Left Ctrl" - }, - { - "keysym": "RIGHT_CTRL", - "label": "Right Ctrl" - }, - { - "keysym": "LEFT_SHIFT", - "label": "Left Shift" - }, - { - "keysym": "RIGHT_SHIFT", - "label": "Right Shift" - }, - { - "keysym": "LEFT_SUPER", - "label": "Left Super" - }, - { - "keysym": "RIGHT_SUPER", - "label": "Right Super" - } - ] - }, - { - "label": "Actions", - "cols": 4, - "width": 1, - "keys": [ - { - "keysym": "ENTER", - "label": "Enter" - }, - { - "keysym": "BKSP", - "label": "Bksp" - }, - { - "keysym": "DEL", - "label": "Del" - }, - { - "keysym": "TAB", - "label": "Tab" - }, - { - "keysym": "SPACE", - "label": "Space" - }, - { - "keysym": "CAPS", - "label": "Caps" - }, - { - "keysym": "APP", - "label": "Menu" - }, - { - "keysym": "ESC", - "label": "Esc" - }, - { - "keysym": "PRINT_SCREEN", - "label": "PrtSc\nSysrq" - }, - { - "keysym": "INSERT", - "label": "Ins" - }, - { - "keysym": "SCROLL_LOCK", - "label": "Scroll Lock" - }, - { - "keysym": "PAUSE", - "label": "Pause\nBreak" - }, - { - "keysym": "RESET", - "label": "Reset" - }, - { - "keysym": "ROLL_OVER", - "label": "Reuse" - }, - { - "keysym": "NONE", - "label": "None" - } - ] - }, - { - "label": "Function keys", - "cols": 4, - "width": 1, - "keys": [ - { - "keysym": "F1", - "label": "F1" - }, - { - "keysym": "F2", - "label": "F2" - }, - { - "keysym": "F3", - "label": "F3" - }, - { - "keysym": "F4", - "label": "F4" - }, - { - "keysym": "F5", - "label": "F5" - }, - { - "keysym": "F6", - "label": "F6" - }, - { - "keysym": "F7", - "label": "F7" - }, - { - "keysym": "F8", - "label": "F8" - }, - { - "keysym": "F9", - "label": "F9" - }, - { - "keysym": "F10", - "label": "F10" - }, - { - "keysym": "F11", - "label": "F11" - }, - { - "keysym": "F12", - "label": "F12" - } - ] - }, - { - "label": "Numpad", - "cols": 6, - "width": 1, - "keys": [ - { - "keysym": "NUM_LOCK", - "label": "Num Lock" - }, - { - "keysym": "NUM_7", - "label": "7" - }, - { - "keysym": "NUM_8", - "label": "8" - }, - { - "keysym": "NUM_9", - "label": "9" - }, - { - "keysym": "NUM_MINUS", - "label": "-" - }, - { - "keysym": "NUM_PLUS", - "label": "+" - }, - { - "keysym": "NUM_SLASH", - "label": "/" - }, - { - "keysym": "NUM_4", - "label": "4" - }, - { - "keysym": "NUM_5", - "label": "5" - }, - { - "keysym": "NUM_6", - "label": "6" - }, - { - "keysym": "NUM_ASTERISK", - "label": "*" - }, - { - "keysym": "NUM_ENTER", - "label": "Enter" - }, - { - "keysym": "NUM_0", - "label": "0" - }, - { - "keysym": "NUM_1", - "label": "1" - }, - { - "keysym": "NUM_2", - "label": "2" - }, - { - "keysym": "NUM_3", - "label": "3" - }, - { - "keysym": "NUM_PERIOD", - "label": "." - } - ] - }, - { - "label": "Symbols", - "cols": 6, - "width": 1, - "keys": [ - { - "keysym": "TICK", - "label": "~\n`" - }, - { - "keysym": "QUOTE", - "label": "\"\n'" - }, - { - "keysym": "SEMICOLON", - "label": ":\n;" - }, - { - "keysym": "MINUS", - "label": "_\n-" - }, - { - "keysym": "EQUALS", - "label": "+\n=" - }, - { - "keysym": "SLASH", - "label": "?\n/" - }, - { - "keysym": "COMMA", - "label": "<\n," - }, - { - "keysym": "PERIOD", - "label": ">\n." - }, - { - "keysym": "BACKSLASH", - "label": "|\n\\" - }, - { - "keysym": "BRACE_OPEN", - "label": "{\n[" - }, - { - "keysym": "BRACE_CLOSE", - "label": "}\n]" - }, - { - "keysym": "NONUS_HASH", - "label": "Non-US #" - }, - { - "keysym": "NONUS_BSLASH", - "label": "Non-US \\" - } - ] - }, - { - "label": "Navigation", - "cols": 4, - "width": 1, - "keys": [ - { - "keysym": "LEFT", - "label": "Left" - }, - { - "keysym": "UP", - "label": "Up" - }, - { - "keysym": "DOWN", - "label": "Down" - }, - { - "keysym": "RIGHT", - "label": "Right" - }, - { - "keysym": "HOME", - "label": "Home" - }, - { - "keysym": "PGUP", - "label": "PgUp" - }, - { - "keysym": "PGDN", - "label": "PgDn" - }, - { - "keysym": "END", - "label": "End" - } - ] - }, - { - "label": "Media", - "cols": 3, - "width": 1, - "keys": [ - { - "keysym": "MUTE", - "label": "Mute" - }, - { - "keysym": "VOLUME_UP", - "label": "Vol Up" - }, - { - "keysym": "VOLUME_DOWN", - "label": "Vol Down" - }, - { - "keysym": "PLAY_PAUSE", - "label": "Play Pause" - }, - { - "keysym": "MEDIA_NEXT", - "label": "Next Track" - }, - { - "keysym": "MEDIA_PREV", - "label": "Prev Track" - } - ] - }, - { - "label": "Controls", - "cols": 4, - "width": 2, - "keys": [ - { - "keysym": "FAN_TOGGLE", - "label": "Fan Toggle" - }, - { - "keysym": "DISPLAY_TOGGLE", - "label": "Screen Toggle" - }, - { - "keysym": "BRIGHTNESS_UP", - "label": "Screen Up" - }, - { - "keysym": "BRIGHTNESS_DOWN", - "label": "Screen Down" - }, - { - "keysym": "DISPLAY_MODE", - "label": "Screen Mode" - }, - { - "keysym": "SUSPEND", - "label": "Suspend" - }, - { - "keysym": "CAMERA_TOGGLE", - "label": "Camera Toggle" - }, - { - "keysym": "AIRPLANE_MODE", - "label": "Airplane Mode" - }, - { - "keysym": "TOUCHPAD", - "label": "Touchpad Toggle" - }, - { - "keysym": "SYSTEM_POWER", - "label": "Power" - } - ] - }, - { - "label": "LED controls", - "cols": 4, - "width": 1, - "keys": [ - { - "keysym": "KBD_TOGGLE", - "label": "LED On Off" - }, - { - "keysym": "KBD_UP", - "label": "LED Brighten" - }, - { - "keysym": "KBD_DOWN", - "label": "LED Darken" - }, - { - "keysym": "KBD_BKL", - "label": "LED Cycle" - }, - { - "keysym": "KBD_COLOR", - "label": "LED Color" - } - ] - }, - { - "label": "Layer keys", - "cols": 4, - "width": 2, - "keys": [ - { - "keysym": "LAYER_ACCESS_1", - "label": "Access Layer\u00a01" - }, - { - "keysym": "FN", - "label": "Access Layer\u00a02" - }, - { - "keysym": "LAYER_ACCESS_3", - "label": "Access Layer\u00a03" - }, - { - "keysym": "LAYER_ACCESS_4", - "label": "Access Layer\u00a04" - }, - { - "keysym": "LAYER_SWITCH_1", - "label": "Switch to\nLayer\u00a01" - }, - { - "keysym": "LAYER_SWITCH_2", - "label": "Switch to\nLayer\u00a02" - }, - { - "keysym": "LAYER_SWITCH_3", - "label": "Switch to\nLayer\u00a03" - }, - { - "keysym": "LAYER_SWITCH_4", - "label": "Switch to\nLayer\u00a04" - } - ] - } -] diff --git a/layouts/system76/launch_1/meta.json b/layouts/system76/launch_1/meta.json index 7a70c50c..133c9396 100644 --- a/layouts/system76/launch_1/meta.json +++ b/layouts/system76/launch_1/meta.json @@ -5,7 +5,7 @@ "num_layers": 4, "has_brightness": true, "has_color": true, - "has_mod_tap": true, + "is_qmk": true, "no_fn_f": true, "pressed_color": "#202020", "keyboard": "system76/launch_1" diff --git a/layouts/system76/launch_2/meta.json b/layouts/system76/launch_2/meta.json index efa64865..a6afa5e8 100644 --- a/layouts/system76/launch_2/meta.json +++ b/layouts/system76/launch_2/meta.json @@ -5,7 +5,7 @@ "num_layers": 4, "has_brightness": true, "has_color": true, - "has_mod_tap": true, + "is_qmk": true, "no_fn_f": true, "pressed_color": "#202020", "keyboard": "system76/launch_2" diff --git a/layouts/system76/launch_lite_1/meta.json b/layouts/system76/launch_lite_1/meta.json index 69d0ad3c..1bb66ac8 100644 --- a/layouts/system76/launch_lite_1/meta.json +++ b/layouts/system76/launch_lite_1/meta.json @@ -5,7 +5,7 @@ "num_layers": 4, "has_brightness": true, "has_color": true, - "has_mod_tap": true, + "is_qmk": true, "no_fn_f": true, "pressed_color": "#202020", "keyboard": "system76/launch_lite_1" diff --git a/src/configurator_app.rs b/src/configurator_app.rs index bfabed31..2953ca0b 100644 --- a/src/configurator_app.rs +++ b/src/configurator_app.rs @@ -203,6 +203,16 @@ pub fn run() -> i32 { #[cfg(target_os = "windows")] windows_init(); + let css_provider = cascade! { + gtk::CssProvider::new(); + ..load_from_data(include_bytes!("style.css")).unwrap(); + }; + gtk::StyleContext::add_provider_for_screen( + &gdk::Screen::default().unwrap(), + &css_provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + gio::resources_register_include!("compiled.gresource").unwrap(); gtk::Window::set_default_icon_name("com.system76.keyboardconfigurator"); diff --git a/src/keyboard.rs b/src/keyboard.rs index 4166f22b..dd0153d3 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -16,7 +16,7 @@ use std::{ }; use crate::{show_error_dialog, Backlight, KeyboardLayer, MainWindow, Page, Picker, Testing}; -use backend::{Board, DerefCell, KeyMap, Layout, Mode}; +use backend::{Board, DerefCell, KeyMap, Keycode, Layout, Mode}; use widgets::SelectedKeys; #[derive(Default)] @@ -211,6 +211,7 @@ impl Keyboard { ..add(&cascade! { gtk::Label::new(Some(&fl!("stack-keymap-desc"))); ..set_line_wrap(true); + ..set_justify(gtk::Justification::Center); ..set_max_width_chars(100); ..set_halign(gtk::Align::Center); }); @@ -283,7 +284,7 @@ impl Keyboard { } } - fn layout(&self) -> &Layout { + pub fn layout(&self) -> &Layout { &self.inner().board.layout() } @@ -303,11 +304,7 @@ impl Keyboard { &self.inner().layer_stack } - pub fn has_scancode(&self, scancode_name: &str) -> bool { - self.layout().scancode_from_name(scancode_name).is_some() - } - - pub async fn keymap_set(&self, key_index: usize, layer: usize, scancode_name: &str) { + pub async fn keymap_set(&self, key_index: usize, layer: usize, scancode_name: &Keycode) { if let Err(err) = self.board().keys()[key_index] .set_scancode(layer, scancode_name) .await @@ -354,12 +351,17 @@ impl Keyboard { for (k, v) in &keymap.map { for (layer, scancode_name) in v.iter().enumerate() { + let keycode = match Keycode::parse(scancode_name) { + Some(keycode) => keycode, + None => { + error!("Unrecognized keycode: '{}'", scancode_name); + continue; + } // XXX + }; + let n = key_indices[&k]; futures.push(Box::pin(async move { - if let Err(err) = self.board().keys()[n] - .set_scancode(layer, scancode_name) - .await - { + if let Err(err) = self.board().keys()[n].set_scancode(layer, &keycode).await { error!("{}: {:?}", fl!("error-set-keymap"), err); } })); @@ -487,10 +489,10 @@ impl Keyboard { for i in self.layout().f_keys() { let k = &self.board().keys()[key_indices[i]]; - let layer0_keycode = k.get_scancode(0).unwrap().1; - let layer1_keycode = k.get_scancode(1).unwrap().1; + let layer0_keycode = k.get_scancode(0).unwrap().1.unwrap_or_else(Keycode::none); + let layer1_keycode = k.get_scancode(1).unwrap().1.unwrap_or_else(Keycode::none); - if layer1_keycode == "ROLL_OVER" { + if layer1_keycode.is_roll_over() { continue; } @@ -591,7 +593,7 @@ impl Keyboard { let k = &keys[*i]; debug!("{:#?}", k); if let Some(layer) = self.layer() { - if let Some((_scancode, scancode_name)) = k.get_scancode(layer) { + if let Some((_scancode, Some(scancode_name))) = k.get_scancode(layer) { selected_scancodes.push(scancode_name); } } diff --git a/src/keyboard_layer.rs b/src/keyboard_layer.rs index 49f25d1e..0370c88b 100644 --- a/src/keyboard_layer.rs +++ b/src/keyboard_layer.rs @@ -138,7 +138,7 @@ impl WidgetImpl for KeyboardLayerInner { let mut bg_alpha = 1.; if let Some(layer) = self.page.get().layer() { let scancode_name = k.get_scancode(layer).unwrap().1; - if scancode_name == "NONE" || scancode_name == "ROLL_OVER" { + if scancode_name.map_or(false, |x| x.is_none() || x.is_roll_over()) { text_alpha = 0.5; bg_alpha = 0.75; } @@ -161,18 +161,36 @@ impl WidgetImpl for KeyboardLayerInner { cr.stroke().unwrap(); } - // Draw label - let text = widget.page().get_label(k); - let layout = cascade! { - widget.create_pango_layout(Some(&text)); - ..set_width((w * pango::SCALE as f64) as i32); - ..set_alignment(pango::Alignment::Center); - }; - let text_height = layout.pixel_size().1 as f64; + // Draw labels, with line seperators if multiple + let labels = widget.page().get_label(k); + let layouts: Vec<_> = labels + .iter() + .map(|text| { + cascade! { + widget.create_pango_layout(Some(text)); + ..set_width((w * pango::SCALE as f64) as i32); + ..set_alignment(pango::Alignment::Center); + } + }) + .collect(); + let total_height = layouts + .iter() + .map(|layout| layout.pixel_size().1 as f64) + .sum::(); cr.new_path(); - cr.move_to(x, y + (h - text_height) / 2.); cr.set_source_rgba(fg.0, fg.1, fg.2, text_alpha); - pangocairo::show_layout(cr, &layout); + cr.set_line_width(1.); + cr.move_to(x, y + (h - total_height) / 2.); + for (i, layout) in layouts.iter().enumerate() { + pangocairo::show_layout(cr, &layout); + if i < layouts.len() - 1 { + let text_height = layout.pixel_size().1 as f64; + cr.rel_move_to(0.0, text_height); + cr.rel_line_to(w, 0.0); + cr.rel_move_to(-w, 1.0); + } + } + cr.stroke().unwrap(); } Inhibit(false) diff --git a/src/page.rs b/src/page.rs index 8c889217..2def818e 100644 --- a/src/page.rs +++ b/src/page.rs @@ -1,6 +1,13 @@ use crate::fl; -use crate::picker::SCANCODE_LABELS; -use backend::Key; +use crate::picker::{LAYERS, SCANCODE_LABELS}; +use backend::{Key, Keycode, Mods}; + +static MOD_LABELS: &[(Mods, &str)] = &[ + (Mods::CTRL, "Ctrl"), + (Mods::SHIFT, "Shift"), + (Mods::ALT, "Alt"), + (Mods::SUPER, "Super"), +]; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Page { @@ -59,19 +66,21 @@ impl Page { .into_iter() } - pub fn get_label(&self, key: &Key) -> String { + pub fn get_label(&self, key: &Key) -> Vec { match self { Page::Layer1 | Page::Layer2 | Page::Layer3 | Page::Layer4 => { - let scancode_name = key.get_scancode(self.layer().unwrap()).unwrap().1; - SCANCODE_LABELS - .get(&scancode_name) - .unwrap_or(&scancode_name) - .into() + let (scancode, scancode_name) = key.get_scancode(self.layer().unwrap()).unwrap(); + match scancode_name { + Some(keycode) => { + keycode_label(&keycode).unwrap_or_else(|| vec![format!("{:?}", keycode)]) + } + None => vec![format!("{}", scancode)], + } } - Page::Keycaps => key.physical_name.clone(), - Page::Logical => key.logical_name.clone(), - Page::Electrical => key.electrical_name.clone(), - Page::Leds => key.led_name.clone(), + Page::Keycaps => vec![key.physical_name.clone()], + Page::Logical => vec![key.logical_name.clone()], + Page::Electrical => vec![key.electrical_name.clone()], + Page::Leds => vec![key.led_name.clone()], } } } @@ -81,3 +90,57 @@ impl Default for Page { Self::Layer1 } } + +// TODO: represent mod-tap/layer-tap by rendering button with a seperator? +fn keycode_label(keycode: &Keycode) -> Option> { + match keycode { + Keycode::Basic(mods, keycode) => { + if mods.is_empty() { + Some(vec![SCANCODE_LABELS.get(keycode)?.clone()]) + } else { + let mut label = mods_label(*mods); + if keycode != "NONE" { + let keycode_label = SCANCODE_LABELS.get(keycode)?; + label.push_str(" + "); + label.push_str(keycode_label); + } + Some(vec![label]) + } + } + Keycode::MT(mods, keycode) => { + let mods_label = mods_label(*mods); + let keycode_label = SCANCODE_LABELS.get(keycode)?.clone(); + Some(vec![mods_label, keycode_label]) + } + Keycode::LT(layer, keycode) => { + let layer_id = *LAYERS.get(usize::from(*layer))?; + let layer_label = SCANCODE_LABELS.get(layer_id)?.clone(); + let keycode_label = SCANCODE_LABELS.get(keycode)?.clone(); + Some(vec![layer_label, keycode_label]) + } + } +} + +fn mods_label(mods: Mods) -> String { + if mods.is_empty() { + return String::new(); + } + + let mut label = if mods.contains(Mods::RIGHT) { + "Right " + } else { + "Left " + } + .to_string(); + let mut first = true; + for (mod_, mod_label) in MOD_LABELS { + if mods.contains(*mod_) { + if !first { + label.push_str(" + "); + } + label.push_str(mod_label); + first = false; + } + } + label +} diff --git a/src/picker/group_box/basics.rs b/src/picker/group_box/basics.rs new file mode 100644 index 00000000..7e5e2f84 --- /dev/null +++ b/src/picker/group_box/basics.rs @@ -0,0 +1,87 @@ +use super::{picker_ansi_group, picker_numpad_group, PickerBasicGroup, PickerGroupBox}; + +impl PickerGroupBox { + pub fn basics() -> Self { + Self::new(vec![ + Box::new(picker_ansi_group()), + Box::new(PickerBasicGroup::new( + "Navigation", + 4, + 1.0, + &["LEFT", "UP", "DOWN", "RIGHT", "HOME", "PGUP", "PGDN", "END"], + )), + Box::new(PickerBasicGroup::new( + "Other actions", + 4, + 1.5, + &[ + "INSERT", + "PRINT_SCREEN", + "SCROLL_LOCK", + "PAUSE", + "RESET", + "ROLL_OVER", + "NONE", + ], + )), + Box::new(PickerBasicGroup::new( + "Symbols", + 6, + 1.0, + &["NONUS_HASH", "NONUS_BSLASH"], + )), + Box::new(PickerBasicGroup::new( + "Controls", + 4, + 2.0, + &[ + "FAN_TOGGLE", + "DISPLAY_TOGGLE", + "BRIGHTNESS_UP", + "BRIGHTNESS_DOWN", + "DISPLAY_MODE", + "SUSPEND", + "CAMERA_TOGGLE", + "AIRPLANE_MODE", + "TOUCHPAD", + "SYSTEM_POWER", + ], + )), + Box::new(PickerBasicGroup::new( + "LED controls", + 4, + 1.0, + &["KBD_TOGGLE", "KBD_UP", "KBD_DOWN", "KBD_BKL", "KBD_COLOR"], + )), + Box::new(PickerBasicGroup::new( + "Media", + 3, + 1.0, + &[ + "MUTE", + "VOLUME_UP", + "VOLUME_DOWN", + "PLAY_PAUSE", + "MEDIA_NEXT", + "MEDIA_PREV", + ], + )), + Box::new(PickerBasicGroup::new( + "Layer keys", + 4, + 2.0, + &[ + "LAYER_ACCESS_1", + "FN", + "LAYER_ACCESS_3", + "LAYER_ACCESS_4", + "LAYER_SWITCH_1", + "LAYER_SWITCH_2", + "LAYER_SWITCH_3", + "LAYER_SWITCH_4", + ], + )), + Box::new(picker_numpad_group()), + ]) + } +} diff --git a/src/picker/group_box/extras.rs b/src/picker/group_box/extras.rs new file mode 100644 index 00000000..eea9e1b8 --- /dev/null +++ b/src/picker/group_box/extras.rs @@ -0,0 +1,44 @@ +use super::{PickerBasicGroup, PickerGroupBox, PickerInternationalGroup}; + +impl PickerGroupBox { + pub fn extras() -> Self { + Self::new(vec![ + Box::new(PickerBasicGroup::new( + "Additional Function Keys", + 6, + 1.0, + &[ + "F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", + "F24", + ], + )), + Box::new(PickerBasicGroup::new( + "Mouse Actions", + 5, + 2.0, + &[ + "MS_UP", + "MS_DOWN", + "MS_LEFT", + "MS_RIGHT", + "MS_BTN1", + "MS_BTN2", + "MS_BTN3", + "MS_BTN4", + "MS_BTN5", + "MS_BTN6", + "MS_BTN7", + "MS_BTN8", + "MS_WH_UP", + "MS_WH_DOWN", + "MS_WH_LEFT", + "MS_WH_RIGHT", + "MS_ACCEL0", + "MS_ACCEL1", + "MS_ACCEL2", + ], + )), + Box::new(PickerInternationalGroup::new()), + ]) + } +} diff --git a/src/picker/group_box/group/ansi.rs b/src/picker/group_box/group/ansi.rs new file mode 100644 index 00000000..085e27a8 --- /dev/null +++ b/src/picker/group_box/group/ansi.rs @@ -0,0 +1,101 @@ +use super::variable_width::{PickerVariableWidthGroup, KEY_SIZE, KEY_SPACE}; +use crate::fl; + +// A 2U key takes same space as 2 1U including spacing +// 2 1.5U keys take same space as 3 1U +// Space bar is the same as 3 1U + 1 1.5U to line up with previous row +static KEY_WIDTHS: &[(f64, &[&str])] = &[ + ( + 1.5 * KEY_SIZE + 0.5 * KEY_SPACE, + &[ + "DEL", + "BKSP", + "TAB", + "CAPS", + "LEFT_CTRL", + "LEFT_ALT", + "LEFT_SUPER", + "RIGHT_SUPER", + "RIGHT_CTRL", + ], + ), + ( + 2.0 * KEY_SIZE + KEY_SPACE, + &["LEFT_SHIFT", "RIGHT_SHIFT", "ENTER"], + ), + (4.5 * KEY_SIZE + 3.5 * KEY_SPACE, &["SPACE"]), +]; + +static ROWS: &[&[&str]] = &[ + &[ + "ESC", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "DEL", + ], + &[ + "TICK", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "MINUS", "EQUALS", "BKSP", + ], + &[ + "TAB", + "Q", + "W", + "E", + "R", + "T", + "Y", + "U", + "I", + "O", + "P", + "BRACE_OPEN", + "BRACE_CLOSE", + "BACKSLASH", + ], + &[ + "CAPS", + "A", + "S", + "D", + "F", + "G", + "H", + "J", + "K", + "L", + "SEMICOLON", + "QUOTE", + "ENTER", + ], + &[ + "LEFT_SHIFT", + "Z", + "X", + "C", + "V", + "B", + "N", + "M", + "COMMA", + "PERIOD", + "SLASH", + "RIGHT_SHIFT", + ], + &[ + "LEFT_CTRL", + "LEFT_ALT", + "LEFT_SUPER", + "SPACE", + "RIGHT_SUPER", + "RIGHT_ALT", + "APP", + "RIGHT_CTRL", + ], +]; + +pub fn picker_ansi_group() -> PickerVariableWidthGroup { + PickerVariableWidthGroup::new( + ROWS, + KEY_WIDTHS, + &[], + None, + Some(&fl!("picker-shift-click")), + ) +} diff --git a/src/picker/picker_group.rs b/src/picker/group_box/group/basic_group.rs similarity index 52% rename from src/picker/picker_group.rs rename to src/picker/group_box/group/basic_group.rs index 31dadf27..f0fe7ab5 100644 --- a/src/picker/picker_group.rs +++ b/src/picker/group_box/group/basic_group.rs @@ -1,26 +1,30 @@ use cascade::cascade; use gtk::{pango, prelude::*}; -use std::rc::Rc; -use super::PickerKey; +use super::{PickerGroup, PickerKey}; -pub(super) struct PickerGroup { - /// Name of keys in this group - keys: Vec>, - pub vbox: gtk::Box, +trait Group { + fn keys(&self) -> &[PickerKey]; + fn widget(&self) -> >k::Widget; +} + +pub struct PickerBasicGroup { + keys: Vec, + vbox: gtk::Box, flow_box: gtk::FlowBox, } -impl PickerGroup { - pub fn new(name: String, cols: u32) -> Self { +impl PickerBasicGroup { + pub fn new(name: &str, cols: u32, width: f64, key_names: &[&str]) -> Self { let label = cascade! { - gtk::Label::new(Some(&name)); + gtk::Label::new(Some(name)); ..set_attributes(Some(&cascade! { pango::AttrList::new(); ..insert(pango::AttrInt::new_weight(pango::Weight::Bold)); } )); ..set_halign(gtk::Align::Start); ..set_margin_bottom(8); + ..show(); }; let flow_box = cascade! { @@ -29,32 +33,43 @@ impl PickerGroup { ..set_row_spacing(4); ..set_max_children_per_line(cols); ..set_min_children_per_line(cols); - ..set_filter_func(Some(Box::new(|child: >k::FlowBoxChild| child.child().unwrap().is_visible()))); + ..set_filter_func(Some(Box::new(|child: >k::FlowBoxChild| child.child().unwrap().get_visible()))); + ..show(); }; let vbox = cascade! { gtk::Box::new(gtk::Orientation::Vertical, 4); + ..set_no_show_all(true); ..add(&label); ..add(&flow_box); }; + let keys: Vec<_> = key_names + .iter() + .map(|name| PickerKey::new(name, width, 1.0)) + .collect(); + for key in &keys { + flow_box.add(key); + } + Self { - keys: Vec::new(), + keys, vbox, flow_box, } } +} - pub fn add_key(&mut self, key: Rc) { - self.flow_box.add(&key.gtk); - self.keys.push(key); +impl PickerGroup for PickerBasicGroup { + fn keys(&self) -> &[PickerKey] { + &self.keys } - pub fn iter_keys(&self) -> impl Iterator { - self.keys.iter().map(|k| k.as_ref()) + fn widget(&self) -> >k::Widget { + self.vbox.upcast_ref() } - pub fn invalidate_filter(&self) { + fn invalidate_filter(&self) { self.flow_box.invalidate_filter(); } } diff --git a/src/picker/group_box/group/international.rs b/src/picker/group_box/group/international.rs new file mode 100644 index 00000000..576b30d8 --- /dev/null +++ b/src/picker/group_box/group/international.rs @@ -0,0 +1,84 @@ +// International section is displayed in non-standard way: two colums, +// with descriptions. + +use cascade::cascade; +use gtk::prelude::*; + +use super::{PickerGroup, PickerKey}; + +static INT_KEYS: &[(&str, &str)] = &[ + ("INT1", "JIS \\ and _"), + ("INT2", "JIS Katakana/Hiragana"), + ("INT3", "JIS JIS ¥ and |"), + ("INT4", "JIS Henkan"), + ("INT5", "JIS Muhenkan"), + ("INT6", "JIS Numpad ,"), + ("INT7", "International 7"), + ("INT8", "International 8"), + ("INT9", "International 9"), +]; +static LANG_KEYS: &[(&str, &str)] = &[ + ("LANG1", "Hangul/English"), + ("LANG2", "Hanja"), + ("LANG3", "JIS Katakana"), + ("LANG4", "JIS Hiragana"), + ("LANG5", "JIS Zenkaku/Hankaku"), + ("LANG6", "Language 6"), + ("LANG7", "Language 7"), + ("LANG8", "Language 8"), + ("LANG9", "Language 9"), +]; + +pub struct PickerInternationalGroup { + keys: Vec, + widget: gtk::Box, +} + +fn row(keys: &mut Vec, keycode: &str, description: &str) -> gtk::Box { + let key = PickerKey::new(keycode, 1.0, 1.0); + keys.push(key.clone()); + cascade! { + gtk::Box::new(gtk::Orientation::Horizontal, 8); + ..add(&key); + ..add(>k::Label::new(Some(description))); + } +} + +// Consider how this scales +impl PickerInternationalGroup { + pub fn new() -> Self { + let mut keys = Vec::new(); + + let int_box = cascade! { + gtk::Box::new(gtk::Orientation::Vertical, 0); + }; + for (keycode, description) in INT_KEYS { + int_box.add(&row(&mut keys, keycode, description)); + } + + let lang_box = cascade! { + gtk::Box::new(gtk::Orientation::Vertical, 0); + }; + for (keycode, description) in LANG_KEYS { + lang_box.add(&row(&mut keys, keycode, description)); + } + + let widget = cascade! { + gtk::Box::new(gtk::Orientation::Horizontal, 0); + ..add(&int_box); + ..add(&lang_box); + }; + + Self { keys, widget } + } +} + +impl PickerGroup for PickerInternationalGroup { + fn keys(&self) -> &[PickerKey] { + &self.keys + } + + fn widget(&self) -> >k::Widget { + self.widget.upcast_ref() + } +} diff --git a/src/picker/group_box/group/mod.rs b/src/picker/group_box/group/mod.rs new file mode 100644 index 00000000..8432401b --- /dev/null +++ b/src/picker/group_box/group/mod.rs @@ -0,0 +1,17 @@ +use super::super::PickerKey; + +mod ansi; +pub use ansi::picker_ansi_group; +mod basic_group; +pub use basic_group::PickerBasicGroup; +mod international; +pub use international::PickerInternationalGroup; +mod numpad; +pub use numpad::picker_numpad_group; +mod variable_width; + +pub trait PickerGroup { + fn keys(&self) -> &[PickerKey]; + fn widget(&self) -> >k::Widget; + fn invalidate_filter(&self) {} +} diff --git a/src/picker/group_box/group/numpad.rs b/src/picker/group_box/group/numpad.rs new file mode 100644 index 00000000..a23a5afe --- /dev/null +++ b/src/picker/group_box/group/numpad.rs @@ -0,0 +1,17 @@ +use super::variable_width::{PickerVariableWidthGroup, KEY_SIZE, KEY_SPACE}; + +static KEY_WIDTHS: &[(f64, &[&str])] = &[(2.0 * KEY_SIZE + KEY_SPACE, &["NUM_0"])]; + +static KEY_HEIGHTS: &[(f64, &[&str])] = &[(2.0 * KEY_SIZE + KEY_SPACE, &["NUM_PLUS", "NUM_ENTER"])]; + +static ROWS: &[&[&str]] = &[ + &["NUM_LOCK", "NUM_SLASH", "NUM_ASTERISK", "NUM_MINUS"], + &["NUM_7", "NUM_8", "NUM_9", "NUM_PLUS"], + &["NUM_4", "NUM_5", "NUM_6"], + &["NUM_1", "NUM_2", "NUM_3", "NUM_ENTER"], + &["NUM_0", "NUM_PERIOD"], +]; + +pub fn picker_numpad_group() -> PickerVariableWidthGroup { + PickerVariableWidthGroup::new(ROWS, KEY_WIDTHS, KEY_HEIGHTS, Some("Numpad"), None) +} diff --git a/src/picker/group_box/group/variable_width.rs b/src/picker/group_box/group/variable_width.rs new file mode 100644 index 00000000..4ddaecf4 --- /dev/null +++ b/src/picker/group_box/group/variable_width.rs @@ -0,0 +1,99 @@ +use cascade::cascade; +use gtk::{pango, prelude::*}; + +use super::{PickerGroup, PickerKey}; + +pub const KEY_SIZE: f64 = 48.0; +pub const KEY_SPACE: f64 = 4.0; + +pub struct PickerVariableWidthGroup { + keys: Vec, + widget: gtk::Box, +} + +impl PickerVariableWidthGroup { + pub fn new( + rows: &[&[&str]], + widths: &[(f64, &[&str])], + heights: &[(f64, &[&str])], + label: Option<&str>, + desc: Option<&str>, + ) -> Self { + let mut keys = Vec::new(); + + let vbox = cascade! { + gtk::Box::new(gtk::Orientation::Vertical, 4); + ..show(); + }; + + if let Some(label) = label { + let label = cascade! { + gtk::Label::new(Some(&label)); + ..set_attributes(Some(&cascade! { + pango::AttrList::new(); + ..insert(pango::AttrInt::new_weight(pango::Weight::Bold)); + } )); + ..set_halign(gtk::Align::Start); + ..set_margin_bottom(8); + ..show(); + }; + vbox.add(&label); + } + + let fixed = gtk::Fixed::new(); + vbox.add(&fixed); + + let mut y = 0; + for row in rows { + let mut x = 0; + for name in *row { + let width = widths + .iter() + .find_map(|(width, keys)| { + if keys.contains(name) { + Some(*width) + } else { + None + } + }) + .unwrap_or(KEY_SIZE); + let height = heights + .iter() + .find_map(|(height, keys)| { + if keys.contains(name) { + Some(*height) + } else { + None + } + }) + .unwrap_or(KEY_SIZE); + let key = PickerKey::new(name, width / KEY_SIZE, height / KEY_SIZE); + fixed.put(&key, x, y); + keys.push(key); + x += width as i32 + 4 + } + y += KEY_SIZE as i32 + 4; + } + + if let Some(desc) = desc { + let label = cascade! { + gtk::Label::new(Some(&desc)); + ..set_halign(gtk::Align::Start); + ..show(); + }; + vbox.add(&label); + } + + Self { keys, widget: vbox } + } +} + +impl PickerGroup for PickerVariableWidthGroup { + fn keys(&self) -> &[PickerKey] { + &self.keys + } + + fn widget(&self) -> >k::Widget { + self.widget.upcast_ref() + } +} diff --git a/src/picker/picker_group_box.rs b/src/picker/group_box/mod.rs similarity index 51% rename from src/picker/picker_group_box.rs rename to src/picker/group_box/mod.rs index c898fb4b..24737c43 100644 --- a/src/picker/picker_group_box.rs +++ b/src/picker/group_box/mod.rs @@ -1,4 +1,3 @@ -use cascade::cascade; use gtk::{ gdk, glib::{self, clone, subclass::Signal, SignalHandlerId}, @@ -6,32 +5,26 @@ use gtk::{ subclass::prelude::*, }; use once_cell::sync::Lazy; -use std::{cell::RefCell, collections::HashMap, rc::Rc}; +use std::{cell::RefCell, collections::HashMap}; -use backend::DerefCell; +use backend::{DerefCell, Keycode}; -use super::{picker_group::PickerGroup, picker_json::picker_json, picker_key::PickerKey}; +use super::picker_key::PickerKey; + +mod basics; +mod extras; +mod group; +pub use group::*; const DEFAULT_COLS: usize = 3; const HSPACING: i32 = 64; const VSPACING: i32 = 32; -const PICKER_CSS: &str = r#" -button { - margin: 0; - padding: 0; -} - -.selected { - border-color: #fbb86c; - border-width: 4px; -} -"#; #[derive(Default)] pub struct PickerGroupBoxInner { - groups: DerefCell>, - keys: DerefCell>>, - selected: RefCell>, + groups: DerefCell>>, + keys: DerefCell>, + selected: RefCell>, } #[glib::object_subclass] @@ -42,55 +35,11 @@ impl ObjectSubclass for PickerGroupBoxInner { } impl ObjectImpl for PickerGroupBoxInner { - fn constructed(&self, widget: &PickerGroupBox) { - self.parent_constructed(widget); - - let style_provider = cascade! { - gtk::CssProvider::new(); - ..load_from_data(&PICKER_CSS.as_bytes()).expect("Failed to parse css"); - }; - - let mut groups = Vec::new(); - let mut keys = HashMap::new(); - - for json_group in picker_json() { - let mut group = PickerGroup::new(json_group.label, json_group.cols); - - for json_key in json_group.keys { - let key = PickerKey::new( - json_key.keysym.clone(), - json_key.label, - json_group.width, - &style_provider, - ); - - group.add_key(key.clone()); - keys.insert(json_key.keysym, key); - } - - groups.push(group); - } - - for group in &groups { - group.vbox.show(); - group.vbox.set_parent(widget); - } - - self.keys.set(keys); - self.groups.set(groups); - - cascade! { - widget; - ..connect_signals(); - ..show_all(); - }; - } - fn signals() -> &'static [Signal] { static SIGNALS: Lazy> = Lazy::new(|| { vec![Signal::builder( "key-pressed", - &[String::static_type().into()], + &[String::static_type().into(), bool::static_type().into()], glib::Type::UNIT.into(), ) .build()] @@ -105,35 +54,18 @@ impl WidgetImpl for PickerGroupBoxInner { } fn preferred_width(&self, _widget: &Self::Type) -> (i32, i32) { - let minimum_width = self + let width = self .groups .iter() - .map(|x| x.vbox.preferred_width().1) - .max() - .unwrap(); - let natural_width = self - .groups - .chunks(3) - .map(|row| row.iter().map(|x| x.vbox.preferred_width().1).sum::()) + .map(|x| x.widget().preferred_width().1) .max() - .unwrap() - + 2 * HSPACING; - (minimum_width, natural_width) + .unwrap_or(0); + (width, width) } fn preferred_height_for_width(&self, widget: &Self::Type, width: i32) -> (i32, i32) { let rows = widget.rows_for_width(width); - let height = rows - .iter() - .map(|row| { - row.iter() - .map(|x| x.vbox.preferred_height().1) - .max() - .unwrap() - }) - .sum::() - + (rows.len() as i32 - 1) * VSPACING; - + let height = total_height_for_rows(&rows); (height, height) } @@ -142,29 +74,20 @@ impl WidgetImpl for PickerGroupBoxInner { let rows = obj.rows_for_width(allocation.width()); - let total_width = rows - .iter() - .map(|row| { - row.iter().map(|x| x.vbox.preferred_width().1).sum::() - + (row.len() as i32 - 1) * HSPACING - }) - .max() - .unwrap(); - let mut y = 0; for row in rows { - let mut x = (allocation.width() - total_width) / 2; + let mut x = 0; for group in row { - let height = group.vbox.preferred_height().1; - let width = group.vbox.preferred_width().1; + let height = group.widget().preferred_height().1; + let width = group.widget().preferred_width().1; group - .vbox + .widget() .size_allocate(>k::Allocation::new(x, y, width, height)); x += width + HSPACING; } y += row .iter() - .map(|x| x.vbox.preferred_height().1) + .map(|x| x.widget().preferred_height().1) .max() .unwrap() + VSPACING; @@ -200,7 +123,7 @@ impl ContainerImpl for PickerGroupBoxInner { cb: >k::subclass::container::Callback, ) { for group in self.groups.iter() { - cb.call(group.vbox.upcast_ref()); + cb.call(group.widget().upcast_ref()); } } @@ -215,8 +138,24 @@ glib::wrapper! { } impl PickerGroupBox { - pub fn new() -> Self { - glib::Object::new(&[]).unwrap() + pub fn new(groups: Vec>) -> Self { + let widget: Self = glib::Object::new(&[]).unwrap(); + + let mut keys = HashMap::new(); + + for group in &groups { + group.widget().show(); + group.widget().set_parent(&widget); + for key in group.keys() { + keys.insert(key.name().to_string(), key.clone()); + } + } + + widget.inner().keys.set(keys); + widget.inner().groups.set(groups); + widget.connect_signals(); + + widget } fn inner(&self) -> &PickerGroupBoxInner { @@ -226,63 +165,81 @@ impl PickerGroupBox { fn connect_signals(&self) { let picker = self; for group in self.inner().groups.iter() { - for key in group.iter_keys() { - let button = &key.gtk; - let name = key.name.to_string(); - button.connect_clicked(clone!(@weak picker => @default-panic, move |_| { - picker.emit_by_name::<()>("key-pressed", &[&name]); - })); + for key in group.keys() { + let button = &key; + let name = key.name().to_string(); + button.connect_clicked_with_shift( + clone!(@weak picker => @default-panic, move |_, shift| { + picker.emit_by_name::<()>("key-pressed", &[&name, &shift]); + }), + ); } } } - pub fn connect_key_pressed(&self, cb: F) -> SignalHandlerId { + pub fn connect_key_pressed(&self, cb: F) -> SignalHandlerId { self.connect_local("key-pressed", false, move |values| { - cb(values[1].get::().unwrap()); + cb( + values[1].get::().unwrap(), + values[2].get::().unwrap(), + ); None }) } - fn get_button(&self, scancode_name: &str) -> Option<>k::Button> { - self.inner().keys.get(scancode_name).map(|k| &k.gtk) - } - - pub(crate) fn set_key_visibility bool>(&self, f: F) { - for key in self.inner().keys.values() { - key.gtk.set_visible(f(&key.name)); - } - + // XXX need to enable/disable features; show/hide just plain keycodes + pub fn set_key_visibility bool>(&self, f: F) { for group in self.inner().groups.iter() { + let group_visible = group.keys().iter().fold(false, |group_visible, key| { + key.set_visible(f(key.name())); + group_visible || key.get_visible() + }); + + group.widget().set_visible(group_visible); group.invalidate_filter(); } } - pub(crate) fn set_selected(&self, scancode_names: Vec) { - let mut selected = self.inner().selected.borrow_mut(); - - for i in selected.iter() { - if let Some(button) = self.get_button(i) { - button.style_context().remove_class("selected"); - } + pub fn set_key_sensitivity bool>(&self, f: F) { + for key in self.inner().keys.values() { + key.set_sensitive(f(key.name())); } + } - *selected = scancode_names; + pub fn set_selected(&self, scancode_names: Vec) { + for button in self.inner().keys.values() { + button.set_selected(false); + } - for i in selected.iter() { - if let Some(button) = self.get_button(i) { - button.style_context().add_class("selected"); + for i in scancode_names.iter() { + match i { + Keycode::Basic(mods, scancode_name) => { + if let Some(button) = self.inner().keys.get(scancode_name) { + if !(scancode_name == "NONE" && !mods.is_empty()) { + button.set_selected(true); + } + } + for scancode_name in mods.mod_names() { + if let Some(button) = self.inner().keys.get(scancode_name) { + button.set_selected(true); + } + } + } + Keycode::MT(..) | Keycode::LT(..) => {} } } + + *self.inner().selected.borrow_mut() = scancode_names; } - fn rows_for_width(&self, container_width: i32) -> Vec<&[PickerGroup]> { + fn rows_for_width(&self, container_width: i32) -> Vec<&[Box]> { let mut rows = Vec::new(); let groups = &*self.inner().groups; let mut row_start = 0; let mut row_width = 0; for (i, group) in groups.iter().enumerate() { - let width = group.vbox.preferred_width().1; + let width = group.widget().preferred_width().1; row_width += width; if i != 0 { @@ -302,3 +259,27 @@ impl PickerGroupBox { rows } } + +fn max_width_for_rows(rows: &[&[Box]]) -> i32 { + rows.iter() + .map(|row| { + row.iter() + .map(|x| x.widget().preferred_width().1) + .sum::() + + (row.len() as i32 - 1) * HSPACING + }) + .max() + .unwrap_or(0) +} + +fn total_height_for_rows(rows: &[&[Box]]) -> i32 { + rows.iter() + .map(|row| { + row.iter() + .map(|x| x.widget().preferred_height().1) + .max() + .unwrap_or(0) + }) + .sum::() + + (rows.len() as i32 - 1) * VSPACING +} diff --git a/src/picker/mod.rs b/src/picker/mod.rs index c159fcd7..6f48a6aa 100644 --- a/src/picker/mod.rs +++ b/src/picker/mod.rs @@ -1,39 +1,51 @@ use cascade::cascade; use futures::{prelude::*, stream::FuturesUnordered}; use gtk::{ + gdk, glib::{self, clone}, prelude::*, subclass::prelude::*, }; use once_cell::sync::Lazy; -use std::{cell::RefCell, collections::HashMap}; +use std::{ + cell::{Cell, RefCell}, + collections::HashMap, +}; -use crate::Keyboard; -use backend::DerefCell; +use crate::{fl, Keyboard}; +use backend::{is_qmk_basic, DerefCell, Keycode, Mods}; -mod picker_group; -mod picker_group_box; -mod picker_json; +mod group_box; mod picker_key; +mod tap_hold; -use picker_group_box::PickerGroupBox; -use picker_json::picker_json; +use group_box::PickerGroupBox; use picker_key::PickerKey; +use tap_hold::TapHold; + +pub use tap_hold::LAYERS; pub static SCANCODE_LABELS: Lazy> = Lazy::new(|| { - let mut labels = HashMap::new(); - for group in picker_json() { - for key in group.keys { - labels.insert(key.keysym, key.label); - } - } + let base_json = include_str!("../../layouts/keysym/base.json"); + let en_us_json = include_str!("../../layouts/keysym/en_us.json"); + let base: HashMap = serde_json::from_str(base_json).unwrap(); + let en_us: HashMap = serde_json::from_str(en_us_json).unwrap(); + + let mut labels = base; + labels.extend(en_us.into_iter()); labels }); #[derive(Default)] pub struct PickerInner { - group_box: DerefCell, + stack_switcher: DerefCell, + basics_group_box: DerefCell, + extras_group_box: DerefCell, keyboard: RefCell>, + selected: RefCell>, + shift: Cell, + tap_hold: DerefCell, + is_qmk: Cell, } #[glib::object_subclass] @@ -47,26 +59,96 @@ impl ObjectImpl for PickerInner { fn constructed(&self, picker: &Picker) { self.parent_constructed(picker); - let group_box = cascade! { - PickerGroupBox::new(); - ..connect_key_pressed(clone!(@weak picker => move |name| { - picker.key_pressed(name) + let basics_group_box = cascade! { + PickerGroupBox::basics(); + ..connect_key_pressed(clone!(@weak picker => move |name, shift| { + picker.key_pressed(name, shift) })); }; + let extras_group_box = cascade! { + PickerGroupBox::extras(); + ..connect_key_pressed(clone!(@weak picker => move |name, shift| { + picker.key_pressed(name, shift) + })); + }; + + let tap_hold = cascade! { + tap_hold::TapHold::new(); + ..connect_select(clone!(@weak picker => move |keycode| { + picker.set_keycode(keycode); + })); + }; + + // XXX translate + let stack = cascade! { + gtk::Stack::new(); + ..add_titled(&basics_group_box, "basics", &fl!("picker-basics")); + ..add_titled(&extras_group_box, "extras", &fl!("picker-extras")); + ..add_titled(&tap_hold, "tap-hold", &fl!("picker-tap-hold")); + }; + + let stack_switcher = cascade! { + gtk::StackSwitcher::new(); + ..style_context().add_class("picker-stack-switcher"); + ..set_stack(Some(&stack)); + }; + cascade! { picker; - ..add(&group_box); + ..set_spacing(8); + ..set_orientation(gtk::Orientation::Vertical); + ..set_halign(gtk::Align::Center); + ..add(&cascade! { + gtk::Box::new(gtk::Orientation::Vertical, 0); + ..add(>k::Separator::new(gtk::Orientation::Horizontal)); + ..add(&stack_switcher); + ..add(>k::Separator::new(gtk::Orientation::Horizontal)); + }); + ..add(&stack); ..show_all(); }; - self.group_box.set(group_box); + self.stack_switcher.set(stack_switcher); + self.basics_group_box.set(basics_group_box); + self.extras_group_box.set(extras_group_box); + self.tap_hold.set(tap_hold); } } impl BoxImpl for PickerInner {} -impl WidgetImpl for PickerInner {} +impl WidgetImpl for PickerInner { + fn realize(&self, widget: &Self::Type) { + self.parent_realize(widget); + + let window = widget + .toplevel() + .and_then(|x| x.downcast::().ok()); + if let Some(window) = &window { + window.add_events(gdk::EventMask::FOCUS_CHANGE_MASK); + window.connect_event(clone!(@weak widget => @default-return Inhibit(false), move |_, evt| { + use gdk::keys::{Key, constants}; + let is_shift_key = matches!(evt.keyval().map(Key::from), Some(constants::Shift_L | constants::Shift_R)); + // XXX Distinguish lshift, rshift if both are held? + let shift = match evt.event_type() { + gdk::EventType::KeyPress if is_shift_key => true, + gdk::EventType::KeyRelease if is_shift_key => false, + gdk::EventType::FocusChange => false, + _ => { return Inhibit(false); } + }; + widget.inner().shift.set(shift); + widget.invalidate_sensitivity(); + widget.inner().tap_hold.set_shift(shift); + Inhibit(false) + })); + } + } + + fn unrealize(&self, widget: &Self::Type) { + self.parent_unrealize(widget); + } +} impl ContainerImpl for PickerInner {} @@ -84,6 +166,13 @@ impl Picker { PickerInner::from_instance(self) } + fn group_boxes(&self) -> [&PickerGroupBox; 2] { + [ + &*self.inner().basics_group_box, + &*self.inner().extras_group_box, + ] + } + pub(crate) fn set_keyboard(&self, keyboard: Option) { if let Some(old_kb) = &*self.inner().keyboard.borrow() { old_kb.set_picker(None); @@ -91,46 +180,141 @@ impl Picker { if let Some(kb) = &keyboard { // Check that scancode is available for the keyboard - self.inner() - .group_box - .set_key_visibility(|name| kb.has_scancode(name)); - kb.set_picker(Some(&self)); + for group_box in self.group_boxes() { + group_box.set_key_visibility(|name| kb.layout().has_scancode(name)); + } + let is_qmk = kb.layout().meta.is_qmk; + self.inner().extras_group_box.set_visible(is_qmk); + self.inner().tap_hold.set_visible(is_qmk); + self.inner().stack_switcher.set_visible(is_qmk); + self.inner().is_qmk.set(is_qmk); + kb.set_picker(Some(self)); } *self.inner().keyboard.borrow_mut() = keyboard; } - pub(crate) fn set_selected(&self, scancode_names: Vec) { - self.inner().group_box.set_selected(scancode_names); + pub(crate) fn set_selected(&self, scancode_names: Vec) { + for group_box in self.group_boxes() { + group_box.set_selected(scancode_names.clone()); + } + self.inner().tap_hold.set_selected(scancode_names.clone()); + *self.inner().selected.borrow_mut() = scancode_names; + + self.invalidate_sensitivity(); } - fn key_pressed(&self, name: String) { + fn key_pressed(&self, name: String, shift: bool) { + let mod_ = Mods::from_mod_str(&name); + if shift && self.inner().is_qmk.get() { + let selected = self.inner().selected.borrow(); + if selected.len() == 1 { + if let Keycode::Basic(mods, scancode_name) = &selected[0] { + if let Some(mod_) = mod_ { + self.set_keycode(Keycode::Basic( + mods.toggle_mod(mod_), + scancode_name.to_string(), + )); + return; + } else if scancode_name == &name && !mods.is_empty() { + self.set_keycode(Keycode::Basic(*mods, "NONE".to_string())); + return; + } else if scancode_name == "NONE" { + self.set_keycode(Keycode::Basic(*mods, name)); + return; + } + } + } + } + let keycode = if let Some(mod_) = mod_ { + Keycode::Basic(mod_, "NONE".to_string()) + } else { + Keycode::Basic(Mods::empty(), name) + }; + self.set_keycode(keycode); + } + + fn set_keycode(&self, keycode: Keycode) { let kb = match self.inner().keyboard.borrow().clone() { Some(kb) => kb, None => { return; } }; - let layer = kb.layer(); + let layer = kb.layer(); if let Some(layer) = layer { let futures = FuturesUnordered::new(); for i in kb.selected().iter() { let i = *i; - futures.push(clone!(@strong kb, @strong name => async move { - kb.keymap_set(i, layer, &name).await; + futures.push(clone!(@strong kb, @strong keycode => async move { + kb.keymap_set(i, layer, &keycode).await; })); } glib::MainContext::default().spawn_local(async { futures.collect::<()>().await }); } } + + fn invalidate_sensitivity(&self) { + let shift = self.inner().shift.get(); + + let mut allow_left_mods = false; + let mut allow_right_mods = false; + let mut allow_basic = false; + let mut allow_non_basic = false; + + let mut keycode_mods = Mods::empty(); + let mut basic_keycode = None; + + if shift && self.inner().is_qmk.get() { + let selected = self.inner().selected.borrow(); + if selected.len() == 1 { + match &selected[0] { + Keycode::Basic(mods, keycode) => { + // Allow mods only if `keycode` is really basic? + // Allow deselecting current key + let no_mod = mods.is_empty(); + let right = mods.contains(Mods::RIGHT); + allow_left_mods = no_mod || !right; + allow_right_mods = no_mod || right; + allow_basic = keycode == "NONE" && !mods.is_empty(); + keycode_mods = *mods; + basic_keycode = Some(keycode.clone()); + } + Keycode::MT(..) | Keycode::LT(..) => {} + } + } + } else { + allow_left_mods = true; + allow_right_mods = true; + allow_basic = true; + allow_non_basic = true; + } + + for group_box in self.group_boxes() { + group_box.set_key_sensitivity(|name| { + if ["LEFT_SHIFT", "LEFT_ALT", "LEFT_CTRL", "LEFT_SUPER"].contains(&name) { + allow_left_mods + } else if ["RIGHT_SHIFT", "RIGHT_ALT", "RIGHT_CTRL", "RIGHT_SUPER"].contains(&name) + { + allow_right_mods + } else if basic_keycode.as_deref() == Some(name) && !keycode_mods.is_empty() { + true + } else if is_qmk_basic(name) { + allow_basic + } else { + allow_non_basic + } + }); + } + } } #[cfg(test)] mod tests { use crate::*; use backend::{layouts, Layout}; - use std::collections::HashSet; + use std::collections::{HashMap, HashSet}; #[test] fn picker_has_keys() { @@ -145,4 +329,15 @@ mod tests { } assert_eq!(missing, HashSet::new()); } + + #[test] + fn no_duplicating_base_json() { + let base_json = include_str!("../../layouts/keysym/base.json"); + let en_us_json = include_str!("../../layouts/keysym/en_us.json"); + let base: HashMap = serde_json::from_str(base_json).unwrap(); + let en_us: HashMap = serde_json::from_str(en_us_json).unwrap(); + for k in base.keys() { + assert!(!en_us.contains_key(k), "{} in both base and en_us", k); + } + } } diff --git a/src/picker/picker_json.rs b/src/picker/picker_json.rs deleted file mode 100644 index 106a920a..00000000 --- a/src/picker/picker_json.rs +++ /dev/null @@ -1,30 +0,0 @@ -use serde::Deserialize; - -#[derive(Deserialize)] -pub struct PickerJsonKey { - pub keysym: String, - pub label: String, -} - -#[derive(Deserialize)] -pub struct PickerJsonGroup { - pub label: String, - pub cols: u32, - pub width: i32, - pub keys: Vec, -} - -pub fn picker_json() -> Vec { - let picker_json = include_str!("../../layouts/picker.json"); - serde_json::from_str(picker_json).unwrap() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_picker_json() { - picker_json(); - } -} diff --git a/src/picker/picker_key.rs b/src/picker/picker_key.rs index a4c31018..2c78f847 100644 --- a/src/picker/picker_key.rs +++ b/src/picker/picker_key.rs @@ -1,23 +1,33 @@ use cascade::cascade; -use gtk::prelude::*; -use std::rc::Rc; +use gtk::{ + gdk, + glib::{ + self, + translate::{from_glib, ToGlibPtr}, + }, + prelude::*, + subclass::prelude::*, +}; -pub(super) struct PickerKey { - /// Symbolic name of the key - pub(super) name: String, - // GTK button - pub(super) gtk: gtk::Button, +use backend::DerefCell; + +#[derive(Default)] +pub struct PickerKeyInner { + label: DerefCell, + name: DerefCell, } -impl PickerKey { - pub(super) fn new>( - name: String, - text: String, - width: i32, - style_provider: &P, - ) -> Rc { +#[glib::object_subclass] +impl ObjectSubclass for PickerKeyInner { + const NAME: &'static str = "S76PickerKey"; + type ParentType = gtk::Button; + type Type = PickerKey; +} + +impl ObjectImpl for PickerKeyInner { + fn constructed(&self, widget: &Self::Type) { let label = cascade! { - gtk::Label::new(Some(&text)); + gtk::Label::new(None); ..set_line_wrap(true); ..set_max_width_chars(1); ..set_margin_start(5); @@ -25,13 +35,76 @@ impl PickerKey { ..set_justify(gtk::Justification::Center); }; - let button = cascade! { - gtk::Button::new(); - ..set_size_request(48 * width, 48); - ..style_context().add_provider(style_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION); + cascade! { + widget; + ..style_context().add_class("picker-key"); ..add(&label); + ..show_all(); }; - Rc::new(Self { name, gtk: button }) + self.label.set(label); + } +} +impl WidgetImpl for PickerKeyInner {} +impl ContainerImpl for PickerKeyInner {} +impl BinImpl for PickerKeyInner {} +impl ButtonImpl for PickerKeyInner {} + +glib::wrapper! { + pub struct PickerKey(ObjectSubclass) + @extends gtk::Button, gtk::Bin, gtk::Container, gtk::Widget, @implements gtk::Orientable; +} + +impl PickerKey { + pub fn new(name: &str, width: f64, height: f64) -> Self { + let keysym_label = super::SCANCODE_LABELS.get(name).unwrap(); + + let widget: Self = glib::Object::new(&[]).unwrap(); + widget.inner().name.set(name.to_string()); + widget.inner().label.set_label(keysym_label); + widget.set_size_request((48.0 * width) as i32, (48.0 * height) as i32); + widget + } + + fn inner(&self) -> &PickerKeyInner { + PickerKeyInner::from_instance(self) + } + + /// Symbolic name of the key + pub fn name(&self) -> &str { + &*self.inner().name + } + + pub fn set_selected(&self, selected: bool) { + if selected { + self.style_context().add_class("selected"); + } else { + self.style_context().remove_class("selected"); + } + } + + pub fn connect_clicked_with_shift(&self, f: F) { + self.connect_clicked(move |widget| { + let shift = gtk::current_event() + .and_then(|x| event_state(&x)) + .map_or(false, |x| x.contains(gdk::ModifierType::SHIFT_MASK)); + f(widget, shift) + }); + } +} + +// Work around binding bug +// https://github.com/gtk-rs/gtk3-rs/pull/769 +pub fn event_state(evt: &gdk::Event) -> Option { + unsafe { + let mut state = std::mem::MaybeUninit::uninit(); + if from_glib(gdk::ffi::gdk_event_get_state( + evt.to_glib_none().0, + state.as_mut_ptr(), + )) { + Some(from_glib(state.assume_init() as u32)) + } else { + None + } } } diff --git a/src/picker/tap_hold.rs b/src/picker/tap_hold.rs new file mode 100644 index 00000000..6dabf3ae --- /dev/null +++ b/src/picker/tap_hold.rs @@ -0,0 +1,279 @@ +use cascade::cascade; +use gtk::{ + glib::{self, clone, subclass::Signal}, + pango, + prelude::*, + subclass::prelude::*, +}; +use once_cell::sync::Lazy; +use std::cell::{Cell, RefCell}; + +use super::{group_box::PickerBasicGroup, PickerGroupBox}; +use crate::fl; +use backend::{is_qmk_basic, DerefCell, Keycode, Mods}; + +#[derive(Clone, Copy, PartialEq)] +enum Hold { + Mods(Mods), + Layer(u8), +} + +impl Default for Hold { + fn default() -> Self { + Self::Mods(Mods::default()) + } +} + +static MODIFIERS: &[&str] = &[ + "LEFT_SHIFT", + "LEFT_CTRL", + "LEFT_SUPER", + "LEFT_ALT", + "RIGHT_SHIFT", + "RIGHT_CTRL", + "RIGHT_SUPER", + "RIGHT_ALT", +]; +pub static LAYERS: &[&str] = &["LAYER_ACCESS_1", "FN", "LAYER_ACCESS_3", "LAYER_ACCESS_4"]; + +#[derive(Default)] +pub struct TapHoldInner { + shift: Cell, + hold: Cell, + keycode: RefCell>, + hold_group_box: DerefCell, + picker_group_box: DerefCell, +} + +#[glib::object_subclass] +impl ObjectSubclass for TapHoldInner { + const NAME: &'static str = "S76KeyboardTapHold"; + type ParentType = gtk::Box; + type Type = TapHold; +} + +impl ObjectImpl for TapHoldInner { + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = Lazy::new(|| { + vec![Signal::builder( + "select", + &[Keycode::static_type().into()], + glib::Type::UNIT.into(), + ) + .build()] + }); + SIGNALS.as_ref() + } + + fn constructed(&self, widget: &Self::Type) { + self.parent_constructed(widget); + + let picker_group_box = cascade! { + PickerGroupBox::basics(); + ..set_sensitive(false); + ..connect_key_pressed(clone!(@weak widget => move |name, _shift| { + *widget.inner().keycode.borrow_mut() = Some(name); + widget.update(); + })); + ..set_key_visibility(is_qmk_basic); + }; + + let hold_group_box = cascade! { + PickerGroupBox::new(vec![ + Box::new(PickerBasicGroup::new( + "Modifiers", + 4, + 1.5, + MODIFIERS, + )), + Box::new(PickerBasicGroup::new( + "Layer Keys", + 4, + 1.5, + LAYERS, + )), + ]); + ..connect_key_pressed(clone!(@weak widget => move |name, shift| { + let new_hold = if let Some(mod_) = Mods::from_mod_str(&name) { + let mut new_mods = mod_; + if shift { + if let Hold::Mods(mods) = widget.inner().hold.get() { + new_mods = mods.toggle_mod(mod_); + } + } + Hold::Mods(new_mods) + } else { + let n = LAYERS.iter().position(|x| *x == name).unwrap() as u8; + Hold::Layer(n) + }; + widget.inner().hold.set(new_hold); + widget.update(); + })); + }; + + // TODO indent + cascade! { + widget; + ..set_spacing(8); + ..set_orientation(gtk::Orientation::Vertical); + ..add(&cascade! { + gtk::Label::new(Some(&fl!("tap-hold-step1"))); + ..set_attributes(Some(&cascade! { + pango::AttrList::new(); + ..insert(pango::AttrInt::new_weight(pango::Weight::Bold)); + })); + ..set_halign(gtk::Align::Start); + }); + ..add(cascade! { + &hold_group_box; + ..set_margin_start(8); + }); + ..add(&cascade! { + gtk::Label::new(Some(&fl!("tap-hold-multiple-mod"))); + ..set_halign(gtk::Align::Start); + ..set_margin_start(8); + }); + // XXX grey? + ..add(&cascade! { + gtk::Label::new(Some(&fl!("tap-hold-step2"))); + ..set_attributes(Some(&cascade! { + pango::AttrList::new(); + ..insert(pango::AttrInt::new_weight(pango::Weight::Bold)); + })); + ..set_halign(gtk::Align::Start); + }); + ..add(cascade! { + &picker_group_box; + ..set_margin_start(8); + }); + }; + + self.hold_group_box.set(hold_group_box); + self.picker_group_box.set(picker_group_box); + } +} + +impl BoxImpl for TapHoldInner {} +impl WidgetImpl for TapHoldInner {} +impl ContainerImpl for TapHoldInner {} + +glib::wrapper! { + pub struct TapHold(ObjectSubclass) + @extends gtk::Box, gtk::Container, gtk::Widget, @implements gtk::Orientable; +} + +impl TapHold { + pub fn new() -> Self { + glib::Object::new(&[]).unwrap() + } + + fn inner(&self) -> &TapHoldInner { + TapHoldInner::from_instance(self) + } + + fn update(&self) { + let keycode = self.inner().keycode.borrow(); + let keycode = keycode.as_deref().unwrap_or("NONE"); + match self.inner().hold.get() { + Hold::Mods(mods) => { + if !mods.is_empty() { + self.emit_by_name::<()>("select", &[&Keycode::MT(mods, keycode.to_string())]); + } + } + Hold::Layer(layer) => { + self.emit_by_name::<()>("select", &[&Keycode::LT(layer, keycode.to_string())]); + } + } + } + + pub fn connect_select(&self, cb: F) -> glib::SignalHandlerId { + self.connect_local("select", false, move |values| { + cb(values[1].get::().unwrap()); + None + }) + } + + pub fn set_selected(&self, scancode_names: Vec) { + // XXX how to handle > 1? + let (mods, layer, keycode) = if scancode_names.len() == 1 { + match scancode_names.into_iter().next().unwrap() { + Keycode::MT(mods, keycode) => (mods, None, Some(keycode)), + Keycode::LT(layer, keycode) => (Mods::empty(), Some(layer), Some(keycode)), + Keycode::Basic(..) => Default::default(), + } + } else { + Default::default() + }; + + let mut selected_hold = Vec::new(); + for i in MODIFIERS { + let mod_ = Mods::from_mod_str(i).unwrap(); + if mods.contains(mod_) && (mods.contains(Mods::RIGHT) == mod_.contains(Mods::RIGHT)) { + selected_hold.push(Keycode::Basic(mod_, "NONE".to_string())); + } + } + if let Some(layer) = layer { + selected_hold.push(Keycode::Basic( + Mods::empty(), + LAYERS[layer as usize].to_string(), + )); + } + self.inner().hold_group_box.set_selected(selected_hold); + + if let Some(keycode) = keycode.clone() { + self.inner() + .picker_group_box + .set_selected(vec![Keycode::Basic(Mods::empty(), keycode)]); + } else { + self.inner().picker_group_box.set_selected(Vec::new()); + } + + self.inner().hold.set(if let Some(layer) = layer { + Hold::Layer(layer) + } else { + Hold::Mods(mods) + }); + *self.inner().keycode.borrow_mut() = keycode; + + self.invalidate_sensitivity(); + } + + pub fn set_shift(&self, shift: bool) { + self.inner().shift.set(shift); + self.invalidate_sensitivity(); + } + + fn invalidate_sensitivity(&self) { + let shift = self.inner().shift.get(); + let hold = self.inner().hold.get(); + let hold_empty = hold == Hold::Mods(Mods::empty()); + let keycode = self.inner().keycode.borrow(); + + self.inner().hold_group_box.set_key_sensitivity(|name| { + let left_mod = name.starts_with("LEFT_"); + let right_mod = name.starts_with("RIGHT_"); + // Modifer + if left_mod || right_mod { + if shift { + match hold { + Hold::Mods(mods) => { + mods.is_empty() || (right_mod == mods.contains(Mods::RIGHT)) + } + Hold::Layer(_) => false, + } + } else { + true + } + // Layer + } else { + !shift || (hold == Hold::Mods(Mods::empty())) + } + }); + + self.inner().picker_group_box.set_sensitive(if shift { + !hold_empty && keycode.is_none() + } else { + !hold_empty + }); + } +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 00000000..d8dafcfb --- /dev/null +++ b/src/style.css @@ -0,0 +1,23 @@ +.picker-key { + margin: 0; + padding: 0; +} + +.picker-key.selected { + border-color: #fbb86c; + border-width: 4px; +} + +.picker-stack-switcher button { + border: none; + box-shadow: none; + background: none; +} + +.picker-stack-switcher button:checked { + border-bottom: 2px solid #fbb86c; +} + +.picker-stack-switcher button:selected { + background: black; +}