Add emuname for telling apart multiple emulators
[jpcrr.git] / org / jpc / plugins / VirtualKeyboard.java
blob9ed032b4adc78bd007b811a6263f8833bfa45f3e
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 org.jpc.Misc;
41 import static org.jpc.Misc.errorDialog;
42 import static org.jpc.Misc.moveWindow;
43 import static org.jpc.Misc.parseStringToComponents;
44 import static org.jpc.Misc.openStream;
46 import java.io.*;
47 import javax.swing.*;
48 import javax.swing.border.*;
49 import java.util.*;
50 import java.awt.event.*;
51 import java.awt.*;
52 import javax.swing.plaf.basic.*;
53 import javax.swing.plaf.*;
55 public class VirtualKeyboard implements ActionListener, Plugin, KeyboardStatusListener
57 private JFrame window;
58 private JPanel panel;
59 private HashMap<String, Integer> commandToKey;
60 private HashMap<String, JToggleButton> commandToButton;
61 private JToggleButton capsLock;
62 private JToggleButton numLock;
63 private JToggleButton scrollLock;
64 private Font keyFont;
65 private Font smallKeyFont;
66 private Border keyBorder, smallKeyBorder, classicBorder;
67 private boolean nativeButtons;
68 private static String DEFAULT_KEYBOARD_FILENAME = "datafiles/keyboards/default";
70 private org.jpc.emulator.peripheral.Keyboard keyboard;
71 private int keyNo;
72 private boolean[] cachedState;
73 private Plugins pluginManager;
74 private int nativeWidth, nativeHeight;
76 public JToggleButton addKey(String name, String topKey, int scanCode, int x, int y, int w, int h, char sizeCode,
77 boolean special)
79 String cmdName = name + "-" + (keyNo++);
80 String label = name;
81 if(topKey != null) {
82 label = "<html>" + topKey + "<br>" + name + "</html>";
83 } else if(label.indexOf('&') >= 0) {
84 label = "<html>" + name + "</html>";
86 JToggleButton button = new JToggleButton(label, false);
87 if(sizeCode == 'N') {
88 button.setFont(keyFont);
89 button.setBorder(keyBorder);
90 } else if(sizeCode == 'S') {
91 button.setFont(smallKeyFont);
92 button.setBorder(smallKeyBorder);
93 } else if(sizeCode == 'C' && !nativeButtons) {
94 button.setBorder(classicBorder);
97 button.setRolloverEnabled(false);
98 if(special) {
99 button.setEnabled(false);
100 button.setVisible(false);
101 } else {
102 commandToKey.put(cmdName, new Integer(scanCode));
103 commandToButton.put(cmdName, button);
104 button.setActionCommand(cmdName);
105 button.addActionListener(this);
108 if(!nativeButtons) button.setUI(new KeyboardButtonUI());
110 panel.add(button, new ConstantTableLayout.Placement(x, y, w, h));
111 return button;
114 public void eci_virtualkeyboard_setwinpos(Integer x, Integer y)
116 moveWindow(window, x.intValue(), y.intValue(), nativeWidth, nativeHeight);
119 public VirtualKeyboard(Plugins _pluginManager) throws IOException
121 this(_pluginManager, "");
124 public VirtualKeyboard(Plugins _pluginManager, String args) throws IOException
126 pluginManager = _pluginManager;
127 Map<String, String> params = parseStringToComponents(args);
128 String keyboardPath = params.get("keyboard");
130 nativeButtons = "native".equalsIgnoreCase(params.get("style")) || (keyboardPath == null);
132 keyNo = 0;
133 keyboard = null;
134 commandToKey = new HashMap<String, Integer>();
135 commandToButton = new HashMap<String, JToggleButton>();
136 window = new JFrame("Virtual Keyboard" + Misc.emuname);
137 ConstantTableLayout layout = new ConstantTableLayout();
138 cachedState = new boolean[256];
139 panel = new JPanel(layout);
140 keyFont = new Font("SanSerif", Font.PLAIN, 11);
141 smallKeyFont = keyFont.deriveFont(9.0f);
143 if(nativeButtons) {
144 keyBorder = new EmptyBorder(0, 5, 0, 5);
145 smallKeyBorder = new EmptyBorder(0, 1, 0, 1);
146 // classicBorder isn't used with native buttons
147 } else {
148 Border outerBorder = new CompoundBorder(new EmptyBorder(1, 1, 0, 0), new SimpleButtonBorder(false));
149 keyBorder = new CompoundBorder(outerBorder, new EmptyBorder(0, 3, 0, 3));
150 smallKeyBorder = new CompoundBorder(outerBorder, new EmptyBorder(0, 1, 0, 1));
151 classicBorder = new SimpleButtonBorder(true);
154 window.add(panel);
156 parseKeyboardFile(keyboardPath);
158 window.pack();
159 window.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
160 Dimension d = window.getSize();
161 nativeWidth = d.width;
162 nativeHeight = d.height;
163 window.setVisible(true);
166 private static int parseCoord(String value, int next)
168 if("-".equals(value))
169 return next;
170 else
171 return Integer.valueOf(value);
174 private void parseKeyboardFile(String filename) throws IOException
176 InputStream in = openStream(filename, DEFAULT_KEYBOARD_FILENAME);
177 if((in = openStream(filename, DEFAULT_KEYBOARD_FILENAME)) == null)
178 throw new IOException("Neither primary keyboard file nor fallback file exists.");
180 UTFInputLineStream keyboardFile = new UTFInputLineStream(in);
181 String[] line;
182 int nextX = 0, nextY = 0;
184 while((line = Misc.nextParseLine(keyboardFile)) != null)
185 if(line.length <= 1)
186 continue;
187 else if(line.length == 7 || line.length == 8) {
188 int x = parseCoord(line[1], nextX), y = parseCoord(line[2], nextY);
189 int w = Integer.parseInt(line[3]), h = Integer.parseInt(line[4]);
190 char sizeCode = line[5].charAt(0);
191 String name = line[6], shifted = null;
192 if(line.length == 8)
193 shifted = line[7];
195 try {
196 int scanCode = Integer.parseInt(line[0]);
197 addKey(name, shifted, scanCode, x, y, w, h, sizeCode, false);
198 } catch(NumberFormatException nfe) { // The scanCode wasn't a number, so this must be a special key
199 String scanName = line[0];
200 JToggleButton specialButton = addKey(name, null, 0, x, y, w, h, sizeCode, true);
201 if(scanName.equalsIgnoreCase("numlock"))
202 numLock=specialButton;
203 else if(scanName.equalsIgnoreCase("capslock"))
204 capsLock=specialButton;
205 else if(scanName.equalsIgnoreCase("scrolllock"))
206 scrollLock=specialButton;
209 nextX = x + w;
210 nextY = y;
211 } else
212 throw new IOException("Invalid line in keyboard layout.");
215 //-1 if unknown, bit 2 is capslock, bit 1 is numlock, bit 0 is scrollock.
216 private void updateLEDs(int status)
218 if(status < 0) {
219 numLock.setVisible(false);
220 numLock.setSelected(false);
221 capsLock.setVisible(false);
222 capsLock.setSelected(false);
223 scrollLock.setVisible(false);
224 scrollLock.setSelected(false);
225 } else {
226 numLock.setVisible((status & 2) != 0);
227 capsLock.setVisible((status & 4) != 0);
228 scrollLock.setVisible((status & 1) != 0);
232 public void resetButtons()
234 for(Map.Entry<String, Integer> entry : commandToKey.entrySet()) {
235 int scan = entry.getValue().intValue();
236 JToggleButton button = commandToButton.get(entry.getKey());
237 if(keyboard.getKeyStatus((byte)scan) != cachedState[scan]) {
238 cachedState[scan] = keyboard.getKeyStatus((byte)scan);
239 button.setSelected(cachedState[scan]);
242 updateLEDs(keyboard.getLEDStatus());
245 private void keyStatusChangeEventThread(int scancode, boolean pressed)
247 /* THIS IS JUST PLAIN BROKEN.
248 for(Map.Entry<String, Integer> entry : commandToKey.entrySet()) {
249 int scan = entry.getValue().intValue();
250 if(scan != scancode)
251 continue;
252 JToggleButton button = commandToButton.get(entry.getKey());
253 if(pressed != cachedState[scan]) {
254 cachedState[scan] = pressed;
255 button.setSelected(pressed);
261 public void keyExecStatusChange(int scancode, boolean pressed)
263 //These aren't currently shown.
266 public void keyStatusChange(int scancode, boolean pressed)
268 if(!SwingUtilities.isEventDispatchThread())
269 try {
270 final int _scancode = scancode;
271 final boolean _pressed = pressed;
272 SwingUtilities.invokeLater(new Thread() { public void run() {
273 VirtualKeyboard.this.keyStatusChangeEventThread(_scancode, _pressed); }});
274 } catch(Exception e) {
276 else
277 keyStatusChangeEventThread(scancode, pressed);
280 public void keyStatusReload()
282 if(!SwingUtilities.isEventDispatchThread())
283 try {
284 SwingUtilities.invokeLater(new Thread() { public void run() { VirtualKeyboard.this.resetButtons(); }});
285 } catch(Exception e) {
287 else
288 resetButtons();
291 public void ledStatusChange(int newstatus)
293 if(!SwingUtilities.isEventDispatchThread())
294 try {
295 final int _newstatus = newstatus;
296 SwingUtilities.invokeLater(new Thread() { public void run() {
297 VirtualKeyboard.this.updateLEDs(_newstatus); }});
298 } catch(Exception e) {
300 else
301 updateLEDs(newstatus);
304 public void mouseButtonsChange(int newstatus)
306 //Not interesting.
309 public void mouseExecButtonsChange(int newstatus)
311 //Not interesting.
314 public void main()
316 //This runs entierely in UI thread.
319 public boolean systemShutdown()
321 //OK to proceed with JVM shutdown.
322 return true;
325 public void pcStarting()
327 //Not interested.
330 public void pcStopping()
332 if(pluginManager.isShuttingDown())
333 return; //Too much of deadlock risk.
335 if(!SwingUtilities.isEventDispatchThread())
336 try {
337 SwingUtilities.invokeAndWait(new Thread() { public void run() { VirtualKeyboard.this.resetButtons(); }});
338 } catch(Exception e) {
340 else
341 resetButtons();
344 public void reconnect(org.jpc.emulator.PC pc)
346 if(keyboard != null)
347 keyboard.removeStatusListener(this);
348 if(pc != null) {
349 Keyboard keys = (Keyboard)pc.getComponent(Keyboard.class);
350 keyboard = keys;
351 keyboard.addStatusListener(this);
352 keyStatusReload();
353 } else {
354 keyboard = null;
355 Iterator<Map.Entry<String, Integer> > itt = commandToKey.entrySet().iterator();
356 while (itt.hasNext())
358 Map.Entry<String, Integer> entry = itt.next();
359 String n = entry.getKey();
360 Integer s = entry.getValue();
361 cachedState[s.intValue()] = false;
362 commandToButton.get(n).setSelected(false);
363 ledStatusChange(-1);
368 public void actionPerformed(ActionEvent evt)
370 if(keyboard == null)
371 return;
373 String command = evt.getActionCommand();
374 JToggleButton button = commandToButton.get(command);
375 int scan = commandToKey.get(command).intValue();
376 boolean doubleEdge = (scan != 255) && ((evt.getModifiers() & ActionEvent.SHIFT_MASK) != 0);
377 if(button.isSelected())
378 if(doubleEdge)
379 System.err.println("Informational: Keyhit on key " + scan + ".");
380 else
381 System.err.println("Informational: Keydown on key " + scan + ".");
382 else
383 if(doubleEdge)
384 System.err.println("Informational: Keyupdown on key " + scan + ".");
385 else
386 System.err.println("Informational: Keyup on key " + scan + ".");
387 try {
388 keyboard.sendEdge(scan);
389 if(doubleEdge)
390 keyboard.sendEdge(scan);
391 } catch(Exception e) {
392 System.err.println("Error: Sending command failed: " + e);
393 errorDialog(e, "Failed to send keyboard key edge", null, "Dismiss");
395 if(!doubleEdge)
396 cachedState[scan] = !cachedState[scan];
397 button.setSelected(cachedState[scan]);
400 protected static class KeyboardButtonUI extends BasicToggleButtonUI
402 protected static Color highlightColor = new Color(200, 200, 200);
403 protected static Color backgroundColor = new Color(220, 220, 220);
405 protected void simplePaint(Graphics g, JComponent c, Color color)
407 Rectangle viewRect = new Rectangle(c.getSize());
408 Insets margin;
409 try {
410 /* We want to ignore the inner margins while calculating the background size, so we have to
411 * pull the outside border out of the compound border */
412 CompoundBorder border = (CompoundBorder)c.getBorder();
413 margin = border.getOutsideBorder().getBorderInsets(c);
414 } catch(ClassCastException cce) {
415 // We were called on a button without our elaborate triple-border, so default to the whole inset
416 margin = c.getBorder().getBorderInsets(c);
419 g.setColor(color);
421 g.fillRect(viewRect.x + margin.left, viewRect.y + margin.top,
422 viewRect.width - (margin.left + margin.right),
423 viewRect.height - (margin.top + margin.bottom));
426 protected void paintButtonPressed(Graphics g, AbstractButton b)
428 simplePaint(g, b, highlightColor);
431 public void paint(Graphics g, JComponent c)
433 simplePaint(g, c, backgroundColor);
434 super.paint(g, c);
438 protected static class SimpleButtonBorder extends LineBorder
440 private static final long serialVersionUID = 1L;
442 protected static Color nwColor=new Color(240, 240, 240);
443 protected static Color seColor=new Color(130, 130, 130);
444 protected static Color pressedColor=new Color(160, 160, 160);
445 protected boolean thin;
446 public SimpleButtonBorder(boolean thin)
448 super(Color.BLACK, 1, true);
449 this.thin = thin;
452 public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width,
453 final int height)
455 Color oldColor = g.getColor();
456 JToggleButton button = (JToggleButton)c;
457 ButtonModel model = button.getModel();
458 int adjust = thin ? 0 : 1;
460 // Draw inner highlights
461 if(model.isSelected() || model.isPressed()) {
462 // Draw the north-west highlight, but in the south-east color
463 g.setColor(seColor);
464 g.drawRect(x + 1, y + 1, width - 2, 0);
465 g.drawRect(x + 1, y + 1, 0, height - 2);
466 } else {
467 // Draw the north-west highlight
468 g.setColor(nwColor);
469 g.drawRect(x + 1, y + 1, width - 2, adjust);
470 g.drawRect(x + 1, y + 1, adjust, height - 2);
471 // Draw the south-east highlight
472 g.setColor(seColor);
473 g.drawRect(x + 1, y + height - 2, width - 2, 0);
474 g.drawRect(x + width - 2, y + 1, 0, height - 2);
475 if(!thin) { // Draw inner line of shadow
476 g.drawRect(x + 2, y + height - 3, width - 3, 0);
477 g.drawRect(x + width - 3, y + 2, 0, height - 3);
481 // Draw actual border
482 g.setColor(model.isPressed() ? pressedColor : lineColor);
483 g.drawRoundRect(x, y, width - 1, height - 1, 2, 2);
485 // Restore color state
486 g.setColor(oldColor);