feat: Editable Tap Dance + combo creation validation

Tap Dance:
- Each TD action is now a clickable button that opens the key selector
- Clicking an action -> pick key from popup -> sends TD_SET binary to firmware
- Labels show 1-tap / 2-tap / 3-tap / hold columns

Combo creation:
- Added validation: "Pick both keys first" message if keys not selected
- Debug log for COMBOSET command

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mae PUGIN 2026-04-07 09:09:41 +02:00
parent d69d931ea9
commit 4cfe2fbb7b
3 changed files with 82 additions and 4 deletions

View file

@ -672,6 +672,7 @@ fn main() {
let dispatch_keycode = {
let apply_keycode = apply_keycode.clone();
let keys_arc = keys_arc.clone();
let serial = serial.clone();
let macro_steps = macro_steps.clone();
let refresh_macro_display = refresh_macro_display.clone();
let window_weak = window.as_weak();
@ -738,6 +739,41 @@ fn main() {
}
if count < 4 { adv.set_new_leader_seq_count(count + 1); }
}
"td-action" => {
let adv = w.global::<AdvancedBridge>();
let td_idx = adv.get_editing_td_index();
let slot = adv.get_editing_td_slot() as usize;
if td_idx >= 0 && slot < 4 {
// Update model in place
let tds = adv.get_tap_dances();
for i in 0..tds.row_count() {
let td = tds.row_data(i).unwrap();
if td.index == td_idx {
let actions = td.actions;
let mut a = actions.row_data(slot).unwrap();
a.name = name.clone();
a.code = code as i32;
actions.set_row_data(slot, a);
// Collect all 4 action codes and send to firmware
let mut codes = [0u16; 4];
for j in 0..4.min(actions.row_count()) {
codes[j] = actions.row_data(j).unwrap().code as u16;
}
let payload = logic::binary_protocol::td_set_payload(td_idx as u8, &codes);
let serial = serial.clone();
std::thread::spawn(move || {
let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner());
let _ = ser.send_binary(logic::binary_protocol::cmd::TD_SET, &payload);
});
w.global::<AppState>().set_status_text(
SharedString::from(format!("TD{} slot {} = {}", td_idx, slot, name))
);
break;
}
}
}
}
"macro-step" => {
// Add key press (Down + Up) to macro steps
let mut steps = macro_steps.borrow_mut();
@ -1018,7 +1054,14 @@ fn main() {
let r2 = adv.get_new_combo_r2() as u8;
let c2 = adv.get_new_combo_c2() as u8;
let result = adv.get_new_combo_result_code() as u8;
let key1_name = adv.get_new_combo_key1_name();
let key2_name = adv.get_new_combo_key2_name();
if key1_name == "Pick..." || key2_name == "Pick..." {
w.global::<AppState>().set_status_text("Pick both keys first".into());
return;
}
let cmd = logic::protocol::cmd_comboset(255, r1, c1, r2, c2, result);
eprintln!("COMBOSET: {}", cmd);
let serial = serial.clone();
let tx = tx.clone();
std::thread::spawn(move || {
@ -1481,7 +1524,10 @@ fn main() {
.map(|(i, actions)| TapDanceData {
index: i as i32,
actions: ModelRc::from(Rc::new(VecModel::from(
actions.iter().map(|&a| SharedString::from(keycode::decode_keycode(a))).collect::<Vec<_>>()
actions.iter().map(|&a| TapDanceAction {
name: SharedString::from(keycode::decode_keycode(a)),
code: a as i32,
}).collect::<Vec<_>>()
))),
})
.collect();

View file

@ -121,9 +121,14 @@ export global StatsBridge {
// ---- Advanced ----
export struct TapDanceAction {
name: string,
code: int,
}
export struct TapDanceData {
index: int,
actions: [string],
actions: [TapDanceAction],
}
export struct ComboData {
@ -183,6 +188,11 @@ export global AdvancedBridge {
in-out property <int> new-leader-result-code: 0;
in-out property <string> new-leader-result-name: "Pick...";
in-out property <int> new-leader-mod-idx: 0;
// TD editing: stores which TD slot+action is being edited
in-out property <int> editing-td-index: -1;
in-out property <int> editing-td-slot: -1; // 0-3 = which action
callback save-td(int, int, int, int, int); // index, a1, a2, a3, a4 (keycodes)
callback edit-td-action(int, int); // td_index, action_slot -> opens popup
callback refresh-advanced();
callback delete-combo(int);
callback delete-leader(int);

View file

@ -88,14 +88,36 @@ export component TabAdvanced inherits Rectangle {
spacing: 4px;
SectionHeader { text: "Tap Dance"; }
if AdvancedBridge.tap-dances.length == 0 : Text { text: "No tap dances"; color: Theme.fg-secondary; font-size: 11px; }
Text { text: "1-tap 2-tap 3-tap hold"; color: Theme.fg-secondary; font-size: 10px; }
for td in AdvancedBridge.tap-dances : Rectangle {
background: Theme.bg-primary;
border-radius: 4px;
height: 30px;
HorizontalLayout {
padding-left: 8px; padding-right: 8px; spacing: 8px;
padding-left: 8px; padding-right: 8px; spacing: 4px;
Text { text: "TD" + td.index; color: Theme.accent-purple; font-size: 11px; vertical-alignment: center; width: 35px; }
for action in td.actions : Text { text: action; color: Theme.fg-primary; font-size: 11px; vertical-alignment: center; }
for action[a-idx] in td.actions : Rectangle {
width: 60px;
height: 24px;
border-radius: 3px;
background: td-action-ta.has-hover ? Theme.button-hover : Theme.button-bg;
border-width: 1px;
border-color: Theme.button-border;
Text { text: action.name; color: Theme.fg-primary; font-size: 10px; horizontal-alignment: center; vertical-alignment: center; }
td-action-ta := TouchArea {
clicked => {
AdvancedBridge.editing-td-index = td.index;
AdvancedBridge.editing-td-slot = a-idx;
KeymapBridge.selector-target = "td-action";
KeymapBridge.key-selector-open = true;
}
mouse-cursor: pointer;
}
}
}
}
}