← KD100

Implement triple-click wheel toggle with set-based navigation

3e12f20f8a8eb5d4df08f365ef0d98cd44fe3d66 · Claude · Jan 22, 2026 19:31

Add multi-click detection for button 18 (wheel toggle) to enable
set-based navigation between up to 6 wheel functions organized in
3 sets of 2 functions each.

Features:
- Single-click: Toggle between functions within current set (0↔1)
- Double-click: Switch between Set 1 (functions 0-1) ↔ Set 2 (functions 2-3)
- Triple-click: Toggle to/from Set 3 (functions 4-5)
- Configurable click timeout (20-990ms, default 300ms)

Implementation:
- Add wheel_click_timeout_ms to config structure
- Add multi-click detection state tracking
- Replace sequential wheel cycling with set-based navigation
- Add wheel_click_timeout config option to default.cfg

Example use case:
- Set 1: Brush size ↔ Opacity (most used, single-click toggle)
- Set 2: Flow ↔ Rotation (double-click to access)
- Set 3: Scatter ↔ Spacing (triple-click for occasional use)

Files modified:
- src/config.h: Add wheel_click_timeout_ms field
- src/config.c: Add config parsing and initialization
- src/device.c: Implement multi-click detection and set-based logic
- default.cfg: Add configuration documentation and example

+126 −9 · 4 files changed

  • default.cfg +19 −1
  • src/config.c +17 −0
  • src/config.h +1 −0
  • src/device.c +89 −8
diff --git a/default.cfg b/default.cfg
index d1d27b5..0016120 100644
--- a/default.cfg
+++ b/default.cfg
@@ -76,7 +76,25 @@ leader_function: shift
 leader_timeout: 1000
 leader_mode: toggle
 
-// 
+//
+//     Wheel Click Timeout Configuration
+//     wheel_click_timeout: Time in milliseconds for multi-click detection on button 18 (wheel toggle)
+//     Range:     20-990 ms (hard limit enforced)
+//     Default:   300 ms
+//
+//     Multi-click behavior for button 18 (wheel toggle):
+//     - Single-click:  Toggle within current set (e.g., function 0 ↔ 1)
+//     - Double-click:  Switch between Set 1 (functions 0-1) ↔ Set 2 (functions 2-3)
+//     - Triple-click:  Toggle to/from Set 3 (functions 4-5)
+//
+//     Example: With 6 wheel functions defined:
+//       Set 1: Wheel functions 0-1 (e.g., brush size ↔ opacity)
+//       Set 2: Wheel functions 2-3 (e.g., flow ↔ rotation)
+//       Set 3: Wheel functions 4-5 (e.g., scatter ↔ spacing)
+//
+wheel_click_timeout: 300
+
+//
 //     hid_uclogic compatibility configuration
 //     enabe_uclogic: [true|false] - OpenTabletDriver requires hid_uclogic to be unloaded from kernel.
 //     Default:       false
diff --git a/src/config.c b/src/config.c
index 70a1214..4a5bf0e 100644
--- a/src/config.c
+++ b/src/config.c
@@ -31,6 +31,7 @@ config_t* config_create(void) {
     config->totalButtons = 0;
     config->totalWheels = 0;
     config->enable_uclogic = 0;
+    config->wheel_click_timeout_ms = 300;  // 300ms default timeout
 
     // Initialize leader state
     config->leader.leader_button = -1;
@@ -139,6 +140,19 @@ int config_load(config_t* config, const char* filename, int debug) {
             continue;
         }
 
+        // Parse wheel_click_timeout
+        if (strncasecmp(line, "wheel_click_timeout:", 20) == 0) {
+            char* value = line + 20;
+            while (*value == ' ') value++;
+            int timeout = atoi(value);
+            // Enforce hard limits: 20-990ms
+            if (timeout < 20) timeout = 20;
+            if (timeout > 990) timeout = 990;
+            config->wheel_click_timeout_ms = timeout;
+            if (debug) printf("Config: wheel_click_timeout = %d ms\n", config->wheel_click_timeout_ms);
+            continue;
+        }
+
         // Parse leader_button
         if (strncasecmp(line, "leader_button:", 14) == 0) {
             char* value = line + 14;
@@ -348,5 +362,8 @@ void config_print(const config_t* config, int debug) {
     printf("Leader function: '%s'\n", config->leader.leader_function ? config->leader.leader_function : "(null)");
     printf("Leader timeout: %d ms\n", config->leader.timeout_ms);
     printf("Leader mode: %s\n", leader_mode_to_string(config->leader.mode));
+
+    printf("\n=== Wheel Click Configuration ===\n");
+    printf("Multi-click timeout: %d ms\n", config->wheel_click_timeout_ms);
     printf("\n");
 }
diff --git a/src/config.h b/src/config.h
index 4518373..5c0649b 100644
--- a/src/config.h
+++ b/src/config.h
@@ -17,6 +17,7 @@ typedef struct {
     int totalWheels;
     leader_state leader;
     int enable_uclogic;
+    int wheel_click_timeout_ms;  // Multi-click detection timeout (20-990ms)
 } config_t;
 
 // Configuration functions
diff --git a/src/device.c b/src/device.c
index 70f437e..1a5d878 100644
--- a/src/device.c
+++ b/src/device.c
@@ -20,6 +20,12 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in
     prevEvent.function = "";
     prevEvent.type = 0;
 
+    // Multi-click detection state for button 18
+    struct timeval last_button18_time = {0, 0};
+    int button18_click_count = 0;
+    int wheel_current_set = 0;      // Current set: 0 (functions 0-1), 1 (functions 2-3), 2 (functions 4-5)
+    int wheel_position_in_set = 0;  // Position within set: 0 or 1
+
     system("clear");
 
     if (debug > 0) {
@@ -333,14 +339,89 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in
                                         prevEvent.function = "";
                                     }
                                 } else if (strcmp(config->events[button_index].function, "swap") == 0) {
-                                    if (wheelFunction != config->totalWheels - 1) {
-                                        wheelFunction++;
-                                    } else
-                                        wheelFunction = 0;
-                                    if (debug == 1) {
-                                        printf("Function: %s | %s\n",
-                                               config->wheelEvents[wheelFunction].left ? config->wheelEvents[wheelFunction].left : "(null)",
-                                               config->wheelEvents[wheelFunction].right ? config->wheelEvents[wheelFunction].right : "(null)");
+                                    // Multi-click detection for button 18
+                                    struct timeval now;
+                                    gettimeofday(&now, NULL);
+
+                                    long time_since_last_click = 0;
+                                    if (last_button18_time.tv_sec != 0 || last_button18_time.tv_usec != 0) {
+                                        time_since_last_click =
+                                            (now.tv_sec - last_button18_time.tv_sec) * 1000 +
+                                            (now.tv_usec - last_button18_time.tv_usec) / 1000;
+                                    }
+
+                                    // If click is within timeout window, increment count
+                                    if (time_since_last_click > 0 && time_since_last_click < config->wheel_click_timeout_ms) {
+                                        button18_click_count++;
+                                    } else {
+                                        // This is a new click sequence
+                                        button18_click_count = 1;
+                                    }
+
+                                    last_button18_time = now;
+
+                                    // Wait for timeout to process the click(s)
+                                    usleep(config->wheel_click_timeout_ms * 1000);
+
+                                    // Check if more clicks arrived during the wait
+                                    gettimeofday(&now, NULL);
+                                    time_since_last_click =
+                                        (now.tv_sec - last_button18_time.tv_sec) * 1000 +
+                                        (now.tv_usec - last_button18_time.tv_usec) / 1000;
+
+                                    // Only process if no more clicks arrived
+                                    if (time_since_last_click >= config->wheel_click_timeout_ms) {
+                                        int final_click_count = button18_click_count;
+                                        button18_click_count = 0;
+
+                                        if (debug == 1) {
+                                            printf("Button 18 clicks: %d\n", final_click_count);
+                                        }
+
+                                        // Process based on click count
+                                        if (final_click_count == 1) {
+                                            // Single-click: toggle within current set
+                                            wheel_position_in_set = 1 - wheel_position_in_set;
+                                        } else if (final_click_count == 2) {
+                                            // Double-click: toggle between Set 0 and Set 1
+                                            if (wheel_current_set == 0) {
+                                                wheel_current_set = 1;
+                                            } else if (wheel_current_set == 1) {
+                                                wheel_current_set = 0;
+                                            } else {
+                                                // From Set 2, go to Set 0
+                                                wheel_current_set = 0;
+                                            }
+                                            wheel_position_in_set = 0;  // Start at first function in new set
+                                        } else if (final_click_count >= 3) {
+                                            // Triple-click: toggle to/from Set 2
+                                            if (wheel_current_set == 2) {
+                                                // From Set 2, go back to Set 0
+                                                wheel_current_set = 0;
+                                            } else {
+                                                // From Set 0 or 1, go to Set 2
+                                                wheel_current_set = 2;
+                                            }
+                                            wheel_position_in_set = 0;  // Start at first function in new set
+                                        }
+
+                                        // Calculate actual wheel function index
+                                        wheelFunction = (wheel_current_set * 2) + wheel_position_in_set;
+
+                                        // Ensure we don't go beyond available wheels
+                                        if (wheelFunction >= config->totalWheels) {
+                                            wheelFunction = 0;
+                                            wheel_current_set = 0;
+                                            wheel_position_in_set = 0;
+                                        }
+
+                                        if (debug == 1) {
+                                            printf("Set: %d | Position: %d | Wheel Function: %d\n",
+                                                   wheel_current_set, wheel_position_in_set, wheelFunction);
+                                            printf("Function: %s | %s\n",
+                                                   config->wheelEvents[wheelFunction].left ? config->wheelEvents[wheelFunction].left : "(null)",
+                                                   config->wheelEvents[wheelFunction].right ? config->wheelEvents[wheelFunction].right : "(null)");
+                                        }
                                     }
                                 } else if (strcmp(config->events[button_index].function, "mouse1") == 0 ||
                                            strcmp(config->events[button_index].function, "mouse2") == 0 ||