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 26KB

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