package org.comtel.javafx.control; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.Event; import javafx.event.EventHandler; import javafx.geometry.HPos; import javafx.geometry.Pos; import javafx.geometry.VPos; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.RowConstraints; import javafx.scene.transform.Scale; import org.comtel.javafx.event.KeyButtonEvent; import org.comtel.javafx.robot.IRobot; import org.comtel.javafx.xml.KeyboardLayoutHandler; import org.comtel.javafx.xml.layout.Keyboard; import org.slf4j.LoggerFactory; public class KeyboardPane extends Region implements StandardKeyCode, EventHandler<KeyButtonEvent> { private final static org.slf4j.Logger logger = LoggerFactory.getLogger(KeyboardPane.class); private Path layerPath; private Region qwertyKeyboardPane; private Region qwertyShiftedKeyboardPane; private Region symbolKeyboardPane; private Region symbolShiftedKeyboardPane; private Region qwertyCtrlKeyboardPane; private SimpleBooleanProperty symbolProperty = new SimpleBooleanProperty(false); private SimpleBooleanProperty shiftProperty = new SimpleBooleanProperty(false); private SimpleBooleanProperty ctrlProperty = new SimpleBooleanProperty(false); private final double SCALE_OFFSET = 0.2; private final SimpleDoubleProperty scaleProperty = new SimpleDoubleProperty(1.0); private SimpleDoubleProperty minScaleProperty = new SimpleDoubleProperty(0.7); private SimpleDoubleProperty maxScaleProperty = new SimpleDoubleProperty(5.0); private EventHandler<? super Event> closeEventHandler; private double mousePressedX; private double mousePressedY; private final List<IRobot> robotHandler = new ArrayList<>(); private Locale layoutLocale; public KeyboardPane(Path layerpath) { this(layerpath, 1.0, Locale.getDefault()); } public KeyboardPane(Path layerpath, Locale local) { this(layerpath, 1.0, local); } /** * create KeyBoard Region with layer XML root path, intial scale and locale * * @param layerpath * @param scale * @param local */ public KeyboardPane(Path layerpath, double scale, Locale local) { layerPath = layerpath; // setScaleShape(true); layoutLocale = local != null ? local : Locale.getDefault(); setId("key-background"); setFocusTraversable(false); init(); if (scale != 1.0) { scaleProperty.set(scale); getTransforms().setAll(new Scale(scaleProperty.get(), scaleProperty.get(), 1, 0, 0, 0)); } scaleProperty.addListener(new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> arg0, Number arg1, Number s) { getTransforms().setAll(new Scale(s.doubleValue(), s.doubleValue(), 1, 0, 0, 0)); } }); // setOnKeyPressed(new EventHandler<KeyEvent>() { // // public void handle(KeyEvent e) { // // e.consume(); // switch (e.getCode()) { // case SHIFT: // isShiftDown.set(isShiftDown.get()); // break; // // case CONTROL: // // setCtrlDown(!isCtrlDown); // // break; // // case ALT: // // setSymbolDown(!isSymbolDown); // // break; // } // } // }); } private void init() { try { setLayoutLocale(layoutLocale); } catch (IOException | URISyntaxException e) { logger.error(e.getMessage(), e); return; } setKeyboardLayer(KeyboardLayer.QWERTY); shiftProperty.addListener(new ChangeListener<Boolean>() { public void changed(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) { if (ctrlProperty.get()) { logger.warn("ignore in ctrl mode"); return; } setKeyboardLayer(symbolProperty.get() ? KeyboardLayer.SYMBOL : KeyboardLayer.QWERTY); } }); ctrlProperty.addListener(new ChangeListener<Boolean>() { public void changed(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) { if (arg2) { setKeyboardLayer(KeyboardLayer.CTRL); } else { setKeyboardLayer(symbolProperty.get() ? KeyboardLayer.SYMBOL : KeyboardLayer.QWERTY); } } }); symbolProperty.addListener(new ChangeListener<Boolean>() { public void changed(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) { if (ctrlProperty.get()) { logger.warn("ignore in ctrl mode"); return; } setKeyboardLayer(arg2 ? KeyboardLayer.SYMBOL : KeyboardLayer.QWERTY); } }); } public void setLayoutLocale(Locale local) throws MalformedURLException, IOException, URISyntaxException { logger.debug("try to set keyboard local: {}", local); KeyboardLayoutHandler handler = new KeyboardLayoutHandler(); if (layerPath == null) { String xmlPath = "/xml/default" + (local.getLanguage().equals("en") ? "" : "/" + local.getLanguage()); //logger.warn("use default embedded layouts path: {}", xmlPath); getChildren().clear(); qwertyKeyboardPane = createKeyboardPane(handler.getLayout(xmlPath + "/kb-layout.xml")); qwertyShiftedKeyboardPane = createKeyboardPane(handler.getLayout(xmlPath + "/kb-layout-shift.xml")); qwertyCtrlKeyboardPane = createKeyboardPane(handler.getLayout(xmlPath + "/kb-layout-ctrl.xml")); symbolKeyboardPane = createKeyboardPane(handler.getLayout(xmlPath + "/kb-layout-sym.xml")); symbolShiftedKeyboardPane = createKeyboardPane(handler.getLayout(xmlPath + "/kb-layout-sym-shift.xml")); getChildren().addAll(qwertyKeyboardPane, qwertyShiftedKeyboardPane, qwertyCtrlKeyboardPane, symbolKeyboardPane, symbolShiftedKeyboardPane); for (javafx.scene.Node node : getChildren()) { node.setVisible(false); } return; } Path path = layerPath; Map<Locale, Path> localMap = getAvailableLocales(); if (localMap.containsKey(local)) { path = localMap.get(local); } else if (localMap.containsKey(new Locale(local.getLanguage()))) { logger.debug("use language compatible locale: {}", local.getLanguage()); path = localMap.get(new Locale(local.getLanguage())); } else { logger.warn("locale: {} not available. try to use default", local); } if (path != null) { getChildren().clear(); qwertyKeyboardPane = createKeyboardPane(handler.getLayout(path.resolve("kb-layout.xml").toUri().toURL())); qwertyShiftedKeyboardPane = createKeyboardPane(handler.getLayout(path.resolve("kb-layout-shift.xml").toUri().toURL())); qwertyCtrlKeyboardPane = createKeyboardPane(handler.getLayout(path.resolve("kb-layout-ctrl.xml").toUri().toURL())); symbolKeyboardPane = createKeyboardPane(handler.getLayout(path.resolve("kb-layout-sym.xml").toUri().toURL())); symbolShiftedKeyboardPane = createKeyboardPane(handler.getLayout(path.resolve("kb-layout-sym-shift.xml").toUri().toURL())); getChildren().addAll(qwertyKeyboardPane, qwertyShiftedKeyboardPane, qwertyCtrlKeyboardPane, symbolKeyboardPane, symbolShiftedKeyboardPane); for (javafx.scene.Node node : getChildren()) { node.setVisible(false); } } } public Map<Locale, Path> getAvailableLocales() { Map<Locale, Path> localList = new HashMap<>(); if (layerPath == null) { localList.put(new Locale("de"), null); localList.put(new Locale("ru"), null); return localList; } try { try (DirectoryStream<Path> stream = Files.newDirectoryStream(layerPath)) { for (Path entry : stream) { if (entry.toFile().isDirectory()) { for (Locale l : Locale.getAvailableLocales()) { if (entry.getFileName().toString().equals(l.getLanguage() + (l.getCountry().isEmpty() ? "" : "_" + l.getCountry()))) { localList.put(l, entry); break; } } } } } } catch (Exception e) { logger.error(e.getMessage(), e); } logger.debug("available locales: {}", localList.keySet()); return localList; } public void setKeyboardLayer(KeyboardLayer layer) { final Region pane; switch (layer) { case QWERTY: pane = shiftProperty.get() ? qwertyShiftedKeyboardPane : qwertyKeyboardPane; break; case SYMBOL: pane = shiftProperty.get() ? symbolShiftedKeyboardPane : symbolKeyboardPane; break; case CTRL: pane = qwertyCtrlKeyboardPane; break; case NUMBER: pane = qwertyCtrlKeyboardPane; break; default: pane = qwertyKeyboardPane; break; } for (javafx.scene.Node node : getChildren()) { node.setVisible(false); } pane.setVisible(true); } private Region createKeyboardPane(Keyboard layout) { GridPane rPane = new GridPane(); rPane.setAlignment(Pos.CENTER); // pane.setPrefSize(600, 200); if (layout.getVerticalGap() != null) { rPane.setVgap(layout.getVerticalGap()); } rPane.setId("key-background-row"); int defaultKeyWidth = 10; if (layout.getKeyWidth() != null) { defaultKeyWidth = layout.getKeyWidth(); } int defaultKeyHeight = 35; if (layout.getKeyHeight() != null) { defaultKeyHeight = layout.getKeyHeight(); } int rowIdx = 0; for (Keyboard.Row row : layout.getRow()) { int colIdx = 0; GridPane colPane = new GridPane(); colPane.setId("key-background-column"); // colPane.setVgap(20); // colPane.setPrefWidth(Region.USE_COMPUTED_SIZE); RowConstraints rc = new RowConstraints(); rc.setPrefHeight(defaultKeyHeight); if (row.getRowEdgeFlags() != null) { if (row.getRowEdgeFlags().equals("bottom")) { rc.setValignment(VPos.BOTTOM); } if (row.getRowEdgeFlags().equals("top")) { rc.setValignment(VPos.TOP); } } int rowWidth = 0; for (Keyboard.Row.Key key : row.getKey()) { if (key.getHorizontalGap() != null) { colPane.setHgap(key.getHorizontalGap()); } else if (layout.getHorizontalGap() != null) { colPane.setHgap(layout.getHorizontalGap()); } ColumnConstraints cc = new ColumnConstraints(); cc.setHgrow(Priority.SOMETIMES); cc.setFillWidth(true); cc.setPrefWidth(key.getKeyWidth() != null ? key.getKeyWidth() : defaultKeyWidth); if (key.getCodes() == null || key.getCodes().isEmpty()) { // add placeholder Pane placeholder = new Pane(); colPane.add(placeholder, colIdx, 0); colPane.getColumnConstraints().add(cc); logger.trace("placeholder: {}", cc); colIdx++; rowWidth += cc.getPrefWidth(); continue; } MultiKeyButton button = new MultiKeyButton(scaleProperty); button.setFocusTraversable(false); button.setOnShortPressed(this); button.setCache(true); button.setMinHeight(10); button.setPrefHeight(defaultKeyHeight); button.setPrefWidth(defaultKeyWidth); button.setMaxWidth(defaultKeyWidth * 100); String[] codes = key.getCodes().split(","); if (codes.length > 0 && !codes[0].isEmpty()) { int keyCode = Integer.valueOf(codes[0]); button.setKeyCode(keyCode); } if (codes.length > 1) { for (String code : codes) { int keyCode = Integer.valueOf(code); if (keyCode != button.getKeyCode()) { button.addExtKeyCode(keyCode); } } } if (key.getKeyLabelStyle() != null && key.getKeyLabelStyle().startsWith(".")) { for (String style : key.getKeyLabelStyle().split(";")) { button.getStyleClass().add(style.substring(1)); } } if (button.getKeyCode() == LOCALE_SWITCH) { button.addExtKeyCode(LOCALE_SWITCH, Locale.ENGLISH.getLanguage().toUpperCase(Locale.ENGLISH), button.getStyleClass()); for (Locale l : getAvailableLocales().keySet()) { button.addExtKeyCode(LOCALE_SWITCH, l.getLanguage().toUpperCase(Locale.ENGLISH), button.getStyleClass()); } } if (key.getKeyIconStyle() != null && key.getKeyIconStyle().startsWith(".")) { logger.trace("Load css style: {}", key.getKeyIconStyle()); Label icon = new Label(); // icon.setSnapToPixel(true); // do not reduce css shape quality JavaFX8 // icon.setCacheShape(false); for (String style : key.getKeyIconStyle().split(";")) { icon.getStyleClass().add(style.substring(1)); } icon.setMaxSize(40, 40); button.setContentDisplay(ContentDisplay.CENTER); button.setGraphic(icon); } else if (key.getKeyIconStyle() != null && key.getKeyIconStyle().startsWith("@")) { InputStream is = KeyboardPane.class.getResourceAsStream(key.getKeyIconStyle().replace("@", "/") + ".png"); Image image = new Image(is); if (!image.isError()) { button.setGraphic(new ImageView(image)); } else { logger.error("Image: {} not found", key.getKeyIconStyle()); } } button.setText(key.getKeyLabel()); if (button.isContextAvailable() && button.getGraphic() == null) { button.getStyleClass().add("extend-style"); } if (key.getKeyEdgeFlags() != null) { if (key.getKeyEdgeFlags().equals("right")) { cc.setHalignment(HPos.RIGHT); button.setAlignment(Pos.BASELINE_RIGHT); } else if (key.getKeyEdgeFlags().equals("left")) { cc.setHalignment(HPos.LEFT); button.setAlignment(Pos.BASELINE_LEFT); } else { cc.setHalignment(HPos.CENTER); } } else { cc.setHalignment(HPos.CENTER); button.setAlignment(Pos.BASELINE_CENTER); } // use space button as drag pane if (button.getKeyCode() == java.awt.event.KeyEvent.VK_SPACE) { button.setOnMouseMoved(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { //on Double.isNaN(getScene().getWindow().getX()) init window position mousePressedX = getScene().getWindow().getX() - event.getScreenX(); mousePressedY = getScene().getWindow().getY() - event.getScreenY(); } }); button.setOnMouseDragged(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent mouseEvent) { getScene().getWindow().setX(mouseEvent.getScreenX() + mousePressedX); getScene().getWindow().setY(mouseEvent.getScreenY() + mousePressedY); } }); } if (button.getKeyCode() == BACK_SPACE || button.getKeyCode() == DELETE) { button.setOnLongPressed(new EventHandler<Event>() { @Override public void handle(Event e) { e.consume(); sendToComponent((char) 97, true); sendToComponent((char) java.awt.event.KeyEvent.VK_DELETE, ctrlProperty.get()); } }); } colPane.add(button, colIdx, 0); colPane.getColumnConstraints().add(cc); logger.trace("btn: {} {}", button.getText(), cc); colIdx++; rowWidth += cc.getPrefWidth(); } logger.trace("row[{}] - {}", rowIdx, rowWidth); colPane.getRowConstraints().add(rc); // colPane.setGridLinesVisible(true); rPane.add(colPane, 0, rowIdx); rowIdx++; } logger.trace("-----end pane-----"); return rPane; } public boolean isShifted() { return shiftProperty.get(); } public boolean isSymbol() { return symbolProperty.get(); } public boolean isCtrl() { return ctrlProperty.get(); } public void handle(KeyButtonEvent event) { event.consume(); KeyButtonEvent kbEvent = (KeyButtonEvent) event; if (!kbEvent.getEventType().equals(KeyButtonEvent.SHORT_PRESSED)) { logger.warn("ignore non short pressed events"); return; } KeyButton kb = (KeyButton) kbEvent.getSource(); switch (kb.getKeyCode()) { case SHIFT_DOWN: // switch shifted shiftProperty.set(!shiftProperty.get()); break; case SYMBOL_DOWN: // switch sym / qwerty symbolProperty.set(!symbolProperty.get()); break; case CLOSE: if (closeEventHandler == null) { System.exit(0); } else { closeEventHandler.handle(new KeyButtonEvent(KeyButtonEvent.ANY)); } break; case TAB: sendToComponent((char) java.awt.event.KeyEvent.VK_TAB, true); break; case BACK_SPACE: sendToComponent((char) java.awt.event.KeyEvent.VK_BACK_SPACE, true); break; case DELETE: sendToComponent((char) java.awt.event.KeyEvent.VK_DELETE, true); break; case CTRL_DOWN: // switch ctrl ctrlProperty.set(!ctrlProperty.get()); break; case LOCALE_SWITCH: try { Locale l = new Locale(kb.getText()); setLayoutLocale(l); } catch (IOException | URISyntaxException e) { logger.error(e.getMessage(), e); } if (ctrlProperty.get()) { ctrlProperty.set(false); } else if (symbolProperty.get()) { symbolProperty.set(false); } else { setKeyboardLayer(KeyboardLayer.QWERTY); } break; case ENTER: sendToComponent((char) java.awt.event.KeyEvent.VK_ENTER, true); break; case ARROW_UP: sendToComponent((char) java.awt.event.KeyEvent.VK_UP, true); break; case ARROW_DOWN: sendToComponent((char) java.awt.event.KeyEvent.VK_DOWN, true); break; case ARROW_LEFT: sendToComponent((char) java.awt.event.KeyEvent.VK_LEFT, true); break; case ARROW_RIGHT: sendToComponent((char) java.awt.event.KeyEvent.VK_RIGHT, true); break; default: // logger.debug(java.awt.event.KeyEvent.getKeyText(kb.getKeyCode())); if (kb.getKeyCode() > -1) { sendToComponent((char) kb.getKeyCode(), ctrlProperty.get()); } else { logger.debug("unknown key code: {}", kb.getKeyCode()); sendToComponent((char) kb.getKeyCode(), true); } break; } } /** * send keyEvent to iRobot implementation * * @param ch * @param ctrl */ private void sendToComponent(char ch, boolean ctrl) { logger.trace("send ({})", ch); if (ctrl) { switch (Character.toUpperCase(ch)) { case java.awt.event.KeyEvent.VK_MINUS: if (scaleProperty.get() > minScaleProperty.get()) { scaleProperty.set(scaleProperty.get() - SCALE_OFFSET); } return; case 0x2B: if (scaleProperty.get() < maxScaleProperty.get()) { scaleProperty.set(scaleProperty.get() + SCALE_OFFSET); } return; } } if (robotHandler.isEmpty()) { logger.error("no robot handler available"); return; } for (IRobot robot : robotHandler) { robot.sendToComponent(this, ch, ctrl); } } public void addRobotHandler(IRobot robot) { robotHandler.add(robot); } public void removeRobotHandler(IRobot robot) { robotHandler.remove(robot); } public void setOnKeyboardCloseButton(EventHandler<? super Event> value) { closeEventHandler = value; } /** * default keyboard scale * * @param scale */ public double getScale() { return scaleProperty.get(); } public void setScale(double scale) { scaleProperty.set(scale); } public double getMinimumScale() { return minScaleProperty.get(); } public void setMinimumScale(double min) { minScaleProperty.set(min); } public double getMaximumScale() { return maxScaleProperty.get(); } public void setMaximumScale(double max) { maxScaleProperty.set(max); } }