Fzzy Config

Context Actions

As of 0.6.0, there is a robust Context Handling system built into Fzzy Config. This system

  • Captures key inputs with a custom keybind-like system
  • Acts on inputs across "layered" context handling systems
  • Opens actions in a right click menu where and when applicable

The Foundation

The Context Action system, at its core, is a layered user-input handling technique. It was birthed from and inspired by techniques built into Minecraft itself:

  • Keybinds
  • Screen key and mouse input
  • Narration building

The vanilla keybind and screen input handling systems both have flaws that this context action system aims to address.

  • Keybinds in vanilla overwrite each other if you double-map a keybind to the same key
  • Keybinds can't have modifiers (Ctrl, Shift, Alt)
  • To capture inputs on screens, you need to hardcode each interaction, properly ensuring execution order and so on.
  • Minecraft in general has no concept of context menus (right click menus)

The ContextType

The first step in resolving these and other issues was unifying the input capture method. Why should screens not use keybinds just like... keybinds?

Enter ContextType. A ContextType is two things (unless it isn't, see below), an input listener and a Map key. The first is pretty self-explanatory. It accepts calls from a parent system and listens for relevant inputs. As a Map key, ContextType is used when building context menus for organization and ordering.

Any system can interact with ContextTypes in a unified manner. Be it keybind inputs from the keyboard, input codes from screen methods, or otherwise. Either mouse or keyboard input can be handled.

Example

//FC comes with a range of pre-defined common context types
ContextType.SAVE
ContextType.FIND
ContextType.COPY
ContextType.CONTEXT_KEYBOARD
//and so on. These can be used in handling without re-defining them. You should only redefine a type if you need the same input structure for a completely different user action result.
//new context types can be defined with the create method
ContextType NEW = ContextType.create("new_file", ContextInput.KEYBOARD, (int inputCode, boolean ctrl, boolean shift, boolean alt) -> {
inputCode == GLFW.GLFW_KEY_N && ctrl && !shift && !alt
});

Now What?

Now that we have a universal input-capture system, what do we do with it? We handle the input of course! But before we get into that, we have to talk about layers pause for Shrek onion joke. The context type system can detect more than one relevant input for a given user input scenario. If the user pressed Ctrl + Shift + Z, we might have a Ctrl + Shift + Z and plain Z type returning as relevant. This is handled with a layered, event-like approach that passes information back and forth between layers

LayerPurposeDirection
Game EngineHandling in-game keybindsDownstream ⬇️
ScreenScreen-wide context
ListList actions, page up/down etc.
List ElementIndividual element actions, clear, copy, etc.
Element Childspecialized actionsUpstream ⬆️

The ContextHandler

The ContextHandler interface is designed to do just that, handle a passed context type. Any piece of your interaction puzzle that needs to manage inputs should implement this interface.

Handling a received context input starts at the "head" of the applicable game layers and proceeds downstream. For a screen input, that would be at the screen layer. If the screen has a valid need for the passed type, it should handle it and pass back execution there. If it doesn't, pass handling to its child(ren). Rinse and repeat.

You end up with a downstream cascade, passing handling down as needed or returning back on success

LayerNot HandledHandled
ScreenPass to List ⬇️Return true ↗️
ListPass to List Element ⬇️Return true ↗️
List ElementPass to Child ⬇️Return true ↗️
Element ChildReturn false ↗️Return true ↗️

Example

public class Parent implements ContextHandler {
public Parent() {}
private Child child = new Child();
//this handling prioritizes the parents actions over the childs, it can be refactored to care about the child first of course
@Override
public boolean handleContext(ContextType contextType, Position position) {
if (contextType == ContextType.A) {
return handleContextA(position); //some parent-wide action
} else if (contextType == ContextType.B) {
return handleContextB(position); //another parent-wide action
} else {
return child.handleContext(contextType, position); //no applicable parent-wide handling, we move to the child
}
}
}
class Child implements ContextHandler {
//child handles whatever it needs, and then since it's the furthest downstream layer, returns false if it can't handle the input, per the flow diagram above.
@Override
public boolean handleContext(ContextType contextType, Position position) {
//we can scope in the passed position to be relevant to this childs working position
Position newPosition = position.copy(position.contextInput, position.mX, position.mY, this.x, this.y, this.width, this.height, position.screenWidth, position.screenHeight);
//handle relevant context types, otherwise terminate the chain with failure since this is the last layer
if (contextType == ContextType.C) {
return handleContextC(newPosition); //some child-specific action
} else if (contextType == ContextType.D) {
return handleContextB(newPosition); //another another child-specific action
} else {
return false; // no further downstream layers, so we return a failure from here
}
}
}

The Context Action

The class that names the whole system, ContextAction. Context Actions are not strictly necessary in the grand scheme of a Context Action system, but provide two key benefits for use:

  • Provide a structured means of creating context callbacks for later use in a context handler. If you want to see an example of this in FC, you can explore how ConfigEntry handles context.
  • Are used in context menus (right click menus), particularly with the ContextProvider framework (see below).

Outside of these two circumstances, it is generally valid to handle context "manually" inline with where the context handler is.

ValidatedField is the best example of building context handling using actions, paired with 'ConfigEntry' as the final consumer of the building.

//ValidatedField has a method contextActionBuilder which is used to build a context map. You can see the default implementation in ValidatedField itself to get a gist of the process, and in ValidatedList to see an example of layering actions.
@Override
public Map<String, Map<ContextType, ContextAction.Builder>> contextActionBuilder(EntryCreator.CreatorContext context) {
Map<ContextType, ContextAction.Builder> map = new LinkedHashMap(); //linked is important here; maintains the visual ordering of actions if used in a menu
ContextAction.Builder action = ContextAction.Builder("my.action.translation.key".translate(), position -> {
//code upon action being fired goes here
return true; }) //return true/false based on handling result
.icon(MY_CUSTOM_CONTEXT_ICON) //a small icon can be passed here; it will appear in the context menu to the left of the action name.
.active(() -> /* supplier for when the action can be performed */);
map.put(MY_CONTEXT_TYPE, action); //add our action into a map using a ContextType as a key
Map<String, Map<ContextType, ContextAction.Builder>> map2 = super.contextActionBuilder(context); //getting the builder map from the base builder method
map2.computeIfAbsent(ContextResultBuilder.ENTRY, k -> new LinkedHashMap()).putAll(map); //apply the new action to the previously built ENTRY section, if any.
return map2;
}

Context Providers

The ContextProvider interface helps tie everything discussed above together. While the ContextHandler works downstream, ContextProvider works upstream, passing context information back up the chain for consumption by a further upstream handler. This achieves two major goals:

  • Providing a mechanism for a context handler to request context actions from children without them needing to themselves be handlers.
  • Building a structure for a context menu (right click menu)

Considering the context handler flow diagram above, we can see how a handler and provider could work together:

LayerDownstreamUpstream
ScreenPass to List ⬇️Return true ↗️
ListRequest context for menu ⬇️Use builder to open context menu

Return true ↗️
List ElementPass ContextBuilder ⬇️Append own actions

Builder ready for parent layer ⬆️
Element ChildAdd actions to builder ➡️Builder ready for parent layer ⬆️

Using the same context handling example from above, we'll add in a provider system

class Parent implements ContextHandler, ContextProvider {
public Parent() {}
private Child child = new Child();
//this handling prioritizes the parents actions over the childs, it can be refactored to care about the child first of course
@Override
public boolean handleContext(contextType: ContextType, position: Position) {
if (contextType == ContextType.A) {
return handleContextA(position); //some parent-wide action
} else if (contextType == ContextType.B) {
return handleContextB(position); //another parent-wide action
} else {
Position position = Position(/* Position information */) //create a position context for this element
ContextResultProvider contextBuilder = ContextProvider.empty(position); //create an empty context builder to pass downstream
provideContext(contextBuilder); //build into the empty context
if (contextType == ContextType.CONTEXT_KEYBOARD || contextType == ContextType.CONTEXT_MOUSE) {
Popups.INSTANCE.openContextMenuPopup(contextBuilder);
return true;
} else {
//build and flatten the context inputs and act on a provided action if one matching the contextType exists
//Note that flatBuild only works well if all of the provided contextTypes are unique across groups. If groups might have duplicate keys, use build and handle duplicate "hits" somehow
Map<ContextType, ContextAction> m = contextBuilder.flatBuild();
if (!m.containsKey(contextType)) {
return false;
} else {
return m.get(contextType).getAction().apply(contextBuilder.position());
}
}
}
}
// pass the request downstream first, build onto that last. This puts the most scoped-in contexts at the top of the context menu, and the most generalized actions at the bottom.
@Override
public void provideContext(builder: ContextResultBuilder) {
child.provideContext(builder);
builder.add("parent_group", PARENT_CONTEXT_TYPE, parentContextActionBuilder);
}
}
class Child implements ContextProvider {
// child builds in its actions as needed
@Override
public void provideContext(builder: ContextResultBuilder) {
builder.add("child_group", CHILD_CONTEXT_TYPE_A, builderA);
builder.add("child_group", CHILD_CONTEXT_TYPE_B, builderB);
}
}