Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce new LongPress plugin #1423

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

hupfdule
Copy link

@hupfdule hupfdule commented May 17, 2024

This PR replaces #1340 by providing a new “LongPress” plugin instead of
modifying the exisiting “AutoShift“ plugin.

It is still based on AutoShift, but has a few adaptations:

  • The main focus of functionality is defining long press behaviour for
    keys. Autoshifting is only one such use case (with special support for).
  • Some methods have been renamed to reflect this change. The term “auto
    shift” is only used where such functionality is affected.
  • Autoshifting is not enabled by default anymore, but needs to be activated
    via enableAutoshift(…) or setAutoshiftEnabled(…).

Compared to PR #1340 there are also some changes:

  • It is possible to define long-press behaviour for a KeyAddr and for a
    Key. Both variants make sense for different use cases. For example for
    mirroring the number row (generating a 0 by long-pressing 1) it makes
    sense to configure that behaviour for a specific KeyAddr as the same
    long-press behaviour may not be appropriate for the separate NumPad layer.
    On the other hand to generate an ë by long-pressing e is likely
    configured on Key_E, regardless of which physical key that is generated
    with.

The PR is not finished yet. For example the documentation for LongPress in
not yet written. Also support for configuration via Chrysalis is not yet
provided as it is unclear to me, what needs to be done there. Also it probably
needs some cleanup (like additional comments).

But it is in a state to be reviewed.

There are a few things I still need help with (see below).

@hupfdule
Copy link
Author

As written above I have adapted the configuration to provide long-press behaviour on KeyAddr as well as on Key objects. The idea is that configuration on KeyAddr is the most specific one and should override behaviour defined on Key (and long-press configuration on Key should override auto-shifting).

I saw different approaches to achieve this. The first one was to provide different configuration options for KeyAddr and Key. It would look something like this in the .ino file:

LONGPRESS_ADDR(
   kaleidoscope::plugin::LongPressAddr(KeyAddr(1, 0), Key_Y),
   […]
)
LONGPRESS_KEY(
   kaleidoscope::plugin::LongPressKey(Key_E,   Key_Z),
   […]
)

It leads to duplicated data store and methods in the code and (more importantly) harder to read LongPress configuration.

Therefore I adapted it to have only one config object (LongPressMapping) that allows either a KeyAddr or a Key to be configured on. This is what is currently implemented in this PR. It is much cleaner in my opinion, but has one serious drawback. It is dependend on the order the LongPressMappings are defined in. So the note above about configuration on KeyAddr always having priority over configuration on Key does not apply. The first configuration will win. For example for a keyboard where all keys are defined as Key_E and the following LongPress configuration:

LONGPRESS(
  kaleidoscope::plugin::LongPressMapping(KeyAddr(1, 1), Key_Y),
  kaleidoscope::plugin::LongPressMapping(Key_E,         Key_Z),
)

the first one would take precedence over the second one (long-pressing KeyAddr(1, 1) would produce a Y instead of a Z. But changing the order to:

LONGPRESS(
  kaleidoscope::plugin::LongPressMapping(Key_E,         Key_Z),
  kaleidoscope::plugin::LongPressMapping(KeyAddr(1, 1), Key_Y),
)

would produce a Z in all cases, even on KeyAddr(1, 1).

This could be handled by sorting the configured LongPressMappings to always put the configuration on KeyAddr first, regardless of the order they were defined in. I already tried that for this PR, but unfortunately failed on the nitty-gritty details of C++ for applying a qsort() on the list of LongPressMappings. The second (uncompilable) commit shows what I tried. It fails with:

In file included from /home/mherrn/projekte/Kaleidoscope/plugins/Kaleidoscope-LongPress/src/Kaleidoscope-LongPress.h:20:0,                                                      
                 from /home/mherrn/projekte/Kaleidoscope/examples/Keystrokes/LongPress/LongPress.ino:5:                                                                         
/home/mherrn/projekte/Kaleidoscope/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPress.h: In instantiation of 'void kaleidoscope::plugin::LongPress::configureLongPresses(const kaleidoscope::plugin::LongPressMapping (&)[_explicitmappings_count]) [with unsigned char _explicitmappings_count = 2]':                                            
/home/mherrn/projekte/Kaleidoscope/examples/Keystrokes/LongPress/LongPress.ino:69:3:   required from here
/home/mherrn/projekte/Kaleidoscope/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPress.h:309:13: error: invalid conversion from 'const void*' to 'void*' [-fpermissive]
       qsort(explicitmappings_,
             ^~~~~~~~~~~~~~~~~
In file included from /home/mherrn/projekte/Kaleidoscope/.arduino/user/hardware/keyboardio/avr/cores/keyboardio/Arduino.h:23:0,
                 from /tmp/kaleidoscope-mherrn/build/4140205584-LongPress.ino/sketch/LongPress.ino.cpp:1:
/home/mherrn/projekte/Kaleidoscope/.arduino/data/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7/avr/include/stdlib.h:185:13: note:   initializing argument 1 of 'void qsort(void*, size_t, size_t, __compar_fn_t)' 
 extern void qsort(void *__base, size_t __nmemb, size_t __size,
             ^~~~~
In file included from /home/mherrn/projekte/Kaleidoscope/plugins/Kaleidoscope-LongPress/src/Kaleidoscope-LongPress.h:20:0,
                 from /home/mherrn/projekte/Kaleidoscope/examples/Keystrokes/LongPress/LongPress.ino:5:
/home/mherrn/projekte/Kaleidoscope/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPress.h:309:12: error: invalid conversion from 'int (*)(kaleidoscope::plugin::LongPressMapping, kaleidoscope::plugin::LongPressMapping)' to '__compar_fn_t {aka int (*)(const void*, const void*)}' [-fpermissive]
       qsort(explicitmappings_,
       ~~~~~^~~~~~~~~~~~~~~~~~~
             length,
             ~~~~~~~
             sizeof(explicitmappings_[0]),
             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
             LongPressMapping::compare);
             ~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /home/mherrn/projekte/Kaleidoscope/.arduino/user/hardware/keyboardio/avr/cores/keyboardio/Arduino.h:23:0,
                 from /tmp/kaleidoscope-mherrn/build/4140205584-LongPress.ino/sketch/LongPress.ino.cpp:1:
/home/mherrn/projekte/Kaleidoscope/.arduino/data/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7/avr/include/stdlib.h:185:13: note:   initializing argument 4 of 'void qsort(void*, size_t, size_t, __compar_fn_t)' 
 extern void qsort(void *__base, size_t __nmemb, size_t __size,
             ^~~~~

So apparently the explicitmapping array being defined as const prohibits applying the sort operation directly (there may be more errors regarding pointer/reference handling). I don’t grok C++ enough to be able to resolve this issue.

Can you please help me on how to change it to correctly sort that list. Or do you have some other idea on how to solve the basic problem?

@hupfdule
Copy link
Author

Maybe I am overthinking this a bit. I am running this LongPress plugin now for a few days (without the last commit regarding sorting of course) and am rather happy with it. I even have such a combination of configuration on KeyAddr and on Key. I don’t think it is that bad to let the earlier defined mappings “win” over the later defined ones. It needs to be documented, for sure. But maybe that is the better approach.

What do you think?

@obra
Copy link
Member

obra commented May 20, 2024

I think that having the documentation clearly state which one "wins" is just great.

@obra
Copy link
Member

obra commented May 21, 2024

(It does look like the README needs to be updated away from 'AutoShift' to 'LongPress')

@hupfdule
Copy link
Author

(It does look like the README needs to be updated away from 'AutoShift' to 'LongPress')

Yes, I am working on that right now. Will write a note here when I am done with it.

@hupfdule
Copy link
Author

Do have some hints on how I can resolve the failing tests?

@obra
Copy link
Member

obra commented May 21, 2024

https://github.com/keyboardio/Kaleidoscope/actions/runs/9169594412/job/25244886228?pr=1423#step:7:1632

do those tests pass for you locally? Are the timings the tests show right?

@hupfdule
Copy link
Author

https://github.com/keyboardio/Kaleidoscope/actions/runs/9169594412/job/25244886228?pr=1423#step:7:1632

do those tests pass for you locally? Are the timings the tests show right?

For some reason I can’t execute the tests locally. I activated them in the Github Actions of my fork and they fail in the same way.

Apparently the testing framework doesn’t work as I expect.

The failing tests all test the case that no long-press behaviour has been defined for a key and a long-press does not produce a different key than a short tap. But this seems to produce these timing problems.
This one here presses C, then waits 20ms (the time which is needed to trigger the long-press behaviour) and then tests that the usual Key_C has been produced. But that apparently does not work like I expected. It seems that these 20ms are exactly the difference in the expected an actual timestamp.

@obra
Copy link
Member

obra commented May 21, 2024

I'd love to help get the simulator tests working locally for you. That'll make testing much less painful. What OS are you on? How does 'make simulator-tests' fail?

@hupfdule
Copy link
Author

I have now updated the README to correctly describe the LongPress plugin.

At the same time I have moved the AutoShiftCategory class to a separate namespace in a separate file to enhance readability of the main plugin class.

I also rename the LongPressMapping struct to LongPressKey. I think that is easier to read and understand by users of the plugin.

I leave these changes as separate commits for now for easier tracking of the changes. Before this plugin gets merged I would like to squash it into a single commit.

But we need to resolve the failing test beforehand anyway…

@hupfdule
Copy link
Author

I'd love to help get the simulator tests working locally for you. That'll make testing much less painful. What OS are you on? How does 'make simulator-tests' fail?

It’s a bit complicated… I am using a Debian Linux. But that one is running inside a virtual machine on a Windows host.
As I don’t want to clutter my OS with too many tools only for testing I tried to use make docker-simulator-tests, but it fails in a very strange way. It always fails when trying to install the necessary tools for building the docker container. It seems that it is able to download some of the packages, but at some point the network connection collapses and it runs into a timeout. What is even more strange is that this broken network connection is not only broken inside the docker container, but also in my Debian OS, although it automatically comes back without intervention in less than a minute.

I will try to avoid docker and install the necessary packages locally to run the tests without docker. I will get back to you when I have done that. But this will be tomorrow. It is 22:45 here and I need to get to bed ;-)

@obra
Copy link
Member

obra commented May 21, 2024 via email

@hupfdule
Copy link
Author

I just tried it, but running the tests does not work:

$ make simulator-tests
Building in quiet mode. For a lot more information, add 'VERBOSE=1' to the beginning of your call to make
make -C tests all
make[1]: Entering directory '/home/mherrn/projekte/Kaleidoscope/tests'
make -C /home/mherrn/projekte/Kaleidoscope/testing/googletest/build
[ 25%] Built target gtest
[ 50%] Built target gmock
[ 75%] Built target gmock_main
[100%] Built target gtest_main
compile /home/mherrn/projekte/Kaleidoscope/_build/keyboardio_virtual_model01/obj/AbsoluteMouseReport.o
In file included from /home/mherrn/projekte/Kaleidoscope/src/kaleidoscope/device/virtual/Virtual.h:25,
                 from /home/mherrn/projekte/Kaleidoscope/src/kaleidoscope/device/device.h:55,
                 from /home/mherrn/projekte/Kaleidoscope/src/kaleidoscope/KeyAddr.h:19,
                 from /home/mherrn/projekte/Kaleidoscope/src/kaleidoscope/Runtime.h:21,
                 from /home/mherrn/projekte/Kaleidoscope/testing/AbsoluteMouseReport.cpp:24:
/home/mherrn/projekte/Kaleidoscope/plugins/Kaleidoscope-Hardware-Model01/src/Kaleidoscope-Hardware-Model01.h:20:10: fatal error: Kaleidoscope-Hardware-Keyboardio-Model01.h: No such file or directory
   20 | #include "Kaleidoscope-Hardware-Keyboardio-Model01.h"  // IWYU pragma: keep
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
compilation terminated.
make[3]: *** [/home/mherrn/projekte/Kaleidoscope/testing/makefiles/libcommon.mk:37: /home/mherrn/projekte/Kaleidoscope/_build/keyboardio_virtual_model01/obj/AbsoluteMouseReport.o] Error 1
make[2]: *** [/home/mherrn/projekte/Kaleidoscope/testing/makefiles/testcase.mk:107: /home/mherrn/projekte/Kaleidoscope/_build/keyboardio_virtual_model01/lib/libcommon.a] Error 2
make -C /home/mherrn/projekte/Kaleidoscope/testing/googletest/build
[ 25%] Built target gtest
[ 50%] Built target gmock
[ 75%] Built target gmock_main
[100%] Built target gtest_main
compile /home/mherrn/projekte/Kaleidoscope/_build/keyboardio_virtual_model01/obj/AbsoluteMouseReport.o
In file included from /home/mherrn/projekte/Kaleidoscope/src/kaleidoscope/device/virtual/Virtual.h:25,
                 from /home/mherrn/projekte/Kaleidoscope/src/kaleidoscope/device/device.h:55,
                 from /home/mherrn/projekte/Kaleidoscope/src/kaleidoscope/KeyAddr.h:19,
                 from /home/mherrn/projekte/Kaleidoscope/src/kaleidoscope/Runtime.h:21,
                 from /home/mherrn/projekte/Kaleidoscope/testing/AbsoluteMouseReport.cpp:24:
/home/mherrn/projekte/Kaleidoscope/plugins/Kaleidoscope-Hardware-Model01/src/Kaleidoscope-Hardware-Model01.h:20:10: fatal error: Kaleidoscope-Hardware-Keyboardio-Model01.h: No such file or directory
   20 | #include "Kaleidoscope-Hardware-Keyboardio-Model01.h"  // IWYU pragma: keep
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
compilation terminated.
make[4]: *** [/home/mherrn/projekte/Kaleidoscope/testing/makefiles/libcommon.mk:37: /home/mherrn/projekte/Kaleidoscope/_build/keyboardio_virtual_model01/obj/AbsoluteMouseReport.o] Error 1
make[3]: *** [/home/mherrn/projekte/Kaleidoscope/testing/makefiles/testcase.mk:107: /home/mherrn/projekte/Kaleidoscope/_build/keyboardio_virtual_model01/lib/libcommon.a] Error 2

and so on.

When trying it to restrict it to testing the LongPress plugin it fails differently:

$ make simulator-tests TEST_PATH=tests/plugins/LongPress
Building in quiet mode. For a lot more information, add 'VERBOSE=1' to the beginning of your call to make                                                                       
make -C tests all                                                                                                                                                               
make[1]: Entering directory '/home/mherrn/projekte/Kaleidoscope/tests'                                                                                                          
find: ‘tests/plugins/LongPress’: No such file or directory                                                                                                                      
make[1]: Leaving directory '/home/mherrn/projekte/Kaleidoscope/tests'                                                                                                           
make[1]: Entering directory '/home/mherrn/projekte/Kaleidoscope/tests'                                                                                                          
make -C /home/mherrn/projekte/Kaleidoscope/testing/googletest/build                                                                                                             
[ 25%] Built target gtest                                                                                                                                                       
[ 50%] Built target gmock                                                                                                                                                       
[ 75%] Built target gmock_main                                                                                                                                                  
[100%] Built target gtest_main                                                                                                                                                  
find: ‘tests/plugins/LongPress’: No such file or directory                                                                                                                      
make -C /home/mherrn/projekte/Kaleidoscope/testing/googletest/build                                                                                                             
[ 25%] Built target gtest                                                                                                                                                       
[ 50%] Built target gmock                                                                                                                                                       
[ 75%] Built target gmock_main                                                                                                                                                  
[100%] Built target gtest_main                                                                                                                                                  
find: ‘tests/plugins/LongPress’: No such file or directory           

And it goes on like this and seems to never stop.

@obra
Copy link
Member

obra commented May 23, 2024

@hupfdule - what debian are you on? I'm very happy to set up a VM to try to replicate this, since we should at least be failing more sanely.

@obra
Copy link
Member

obra commented May 24, 2024

I was just starting to dig into the fails, but I see that you're actively pushing updates.

@hupfdule
Copy link
Author

@hupfdule - what debian are you on? I'm very happy to set up a VM to try to replicate this, since we should at least be failing more sanely.

It’s a normal Debian Bookworm.
I have tried it now on a separate computer with a Debian Bullseye and it is working there. Then I had the idea to try it with a fresh clone of this repository and indeed, I can now run the tests (I can restrict it to the LongPress tests by calling it as make simulator-tests TEST_PATH=plugins/LongPress; the tests directory must not be specified).

However, the result is the same. The tests fail with a timestamp deviation.
I was able to resolve two of them (I think I understood what I did wrong). But one test still fails and I don’t know why.

@obra Are you able to help me correct these tests?

I describe my intentions for this example (I have reduced it to the minimum):

In this case I want to test that the plugin has no effect if a key is only tapped instead of long-pressed.

# press key “A” → this should produce the key A in the keyboard-report
RUN 4 ms
PRESS A
RUN 1 cycle
EXPECT keyboard-report Key_A

# release key "A" again → this should remove the A from the keyboard-report
RUN 4 ms
RELEASE A
RUN 1 cycle
EXPECT keyboard-report empty

RUN 5 ms

However, the test fails with this message:

Expected equality of these values:
  observed_report.Timestamp()
    Which is: 10
  expected_report.Timestamp()
    Which is: 5
Report timestamps don't match (i=0)

I played around a bit with the order of the test statements, but I just don’t understand what is the problem here.

@hupfdule
Copy link
Author

I was just starting to dig into the fails, but I see that you're actively pushing updates.

Ah yes, sorry. Just found the problem with two of them. But the last one is still a mystery to me.

@obra
Copy link
Member

obra commented May 24, 2024

This is the test script as I'm testing:

VERSION 1

KEYSWITCH A         1 0
NAME LongPress AutoShift tap

RUN 4 ms
PRESS A
RUN 1 cycle
EXPECT keyboard-report Key_A

RUN 4 ms
RELEASE A
RUN 1 cycle
EXPECT keyboard-report empty

With this, I get the same results you do.

Digging into how to debug this:

.ktest files are transformed into googletest files. In this case, tests/plugins/LongPress/autoshift/test/generated-testcase.cpp

The generated content of that is

// ==============================================================================
TEST_F(GeneratedKTest, 1_LongPressAutoShiftTap) {
  ClearState(); // Clear any state from previous tests
  // This tests that short tapping any of the keys should always produce the
  // normal key
  sim_.RunForMillis(4);
  PressKey(key_addr_A); // 
  sim_.RunCycles(1);
  ExpectKeyboardReport(Keycodes{Key_A}, "No explanatory comment specified");

  sim_.RunForMillis(4);
  ReleaseKey(key_addr_A); // 
  sim_.RunCycles(1);
  ExpectKeyboardReport(Keycodes{}, "No explanatory comment specified");


  LoadState();
  CheckReports();
} // TEST_F

The CheckReports call is what's throwing the error.

That's defined in testing/VirtualDeviceTest.cpp

void VirtualDeviceTest::CheckReports() const {
  CheckKeyboardReports();
  CheckMouseReports();
}

In this case, a keyboard report is at issue:

void VirtualDeviceTest::CheckKeyboardReports() const {
  int observed_keyboard_report_count = HIDReports()->Keyboard().size();
  int expected_keyboard_report_count = expected_keyboard_reports_.size();

  EXPECT_EQ(observed_keyboard_report_count, expected_keyboard_report_count);

  int max_count = std::max(observed_keyboard_report_count,
                           expected_keyboard_report_count);

  for (int i = 0; i < observed_keyboard_report_count; ++i) {
    auto observed_report   = HIDReports()->Keyboard(i);
    auto observed_keycodes = observed_report.ActiveKeycodes();

    if (i < expected_keyboard_report_count) {
      auto expected_report   = expected_keyboard_reports_[i];
      auto expected_keycodes = expected_report.Keycodes();

      EXPECT_THAT(observed_keycodes,
                  ::testing::ElementsAreArray(expected_keycodes))
        << expected_keyboard_reports_[i].Message() << " (i=" << i << ")";
      EXPECT_EQ(observed_report.Timestamp(), expected_report.Timestamp())
        << "Report timestamps don't match (i=" << i << ")";

    } else {
 ...

The issue we're running into is that the...first? key report is being outputted 5ms later than implied by the test's code.

And thinking about it a little bit... LongPress would have to be delaying that initial keypress of A until after it figures out it's a tap, right?

It shouldn't be sending output to the host until it's decided what to do. It looks like what it's doing is waiting until the switch is released and then sending both usb key reports in the same device cycle, right?

@hupfdule
Copy link
Author

Many thanks for your help and the thorough explanation!

And thinking about it a little bit... LongPress would have to be delaying that initial keypress of A until after it figures out it's a tap, right?

It shouldn't be sending output to the host until it's decided what to do. It looks like what it's doing is waiting until the switch is released and then sending both usb key reports in the same device cycle, right?

You are absolutely right. It was a misinterpretation on my side. My mistake was that I was thinking like that key would not be configured at all in the LongPress plugin. But actually it is (auto-shifting is active for all letter keys). And in that case, the plugin needs to wait until either the long-press timeout occurs (20ms) or the key has been released again.

If I change the test case to use Key_1 (which has no configured long-press behaviour in this test case) the test runs correctly like defined above. The plugin does not affect the key at all and therefore the key report is sent after the first cycle.

Many thanks!

I will now fix the test case, squash the commits and then the PR should be ready for review.

@hupfdule
Copy link
Author

Hmm, unfortunately there is still a problem.

Compared to the AutoShift plugin I moved one class out of the main plugin into a separate header file (and a separate namespace). I have done this to not clutter the main LongPress.h file with too much auto-shifting functionality and therefore improve its readability.

Compilation of Kaleidoscope with make works.

But when calling make simulator-tests I get this compilation error:

/home/runner/work/Kaleidoscope/Kaleidoscope/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPress.h:111:3: error: 'longpress' does not name a type; did you mean 'LongPress'?
  111 |   longpress::AutoShiftCategories enabledAutoShiftCategories() {
      |   ^~~~~~~~~
      |   LongPress
/home/runner/work/Kaleidoscope/Kaleidoscope/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPress.h:126:24: error: 'longpress' has not been declared
  126 |   void enableAutoshift(longpress::AutoShiftCategories category) {
      |                        ^~~~~~~~~
[…]

It seems that the namespace cannot be used as I intended.

Can someone suggest on how to resolve this? Why does it fail only when compiling the simulator-tests, but not on a normal build?

Any help is appreciated!

@obra
Copy link
Member

obra commented May 24, 2024

If your code is all pushed up, I'll try to have a look at it this evening.

@hupfdule
Copy link
Author

If your code is all pushed up, I'll try to have a look at it this evening.

Yes it is.

Many thanks in advance!

@obra
Copy link
Member

obra commented May 28, 2024

So sorry. I didn't manage this before the weekend. looking now.

@obra
Copy link
Member

obra commented May 28, 2024

Ok. I think the issue you're running into is that you've named your header file the same thing as the header in the AutoShift plugin. And so GCC can't figure out which one you mean. If you rename AutoShift.h to LongPressAutoShift.h, everything works.

@hupfdule
Copy link
Author

Many thanks! I have changed that.
It is finished now and ready for review.

LongPress.setAutoshiftEnabled(LongPress.letterKeys());

LONGPRESS(
// ATTENTION! The order matters here!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of saying the order matters, perhaps explain how it matters?

To define the keys that should behave differently on long-press use include a definition like the following:

```c++
LONGPRESS(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a naive question - how does the configuration by KeyAddr interact with multiple layers on the keyboard? Is long press behavior scoped to a single layer or does it override the behavior no matter what layer someone is on?
Ideally, long-press behavior would be configurable by layer, I think.

@obra
Copy link
Member

obra commented May 29, 2024

Added a couple of notes/questions. In general, the code is well documented and well commented and appears pretty well tested. I think the only big thing is understanding how the configuration by KeyAddr interacts with layers. If it is the case that keyaddr based configuration currently ignores the layer system, that's probably worth getting described in the README. I think that it would be better if keyaddr based configuration respected layers, but I'd be ok with that being a "version 2" feature.

@hupfdule
Copy link
Author

Added a couple of notes/questions. In general, the code is well documented and well commented and appears pretty well tested. I think the only big thing is understanding how the configuration by KeyAddr interacts with layers. If it is the case that keyaddr based configuration currently ignores the layer system, that's probably worth getting described in the README. I think that it would be better if keyaddr based configuration respected layers, but I'd be ok with that being a "version 2" feature.

Yes, the LongPress plugin currently ignores the layers. And I agree that it may be desirable to be able to apply it to specific layers. I will look into it and then make a proposal of whether to try to include it from the start or postpone it to a later version.

Many thanks for your review! I write here when I have reworked it.

One question: Do you prefer that I do the changes in separate commits for easier review of those changes or shall I squash them into a the already existing commit?

@obra
Copy link
Member

obra commented May 30, 2024 via email

@hupfdule
Copy link
Author

how does the configuration by KeyAddr interact with multiple layers on the keyboard? Is long press behavior scoped to a single layer or does it override the behavior no matter what layer someone is on?
Ideally, long-press behavior would be configurable by layer, I think.

I looked over it and it seemed rather easy and straight-forward to support restricting it to specific layers. I would also like to introduce that now as it has an influence on the public API.

I changed it now to allow to restrict the long-press behaviour to a single layer.

Other than requested by you I did not only do this for mappings on KeyAddr but also for mappings on Key.
These are the reasons I did this:

  • It doesn’t complicate the implementation at all.
  • It doesn’t complicate the configuration of LongPress. In fact it is even easier to grasp if the restriction to a specific layer can be applied to both, KeyAddr and Key.

It is still possible to apply these mappings to all layers. There are two options to do this (as also described in the plugins README now):

  • omitting the layer
  • specifying the special constant ALL_LAYERS as the layer number

So these are the possibilities for configuring a long-press key:

  LONGPRESS(
    // Key at 1,0 should produce a Z on long press on all layers
    kaleidoscope::plugin::LongPressMapping(            KeyAddr(1, 0), Key_Z),

    // Key at 1,0 should produce a Z on long press on all layers
    kaleidoscope::plugin::LongPressMapping(ALL_LAYERS, KeyAddr(1, 0), Key_Z),

    // Key at 1,0 should produce a Z on long press on the first layer
    kaleidoscope::plugin::LongPressMapping(0,          KeyAddr(1, 0), Key_Z),


    // Keys generating a B should produce a Y on long press on all layers
    kaleidoscope::plugin::LongPressMapping(            Key_B,         Key_Y),

    // Keys generating a B should produce a Y on long press on all layers
    kaleidoscope::plugin::LongPressMapping(ALL_LAYERS, Key_B,         Key_Y),

    // Keys generating a B should produce a Y on long press on the first layer
    kaleidoscope::plugin::LongPressMapping(0,          Key_B,         Key_Y),
  )

I have one additional question:
The special constant ALL_LAYERS is now defined in LongPress.h. It is a public constant and (especially regarding its name) may theoretically be used by other plugins. Would it make more sense to define this constant in layers.h instead? Is the value -1 still correct then?

@hupfdule
Copy link
Author

I have also updated the README and the tests to cover the restriction to a single layer.

So please have another look on it now.

@obra
Copy link
Member

obra commented Jun 3, 2024

The special constant ALL_LAYERS is now defined in LongPress.h. It is a public constant and (especially regarding its name) may theoretically be used by other plugins. Would it make more sense to define this constant in layers.h instead? Is the value -1 still correct then?

I'd probably keep it as a namespaced constant for now until we're more sure how we'll use it globally.

In the code, I see this: #include "kaleidoscope/layers.h" // for ALL_LAYERS

I think that's probably not the case currently?

Also, the README I'm seeing doesn't seem to have the updates for always having layer number defined? Are you fully pushed up?

This commit provides a new plugin “LongPress” that allows producing different
Keys when keys are held for a short time instead of only tapped. It is
based on the existing “AutoShift” plugin and contains its functionality,
but extends it for a broader area of application.

Signed-off-by: Marco Herrn <[email protected]>
@hupfdule
Copy link
Author

hupfdule commented Jun 3, 2024

The special constant ALL_LAYERS is now defined in LongPress.h. It is a public constant and (especially regarding its name) may theoretically be used by other plugins. Would it make more sense to define this constant in layers.h instead? Is the value -1 still correct then?

I'd probably keep it as a namespaced constant for now until we're more sure how we'll use it globally.

That would be a rather long and unwieldy name for such an optional constant.

Therefore I decided to only use it internally. It means that to apply a LongPressKey to all layers the layer must be omitted (see the README). While the constant kaleidoscope::plugin::longpress::ALL_LAYERS is in fact public it is not mentioned in the documentation anymore.

If we in the future decide to advertise that constant, no change to the public API is necessary. That is even the case if this constant is being moved to layers.h without a longpress namespace.

In the code, I see this: #include "kaleidoscope/layers.h" // for ALL_LAYERS

I think that's probably not the case currently?

Yes, that was a leftover from playing around with it. I have removed that reference now.

Also, the README I'm seeing doesn't seem to have the updates for always having layer number defined? Are you fully pushed up?

It is mentioned here as an example and later in text form.

Everything is pushed now again. Please have a look.

@obra
Copy link
Member

obra commented Jun 3, 2024

I feel like an optional first parameter is going to lead to user confusion.

Is the most common use-case here really "all layers"? I'm having trouble envisioning when users would really want that as the default behavior. Can you talk about the use cases you're envisioning?

If the "all layers" case is relatively rare, having it be a longer constant seems preferable to me, since the behavior feels like it could end up somewhat surprising.

@hupfdule
Copy link
Author

hupfdule commented Jun 4, 2024

Hmm,

actually I don’t think it is confusing to have a first optional parameter. We could put it to the end of the parameterlist, but that would be different than the configuration in other plugins (and in fact I would expect it to be the first parameter) which I think would be even more confusing. I think just omitting it is clearly enough indicating that is applied to all layers. It can be expressed more clearly in the documentation though.

I am also not very keen on making it a mandatory parameter. You are absolutely right that the most common use case (as long as I imagine them) is applying a LongPress configuration to a specific layer (for example to generate a different printable key) and only rarely on all layers (for example to execute a common key like Esc or Enter or some key sequence like Ctrl-C).
Still it is not really nice to need to specify that long constant in that case. Especially since this name is then part of the public API, which would mean that changing that later on to a public constant (without an explicit namespace) would be a breaking change (making that unlikely to happen).

OK, so we have expressed our arguments/reasoning here. I think it is up to you now to decide what to do. I will then change the code accordingly.

@obra
Copy link
Member

obra commented Jun 7, 2024 via email

@hupfdule
Copy link
Author

Given that the global use case is relatively rare, one option we haven't
considered yet is to break it out as a separate method that isn't too long,
doesn't require an ugly constant and is clearly different. Like
GlobalLongPressKey...

Although, looking at the current code examples, I'm
trying to find other examples where plugin configs are sticking multiple
methods into the Kaleidoscope::plugin:: namespace for sketches and not
seeing a lot. Thoughts?

Yes, it seems that isn’t used in other plugins at the moment.

I would say there are mostly these categories of typical possible configurations
for key-related plugins:

Configuration on Key or KeyAddr. And either can be global or per layer.
Every plugin could possibly provide each configuration combination.

Key KeyAddr
per layer
global

Looking at some of them I see:

Qukeys

Key KeyAddr
per layer
global

Qukeys are always configured on KeyAddr, never on Key (and in my
opinion that would not make much sense for Qukeys). It also always requires
specifying the layer. I am not sure whether having global Qukeys makes
sense, but I assume not.

CharShift

Key KeyAddr
per layer
global

CharShift is always configured on Key, never on KeyAddr. It also always
applies to all layers.

While I could imagine a similar functionality as CharShift to be applied to
specific layers only and even to KeyAdd this is not how CharShift works
and be a different plugin then.

Chords

Key KeyAddr
per layer
global

Chords are always configured on Key and are always global.
In fact this is were I think this decision is suboptimal. I think it makes
total sense to configure Chords on KeyAddr (and that this is the much
more common use case). I think that is really missing from that plugin.

I think it may also be useful to restrict chords to certain layers, but
am not sure about that. Even then I am not sure that should apply to the
configuration on Key or KeyAddr or both then.

MagicCombo

Key KeyAddr
per layer
global

MagicCombos are always configured on KeyAddr and are always global.

MagicCombo seems to be obsoleted by Chords (I cannot imagine any use case
where it is worthwile to execute the chored action additionally to the
normal key presses). But you can see here that it is configured on
KeyAddr rather than on Key (other than the Chords plugin) which I
assume is more useful.

ShapeShifter

Key KeyAddr
per layer
global

ShapeShifter is always configured on Key and is always global.

Basically ShapeShifter is similar to CharShift with a different approach
(and even a different approach than the alternative I described above
regarding CharShift).

While I can imagine it being restricted to certain layers I am not sure if
that would be a common use case.

“Conclusion”

I don’t see a similar example where a plugin allows configuration per layer
and for all layers. I can imagine that some plugins may benefit from it,
but am not sure about that.

What I do see clearly by looking at all these plugins is that nearly all of
them are configured in a totally different way than all the others. Some
are even hard to understand (like the ShapeShifter plugin that requires a
{Key_NoKey, Key_NoKey} entry as the last one in the configuration.

Looking into the alternatives of either a LongPressKey with an optional
layer or a LongPressKey with a mandatory layer and a GlobalLongPressKey
without a layer, I would still prefer the first variant.

I am sorry, I have written a long sermon now, but still have no conclusion.
One thing that bugs me is the fact that there is no consistency in
the way the existing plugins are configured. It would be nice if we could get
to some consistency for (hypothetical) future plugins, but whether we are
able to introduce a blueprint via this LongPress plugin is questionable.

So (again) sorry, I couldn’t bring the discussion further here.

@obra
Copy link
Member

obra commented Jun 11, 2024

It's going to take me a bit of time to properly digest this, but this is awesome. You have nothing to apologize for here. I would be absolutely delighted for us to figure out a good pattern for this kind of thing so that at least all new plugins could match it (and then maybe we could backfill matching apis for everything else)

Thank you. I really appreciate all the effort you've put into this so far.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants