Remove default keyboard layout and read layouts from .jar
[jpcrr.git] / org / jpc / plugins / VirtualKeyboard.java
blob44c13f45a19058910cdf02b5593dcad9c8928486
1 /*
2 JPC-RR: A x86 PC Hardware Emulator
3 Release 1
5 Copyright (C) 2007-2009 Isis Innovation Limited
6 Copyright (C) 2009-2010 H. Ilari Liusvaara
7 Copyright (C) 2010 Foone Debonte
9 This program is free software; you can redistribute it and/or modify
10 it under the terms of the GNU General Public License version 2 as published by
11 the Free Software Foundation.
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
18 You should have received a copy of the GNU General Public License along
19 with this program; if not, write to the Free Software Foundation, Inc.,
20 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 Based on JPC x86 PC Hardware emulator,
23 A project from the Physics Dept, The University of Oxford
25 Details about original JPC can be found at:
27 www-jpc.physics.ox.ac.uk
31 package org.jpc.plugins;
33 import org.jpc.Misc;
34 import org.jpc.emulator.peripheral.Keyboard;
35 import org.jpc.emulator.KeyboardStatusListener;
36 import org.jpc.jrsr.UTFInputLineStream;
37 import org.jpc.pluginsbase.Plugins;
38 import org.jpc.pluginsbase.Plugin;
39 import org.jpc.pluginsaux.ConstantTableLayout;
40 import static org.jpc.Misc.errorDialog;
41 import static org.jpc.Misc.moveWindow;
42 import static org.jpc.Misc.parseStringToComponents;
44 import java.io.*;
45 import javax.swing.*;
46 import javax.swing.border.*;
47 import java.util.*;
48 import java.awt.event.*;
49 import java.awt.*;
50 import javax.swing.plaf.basic.*;
51 import javax.swing.plaf.*;
53 public class VirtualKeyboard implements ActionListener, Plugin, KeyboardStatusListener
55 private JFrame window;
56 private JPanel panel;
57 private HashMap<String, Integer> commandToKey;
58 private HashMap<String, JToggleButton> commandToButton;
59 private JToggleButton capsLock;
60 private JToggleButton numLock;
61 private JToggleButton scrollLock;
62 private Font keyFont;
63 private Font smallKeyFont;
64 private Border keyBorder, smallKeyBorder, classicBorder;
65 private boolean nativeButtons;
66 private static String DEFAULT_KEYBOARD_FILENAME = "datafiles/keyboards/default";
68 private org.jpc.emulator.peripheral.Keyboard keyboard;
69 private int keyNo;
70 private boolean[] cachedState;
71 private Plugins pluginManager;
72 private int nativeWidth, nativeHeight;
74 public JToggleButton addKey(String name, String topKey, int scanCode, int x, int y, int w, int h, char sizeCode,
75 boolean special)
77 String cmdName = name + "-" + (keyNo++);
78 String label = name;
79 if(topKey != null) {
80 label = "<html>" + topKey + "<br>" + name + "</html>";
81 } else if(label.indexOf('&') >= 0) {
82 label = "<html>" + name + "</html>";
84 JToggleButton button = new JToggleButton(label, false);
85 if(sizeCode == 'N') {
86 button.setFont(keyFont);
87 button.setBorder(keyBorder);
88 } else if(sizeCode == 'S') {
89 button.setFont(smallKeyFont);
90 button.setBorder(smallKeyBorder);
91 } else if(sizeCode == 'C' && !nativeButtons) {
92 button.setBorder(classicBorder);
95 button.setRolloverEnabled(false);
96 if(special) {
97 button.setEnabled(false);
98 button.setVisible(false);
99 } else {
100 commandToKey.put(cmdName, new Integer(scanCode));
101 commandToButton.put(cmdName, button);
102 button.setActionCommand(cmdName);
103 button.addActionListener(this);
106 if(!nativeButtons) button.setUI(new KeyboardButtonUI());
108 panel.add(button, new ConstantTableLayout.Placement(x, y, w, h));
109 return button;
112 public void eci_virtualkeyboard_setwinpos(Integer x, Integer y)
114 moveWindow(window, x.intValue(), y.intValue(), nativeWidth, nativeHeight);
117 public VirtualKeyboard(Plugins _pluginManager) throws IOException
119 this(_pluginManager, "");
122 public VirtualKeyboard(Plugins _pluginManager, String args) throws IOException
124 pluginManager = _pluginManager;
125 Map<String, String> params = parseStringToComponents(args);
126 String keyboardPath = params.get("keyboard");
128 nativeButtons = "native".equalsIgnoreCase(params.get("style")) || (keyboardPath == null);
130 keyNo = 0;
131 keyboard = null;
132 commandToKey = new HashMap<String, Integer>();
133 commandToButton = new HashMap<String, JToggleButton>();
134 window = new JFrame("Virtual Keyboard");
135 ConstantTableLayout layout = new ConstantTableLayout();
136 cachedState = new boolean[256];
137 panel = new JPanel(layout);
138 keyFont = new Font("SanSerif", Font.PLAIN, 11);
139 smallKeyFont = keyFont.deriveFont(9.0f);
141 if(nativeButtons) {
142 keyBorder = new EmptyBorder(0, 5, 0, 5);
143 smallKeyBorder = new EmptyBorder(0, 1, 0, 1);
144 // classicBorder isn't used with native buttons
145 } else {
146 Border outerBorder = new CompoundBorder(new EmptyBorder(1, 1, 0, 0), new SimpleButtonBorder(false));
147 keyBorder = new CompoundBorder(outerBorder, new EmptyBorder(0, 3, 0, 3));
148 smallKeyBorder = new CompoundBorder(outerBorder, new EmptyBorder(0, 1, 0, 1));
149 classicBorder = new SimpleButtonBorder(true);
152 window.add(panel);
154 parseKeyboardFile(keyboardPath);
156 window.pack();
157 window.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
158 Dimension d = window.getSize();
159 nativeWidth = d.width;
160 nativeHeight = d.height;
161 window.setVisible(true);
164 private static int parseCoord(String value, int next)
166 if("-".equals(value))
167 return next;
168 else
169 return Integer.valueOf(value);
172 private void parseKeyboardFile(String filename) throws IOException
174 InputStream in = null;
175 in = ClassLoader.getSystemResourceAsStream((filename != null) ? filename : DEFAULT_KEYBOARD_FILENAME);
176 if(in == null) {
177 System.err.println("Can't load keyboard file '" + filename + "', falling back to '" + DEFAULT_KEYBOARD_FILENAME + "'.");
178 if((in = ClassLoader.getSystemResourceAsStream(DEFAULT_KEYBOARD_FILENAME)) == null)
179 throw new IOException("Neither primary keyboard file nor fallback file exists.");
182 UTFInputLineStream keyboardFile = new UTFInputLineStream(in);
183 String[] line;
184 int nextX = 0, nextY = 0;
186 while((line = Misc.nextParseLine(keyboardFile)) != null)
187 if(line.length <= 1)
188 continue;
189 else if(line.length == 7 || line.length == 8) {
190 int x = parseCoord(line[1], nextX), y = parseCoord(line[2], nextY);
191 int w = Integer.parseInt(line[3]), h = Integer.parseInt(line[4]);
192 char sizeCode = line[5].charAt(0);
193 String name = line[6], shifted = null;
194 if(line.length == 8)
195 shifted = line[7];
197 try {
198 int scanCode = Integer.parseInt(line[0]);
199 addKey(name, shifted, scanCode, x, y, w, h, sizeCode, false);
200 } catch(NumberFormatException nfe) { // The scanCode wasn't a number, so this must be a special key
201 String scanName = line[0];
202 JToggleButton specialButton = addKey(name, null, 0, x, y, w, h, sizeCode, true);
203 if(scanName.equalsIgnoreCase("numlock"))
204 numLock=specialButton;
205 else if(scanName.equalsIgnoreCase("capslock"))
206 capsLock=specialButton;
207 else if(scanName.equalsIgnoreCase("scrolllock"))
208 scrollLock=specialButton;
211 nextX = x + w;
212 nextY = y;
213 } else
214 throw new IOException("Invalid line in keyboard layout.");
217 //-1 if unknown, bit 2 is capslock, bit 1 is numlock, bit 0 is scrollock.
218 private void updateLEDs(int status)
220 if(status < 0) {
221 numLock.setVisible(false);
222 numLock.setSelected(false);
223 capsLock.setVisible(false);
224 capsLock.setSelected(false);
225 scrollLock.setVisible(false);
226 scrollLock.setSelected(false);
227 } else {
228 numLock.setVisible((status & 2) != 0);
229 capsLock.setVisible((status & 4) != 0);
230 scrollLock.setVisible((status & 1) != 0);
234 public void resetButtons()
236 for(Map.Entry<String, Integer> entry : commandToKey.entrySet()) {
237 int scan = entry.getValue().intValue();
238 JToggleButton button = commandToButton.get(entry.getKey());
239 if(keyboard.getKeyStatus((byte)scan) != cachedState[scan]) {
240 cachedState[scan] = keyboard.getKeyStatus((byte)scan);
241 button.setSelected(cachedState[scan]);
244 updateLEDs(keyboard.getLEDStatus());
247 private void keyStatusChangeEventThread(int scancode, boolean pressed)
249 /* THIS IS JUST PLAIN BROKEN.
250 for(Map.Entry<String, Integer> entry : commandToKey.entrySet()) {
251 int scan = entry.getValue().intValue();
252 if(scan != scancode)
253 continue;
254 JToggleButton button = commandToButton.get(entry.getKey());
255 if(pressed != cachedState[scan]) {
256 cachedState[scan] = pressed;
257 button.setSelected(pressed);
263 public void keyExecStatusChange(int scancode, boolean pressed)
265 //These aren't currently shown.
268 public void keyStatusChange(int scancode, boolean pressed)
270 if(!SwingUtilities.isEventDispatchThread())
271 try {
272 final int _scancode = scancode;
273 final boolean _pressed = pressed;
274 SwingUtilities.invokeLater(new Thread() { public void run() {
275 VirtualKeyboard.this.keyStatusChangeEventThread(_scancode, _pressed); }});
276 } catch(Exception e) {
278 else
279 keyStatusChangeEventThread(scancode, pressed);
282 public void keyStatusReload()
284 if(!SwingUtilities.isEventDispatchThread())
285 try {
286 SwingUtilities.invokeLater(new Thread() { public void run() { VirtualKeyboard.this.resetButtons(); }});
287 } catch(Exception e) {
289 else
290 resetButtons();
293 public void ledStatusChange(int newstatus)
295 if(!SwingUtilities.isEventDispatchThread())
296 try {
297 final int _newstatus = newstatus;
298 SwingUtilities.invokeLater(new Thread() { public void run() {
299 VirtualKeyboard.this.updateLEDs(_newstatus); }});
300 } catch(Exception e) {
302 else
303 updateLEDs(newstatus);
306 public void mouseButtonsChange(int newstatus)
308 //Not interesting.
311 public void mouseExecButtonsChange(int newstatus)
313 //Not interesting.
316 public void main()
318 //This runs entierely in UI thread.
321 public boolean systemShutdown()
323 //OK to proceed with JVM shutdown.
324 return true;
327 public void pcStarting()
329 //Not interested.
332 public void pcStopping()
334 if(pluginManager.isShuttingDown())
335 return; //Too much of deadlock risk.
337 if(!SwingUtilities.isEventDispatchThread())
338 try {
339 SwingUtilities.invokeAndWait(new Thread() { public void run() { VirtualKeyboard.this.resetButtons(); }});
340 } catch(Exception e) {
342 else
343 resetButtons();
346 public void reconnect(org.jpc.emulator.PC pc)
348 if(keyboard != null)
349 keyboard.removeStatusListener(this);
350 if(pc != null) {
351 Keyboard keys = (Keyboard)pc.getComponent(Keyboard.class);
352 keyboard = keys;
353 keyboard.addStatusListener(this);
354 keyStatusReload();
355 } else {
356 keyboard = null;
357 Iterator<Map.Entry<String, Integer> > itt = commandToKey.entrySet().iterator();
358 while (itt.hasNext())
360 Map.Entry<String, Integer> entry = itt.next();
361 String n = entry.getKey();
362 Integer s = entry.getValue();
363 cachedState[s.intValue()] = false;
364 commandToButton.get(n).setSelected(false);
365 ledStatusChange(-1);
370 public void actionPerformed(ActionEvent evt)
372 if(keyboard == null)
373 return;
375 String command = evt.getActionCommand();
376 JToggleButton button = commandToButton.get(command);
377 int scan = commandToKey.get(command).intValue();
378 boolean doubleEdge = (scan != 255) && ((evt.getModifiers() & ActionEvent.SHIFT_MASK) != 0);
379 if(button.isSelected())
380 if(doubleEdge)
381 System.err.println("Informational: Keyhit on key " + scan + ".");
382 else
383 System.err.println("Informational: Keydown on key " + scan + ".");
384 else
385 if(doubleEdge)
386 System.err.println("Informational: Keyupdown on key " + scan + ".");
387 else
388 System.err.println("Informational: Keyup on key " + scan + ".");
389 try {
390 keyboard.sendEdge(scan);
391 if(doubleEdge)
392 keyboard.sendEdge(scan);
393 } catch(Exception e) {
394 System.err.println("Error: Sending command failed: " + e);
395 errorDialog(e, "Failed to send keyboard key edge", null, "Dismiss");
397 if(!doubleEdge)
398 cachedState[scan] = !cachedState[scan];
399 button.setSelected(cachedState[scan]);
402 protected static class KeyboardButtonUI extends BasicToggleButtonUI
404 protected static Color highlightColor = new Color(200, 200, 200);
405 protected static Color backgroundColor = new Color(220, 220, 220);
407 protected void simplePaint(Graphics g, JComponent c, Color color)
409 Rectangle viewRect = new Rectangle(c.getSize());
410 Insets margin;
411 try {
412 /* We want to ignore the inner margins while calculating the background size, so we have to
413 * pull the outside border out of the compound border */
414 CompoundBorder border = (CompoundBorder)c.getBorder();
415 margin = border.getOutsideBorder().getBorderInsets(c);
416 } catch(ClassCastException cce) {
417 // We were called on a button without our elaborate triple-border, so default to the whole inset
418 margin = c.getBorder().getBorderInsets(c);
421 g.setColor(color);
423 g.fillRect(viewRect.x + margin.left, viewRect.y + margin.top,
424 viewRect.width - (margin.left + margin.right),
425 viewRect.height - (margin.top + margin.bottom));
428 protected void paintButtonPressed(Graphics g, AbstractButton b)
430 simplePaint(g, b, highlightColor);
433 public void paint(Graphics g, JComponent c)
435 simplePaint(g, c, backgroundColor);
436 super.paint(g, c);
440 protected static class SimpleButtonBorder extends LineBorder
442 private static final long serialVersionUID = 1L;
444 protected static Color nwColor=new Color(240, 240, 240);
445 protected static Color seColor=new Color(130, 130, 130);
446 protected static Color pressedColor=new Color(160, 160, 160);
447 protected boolean thin;
448 public SimpleButtonBorder(boolean thin)
450 super(Color.BLACK, 1, true);
451 this.thin = thin;
454 public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width,
455 final int height)
457 Color oldColor = g.getColor();
458 JToggleButton button = (JToggleButton)c;
459 ButtonModel model = button.getModel();
460 int adjust = thin ? 0 : 1;
462 // Draw inner highlights
463 if(model.isSelected() || model.isPressed()) {
464 // Draw the north-west highlight, but in the south-east color
465 g.setColor(seColor);
466 g.drawRect(x + 1, y + 1, width - 2, 0);
467 g.drawRect(x + 1, y + 1, 0, height - 2);
468 } else {
469 // Draw the north-west highlight
470 g.setColor(nwColor);
471 g.drawRect(x + 1, y + 1, width - 2, adjust);
472 g.drawRect(x + 1, y + 1, adjust, height - 2);
473 // Draw the south-east highlight
474 g.setColor(seColor);
475 g.drawRect(x + 1, y + height - 2, width - 2, 0);
476 g.drawRect(x + width - 2, y + 1, 0, height - 2);
477 if(!thin) { // Draw inner line of shadow
478 g.drawRect(x + 2, y + height - 3, width - 3, 0);
479 g.drawRect(x + width - 3, y + 2, 0, height - 3);
483 // Draw actual border
484 g.setColor(model.isPressed() ? pressedColor : lineColor);
485 g.drawRoundRect(x, y, width - 1, height - 1, 2, 2);
487 // Restore color state
488 g.setColor(oldColor);