Add Enhanced Keyboard Shortcut Mechanism #91

Merged
delvh merged 5 commits from enhanced-shortcut-mechanism into develop 2020-10-12 16:12:25 +02:00
7 changed files with 217 additions and 21 deletions
Showing only changes of commit 1a4e05b02f - Show all commits

View File

@ -0,0 +1,42 @@
package envoy.client.data.shortcuts;
import javafx.scene.input.*;
import envoy.client.data.Context;
import envoy.client.helper.ShutdownHelper;
import envoy.client.ui.SceneContext.SceneInfo;
/**
* Envoy-specific implementation of the keyboard-shortcut interaction offered by
* {@link GlobalKeyShortcuts}.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public class EnvoyShortcutConfig {
private EnvoyShortcutConfig() {}
/**
* Supplies the default shortcuts for {@link GlobalKeyShortcuts}.
*
* @since Envoy Client v0.3-beta
*/
public static void initializeEnvoyShortcuts() {
final var instance = GlobalKeyShortcuts.getInstance();
// Add the option to exit Linux-like with "Control" + "Q" or "Alt" + "F4"
delvh marked this conversation as resolved
Review

Stop calling this Linux-like. It has nothing to do with Linux. At best it's GNOME-like or GTK-like.

Stop calling this Linux-like. It has nothing to do with Linux. At best it's GNOME-like or GTK-like.
instance.add(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN), ShutdownHelper::exit);
// Add the option to logout using "Control"+"Shift"+"L" if not in login scene
instance.addForNotExcluded(new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN),
ShutdownHelper::logout,
SceneInfo.LOGIN_SCENE);
// Add option to open settings scene with "Control"+"S", if not in login scene
instance.addForNotExcluded(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN),
() -> Context.getInstance().getSceneContext().load(SceneInfo.SETTINGS_SCENE),
SceneInfo.SETTINGS_SCENE,
SceneInfo.LOGIN_SCENE);
}
}

View File

@ -0,0 +1,118 @@
package envoy.client.data.shortcuts;
import java.util.*;
import java.util.Map.Entry;
import javafx.scene.input.KeyCombination;
import envoy.client.ui.SceneContext.SceneInfo;
/**
* Contains all KeyBoardshotcuts used throughout the application.
delvh marked this conversation as resolved
Review

Change "KeyBoardshortcuts" to "keyboard shortcuts".

Change "KeyBoardshortcuts" to "keyboard shortcuts".
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public final class GlobalKeyShortcuts {
/**
* Helper class for the Entry interface.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public final class KeyCombinationAction implements Entry<KeyCombination, Runnable> {
delvh marked this conversation as resolved
Review

What purpose does this serve, and why is this an inner class instead of a nested one?

What purpose does this serve, and why is this an inner class instead of a nested one?
Review
  1. Now it's a nested class
  2. I need to store two parameters for an unknown amount of time, and I don't want to always redeclare the Entry interface when implementing it.
1. Now it's a nested class 2. I need to store two parameters for an unknown amount of time, and I don't want to always redeclare the `Entry` interface when implementing it.
Review

Okay. I agree. It's unnecessary.

Okay. I agree. It's unnecessary.
private final KeyCombination keys;
private final Runnable action;
/**
* Creates a new {@code KeyCombinationAction}.
*
* @param keys the keys to press to perform the given action
* @param action the action to perform
* @since Envoy Client v0.3-beta
*/
public KeyCombinationAction(KeyCombination keys, Runnable action) {
if (keys == null || action == null) throw new NullPointerException("Cannot create key combination action");
this.keys = keys;
this.action = action;
}
@Override
public KeyCombination getKey() { return keys; }
@Override
public Runnable getValue() { return action; }
@Override
public Runnable setValue(Runnable none) {
throw new UnsupportedOperationException("Reassignment of keyboard shortcut actions is not supported");
}
}
private final EnumMap<SceneInfo, Collection<KeyCombinationAction>> shortcuts = new EnumMap<>(SceneInfo.class);
private static GlobalKeyShortcuts instance = new GlobalKeyShortcuts();
private GlobalKeyShortcuts() {
for (final var sceneInfo : SceneInfo.values())
shortcuts.put(sceneInfo, new HashSet<KeyCombinationAction>());
}
/**
* @return the instance of global keyboard shortcuts.
* @since Envoy Client v0.3-beta
*/
public static GlobalKeyShortcuts getInstance() { return instance; }
/**
* Adds the given keyboard shortcut and its action to all scenes.
*
* @param keys the keys to press to perform the given action
* @param action the action to perform
* @since Envoy Client v0.3-beta
*/
public void add(KeyCombination keys, Runnable action) {
final var keyCombinationAction = new KeyCombinationAction(keys, action);
shortcuts.values().forEach(collection -> collection.add(keyCombinationAction));
}
/**
* Adds the given keyboard shortcut and its action to all scenes that are not
* part of exclude.
*
* @param keys the keys to press to perform the given action
* @param action the action to perform
* @param exclude the scenes that should be excluded from receiving this
* keyboard shortcut
* @since Envoy Client v0.3-beta
*/
public void addForNotExcluded(KeyCombination keys, Runnable action, SceneInfo... exclude) {
// Computing the remaining sceneInfos
final var include = new SceneInfo[SceneInfo.values().length - exclude.length];
int index = 0;
outer:
for (final var sceneInfo : SceneInfo.values()) {
for (final var excluded : exclude)
if (sceneInfo.equals(excluded)) continue outer;
include[index++] = sceneInfo;
}
// Adding the action to the remaining sceneInfos
final var keyCombinationAction = new KeyCombinationAction(keys, action);
for (final var sceneInfo : include)
shortcuts.get(sceneInfo).add(keyCombinationAction);
}
/**
* Returns all stored keyboard shortcuts for the given scene constant
*
* @param sceneInfo the currently loading scene
* @return all stored keyboard shortcuts for this scene
* @since Envoy Client v0.3-beta
*/
public Collection<KeyCombinationAction> getKeyboardShortcuts(SceneInfo sceneInfo) { return shortcuts.get(sceneInfo); }
}

View File

@ -0,0 +1,25 @@
package envoy.client.data.shortcuts;
import java.util.Map;
import javafx.scene.input.KeyCombination;
import envoy.client.ui.SceneContext;
/**
* Provides methods to set the keyboard shortcuts for a specific scene.
* Should only be implemented by Controllers of Scenes so that these methods can
* automatically be called inside {@link SceneContext} as soon
* as the underlying fxml file has been loaded.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public interface KeyboardMapping {
/**
* @return all keyboard shortcuts of a scene
* @since Envoy Client v0.3-beta
*/
Map<KeyCombination, Runnable> getKeyboardShortcuts();
}

View File

@ -0,0 +1,7 @@
/**
* Contains the necessary classes to enable using keyboard shortcuts in Envoy.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
package envoy.client.data.shortcuts;

View File

@ -7,12 +7,11 @@ import java.util.logging.Level;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.*; import javafx.scene.*;
import javafx.scene.input.*;
import javafx.stage.Stage; import javafx.stage.Stage;
import envoy.client.data.Settings; import envoy.client.data.Settings;
import envoy.client.data.shortcuts.*;
import envoy.client.event.*; import envoy.client.event.*;
import envoy.client.helper.ShutdownHelper;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import dev.kske.eventbus.*; import dev.kske.eventbus.*;
@ -100,12 +99,18 @@ public final class SceneContext implements EventListener {
try { try {
final var rootNode = (Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path)); final var rootNode = (Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path));
final var scene = new Scene(rootNode); final var scene = new Scene(rootNode);
controllerStack.push(loader.getController()); final var controller = loader.getController();
controllerStack.push(controller);
sceneStack.push(scene); sceneStack.push(scene);
stage.setScene(scene); stage.setScene(scene);
supplyKeyboardShortcuts(sceneInfo, scene); // Supply the global custom keyboard shortcuts for that scene
for (final var shortcut : GlobalKeyShortcuts.getInstance().getKeyboardShortcuts(sceneInfo))
scene.getAccelerators().put(shortcut.getKey(), shortcut.getValue());
// Supply the scene specific keyboard shortcuts
if (controller instanceof KeyboardMapping) scene.getAccelerators().putAll(((KeyboardMapping) controller).getKeyboardShortcuts());
// The LoginScene is the only scene not intended to be resized // The LoginScene is the only scene not intended to be resized
// As strange as it seems, this is needed as otherwise the LoginScene won't be // As strange as it seems, this is needed as otherwise the LoginScene won't be
@ -120,22 +125,6 @@ public final class SceneContext implements EventListener {
} }
} }
private void supplyKeyboardShortcuts(SceneInfo sceneInfo, final Scene scene) {
final var accelerators = scene.getAccelerators();
// Add the option to exit Linux-like with "Control" + "Q" or "Alt" + "F4"
accelerators.put(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN), ShutdownHelper::exit);
// Add the option to logout using "Control"+"Shift"+"L" if not in login scene
if (sceneInfo != SceneInfo.LOGIN_SCENE)
accelerators.put(new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), ShutdownHelper::logout);
// Add the option to open the settings scene with "Control"+"S", if being in
// chat scene
if (sceneInfo == SceneInfo.CHAT_SCENE)
accelerators.put(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN), () -> load(SceneInfo.SETTINGS_SCENE));
}
/** /**
* Removes the current scene and displays the previous one. * Removes the current scene and displays the previous one.
* *

View File

@ -11,6 +11,7 @@ import javafx.scene.control.Alert.AlertType;
import javafx.stage.Stage; import javafx.stage.Stage;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.data.shortcuts.EnvoyShortcutConfig;
import envoy.client.helper.ShutdownHelper; import envoy.client.helper.ShutdownHelper;
import envoy.client.net.Client; import envoy.client.net.Client;
import envoy.client.ui.SceneContext.SceneInfo; import envoy.client.ui.SceneContext.SceneInfo;
@ -84,6 +85,9 @@ public final class Startup extends Application {
stage.setTitle("Envoy"); stage.setTitle("Envoy");
stage.getIcons().add(IconUtil.loadIcon("envoy_logo")); stage.getIcons().add(IconUtil.loadIcon("envoy_logo"));
// Configure global shortcuts used
EnvoyShortcutConfig.initializeEnvoyShortcuts();
// Create scene context // Create scene context
final var sceneContext = new SceneContext(stage); final var sceneContext = new SceneContext(stage);
context.setSceneContext(sceneContext); context.setSceneContext(sceneContext);

View File

@ -1,9 +1,13 @@
package envoy.client.ui.controller; package envoy.client.ui.controller;
import java.util.*;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.input.*;
import envoy.client.data.Context; import envoy.client.data.Context;
import envoy.client.data.shortcuts.KeyboardMapping;
import envoy.client.ui.listcell.ListCellFactory; import envoy.client.ui.listcell.ListCellFactory;
import envoy.client.ui.settings.*; import envoy.client.ui.settings.*;
@ -13,7 +17,7 @@ import envoy.client.ui.settings.*;
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public final class SettingsScene { public final class SettingsScene implements KeyboardMapping {
@FXML @FXML
private ListView<SettingsPane> settingsList; private ListView<SettingsPane> settingsList;
@ -38,4 +42,11 @@ public final class SettingsScene {
@FXML @FXML
private void backButtonClicked() { Context.getInstance().getSceneContext().pop(); } private void backButtonClicked() { Context.getInstance().getSceneContext().pop(); }
@Override
public Map<KeyCombination, Runnable> getKeyboardShortcuts() {
final var map = new HashMap<KeyCombination, Runnable>();
map.put(new KeyCodeCombination(KeyCode.B, KeyCombination.CONTROL_DOWN), this::backButtonClicked);
return map;
}
} }