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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838
  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. 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
  132. 0xffffffff, 0xff000000, 0xff000000, 0xff000000, 0xffffffff,
  133. 0xffffffff, 0xff000000, 0xff000000, 0xff000000, 0xffffffff,
  134. 0xffffffff, 0xff000000, 0xff000000, 0xff000000, 0xffffffff,
  135. 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
  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 = (int)Math.floor((float)cursorHotspot.x * scaleRatioX);
  168. int y = (int)Math.floor((float)cursorHotspot.y * scaleRatioY);
  169. Dimension cs = tk.getBestCursorSize(cw, ch);
  170. if (cs.width != cw && cs.height != ch) {
  171. cw = Math.min(cw, cs.width);
  172. ch = Math.min(ch, cs.height);
  173. x = (int)Math.min(x, Math.max(cs.width - 1, 0));
  174. y = (int)Math.min(y, Math.max(cs.height - 1, 0));
  175. BufferedImage tmp =
  176. new BufferedImage(cs.width, cs.height, BufferedImage.TYPE_INT_ARGB_PRE);
  177. Graphics2D g2 = tmp.createGraphics();
  178. g2.drawImage(cursor, 0, 0, cw, ch, 0, 0, width, height, null);
  179. g2.dispose();
  180. cursor = tmp;
  181. }
  182. setCursor(cursor, x, y);
  183. }
  184. private void setCursor(Image img, int x, int y)
  185. {
  186. java.awt.Point hotspot;
  187. java.awt.Cursor softCursor;
  188. String name = "rfb cursor";
  189. hotspot = new java.awt.Point(x, y);
  190. softCursor = tk.createCustomCursor(img, hotspot, name);
  191. setCursor(softCursor);
  192. }
  193. public void resize(int x, int y, int w, int h) {
  194. if ((w != frameBuffer.width()) || (h != frameBuffer.height())) {
  195. vlog.debug("Resizing framebuffer from "+frameBuffer.width()+"x"+
  196. frameBuffer.height()+" to "+w+"x"+h);
  197. frameBuffer = createFramebuffer(frameBuffer.getPF(), w, h);
  198. assert(frameBuffer != null);
  199. cc.setFramebuffer(frameBuffer);
  200. }
  201. setScaledSize(w, h);
  202. }
  203. public int handle(MouseEvent e)
  204. {
  205. int buttonMask, wheelMask;
  206. switch (e.getID()) {
  207. case MouseEvent.MOUSE_ENTERED:
  208. if (cursor != null)
  209. setCursor(cursor, cursorHotspot.x, cursorHotspot.y);
  210. return 1;
  211. case MouseEvent.MOUSE_EXITED:
  212. setCursor(java.awt.Cursor.getDefaultCursor());
  213. return 1;
  214. case MouseEvent.MOUSE_PRESSED:
  215. case MouseEvent.MOUSE_RELEASED:
  216. case MouseEvent.MOUSE_DRAGGED:
  217. case MouseEvent.MOUSE_MOVED:
  218. case MouseEvent.MOUSE_WHEEL:
  219. buttonMask = 0;
  220. if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) != 0)
  221. buttonMask |= 1;
  222. if ((e.getModifiersEx() & MouseEvent.BUTTON2_DOWN_MASK) != 0)
  223. buttonMask |= 2;
  224. if ((e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) != 0)
  225. buttonMask |= 4;
  226. if (e.getID() == MouseEvent.MOUSE_WHEEL) {
  227. wheelMask = 0;
  228. int clicks = ((MouseWheelEvent)e).getWheelRotation();
  229. if (clicks < 0)
  230. wheelMask |= e.isShiftDown() ? 32 : 8;
  231. else
  232. wheelMask |= e.isShiftDown() ? 64 : 16;
  233. Point pt = new Point(e.getX(), e.getY());
  234. for (int i = 0; i < Math.abs(clicks); i++) {
  235. handlePointerEvent(pt, buttonMask|wheelMask);
  236. handlePointerEvent(pt, buttonMask);
  237. }
  238. return 1;
  239. }
  240. handlePointerEvent(new Point(e.getX(), e.getY()), buttonMask);
  241. return 1;
  242. }
  243. return -1;
  244. }
  245. private PlatformPixelBuffer createFramebuffer(PixelFormat pf, int w, int h)
  246. {
  247. PlatformPixelBuffer fb;
  248. fb = new JavaPixelBuffer(w, h);
  249. return fb;
  250. }
  251. //
  252. // Callback methods to determine geometry of our Component.
  253. //
  254. public Dimension getPreferredSize() {
  255. return new Dimension(scaledWidth, scaledHeight);
  256. }
  257. public Dimension getMinimumSize() {
  258. return new Dimension(scaledWidth, scaledHeight);
  259. }
  260. public Dimension getMaximumSize() {
  261. return new Dimension(scaledWidth, scaledHeight);
  262. }
  263. public void paintComponent(Graphics g) {
  264. Graphics2D g2 = (Graphics2D)g;
  265. synchronized(frameBuffer.getImage()) {
  266. if (cc.server.width() != scaledWidth ||
  267. cc.server.height() != scaledHeight) {
  268. g2.setRenderingHint(RenderingHints.KEY_RENDERING,
  269. RenderingHints.VALUE_RENDER_QUALITY);
  270. g2.drawImage(frameBuffer.getImage(), 0, 0,
  271. scaledWidth, scaledHeight, null);
  272. } else {
  273. g2.drawImage(frameBuffer.getImage(), 0, 0, null);
  274. }
  275. }
  276. g2.dispose();
  277. }
  278. public void setScaledSize(int width, int height)
  279. {
  280. assert(width != 0 && height != 0);
  281. String scaleString = scalingFactor.getValue();
  282. if (remoteResize.getValue()) {
  283. scaledWidth = width;
  284. scaledHeight = height;
  285. scaleRatioX = 1.00f;
  286. scaleRatioY = 1.00f;
  287. } else {
  288. if (scaleString.matches("^[0-9]+$")) {
  289. int scalingFactor = Integer.parseInt(scaleString);
  290. scaledWidth =
  291. (int)Math.floor((float)width * (float)scalingFactor/100.0);
  292. scaledHeight =
  293. (int)Math.floor((float)height * (float)scalingFactor/100.0);
  294. } else if (scaleString.equalsIgnoreCase("Auto")) {
  295. scaledWidth = width;
  296. scaledHeight = height;
  297. } else {
  298. float widthRatio = (float)width / (float)cc.server.width();
  299. float heightRatio = (float)height / (float)cc.server.height();
  300. float ratio = Math.min(widthRatio, heightRatio);
  301. scaledWidth = (int)Math.floor(cc.server.width() * ratio);
  302. scaledHeight = (int)Math.floor(cc.server.height() * ratio);
  303. }
  304. scaleRatioX = (float)scaledWidth / (float)cc.server.width();
  305. scaleRatioY = (float)scaledHeight / (float)cc.server.height();
  306. }
  307. if (scaledWidth != getWidth() || scaledHeight != getHeight())
  308. setSize(new Dimension(scaledWidth, scaledHeight));
  309. }
  310. private void handlePointerEvent(Point pos, int buttonMask)
  311. {
  312. if (!viewOnly.getValue()) {
  313. if (buttonMask != lastButtonMask || !pos.equals(lastPointerPos)) {
  314. try {
  315. if (cc.server.width() != scaledWidth ||
  316. cc.server.height() != scaledHeight) {
  317. int sx = (scaleRatioX == 1.00) ?
  318. pos.x : (int)Math.floor(pos.x / scaleRatioX);
  319. int sy = (scaleRatioY == 1.00) ?
  320. pos.y : (int)Math.floor(pos.y / scaleRatioY);
  321. pos = pos.translate(new Point(sx - pos.x, sy - pos.y));
  322. }
  323. cc.writer().writePointerEvent(pos, buttonMask);
  324. } catch (Exception e) {
  325. vlog.error("%s", e.getMessage());
  326. cc.close();
  327. }
  328. }
  329. lastPointerPos = pos;
  330. lastButtonMask = buttonMask;
  331. }
  332. }
  333. public void handleKeyPress(long keyCode, int keySym)
  334. {
  335. // Prevent recursion if the menu wants to send it's own
  336. // activation key.
  337. if ((menuKeySym != 0) && keySym == menuKeySym && !menuRecursion) {
  338. popupContextMenu();
  339. return;
  340. }
  341. if (viewOnly.getValue())
  342. return;
  343. if (keyCode == 0) {
  344. vlog.error("No key code specified on key press");
  345. return;
  346. }
  347. if (VncViewer.os.startsWith("mac os x")) {
  348. // Alt on OS X behaves more like AltGr on other systems, and to get
  349. // sane behaviour we should translate things in that manner for the
  350. // remote VNC server. However that means we lose the ability to use
  351. // Alt as a shortcut modifier. Do what RealVNC does and hijack the
  352. // left command key as an Alt replacement.
  353. switch (keySym) {
  354. case XK_Meta_L:
  355. keySym = XK_Alt_L;
  356. break;
  357. case XK_Meta_R:
  358. keySym = XK_Super_L;
  359. break;
  360. case XK_Alt_L:
  361. keySym = XK_Mode_switch;
  362. break;
  363. case XK_Alt_R:
  364. keySym = XK_ISO_Level3_Shift;
  365. break;
  366. }
  367. }
  368. if (VncViewer.os.startsWith("windows")) {
  369. // Ugly hack alert!
  370. //
  371. // Windows doesn't have a proper AltGr, but handles it using fake
  372. // Ctrl+Alt. Unfortunately X11 doesn't generally like the combination
  373. // Ctrl+Alt+AltGr, which we usually end up with when Xvnc tries to
  374. // get everything in the correct state. Cheat and temporarily release
  375. // Ctrl and Alt when we send some other symbol.
  376. if (downKeySym.containsValue(XK_Control_L) &&
  377. downKeySym.containsValue(XK_Alt_R)) {
  378. vlog.debug("Faking release of AltGr (Ctrl_L+Alt_R)");
  379. try {
  380. cc.writer().writeKeyEvent(XK_Control_L, false);
  381. cc.writer().writeKeyEvent(XK_Alt_R, false);
  382. } catch (Exception e) {
  383. vlog.error("%s", e.getMessage());
  384. cc.close();
  385. }
  386. }
  387. }
  388. // Because of the way keyboards work, we cannot expect to have the same
  389. // symbol on release as when pressed. This breaks the VNC protocol however,
  390. // so we need to keep track of what keysym a key _code_ generated on press
  391. // and send the same on release.
  392. downKeySym.put(keyCode, keySym);
  393. vlog.debug("Key pressed: 0x%016x => 0x%04x", keyCode, keySym);
  394. try {
  395. // Fake keycode?
  396. if (keyCode > 0xffffffffL)
  397. cc.writer().writeKeyEvent(keySym, true);
  398. else
  399. cc.writer().writeKeyEvent(keySym, true);
  400. } catch (Exception e) {
  401. vlog.error("%s", e.getMessage());
  402. cc.close();
  403. }
  404. if (VncViewer.os.startsWith("windows")) {
  405. // Ugly hack continued...
  406. if (downKeySym.containsValue(XK_Control_L) &&
  407. downKeySym.containsValue(XK_Alt_R)) {
  408. vlog.debug("Restoring AltGr state");
  409. try {
  410. cc.writer().writeKeyEvent(XK_Control_L, true);
  411. cc.writer().writeKeyEvent(XK_Alt_R, true);
  412. } catch (Exception e) {
  413. vlog.error("%s", e.getMessage());
  414. cc.close();
  415. }
  416. }
  417. }
  418. }
  419. public void handleKeyRelease(long keyCode)
  420. {
  421. Integer iter;
  422. if (viewOnly.getValue())
  423. return;
  424. iter = downKeySym.get(keyCode);
  425. if (iter == null) {
  426. // These occur somewhat frequently so let's not spam them unless
  427. // logging is turned up.
  428. vlog.debug("Unexpected release of key code %d", keyCode);
  429. return;
  430. }
  431. vlog.debug("Key released: 0x%016x => 0x%04x", keyCode, iter);
  432. try {
  433. if (keyCode > 0xffffffffL)
  434. cc.writer().writeKeyEvent(iter, false);
  435. else
  436. cc.writer().writeKeyEvent(iter, false);
  437. } catch (Exception e) {
  438. vlog.error("%s", e.getMessage());
  439. cc.close();
  440. }
  441. downKeySym.remove(keyCode);
  442. }
  443. private int handleSystemEvent(AWTEvent event)
  444. {
  445. if (event instanceof KeyEvent) {
  446. KeyEvent ev = (KeyEvent)event;
  447. if (KeyMap.get_keycode_fallback_extended(ev) == 0) {
  448. // Not much we can do with this...
  449. vlog.debug("Ignoring KeyEvent with unknown Java keycode");
  450. return 0;
  451. }
  452. if (ev.getID() == KeyEvent.KEY_PRESSED) {
  453. // Generate a fake keycode just for tracking if we can't figure
  454. // out the proper one. Java virtual key codes aren't unique
  455. // between left/right versions of keys, so we can't use them as
  456. // indexes to the downKeySym map.
  457. long keyCode = KeyMap.get_keycode_fallback_extended(ev) | ((long)ev.getKeyLocation()<<32);
  458. // Pressing Ctrl wreaks havoc with the symbol lookup, so turn
  459. // that off. But AltGr shows up as Ctrl_L+Alt_R in Windows, so
  460. // construct a new KeyEvent that uses a proper AltGraph for the
  461. // symbol lookup.
  462. int keySym;
  463. if (VncViewer.os.startsWith("windows") &&
  464. downKeySym.containsValue(XK_Control_L) &&
  465. downKeySym.containsValue(XK_Alt_R)) {
  466. int mask = ev.getModifiers();
  467. mask &= ~CTRL_MASK;
  468. mask &= ~ALT_MASK;
  469. mask |= ALT_GRAPH_MASK;
  470. AWTKeyStroke ks =
  471. AWTKeyStroke.getAWTKeyStroke(KeyMap.get_keycode_fallback_extended(ev), mask);
  472. // The mask manipulations above break key combinations involving AltGr
  473. // and a key with an accented letter on some keyboard layouts (i.e. IT).
  474. // So the code should first try the modified event, but if it returns no
  475. // symbol, the original event should be used.
  476. final KeyEvent winev = new KeyEvent((JComponent)ev.getSource(), ev.getID(),
  477. ev.getWhen(), mask, KeyMap.get_keycode_fallback_extended(ev),
  478. ks.getKeyChar(), ev.getKeyLocation());
  479. keySym = KeyMap.vkey_to_keysym(winev);
  480. if (keySym == KeyMap.NoSymbol)
  481. keySym = KeyMap.vkey_to_keysym(ev);
  482. else
  483. ev = winev;
  484. } else {
  485. keySym = KeyMap.vkey_to_keysym(ev);
  486. }
  487. if (keySym == KeyMap.NoSymbol)
  488. vlog.error("No symbol for virtual key 0x%016x", keyCode);
  489. if (VncViewer.os.startsWith("linux")) {
  490. switch (keySym) {
  491. // For the first few years, there wasn't a good consensus on what the
  492. // Windows keys should be mapped to for X11. So we need to help out a
  493. // bit and map all variants to the same key...
  494. case XK_Hyper_L:
  495. keySym = XK_Super_L;
  496. break;
  497. case XK_Hyper_R:
  498. keySym = XK_Super_R;
  499. break;
  500. // There has been several variants for Shift-Tab over the years.
  501. // RFB states that we should always send a normal tab.
  502. case XK_ISO_Left_Tab:
  503. keySym = XK_Tab;
  504. break;
  505. }
  506. }
  507. handleKeyPress(keyCode, keySym);
  508. if (VncViewer.os.startsWith("mac os x")) {
  509. // We don't get any release events for CapsLock, so we have to
  510. // send the release right away.
  511. if (keySym == XK_Caps_Lock)
  512. handleKeyRelease(keyCode);
  513. }
  514. return 1;
  515. } else if (ev.getID() == KeyEvent.KEY_RELEASED) {
  516. long keyCode = KeyMap.get_keycode_fallback_extended(ev) | ((long)ev.getKeyLocation()<<32);
  517. handleKeyRelease(keyCode);
  518. return 1;
  519. }
  520. }
  521. return 0;
  522. }
  523. private void initContextMenu()
  524. {
  525. contextMenu.setLightWeightPopupEnabled(false);
  526. contextMenu.removeAll();
  527. menu_add(contextMenu, "Exit viewer", KeyEvent.VK_X,
  528. this, ID.EXIT, EnumSet.of(MENU.DIVIDER));
  529. menu_add(contextMenu, "Full screen", KeyEvent.VK_F, this, ID.FULLSCREEN,
  530. window().fullscreen_active() ?
  531. EnumSet.of(MENU.TOGGLE, MENU.VALUE) : EnumSet.of(MENU.TOGGLE));
  532. menu_add(contextMenu, "Minimize", KeyEvent.VK_Z,
  533. this, ID.MINIMIZE, EnumSet.noneOf(MENU.class));
  534. menu_add(contextMenu, "Resize window to session", KeyEvent.VK_W,
  535. this, ID.RESIZE,
  536. window().fullscreen_active() ?
  537. EnumSet.of(MENU.INACTIVE, MENU.DIVIDER) : EnumSet.of(MENU.DIVIDER));
  538. menu_add(contextMenu, "Clipboard viewer...", KeyEvent.VK_UNDEFINED,
  539. this, ID.CLIPBOARD, EnumSet.of(MENU.DIVIDER));
  540. menu_add(contextMenu, "Ctrl", KeyEvent.VK_C,
  541. this, ID.CTRL,
  542. menuCtrlKey ? EnumSet.of(MENU.TOGGLE, MENU.VALUE) : EnumSet.of(MENU.TOGGLE));
  543. menu_add(contextMenu, "Alt", KeyEvent.VK_A,
  544. this, ID.ALT,
  545. menuAltKey ? EnumSet.of(MENU.TOGGLE, MENU.VALUE) : EnumSet.of(MENU.TOGGLE));
  546. if (menuKeySym != 0) {
  547. String sendMenuKey = String.format("Send %s", menuKey.getValueStr());
  548. menu_add(contextMenu, sendMenuKey, menuKeyJava,
  549. this, ID.MENUKEY, EnumSet.noneOf(MENU.class));
  550. }
  551. menu_add(contextMenu, "Send Ctrl-Alt-Del", KeyEvent.VK_D,
  552. this, ID.CTRLALTDEL, EnumSet.of(MENU.DIVIDER));
  553. menu_add(contextMenu, "Refresh screen", KeyEvent.VK_R,
  554. this, ID.REFRESH, EnumSet.of(MENU.DIVIDER));
  555. menu_add(contextMenu, "New connection...", KeyEvent.VK_N,
  556. this, ID.NEWVIEWER, EnumSet.of(MENU.DIVIDER));
  557. menu_add(contextMenu, "Options...", KeyEvent.VK_O,
  558. this, ID.OPTIONS, EnumSet.noneOf(MENU.class));
  559. menu_add(contextMenu, "Connection info...", KeyEvent.VK_I,
  560. this, ID.INFO, EnumSet.noneOf(MENU.class));
  561. menu_add(contextMenu, "About TigerVNC viewer...", KeyEvent.VK_T,
  562. this, ID.ABOUT, EnumSet.of(MENU.DIVIDER));
  563. menu_add(contextMenu, "Dismiss menu", KeyEvent.VK_M,
  564. this, ID.DISMISS, EnumSet.noneOf(MENU.class));
  565. }
  566. static void menu_add(JPopupMenu menu, String text,
  567. int shortcut, ActionListener cb,
  568. ID data, EnumSet<MENU> flags)
  569. {
  570. JMenuItem item;
  571. if (flags.contains(MENU.TOGGLE)) {
  572. item = new JCheckBoxMenuItem(text, flags.contains(MENU.VALUE));
  573. } else {
  574. if (shortcut != 0)
  575. item = new JMenuItem(text, shortcut);
  576. else
  577. item = new JMenuItem(text);
  578. }
  579. item.setActionCommand(data.toString());
  580. item.addActionListener(cb);
  581. item.setEnabled(!flags.contains(MENU.INACTIVE));
  582. menu.add(item);
  583. if (flags.contains(MENU.DIVIDER))
  584. menu.addSeparator();
  585. }
  586. void popupContextMenu()
  587. {
  588. // initialize context menu before display
  589. initContextMenu();
  590. contextMenu.setCursor(java.awt.Cursor.getDefaultCursor());
  591. contextMenu.show(this, lastPointerPos.x, lastPointerPos.y);
  592. }
  593. public void actionPerformed(ActionEvent ev)
  594. {
  595. switch(ID.valueOf(ev.getActionCommand())) {
  596. case EXIT:
  597. cc.close();
  598. break;
  599. case FULLSCREEN:
  600. if (window().fullscreen_active())
  601. window().fullscreen_off();
  602. else
  603. window().fullscreen_on();
  604. break;
  605. case MINIMIZE:
  606. if (window().fullscreen_active())
  607. window().fullscreen_off();
  608. window().setExtendedState(JFrame.ICONIFIED);
  609. break;
  610. case RESIZE:
  611. if (window().fullscreen_active())
  612. break;
  613. int dx = window().getInsets().left + window().getInsets().right;
  614. int dy = window().getInsets().top + window().getInsets().bottom;
  615. window().setSize(getWidth()+dx, getHeight()+dy);
  616. break;
  617. case CLIPBOARD:
  618. ClipboardDialog.showDialog(window());
  619. break;
  620. case CTRL:
  621. if (((JMenuItem)ev.getSource()).isSelected())
  622. handleKeyPress(0x1d, XK_Control_L);
  623. else
  624. handleKeyRelease(0x1d);
  625. menuCtrlKey = !menuCtrlKey;
  626. break;
  627. case ALT:
  628. if (((JMenuItem)ev.getSource()).isSelected())
  629. handleKeyPress(0x38, XK_Alt_L);
  630. else
  631. handleKeyRelease(0x38);
  632. menuAltKey = !menuAltKey;
  633. break;
  634. case MENUKEY:
  635. menuRecursion = true;
  636. handleKeyPress(menuKeyCode, menuKeySym);
  637. menuRecursion = false;
  638. handleKeyRelease(menuKeyCode);
  639. break;
  640. case CTRLALTDEL:
  641. handleKeyPress(0x1d, XK_Control_L);
  642. handleKeyPress(0x38, XK_Alt_L);
  643. handleKeyPress(0xd3, XK_Delete);
  644. handleKeyRelease(0xd3);
  645. handleKeyRelease(0x38);
  646. handleKeyRelease(0x1d);
  647. break;
  648. case REFRESH:
  649. cc.refreshFramebuffer();
  650. break;
  651. case NEWVIEWER:
  652. VncViewer.newViewer();
  653. break;
  654. case OPTIONS:
  655. OptionsDialog.showDialog(cc.desktop);
  656. break;
  657. case INFO:
  658. Window fullScreenWindow =
  659. DesktopWindow.getFullScreenWindow();
  660. if (fullScreenWindow != null)
  661. DesktopWindow.setFullScreenWindow(null);
  662. JOptionPane op = new JOptionPane(cc.connectionInfo(),
  663. JOptionPane.PLAIN_MESSAGE,
  664. JOptionPane.DEFAULT_OPTION);
  665. JDialog dlg = op.createDialog(window(), "VNC connection info");
  666. dlg.setIconImage(VncViewer.frameIcon);
  667. dlg.setAlwaysOnTop(true);
  668. dlg.setVisible(true);
  669. if (fullScreenWindow != null)
  670. DesktopWindow.setFullScreenWindow(fullScreenWindow);
  671. break;
  672. case ABOUT:
  673. VncViewer.about_vncviewer(cc.desktop);
  674. break;
  675. case DISMISS:
  676. break;
  677. }
  678. }
  679. private void setMenuKey()
  680. {
  681. menuKeyJava = MenuKey.getMenuKeyJavaCode();
  682. menuKeyCode = MenuKey.getMenuKeyCode();
  683. menuKeySym = MenuKey.getMenuKeySym();
  684. }
  685. public void handleOptions()
  686. {
  687. setMenuKey();
  688. /*
  689. setScaledSize(cc.server.width(), cc.server.height());
  690. if (!oldSize.equals(new Dimension(scaledWidth, scaledHeight))) {
  691. // Re-layout the DesktopWindow when the scaled size changes.
  692. // Ideally we'd do this with a ComponentListener, but unfortunately
  693. // sometimes a spurious resize event is triggered on the viewport
  694. // when the DesktopWindow is manually resized via the drag handles.
  695. if (cc.desktop != null && cc.desktop.isVisible()) {
  696. JScrollPane scroll = (JScrollPane)((JViewport)getParent()).getParent();
  697. scroll.setViewportBorder(BorderFactory.createEmptyBorder(0,0,0,0));
  698. cc.desktop.pack();
  699. }
  700. */
  701. }
  702. public void releaseDownKeys() {
  703. while (!downKeySym.isEmpty())
  704. handleKeyRelease(downKeySym.keySet().iterator().next());
  705. }
  706. private DesktopWindow window() {
  707. return (DesktopWindow)getTopLevelAncestor();
  708. }
  709. private int x() { return getX(); }
  710. private int y() { return getY(); }
  711. private int w() { return getWidth(); }
  712. private int h() { return getHeight(); }
  713. // access to cc by different threads is specified in CConn
  714. private CConn cc;
  715. // access to the following must be synchronized:
  716. private PlatformPixelBuffer frameBuffer;
  717. Point lastPointerPos = new Point(0, 0);
  718. int lastButtonMask = 0;
  719. private class DownMap extends HashMap<Long, Integer> {
  720. public DownMap(int capacity) {
  721. super(capacity);
  722. }
  723. }
  724. DownMap downKeySym = new DownMap(256);
  725. int menuKeySym;
  726. int menuKeyCode, menuKeyJava;
  727. JPopupMenu contextMenu;
  728. boolean menuRecursion = false;
  729. boolean menuCtrlKey = false;
  730. boolean menuAltKey = false;
  731. static Toolkit tk = Toolkit.getDefaultToolkit();
  732. public int scaledWidth = 0, scaledHeight = 0;
  733. float scaleRatioX, scaleRatioY;
  734. static BufferedImage cursor;
  735. Point cursorHotspot = new Point();
  736. }