Skip to content

Commit 2d1e886

Browse files
ids1024jackpot51
authored andcommitted
feat: Import/export layout as a .json file
1 parent b15651d commit 2d1e886

File tree

5 files changed

+207
-23
lines changed

5 files changed

+207
-23
lines changed

src/application/error_dialog.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use cascade::cascade;
2+
use gtk::prelude::*;
3+
use std::fmt::Display;
4+
5+
pub fn error_dialog<W: IsA<gtk::Window>, E: Display>(parent: &W, title: &str, err: E) {
6+
let label = cascade! {
7+
gtk::Label::new(Some(&format!("<b>{}</b>:\n{}", title, err)));
8+
..set_use_markup(true);
9+
..show();
10+
};
11+
12+
let dialog = cascade! {
13+
gtk::Dialog::with_buttons(Some(title), Some(parent), gtk::DialogFlags::MODAL | gtk::DialogFlags::USE_HEADER_BAR, &[("Ok", gtk::ResponseType::Ok)]);
14+
};
15+
16+
let header = dialog.get_header_bar().unwrap();
17+
header.set_show_close_button(false);
18+
19+
let content = dialog.get_content_area();
20+
content.add(&label);
21+
content.set_property_margin(24);
22+
23+
dialog.run();
24+
dialog.close();
25+
}

src/application/keyboard.rs

Lines changed: 153 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ use std::{
1212
RefCell,
1313
},
1414
collections::HashMap,
15-
fs,
15+
ffi::OsStr,
16+
fs::{self, File},
1617
path::{
1718
Path,
1819
},
@@ -23,21 +24,26 @@ use std::{
2324
use crate::daemon::Daemon;
2425
use crate::keyboard::Keyboard as ColorKeyboard;
2526
use crate::keyboard_color_button::KeyboardColorButton;
27+
use crate::keymap::KeyMap;
28+
use super::error_dialog::error_dialog;
2629
use super::key::Key;
2730
use super::layout::Layout;
2831
use super::page::Page;
2932
use super::picker::Picker;
3033

3134
pub struct KeyboardInner {
35+
board: OnceCell<String>,
3236
daemon: OnceCell<Rc<dyn Daemon>>,
3337
daemon_board: OnceCell<usize>,
3438
keymap: OnceCell<HashMap<String, u16>>,
3539
keys: OnceCell<Box<[Key]>>,
40+
load_button: gtk::Button,
3641
page: Cell<Page>,
3742
picker: RefCell<WeakRef<Picker>>,
3843
selected: Cell<Option<usize>>,
3944
color_button_bin: gtk::Frame,
4045
brightness_scale: gtk::Scale,
46+
save_button: gtk::Button,
4147
toolbar: gtk::Box,
4248
hbox: gtk::Box,
4349
stack: gtk::Stack,
@@ -88,26 +94,41 @@ impl ObjectSubclass for KeyboardInner {
8894
..set_stack(Some(&stack));
8995
};
9096

91-
let toolbar = cascade!{
97+
let toolbar = cascade! {
9298
gtk::Box::new(gtk::Orientation::Horizontal, 8);
9399
..set_center_widget(Some(&stack_switcher));
94100
};
95101

102+
let load_button = cascade! {
103+
gtk::Button::with_label("Load");
104+
..set_valign(gtk::Align::Center);
105+
};
106+
107+
let save_button = cascade! {
108+
gtk::Button::with_label("Save");
109+
..set_valign(gtk::Align::Center);
110+
};
111+
96112
let hbox = cascade! {
97113
gtk::Box::new(gtk::Orientation::Horizontal, 8);
98114
..add(&brightness_label);
99115
..add(&brightness_scale);
100116
..add(&color_label);
101117
..add(&color_button_bin);
118+
..add(&load_button);
119+
..add(&save_button);
102120
};
103121

104122
Self {
123+
board: OnceCell::new(),
105124
daemon: OnceCell::new(),
106125
daemon_board: OnceCell::new(),
107126
keymap: OnceCell::new(),
108127
keys: OnceCell::new(),
128+
load_button,
109129
page: Cell::new(Page::Layer1),
110130
picker: RefCell::new(WeakRef::new()),
131+
save_button,
111132
selected: Cell::new(None),
112133
color_button_bin,
113134
brightness_scale,
@@ -149,7 +170,7 @@ glib_wrapper! {
149170
}
150171

151172
impl Keyboard {
152-
pub fn new<P: AsRef<Path>>(dir: P, daemon: Rc<dyn Daemon>, daemon_board: usize) -> Self {
173+
pub fn new<P: AsRef<Path>>(dir: P, board: &str, daemon: Rc<dyn Daemon>, daemon_board: usize) -> Self {
153174
let dir = dir.as_ref();
154175

155176
let keymap_csv = fs::read_to_string(dir.join("keymap.csv"))
@@ -158,10 +179,10 @@ impl Keyboard {
158179
.expect("Failed to load layout.csv");
159180
let physical_json = fs::read_to_string(dir.join("physical.json"))
160181
.expect("Failed to load physical.json");
161-
Self::new_data(&keymap_csv, &layout_csv, &physical_json, daemon, daemon_board)
182+
Self::new_data(board, &keymap_csv, &layout_csv, &physical_json, daemon, daemon_board)
162183
}
163184

164-
fn new_layout(layout: Layout, daemon: Rc<dyn Daemon>, daemon_board: usize) -> Self {
185+
fn new_layout(board: &str, layout: Layout, daemon: Rc<dyn Daemon>, daemon_board: usize) -> Self {
165186
let keyboard: Self = glib::Object::new(Self::static_type(), &[])
166187
.unwrap()
167188
.downcast()
@@ -191,6 +212,7 @@ impl Keyboard {
191212
}
192213
let _ = keyboard.inner().keys.set(keys.into_boxed_slice());
193214

215+
let _ = keyboard.inner().board.set(board.to_string());
194216
let _ = keyboard.inner().daemon.set(daemon);
195217
let _ = keyboard.inner().daemon_board.set(daemon_board);
196218
let _ = keyboard.inner().keymap.set(layout.keymap);
@@ -216,19 +238,23 @@ impl Keyboard {
216238

217239
pub fn new_board(board: &str, daemon: Rc<dyn Daemon>, daemon_board: usize) -> Option<Self> {
218240
Layout::from_board(board).map(|layout|
219-
Self::new_layout(layout, daemon, daemon_board)
241+
Self::new_layout(board, layout, daemon, daemon_board)
220242
)
221243
}
222244

223-
fn new_data(keymap_csv: &str, layout_csv: &str, physical_json: &str, daemon: Rc<dyn Daemon>, daemon_board: usize) -> Self {
245+
fn new_data(board: &str, keymap_csv: &str, layout_csv: &str, physical_json: &str, daemon: Rc<dyn Daemon>, daemon_board: usize) -> Self {
224246
let layout = Layout::from_data(keymap_csv, layout_csv, physical_json);
225-
Self::new_layout(layout, daemon, daemon_board)
247+
Self::new_layout(board, layout, daemon, daemon_board)
226248
}
227249

228250
fn inner(&self) -> &KeyboardInner {
229251
KeyboardInner::from_instance(self)
230252
}
231253

254+
fn board(&self) -> &str {
255+
self.inner().board.get().unwrap()
256+
}
257+
232258
fn daemon(&self) -> &Rc<dyn Daemon> {
233259
self.inner().daemon.get().unwrap()
234260
}
@@ -241,6 +267,10 @@ impl Keyboard {
241267
self.inner().keymap.get().unwrap()
242268
}
243269

270+
fn window(&self) -> Option<gtk::Window> {
271+
self.get_toplevel()?.downcast().ok()
272+
}
273+
244274
pub fn layer(&self) -> usize {
245275
//TODO: make this more robust
246276
match self.inner().page.get() {
@@ -290,30 +320,130 @@ impl Keyboard {
290320
self.set_selected(self.selected());
291321
}
292322

323+
pub fn export_keymap(&self) -> KeyMap {
324+
let mut map = HashMap::new();
325+
for key in self.keys() {
326+
let scancodes = key.scancodes.borrow();
327+
let scancodes = scancodes.iter().map(|s| s.1.clone()).collect();
328+
map.insert(key.logical_name.clone(), scancodes);
329+
}
330+
KeyMap {
331+
board: self.board().to_string(),
332+
map: map,
333+
}
334+
}
335+
336+
pub fn import_keymap(&self, keymap: &KeyMap) {
337+
// TODO: don't block UI thread
338+
// TODO: Ideally don't want this function to be O(Keys^2)
339+
340+
if &keymap.board != self.board() {
341+
error_dialog(&self.window().unwrap(),
342+
"Failed to import keymap",
343+
format!("Keymap is for board '{}'", keymap.board));
344+
return;
345+
}
346+
347+
for (k, v) in keymap.map.iter() {
348+
let n = self
349+
.keys()
350+
.iter()
351+
.position(|i| &i.logical_name == k)
352+
.unwrap();
353+
for (layer, scancode_name) in v.iter().enumerate() {
354+
self.keymap_set(n, layer, scancode_name);
355+
}
356+
}
357+
}
358+
293359
fn connect_signals(&self) {
294360
let kb = self;
295361

296-
self.inner().stack.connect_property_visible_child_notify(clone!(@weak kb => @default-panic, move |stack| {
297-
let page: Option<Page> = match stack.get_visible_child() {
298-
Some(child) => unsafe { child.get_data("keyboard_confurator_page").cloned() },
299-
None => None,
362+
self.inner().stack.connect_property_visible_child_notify(
363+
clone!(@weak kb => @default-panic, move |stack| {
364+
let page: Option<Page> = match stack.get_visible_child() {
365+
Some(child) => unsafe { child.get_data("keyboard_confurator_page").cloned() },
366+
None => None,
367+
};
368+
369+
println!("{:?}", page);
370+
let last_layer = kb.layer();
371+
kb.inner().page.set(page.unwrap_or(Page::Layer1));
372+
if kb.layer() != last_layer {
373+
kb.set_selected(kb.selected());
374+
}
375+
}),
376+
);
377+
378+
self.inner().brightness_scale.connect_value_changed(
379+
clone!(@weak kb => @default-panic, move |this| {
380+
let value = this.get_value() as i32;
381+
if let Err(err) = kb.daemon().set_brightness(kb.daemon_board(), value) {
382+
eprintln!("{}", err);
383+
}
384+
println!("{}", value);
385+
}),
386+
);
387+
388+
self.inner().load_button.connect_clicked(clone!(@weak kb => @default-panic, move |_button| {
389+
let filter = cascade! {
390+
gtk::FileFilter::new();
391+
..set_name(Some("JSON"));
392+
..add_mime_type("application/json");
393+
..add_pattern("*.json");
394+
};
395+
396+
let chooser = cascade! {
397+
gtk::FileChooserNative::new::<gtk::Window>(Some("Load Layout"), None, gtk::FileChooserAction::Open, Some("Load"), Some("Cancel"));
398+
..add_filter(&filter);
300399
};
301400

302-
println!("{:?}", page);
303-
let last_layer = kb.layer();
304-
kb.inner().page.set(page.unwrap_or(Page::Layer1));
305-
if kb.layer() != last_layer {
306-
kb.set_selected(kb.selected());
401+
if chooser.run() == gtk::ResponseType::Accept {
402+
let path = chooser.get_filename().unwrap();
403+
match File::open(&path) {
404+
Ok(file) => match KeyMap::from_reader(file) {
405+
Ok(keymap) => kb.import_keymap(&keymap),
406+
Err(err) => error_dialog(&kb.window().unwrap(), "Failed to import keymap", err),
407+
}
408+
Err(err) => error_dialog(&kb.window().unwrap(), "Failed to open file", err),
409+
}
307410
}
308411
}));
309412

310-
self.inner().brightness_scale.connect_value_changed(clone!(@weak kb => @default-panic, move |this| {
311-
let value = this.get_value() as i32;
312-
if let Err(err) = kb.daemon().set_brightness(kb.daemon_board(), value) {
313-
eprintln!("{}", err);
314-
}
315-
println!("{}", value);
413+
self.inner().save_button.connect_clicked(clone!(@weak kb => @default-panic, move |_button| {
414+
let filter = cascade! {
415+
gtk::FileFilter::new();
416+
..set_name(Some("JSON"));
417+
..add_mime_type("application/json");
418+
..add_pattern("*.json");
419+
};
420+
421+
let chooser = cascade! {
422+
gtk::FileChooserNative::new::<gtk::Window>(Some("Save Layout"), None, gtk::FileChooserAction::Save, Some("Save"), Some("Cancel"));
423+
..add_filter(&filter);
424+
};
425+
426+
if chooser.run() == gtk::ResponseType::Accept {
427+
let mut path = chooser.get_filename().unwrap();
428+
match path.extension() {
429+
None => { path.set_extension(OsStr::new("json")); }
430+
Some(ext) if ext == OsStr::new("json") => {}
431+
Some(ext) => {
432+
let mut ext = ext.to_owned();
433+
ext.push(".json");
434+
path.set_extension(&ext);
435+
}
436+
}
437+
let keymap = kb.export_keymap();
316438

439+
match File::create(&path) {
440+
Ok(file) => match keymap.to_writer_pretty(file) {
441+
Ok(()) => {},
442+
Err(err) => error_dialog(&kb.window().unwrap(), "Failed to export keymap", err),
443+
}
444+
Err(err) => error_dialog(&kb.window().unwrap(), "Failed to open file", err),
445+
}
446+
}
317447
}));
318448
}
319449

src/application/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::rc::Rc;
1111

1212
use crate::daemon::{Daemon, DaemonClient, DaemonDummy, daemon_server};
1313

14+
mod error_dialog;
1415
mod key;
1516
mod keyboard;
1617
pub(crate) mod layout;

src/keymap.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use serde::{Deserialize, Serialize};
2+
use std::collections::HashMap;
3+
use std::io::{Read, Write};
4+
5+
#[derive(Debug, Serialize, Deserialize)]
6+
pub struct KeyMap {
7+
pub board: String,
8+
pub map: HashMap<String, Vec<String>>,
9+
}
10+
11+
impl KeyMap {
12+
pub fn from_reader<R: Read>(rdr: R) -> serde_json::Result<Self> {
13+
serde_json::from_reader(rdr)
14+
}
15+
16+
pub fn from_str(s: &str) -> serde_json::Result<Self> {
17+
serde_json::from_str(s)
18+
}
19+
20+
pub fn to_writer_pretty<W: Write>(&self, wtr: W) -> serde_json::Result<()> {
21+
serde_json::to_writer_pretty(wtr, self)
22+
}
23+
24+
pub fn to_string_pretty(&self) -> String {
25+
serde_json::to_string_pretty(self).unwrap()
26+
}
27+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ mod color_wheel;
1111
mod keyboard;
1212
mod keyboard_backlight_widget;
1313
mod keyboard_color_button;
14+
mod keymap;
1415

1516
pub use keyboard_backlight_widget::keyboard_backlight_widget;

0 commit comments

Comments
 (0)