You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Viewport.java 28KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835
  1. /* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved.
  2. * Copyright (C) 2006 Constantin Kaplinsky. All Rights Reserved.
  3. * Copyright (C) 2009 Paul Donohue. All Rights Reserved.
  4. * Copyright (C) 2010, 2012-2013 D. R. Commander. All Rights Reserved.
  5. * Copyright (C) 2011-2019 Brian P. Hinz
  6. *
  7. * This is free software; you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation; either version 2 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * This software is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this software; if not, write to the Free Software
  19. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
  20. * USA.
  21. */
  22. //
  23. // DesktopWindow is an AWT Canvas representing a VNC desktop.
  24. //
  25. // Methods on DesktopWindow are called from both the GUI thread and the thread
  26. // which processes incoming RFB messages ("the RFB thread"). This means we
  27. // need to be careful with synchronization here.
  28. //
  29. package com.tigervnc.vncviewer;
  30. import java.awt.*;
  31. import java.awt.event.*;
  32. import java.awt.geom.AffineTransform;
  33. import java.awt.image.*;
  34. import java.nio.*;
  35. import java.util.*;
  36. import javax.swing.*;
  37. import javax.imageio.*;
  38. import java.io.*;
  39. import com.tigervnc.rfb.*;
  40. import com.tigervnc.rfb.Cursor;
  41. import com.tigervnc.rfb.Exception;
  42. import com.tigervnc.rfb.Point;
  43. import static java.awt.event.KeyEvent.*;
  44. import static com.tigervnc.vncviewer.Parameters.*;
  45. import static com.tigervnc.rfb.Keysymdef.*;
  46. class Viewport extends JPanel implements ActionListener {
  47. static LogWriter vlog = new LogWriter("Viewport");
  48. enum ID { EXIT, FULLSCREEN, MINIMIZE, RESIZE, NEWVIEWER,
  49. CTRL, ALT, MENUKEY, CTRLALTDEL, CLIPBOARD,
  50. REFRESH, OPTIONS, INFO, ABOUT, DISMISS }
  51. enum MENU { INACTIVE, TOGGLE, VALUE, RADIO,
  52. INVISIBLE, SUBMENU_POINTER, SUBMENU, DIVIDER }
  53. public Viewport(int w, int h, PixelFormat serverPF, CConn cc_)
  54. {
  55. cc = cc_;
  56. setScaledSize(w, h);
  57. frameBuffer = createFramebuffer(serverPF, w, h);
  58. assert(frameBuffer != null);
  59. setBackground(Color.BLACK);
  60. cc.setFramebuffer(frameBuffer);
  61. contextMenu = new JPopupMenu();
  62. OptionsDialog.addCallback("handleOptions", this);
  63. addMouseListener(new MouseAdapter() {
  64. public void mouseClicked(MouseEvent e) { }
  65. public void mouseEntered(MouseEvent e) { handle(e); }
  66. public void mouseExited(MouseEvent e) { handle(e); }
  67. public void mouseReleased(MouseEvent e) { handle(e); }
  68. public void mousePressed(MouseEvent e) { handle(e); }
  69. });
  70. addMouseWheelListener(new MouseAdapter() {
  71. public void mouseWheelMoved(MouseWheelEvent e) { handle(e); }
  72. });
  73. addMouseMotionListener(new MouseMotionAdapter() {
  74. public void mouseDragged(MouseEvent e) { handle(e); }
  75. public void mouseMoved(MouseEvent e) { handle(e); }
  76. });
  77. addKeyListener(new KeyAdapter() {
  78. public void keyTyped(KeyEvent e) { }
  79. public void keyPressed(KeyEvent e) { handleSystemEvent(e); }
  80. public void keyReleased(KeyEvent e) { handleSystemEvent(e); }
  81. });
  82. addFocusListener(new FocusAdapter() {
  83. public void focusGained(FocusEvent e) {
  84. ClipboardDialog.clientCutText();
  85. }
  86. public void focusLost(FocusEvent e) {
  87. releaseDownKeys();
  88. }
  89. });
  90. // Override default key bindings from L&F
  91. getActionMap().put("null", new AbstractAction() {
  92. public void actionPerformed(ActionEvent e) { }
  93. });
  94. ArrayList<KeyStroke> keys = new ArrayList<KeyStroke>();
  95. keys.add(KeyStroke.getKeyStroke(KeyEvent.VK_F10, 0, true));
  96. keys.add(KeyStroke.getKeyStroke(KeyEvent.VK_ALT, 0, true));
  97. for (int i=0; i<keys.size(); i++)
  98. getInputMap(JComponent.WHEN_FOCUSED).put(keys.get(i), "null");
  99. setFocusTraversalKeysEnabled(false);
  100. setFocusable(true);
  101. setMenuKey();
  102. // Send a fake pointer event so that the server will stop rendering
  103. // a server-side cursor. Ideally we'd like to send the actual pointer
  104. // position, but we can't really tell when the window manager is done
  105. // placing us so we don't have a good time for that.
  106. handlePointerEvent(new Point(w/2, h/2), 0);
  107. }
  108. // Most efficient format (from Viewport's point of view)
  109. public PixelFormat getPreferredPF()
  110. {
  111. return frameBuffer.getPF();
  112. }
  113. // Copy the areas of the framebuffer that have been changed (damaged)
  114. // to the displayed window.
  115. public void updateWindow() {
  116. Rect r = frameBuffer.getDamage();
  117. if (!r.is_empty()) {
  118. if (cc.server.width() != scaledWidth ||
  119. cc.server.height() != scaledHeight) {
  120. AffineTransform t = new AffineTransform();
  121. t.scale((double)scaleRatioX, (double)scaleRatioY);
  122. Rectangle s = new Rectangle(r.tl.x, r.tl.y, r.width(), r.height());
  123. s = t.createTransformedShape(s).getBounds();
  124. paintImmediately(s.x, s.y, s.width, s.height);
  125. } else {
  126. paintImmediately(r.tl.x, r.tl.y, r.width(), r.height());
  127. }
  128. }
  129. }
  130. static final int[] dotcursor_xpm = {
  131. 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
  132. 0x00000000, 0xff000000, 0xff000000, 0xff000000, 0x00000000,
  133. 0x00000000, 0xff000000, 0xff000000, 0xff000000, 0x00000000,
  134. 0x00000000, 0xff000000, 0xff000000, 0xff000000, 0x00000000,
  135. 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
  136. };
  137. public void setCursor(int width, int height, Point hotspot,
  138. byte[] data)
  139. {
  140. int i;
  141. if (cursor != null)
  142. cursor.flush();
  143. for (i = 0; i < width*height; i++)
  144. if (data[i*4 + 3] != 0) break;
  145. if ((i == width*height) && dotWhenNoCursor.getValue()) {
  146. vlog.debug("cursor is empty - using dot");
  147. cursor = new BufferedImage(5, 5, BufferedImage.TYPE_INT_ARGB_PRE);
  148. cursor.setRGB(0, 0, 5, 5, dotcursor_xpm, 0, 5);
  149. cursorHotspot.x = cursorHotspot.y = 3;
  150. } else {
  151. if ((width == 0) || (height == 0)) {
  152. cursor = new BufferedImage(tk.getBestCursorSize(0, 0).width,
  153. tk.getBestCursorSize(0, 0).height,
  154. BufferedImage.TYPE_INT_ARGB_PRE);
  155. cursorHotspot.x = cursorHotspot.y = 0;
  156. } else {
  157. IntBuffer buffer = IntBuffer.allocate(width*height);
  158. buffer.put(ByteBuffer.wrap(data).asIntBuffer());
  159. cursor =
  160. new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
  161. cursor.setRGB(0, 0, width, height, buffer.array(), 0, width);
  162. cursorHotspot = hotspot;
  163. }
  164. }
  165. int cw = (int) Math.floor((float) cursor.getWidth() * scaleRatioX);
  166. int ch = (int) Math.floor((float) cursor.getHeight() * scaleRatioY);
  167. int x = cursorHotspot.x;
  168. int y = cursorHotspot.y;
  169. Dimension cs = tk.getBestCursorSize(cw, ch);
  170. if (cs.width != cursor.getWidth() || cs.height != cursor.getHeight()) {
  171. cw = VncViewer.os.startsWith("windows") ? Math.min(cw, cs.width) : cs.width;
  172. ch = VncViewer.os.startsWith("windows") ? Math.min(ch, cs.height) : cs.height;
  173. BufferedImage tmp = new BufferedImage(cs.width, cs.height, BufferedImage.TYPE_INT_ARGB_PRE);
  174. Graphics2D g2 = tmp.createGraphics();
  175. g2.drawImage(cursor, 0, 0, cw, ch, 0, 0, width, height, null);
  176. g2.dispose();
  177. x = (int) Math.min(Math.floor((float) x * (float) cw / (float) width), cw - 1);
  178. y = (int) Math.min(Math.floor((float) y * (float) ch / (float) height), ch - 1);
  179. cursor = tmp;
  180. }
  181. setCursor(cursor, x, y);
  182. }
  183. private void setCursor(Image img, int x, int y)
  184. {
  185. java.awt.Point hotspot;
  186. java.awt.Cursor softCursor;
  187. String name = "rfb cursor";
  188. hotspot = new java.awt.Point(x, y);
  189. softCursor = tk.createCustomCursor(img, hotspot, name);
  190. setCursor(softCursor);
  191. }
  192. public void resize(int x, int y, int w, int h) {
  193. if ((w != frameBuffer.width()) || (h != frameBuffer.height())) {
  194. vlog.debug("Resizing framebuffer from "+frameBuffer.width()+"x"+
  195. frameBuffer.height()+" to "+w+"x"+h);
  196. frameBuffer = createFramebuffer(frameBuffer.getPF(), w, h);
  197. assert(frameBuffer != null);
  198. cc.setFramebuffer(frameBuffer);
  199. }
  200. setScaledSize(w, h);
  201. }
  202. public int handle(MouseEvent e)
  203. {
  204. int buttonMask, wheelMask;
  205. switch (e.getID()) {
  206. case MouseEvent.MOUSE_ENTERED:
  207. if (cursor != null)
  208. setCursor(cursor, cursorHotspot.x, cursorHotspot.y);
  209. return 1;
  210. case MouseEvent.MOUSE_EXITED:
  211. setCursor(java.awt.Cursor.getDefaultCursor());
  212. return 1;
  213. case MouseEvent.MOUSE_PRESSED:
  214. case MouseEvent.MOUSE_RELEASED:
  215. case MouseEvent.MOUSE_DRAGGED:
  216. case MouseEvent.MOUSE_MOVED:
  217. case MouseEvent.MOUSE_WHEEL:
  218. buttonMask = 0;
  219. if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) != 0)
  220. buttonMask |= 1;
  221. if ((e.getModifiersEx() & MouseEvent.BUTTON2_DOWN_MASK) != 0)
  222. buttonMask |= 2;
  223. if ((e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) != 0)
  224. buttonMask |= 4;
  225. if (e.getID() == MouseEvent.MOUSE_WHEEL) {
  226. wheelMask = 0;
  227. int clicks = ((MouseWheelEvent)e).getWheelRotation();
  228. if (clicks < 0)
  229. wheelMask |= e.isShiftDown() ? 32 : 8;
  230. else
  231. wheelMask |= e.isShiftDown() ? 64 : 16;
  232. Point pt = new Point(e.getX(), e.getY());
  233. for (int i = 0; i < Math.abs(clicks); i++) {
  234. handlePointerEvent(pt, buttonMask|wheelMask);
  235. handlePointerEvent(pt, buttonMask);
  236. }
  237. return 1;
  238. }
  239. handlePointerEvent(new Point(e.getX(), e.getY()), buttonMask);
  240. return 1;
  241. }
  242. return -1;
  243. }
  244. private PlatformPixelBuffer createFramebuffer(PixelFormat pf, int w, int h)
  245. {
  246. PlatformPixelBuffer fb;
  247. fb = new JavaPixelBuffer(w, h);
  248. return fb;
  249. }
  250. //
  251. // Callback methods to determine geometry of our Component.
  252. //
  253. public Dimension getPreferredSize() {
  254. return new Dimension(scaledWidth, scaledHeight);
  255. }
  256. public Dimension getMinimumSize() {
  257. return new Dimension(scaledWidth, scaledHeight);
  258. }
  259. public Dimension getMaximumSize() {
  260. return new Dimension(scaledWidth, scaledHeight);
  261. }
  262. public void paintComponent(Graphics g) {
  263. Graphics2D g2 = (Graphics2D)g;
  264. synchronized(frameBuffer.getImage()) {
  265. if (cc.server.width() != scaledWidth ||
  266. cc.server.height() != scaledHeight) {
  267. g2.setRenderingHint(RenderingHints.KEY_RENDERING,
  268. RenderingHints.VALUE_RENDER_QUALITY);
  269. g2.drawImage(frameBuffer.getImage(), 0, 0,
  270. scaledWidth, scaledHeight, null);
  271. } else {
  272. g2.drawImage(frameBuffer.getImage(), 0, 0, null);
  273. }
  274. }
  275. g2.dispose();
  276. }
  277. public void setScaledSize(int width, int height)
  278. {
  279. assert(width != 0 && height != 0);
  280. String scaleString = scalingFactor.getValue();
  281. if (remoteResize.getValue()) {
  282. scaledWidth = width;
  283. scaledHeight = height;
  284. scaleRatioX = 1.00f;
  285. scaleRatioY = 1.00f;
  286. } else {
  287. if (scaleString.matches("^[0-9]+$")) {
  288. int scalingFactor = Integer.parseInt(scaleString);
  289. scaledWidth =
  290. (int)Math.floor((float)width * (float)scalingFactor/100.0);
  291. scaledHeight =
  292. (int)Math.floor((float)height * (float)scalingFactor/100.0);
  293. } else if (scaleString.equalsIgnoreCase("Auto")) {
  294. scaledWidth = width;
  295. scaledHeight = height;
  296. } else {
  297. float widthRatio = (float)width / (float)cc.server.width();
  298. float heightRatio = (float)height / (float)cc.server.height();
  299. float ratio = Math.min(widthRatio, heightRatio);
  300. scaledWidth = (int)Math.floor(cc.server.width() * ratio);
  301. scaledHeight = (int)Math.floor(cc.server.height() * ratio);
  302. }
  303. scaleRatioX = (float)scaledWidth / (float)cc.server.width();
  304. scaleRatioY = (float)scaledHeight / (float)cc.server.height();
  305. }
  306. if (scaledWidth != getWidth() || scaledHeight != getHeight())
  307. setSize(new Dimension(scaledWidth, scaledHeight));
  308. }
  309. private void handlePointerEvent(Point pos, int buttonMask)
  310. {
  311. if (!viewOnly.getValue()) {
  312. if (buttonMask != lastButtonMask || !pos.equals(lastPointerPos)) {
  313. try {
  314. if (cc.server.width() != scaledWidth ||
  315. cc.server.height() != scaledHeight) {
  316. int sx = (scaleRatioX == 1.00) ?
  317. pos.x : (int)Math.floor(pos.x / scaleRatioX);
  318. int sy = (scaleRatioY == 1.00) ?
  319. pos.y : (int)Math.floor(pos.y / scaleRatioY);
  320. pos = pos.translate(new Point(sx - pos.x, sy - pos.y));
  321. }
  322. cc.writer().writePointerEvent(pos, buttonMask);
  323. } catch (Exception e) {
  324. vlog.error("%s", e.getMessage());
  325. cc.close();
  326. }
  327. }
  328. lastPointerPos = pos;
  329. lastButtonMask = buttonMask;
  330. }
  331. }
  332. public void handleKeyPress(long keyCode, int keySym)
  333. {
  334. // Prevent recursion if the menu wants to send it's own
  335. // activation key.
  336. if ((menuKeySym != 0) && keySym == menuKeySym && !menuRecursion) {
  337. popupContextMenu();
  338. return;
  339. }
  340. if (viewOnly.getValue())
  341. return;
  342. if (keyCode == 0) {
  343. vlog.error("No key code specified on key press");
  344. return;
  345. }
  346. if (VncViewer.os.startsWith("mac os x")) {
  347. // Alt on OS X behaves more like AltGr on other systems, and to get
  348. // sane behaviour we should translate things in that manner for the
  349. // remote VNC server. However that means we lose the ability to use
  350. // Alt as a shortcut modifier. Do what RealVNC does and hijack the
  351. // left command key as an Alt replacement.
  352. switch (keySym) {
  353. case XK_Meta_L:
  354. keySym = XK_Alt_L;
  355. break;
  356. case XK_Meta_R:
  357. keySym = XK_Super_L;
  358. break;
  359. case XK_Alt_L:
  360. keySym = XK_Mode_switch;
  361. break;
  362. case XK_Alt_R:
  363. keySym = XK_ISO_Level3_Shift;
  364. break;
  365. }
  366. }
  367. if (VncViewer.os.startsWith("windows")) {
  368. // Ugly hack alert!
  369. //
  370. // Windows doesn't have a proper AltGr, but handles it using fake
  371. // Ctrl+Alt. Unfortunately X11 doesn't generally like the combination
  372. // Ctrl+Alt+AltGr, which we usually end up with when Xvnc tries to
  373. // get everything in the correct state. Cheat and temporarily release
  374. // Ctrl and Alt when we send some other symbol.
  375. if (downKeySym.containsValue(XK_Control_L) &&
  376. downKeySym.containsValue(XK_Alt_R)) {
  377. vlog.debug("Faking release of AltGr (Ctrl_L+Alt_R)");
  378. try {
  379. cc.writer().writeKeyEvent(XK_Control_L, false);
  380. cc.writer().writeKeyEvent(XK_Alt_R, false);
  381. } catch (Exception e) {
  382. vlog.error("%s", e.getMessage());
  383. cc.close();
  384. }
  385. }
  386. }
  387. // Because of the way keyboards work, we cannot expect to have the same
  388. // symbol on release as when pressed. This breaks the VNC protocol however,
  389. // so we need to keep track of what keysym a key _code_ generated on press
  390. // and send the same on release.
  391. downKeySym.put(keyCode, keySym);
  392. vlog.debug("Key pressed: 0x%016x => 0x%04x", keyCode, keySym);
  393. try {
  394. // Fake keycode?
  395. if (keyCode > 0xffffffffL)
  396. cc.writer().writeKeyEvent(keySym, true);
  397. else
  398. cc.writer().writeKeyEvent(keySym, true);
  399. } catch (Exception e) {
  400. vlog.error("%s", e.getMessage());
  401. cc.close();
  402. }
  403. if (VncViewer.os.startsWith("windows")) {
  404. // Ugly hack continued...
  405. if (downKeySym.containsValue(XK_Control_L) &&
  406. downKeySym.containsValue(XK_Alt_R)) {
  407. vlog.debug("Restoring AltGr state");
  408. try {
  409. cc.writer().writeKeyEvent(XK_Control_L, true);
  410. cc.writer().writeKeyEvent(XK_Alt_R, true);
  411. } catch (Exception e) {
  412. vlog.error("%s", e.getMessage());
  413. cc.close();
  414. }
  415. }
  416. }
  417. }
  418. public void handleKeyRelease(long keyCode)
  419. {
  420. Integer iter;
  421. if (viewOnly.getValue())
  422. return;
  423. iter = downKeySym.get(keyCode);
  424. if (iter == null) {
  425. // These occur somewhat frequently so let's not spam them unless
  426. // logging is turned up.
  427. vlog.debug("Unexpected release of key code %d", keyCode);
  428. return;
  429. }
  430. vlog.debug("Key released: 0x%016x => 0x%04x", keyCode, iter);
  431. try {
  432. if (keyCode > 0xffffffffL)
  433. cc.writer().writeKeyEvent(iter, false);
  434. else
  435. cc.writer().writeKeyEvent(iter, false);
  436. } catch (Exception e) {
  437. vlog.error("%s", e.getMessage());
  438. cc.close();
  439. }
  440. downKeySym.remove(keyCode);
  441. }
  442. private int handleSystemEvent(AWTEvent event)
  443. {
  444. if (event instanceof KeyEvent) {
  445. KeyEvent ev = (KeyEvent)event;
  446. if (KeyMap.get_keycode_fallback_extended(ev) == 0) {
  447. // Not much we can do with this...
  448. vlog.debug("Ignoring KeyEvent with unknown Java keycode");
  449. return 0;
  450. }
  451. if (ev.getID() == KeyEvent.KEY_PRESSED) {
  452. // Generate a fake keycode just for tracking if we can't figure
  453. // out the proper one. Java virtual key codes aren't unique
  454. // between left/right versions of keys, so we can't use them as
  455. // indexes to the downKeySym map.
  456. long keyCode = KeyMap.get_keycode_fallback_extended(ev) | ((long)ev.getKeyLocation()<<32);
  457. // Pressing Ctrl wreaks havoc with the symbol lookup, so turn
  458. // that off. But AltGr shows up as Ctrl_L+Alt_R in Windows, so
  459. // construct a new KeyEvent that uses a proper AltGraph for the
  460. // symbol lookup.
  461. int keySym;
  462. if (VncViewer.os.startsWith("windows") &&
  463. downKeySym.containsValue(XK_Control_L) &&
  464. downKeySym.containsValue(XK_Alt_R)) {
  465. int mask = ev.getModifiers();
  466. mask &= ~CTRL_MASK;
  467. mask &= ~ALT_MASK;
  468. mask |= ALT_GRAPH_MASK;
  469. AWTKeyStroke ks =
  470. AWTKeyStroke.getAWTKeyStroke(KeyMap.get_keycode_fallback_extended(ev), mask);
  471. // The mask manipulations above break key combinations involving AltGr
  472. // and a key with an accented letter on some keyboard layouts (i.e. IT).
  473. // So the code should first try the modified event, but if it returns no
  474. // symbol, the original event should be used.
  475. final KeyEvent winev = new KeyEvent((JComponent)ev.getSource(), ev.getID(),
  476. ev.getWhen(), mask, KeyMap.get_keycode_fallback_extended(ev),
  477. ks.getKeyChar(), ev.getKeyLocation());
  478. keySym = KeyMap.vkey_to_keysym(winev);
  479. if (keySym == KeyMap.NoSymbol)
  480. keySym = KeyMap.vkey_to_keysym(ev);
  481. else
  482. ev = winev;
  483. } else {
  484. keySym = KeyMap.vkey_to_keysym(ev);
  485. }
  486. if (keySym == KeyMap.NoSymbol)
  487. vlog.error("No symbol for virtual key 0x%016x", keyCode);
  488. if (VncViewer.os.startsWith("linux")) {
  489. switch (keySym) {
  490. // For the first few years, there wasn't a good consensus on what the
  491. // Windows keys should be mapped to for X11. So we need to help out a
  492. // bit and map all variants to the same key...
  493. case XK_Hyper_L:
  494. keySym = XK_Super_L;
  495. break;
  496. case XK_Hyper_R:
  497. keySym = XK_Super_R;
  498. break;
  499. // There has been several variants for Shift-Tab over the years.
  500. // RFB states that we should always send a normal tab.
  501. case XK_ISO_Left_Tab:
  502. keySym = XK_Tab;
  503. break;
  504. }
  505. }
  506. handleKeyPress(keyCode, keySym);
  507. if (VncViewer.os.startsWith("mac os x")) {
  508. // We don't get any release events for CapsLock, so we have to
  509. // send the release right away.
  510. if (keySym == XK_Caps_Lock)
  511. handleKeyRelease(keyCode);
  512. }
  513. return 1;
  514. } else if (ev.getID() == KeyEvent.KEY_RELEASED) {
  515. long keyCode = KeyMap.get_keycode_fallback_extended(ev) | ((long)ev.getKeyLocation()<<32);
  516. handleKeyRelease(keyCode);
  517. return 1;
  518. }
  519. }
  520. return 0;
  521. }
  522. private void initContextMenu()
  523. {
  524. contextMenu.setLightWeightPopupEnabled(false);
  525. contextMenu.removeAll();
  526. menu_add(contextMenu, "Exit viewer", KeyEvent.VK_X,
  527. this, ID.EXIT, EnumSet.of(MENU.DIVIDER));
  528. menu_add(contextMenu, "Full screen", KeyEvent.VK_F, this, ID.FULLSCREEN,
  529. window().fullscreen_active() ?
  530. EnumSet.of(MENU.TOGGLE, MENU.VALUE) : EnumSet.of(MENU.TOGGLE));
  531. menu_add(contextMenu, "Minimize", KeyEvent.VK_Z,
  532. this, ID.MINIMIZE, EnumSet.noneOf(MENU.class));
  533. menu_add(contextMenu, "Resize window to session", KeyEvent.VK_W,
  534. this, ID.RESIZE,
  535. window().fullscreen_active() ?
  536. EnumSet.of(MENU.INACTIVE, MENU.DIVIDER) : EnumSet.of(MENU.DIVIDER));
  537. menu_add(contextMenu, "Clipboard viewer...", KeyEvent.VK_UNDEFINED,
  538. this, ID.CLIPBOARD, EnumSet.of(MENU.DIVIDER));
  539. menu_add(contextMenu, "Ctrl", KeyEvent.VK_C,
  540. this, ID.CTRL,
  541. menuCtrlKey ? EnumSet.of(MENU.TOGGLE, MENU.VALUE) : EnumSet.of(MENU.TOGGLE));
  542. menu_add(contextMenu, "Alt", KeyEvent.VK_A,
  543. this, ID.ALT,
  544. menuAltKey ? EnumSet.of(MENU.TOGGLE, MENU.VALUE) : EnumSet.of(MENU.TOGGLE));
  545. if (menuKeySym != 0) {
  546. String sendMenuKey = String.format("Send %s", menuKey.getValueStr());
  547. menu_add(contextMenu, sendMenuKey, menuKeyJava,
  548. this, ID.MENUKEY, EnumSet.noneOf(MENU.class));
  549. }
  550. menu_add(contextMenu, "Send Ctrl-Alt-Del", KeyEvent.VK_D,
  551. this, ID.CTRLALTDEL, EnumSet.of(MENU.DIVIDER));
  552. menu_add(contextMenu, "Refresh screen", KeyEvent.VK_R,
  553. this, ID.REFRESH, EnumSet.of(MENU.DIVIDER));
  554. menu_add(contextMenu, "New connection...", KeyEvent.VK_N,
  555. this, ID.NEWVIEWER, EnumSet.of(MENU.DIVIDER));
  556. menu_add(contextMenu, "Options...", KeyEvent.VK_O,
  557. this, ID.OPTIONS, EnumSet.noneOf(MENU.class));
  558. menu_add(contextMenu, "Connection info...", KeyEvent.VK_I,
  559. this, ID.INFO, EnumSet.noneOf(MENU.class));
  560. menu_add(contextMenu, "About TigerVNC viewer...", KeyEvent.VK_T,
  561. this, ID.ABOUT, EnumSet.of(MENU.DIVIDER));
  562. menu_add(contextMenu, "Dismiss menu", KeyEvent.VK_M,
  563. this, ID.DISMISS, EnumSet.noneOf(MENU.class));
  564. }
  565. static void menu_add(JPopupMenu menu, String text,
  566. int shortcut, ActionListener cb,
  567. ID data, EnumSet<MENU> flags)
  568. {
  569. JMenuItem item;
  570. if (flags.contains(MENU.TOGGLE)) {
  571. item = new JCheckBoxMenuItem(text, flags.contains(MENU.VALUE));
  572. } else {
  573. if (shortcut != 0)
  574. item = new JMenuItem(text, shortcut);
  575. else
  576. item = new JMenuItem(text);
  577. }
  578. item.setActionCommand(data.toString());
  579. item.addActionListener(cb);
  580. item.setEnabled(!flags.contains(MENU.INACTIVE));
  581. menu.add(item);
  582. if (flags.contains(MENU.DIVIDER))
  583. menu.addSeparator();
  584. }
  585. void popupContextMenu()
  586. {
  587. // initialize context menu before display
  588. initContextMenu();
  589. contextMenu.setCursor(java.awt.Cursor.getDefaultCursor());
  590. contextMenu.show(this, lastPointerPos.x, lastPointerPos.y);
  591. }
  592. public void actionPerformed(ActionEvent ev)
  593. {
  594. switch(ID.valueOf(ev.getActionCommand())) {
  595. case EXIT:
  596. cc.close();
  597. break;
  598. case FULLSCREEN:
  599. if (window().fullscreen_active())
  600. window().fullscreen_off();
  601. else
  602. window().fullscreen_on();
  603. break;
  604. case MINIMIZE:
  605. if (window().fullscreen_active())
  606. window().fullscreen_off();
  607. window().setExtendedState(JFrame.ICONIFIED);
  608. break;
  609. case RESIZE:
  610. if (window().fullscreen_active())
  611. break;
  612. int dx = window().getInsets().left + window().getInsets().right;
  613. int dy = window().getInsets().top + window().getInsets().bottom;
  614. window().setSize(getWidth()+dx, getHeight()+dy);
  615. break;
  616. case CLIPBOARD:
  617. ClipboardDialog.showDialog(window());
  618. break;
  619. case CTRL:
  620. if (((JMenuItem)ev.getSource()).isSelected())
  621. handleKeyPress(0x1d, XK_Control_L);
  622. else
  623. handleKeyRelease(0x1d);
  624. menuCtrlKey = !menuCtrlKey;
  625. break;
  626. case ALT:
  627. if (((JMenuItem)ev.getSource()).isSelected())
  628. handleKeyPress(0x38, XK_Alt_L);
  629. else
  630. handleKeyRelease(0x38);
  631. menuAltKey = !menuAltKey;
  632. break;
  633. case MENUKEY:
  634. menuRecursion = true;
  635. handleKeyPress(menuKeyCode, menuKeySym);
  636. menuRecursion = false;
  637. handleKeyRelease(menuKeyCode);
  638. break;
  639. case CTRLALTDEL:
  640. handleKeyPress(0x1d, XK_Control_L);
  641. handleKeyPress(0x38, XK_Alt_L);
  642. handleKeyPress(0xd3, XK_Delete);
  643. handleKeyRelease(0xd3);
  644. handleKeyRelease(0x38);
  645. handleKeyRelease(0x1d);
  646. break;
  647. case REFRESH:
  648. cc.refreshFramebuffer();
  649. break;
  650. case NEWVIEWER:
  651. VncViewer.newViewer();
  652. break;
  653. case OPTIONS:
  654. OptionsDialog.showDialog(cc.desktop);
  655. break;
  656. case INFO:
  657. Window fullScreenWindow =
  658. DesktopWindow.getFullScreenWindow();
  659. if (fullScreenWindow != null)
  660. DesktopWindow.setFullScreenWindow(null);
  661. JOptionPane op = new JOptionPane(cc.connectionInfo(),
  662. JOptionPane.PLAIN_MESSAGE,
  663. JOptionPane.DEFAULT_OPTION);
  664. JDialog dlg = op.createDialog(window(), "VNC connection info");
  665. dlg.setIconImage(VncViewer.frameIcon);
  666. dlg.setAlwaysOnTop(true);
  667. dlg.setVisible(true);
  668. if (fullScreenWindow != null)
  669. DesktopWindow.setFullScreenWindow(fullScreenWindow);
  670. break;
  671. case ABOUT:
  672. VncViewer.about_vncviewer(cc.desktop);
  673. break;
  674. case DISMISS:
  675. break;
  676. }
  677. }
  678. private void setMenuKey()
  679. {
  680. menuKeyJava = MenuKey.getMenuKeyJavaCode();
  681. menuKeyCode = MenuKey.getMenuKeyCode();
  682. menuKeySym = MenuKey.getMenuKeySym();
  683. }
  684. public void handleOptions()
  685. {
  686. setMenuKey();
  687. /*
  688. setScaledSize(cc.server.width(), cc.server.height());
  689. if (!oldSize.equals(new Dimension(scaledWidth, scaledHeight))) {
  690. // Re-layout the DesktopWindow when the scaled size changes.
  691. // Ideally we'd do this with a ComponentListener, but unfortunately
  692. // sometimes a spurious resize event is triggered on the viewport
  693. // when the DesktopWindow is manually resized via the drag handles.
  694. if (cc.desktop != null && cc.desktop.isVisible()) {
  695. JScrollPane scroll = (JScrollPane)((JViewport)getParent()).getParent();
  696. scroll.setViewportBorder(BorderFactory.createEmptyBorder(0,0,0,0));
  697. cc.desktop.pack();
  698. }
  699. */
  700. }
  701. public void releaseDownKeys() {
  702. while (!downKeySym.isEmpty())
  703. handleKeyRelease(downKeySym.keySet().iterator().next());
  704. }
  705. private DesktopWindow window() {
  706. return (DesktopWindow)getTopLevelAncestor();
  707. }
  708. private int x() { return getX(); }
  709. private int y() { return getY(); }
  710. private int w() { return getWidth(); }
  711. private int h() { return getHeight(); }
  712. // access to cc by different threads is specified in CConn
  713. private CConn cc;
  714. // access to the following must be synchronized:
  715. private PlatformPixelBuffer frameBuffer;
  716. Point lastPointerPos = new Point(0, 0);
  717. int lastButtonMask = 0;
  718. private class DownMap extends HashMap<Long, Integer> {
  719. public DownMap(int capacity) {
  720. super(capacity);
  721. }
  722. }
  723. DownMap downKeySym = new DownMap(256);
  724. int menuKeySym;
  725. int menuKeyCode, menuKeyJava;
  726. JPopupMenu contextMenu;
  727. boolean menuRecursion = false;
  728. boolean menuCtrlKey = false;
  729. boolean menuAltKey = false;
  730. static Toolkit tk = Toolkit.getDefaultToolkit();
  731. public int scaledWidth = 0, scaledHeight = 0;
  732. float scaleRatioX, scaleRatioY;
  733. static BufferedImage cursor;
  734. Point cursorHotspot = new Point();
  735. }