Macro that respects one-shot modifier keys

Hi,

I have a macro defined to enter the two characters “qu”:

/**
 * Press the Q key and directly afterwards the U key as this is the most
 * used combination in written texts.
 */
static const macro_t *quMacro(KeyEvent &event) {
  if (keyToggledOn(event.state)) {
    return MACRO(T(DE_Q),
                 T(DE_U)
           );
  }
  return MACRO_NONE;
}

If I hit the Macro key it enters “qu” as expected. If I am holding shift while hitting the Macro key it enters “QU” as I would expect.

The problem arises when I use a OneShot shift before hitting the Macro key. It enters “QU”, but I would like it to produce “Qu” instead (the OneShot modifier is only applied to the first key in the Macro.

Is that possible with the Macros plugin?

Interestingly if I define that Macro as a DynamicMacro in Chrysalis it works like I want it to! But I did not find a way to configure a DynamicMacro within the .ino file. Is that possible somehow?

I’m surprised that DynamicMacros is exhibiting that behaviour, since its code is nearly identical to Macros (for playback). Keys in the one-shot state should be (virtually) released after the key-toggled-on event for the next keypress is finished processing, so the one-shot shift in your example ought to stay pressed until either macro is finished playing.

You can get your Macros macro to behave the way you want it to, however: take a look at the ShiftBlocker example sketch to see how.

I am sorry, but I need a bit more help here.
Do you suggest that I actually use that ShiftBlocker and ignore Shift for the „U“ key?

If that is the case, would this be the correct code then?

Wouldn’t that mean that I am unable to produce „QU“ by holding the shift key then?

What is this line in your example for?

       // Change the Macros key into a plain `E` key before its press event is
      // processed.
      event.key = Key_E;

And what is the actual difference of returning MACRO(T(DE_U)) as in my first example and just calling Macros.tap(DE_U) and returning MACRO_NONE as in the ShiftBlocker example?

There’s no way for your macro to distinguish between a one-shot shift and a shift key that is physically being held. You could, however, modify the ShiftBlocker example to ignore (i.e. not block) a Key_RightShift (for example), and have your OneShot key use left shift instead.

That line (event.key = Key_E is similar to Macros.tap(Key_E), but instead of using the virtual Macros keys, it simply changes the Macros key that was pressed into that key, and lets its press event finish normally. The main user-visible difference is that if you press and hold the key, it’s possible to get repeating characters.

Not a lot. Using the MACRO() preprocessor macro calls Macros.play(), which calls things like Macros.tap() (depending on the arguments to MACRO()). The latter is more flexible, because it allows you to make other function calls (e.g. ::ShiftBlocker.enable()) in between the usual MACRO() steps.

That’s rather unfortunate. It would be better if it could (optionally) behave like the DynamicMacros plugin which is much more appropriate for my use case.

The ShiftBlocker seems like a lot of work that (even in your proposal of differentiating between left and right shift) will not cover all corner cases. The DynamicMacros plugin is more flexible in that regard.

Honestly, I’m not sure how DynamicMacros could be doing that; it’s definitely a bug.

Since your intent is to use it for one very specific case, it doesn’t need to. Instead of (or in addition to) testing the event.key value, you could program it to check event.addr to make sure that it only acts on keys that you want it to. This is the reason that it’s an example of a custom plugin, rather than an official core plugin.

There is an alternative approach. You could make a custom plugin that removes the INJECTED bit from subsequent events when a Macros key toggles on in its onKeyEvent() handler (which would need to come before OneShot in KALEIDOSCOPE_INIT_PLUGINS(), and turn that off with a second handler that uses afterEachCycle().

I don’t see it as such. It might be a bug in the sense that it wasn’t the intended behavior, but it is the behavior I want.

Both plugins behave the same when used without a modifier as well as when a modifier key is held. They only differ in their handling of OneShot modifiers. While the Macros plugin applies the modifier to the whole macro, the DynamicMacros plugin lets the first pressed key swallow the modifier (as would be the case if the keys in the macro were pressed manually).
Either variant may be desirable, depending on the use case. I my case it is the second one.
Therefore I think it would be best if this was somehow configurable for a given macro.

My use case is actually very simple, but not that specific: Apply the modifiers as if I would press the keys manually. That would mean that the OneShot modifier is only applied to the first pressed key in the macro. I don’t want to do this for a specific key. I also don’t want to reduce my choices of which shift key I might use.
Maybe I will want another macro in the future, e. g. for writing the word “table”. I want to be able to write it in all lowercase (by not applying a shift modifier), write it in all uppercase (by either holding shift or locking a OneShot shift modifier) and only capitalize the first character (by pressing a OneShot shift modifier before pressing the macro key).

Trying to handle specific keys with additional plugins is not only very complicated (at least for someone like me that has no experience in that field), but is actually less flexible.

Unfortunately you lost me here. I am more of a user than a developer here. I don’t know anything about writing Kaleisoscope plugins and in fact didn’t even understand what I would have to do here nor what would be the actual outcome of it.

The definition of a bug is that its behaviour is not as designed. I’m telling you that it’s a bug as a warning that it is likely to get fixed, so that it does work as designed in the future. The one-shot key is designed to stay in effect until the event that triggers its release is done being processed by Kaleidoscope, so if it was behaving as designed, it would not be (virtually) released until after the macro had finished playing.

That’s not the behaviour that you want, and that’s fair. I encourage you to submit an issue to the Kaleidoscope GitHub repository requesting the change, so that it can be discussed by other interested parties. Unsurprisingly, I disagree with you about what it should do (I’m the one who most recently redesigned both the Macros and OneShot plugins), but this is a matter of what is most sensible, not a matter of “right” and “wrong”. Macros can be used for other purposes than just typing, so it really isn’t that clear whether OneShot should apply to keys or characters. One downside to the latter is that is substantially complicates the code.

I have now looked into the cause, and can reproduce the bug (or would-be feature, if you will) in the interaction between DynamicMacros and OneShot. While I still haven’t found the cause, I can tell that OneShot is releasing the one-shot modifier key, curiously, after the release event of the first virtual key in the macro sequence. If OneShot was simply treating this as a “normal” key tap, it would release the modifier after the press event, not the release. It’s clear that this bug in plugin-order dependent (there’s no actual salient difference between the two Macros plugins; whether or not it happens depends on whether OneShot comes before or after them in KALEIDOSCOPE_INIT_PLUGINS()). For right now, you can just move OneShot after Macros, and you’ll get what you’re looking for. However, I do have a fix for the bug, and once I’ve determined the cause, I’ll be submitting it as a PR, which will cause it to stop working the way you want.

However, it has occurred to me that you do not need to employ a custom ShiftBlocker plugin to get what you want. While OneShot doesn’t know about Macros, or vice versa, your sketch knows about both, so you can write your macros like this:

const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) {
  if (keyToggledOn(event.state)) {
    switch (macro_id) {
    case MACRO_QU:
      Macros.tap(Key_Q);
      OneShot.cancel();
      event.key = Key_U;
      return MACRO_NONE;
    case MACRO_TABLE:
      Macros.tap(Key_T);
      OneShot.cancel();
      return MACRO(T(A), T(B), T(L), T(E));
    default:
      break;
    }
  }
  return MACRO_NONE;
}

This should do exactly what you want, even after the bug gets fixed, and should have been the first thing I suggested.

I was thinking about that, but the macro definition you described below seems to be good enough to be a general purpose solution to that problem.

To be able to cancel the OneShot modifier inside a macro should solve all these problems. Therefore I think no it would be ok if the macros are always handled as a single key press as it possible with this workaround to get the behaviour I desire here.

Also I hope your example here in this thread is enough for other people searching for the same behaviour to find it.

Many thanks for your help!