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.

DesktopWindow.java 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. /* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved.
  2. * Copyright (C) 2011-2019 Brian P. Hinz
  3. * Copyright (C) 2012-2013 D. R. Commander. All Rights Reserved.
  4. *
  5. * This is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This software is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this software; if not, write to the Free Software
  17. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
  18. * USA.
  19. */
  20. package com.tigervnc.vncviewer;
  21. import java.awt.*;
  22. import java.awt.event.*;
  23. import java.lang.reflect.*;
  24. import java.util.*;
  25. import javax.swing.*;
  26. import javax.swing.Timer;
  27. import javax.swing.border.*;
  28. import com.tigervnc.rfb.*;
  29. import com.tigervnc.rfb.Point;
  30. import java.lang.Exception;
  31. import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER;
  32. import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER;
  33. import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED;
  34. import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED;
  35. import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS;
  36. import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS;
  37. import static com.tigervnc.vncviewer.Parameters.*;
  38. public class DesktopWindow extends JFrame
  39. {
  40. static LogWriter vlog = new LogWriter("DesktopWindow");
  41. public DesktopWindow(int w, int h, String name,
  42. PixelFormat serverPF, CConn cc_)
  43. {
  44. cc = cc_;
  45. firstUpdate = true;
  46. delayedFullscreen = false; delayedDesktopSize = false;
  47. setFocusable(false);
  48. setFocusTraversalKeysEnabled(false);
  49. getToolkit().setDynamicLayout(false);
  50. if (!VncViewer.os.startsWith("mac os x"))
  51. setIconImage(VncViewer.frameIcon);
  52. UIManager.getDefaults().put("ScrollPane.ancestorInputMap",
  53. new UIDefaults.LazyInputMap(new Object[]{}));
  54. scroll = new JScrollPane(new Viewport(w, h, serverPF, cc));
  55. viewport = (Viewport)scroll.getViewport().getView();
  56. scroll.setBorder(BorderFactory.createEmptyBorder(0,0,0,0));
  57. getContentPane().add(scroll);
  58. setName(name);
  59. lastScaleFactor = scalingFactor.getValue();
  60. if (VncViewer.os.startsWith("mac os x"))
  61. if (!noLionFS.getValue())
  62. enableLionFS();
  63. OptionsDialog.addCallback("handleOptions", this);
  64. addWindowFocusListener(new WindowAdapter() {
  65. public void windowGainedFocus(WindowEvent e) {
  66. if (isVisible())
  67. if (scroll.getViewport() != null)
  68. scroll.getViewport().getView().requestFocusInWindow();
  69. }
  70. public void windowLostFocus(WindowEvent e) {
  71. viewport.releaseDownKeys();
  72. }
  73. });
  74. addWindowListener(new WindowAdapter() {
  75. public void windowClosing(WindowEvent e) {
  76. cc.close();
  77. }
  78. public void windowDeiconified(WindowEvent e) {
  79. // ViewportBorder sometimes lost when window is shaded or de-iconified
  80. repositionViewport();
  81. }
  82. });
  83. addWindowStateListener(new WindowAdapter() {
  84. public void windowStateChanged(WindowEvent e) {
  85. int state = e.getNewState();
  86. if ((state & JFrame.MAXIMIZED_BOTH) != JFrame.MAXIMIZED_BOTH) {
  87. Rectangle b = getGraphicsConfiguration().getBounds();
  88. if (!b.contains(getLocationOnScreen()))
  89. setLocation((int)b.getX(), (int)b.getY());
  90. }
  91. // ViewportBorder sometimes lost when restoring on Windows
  92. repositionViewport();
  93. }
  94. });
  95. // Window resize events
  96. timer = new Timer(500, new AbstractAction() {
  97. public void actionPerformed(ActionEvent e) {
  98. handleResizeTimeout();
  99. }
  100. });
  101. timer.setRepeats(false);
  102. addComponentListener(new ComponentAdapter() {
  103. public void componentResized(ComponentEvent e) {
  104. if (remoteResize.getValue()) {
  105. if (timer.isRunning())
  106. timer.restart();
  107. else
  108. // Try to get the remote size to match our window size, provided
  109. // the following conditions are true:
  110. //
  111. // a) The user has this feature turned on
  112. // b) The server supports it
  113. // c) We're not still waiting for a chance to handle DesktopSize
  114. // d) We're not still waiting for startup fullscreen to kick in
  115. if (!firstUpdate && !delayedFullscreen &&
  116. remoteResize.getValue() && cc.server.supportsSetDesktopSize)
  117. timer.start();
  118. } else {
  119. String scaleString = scalingFactor.getValue();
  120. if (!scaleString.matches("^[0-9]+$")) {
  121. Dimension maxSize = getContentPane().getSize();
  122. if ((maxSize.width != viewport.scaledWidth) ||
  123. (maxSize.height != viewport.scaledHeight))
  124. viewport.setScaledSize(maxSize.width, maxSize.height);
  125. if (!scaleString.equals("Auto")) {
  126. if (!isMaximized() && !fullscreen_active()) {
  127. int dx = getInsets().left + getInsets().right;
  128. int dy = getInsets().top + getInsets().bottom;
  129. setSize(viewport.scaledWidth+dx, viewport.scaledHeight+dy);
  130. }
  131. }
  132. }
  133. repositionViewport();
  134. }
  135. }
  136. });
  137. }
  138. // Remove resize listener in order to prevent recursion when resizing
  139. @Override
  140. public void setSize(Dimension d)
  141. {
  142. ComponentListener[] listeners = getListeners(ComponentListener.class);
  143. for (ComponentListener l : listeners)
  144. removeComponentListener(l);
  145. super.setSize(d);
  146. for (ComponentListener l : listeners)
  147. addComponentListener(l);
  148. }
  149. @Override
  150. public void setSize(int width, int height)
  151. {
  152. ComponentListener[] listeners = getListeners(ComponentListener.class);
  153. for (ComponentListener l : listeners)
  154. removeComponentListener(l);
  155. super.setSize(width, height);
  156. for (ComponentListener l : listeners)
  157. addComponentListener(l);
  158. }
  159. @Override
  160. public void setBounds(Rectangle r)
  161. {
  162. ComponentListener[] listeners = getListeners(ComponentListener.class);
  163. for (ComponentListener l : listeners)
  164. removeComponentListener(l);
  165. super.setBounds(r);
  166. for (ComponentListener l : listeners)
  167. addComponentListener(l);
  168. }
  169. private void repositionViewport()
  170. {
  171. scroll.revalidate();
  172. Rectangle r = scroll.getViewportBorderBounds();
  173. int dx = r.width - viewport.scaledWidth;
  174. int dy = r.height - viewport.scaledHeight;
  175. int top = (int)Math.max(Math.floor(dy/2), 0);
  176. int left = (int)Math.max(Math.floor(dx/2), 0);
  177. int bottom = (int)Math.max(dy - top, 0);
  178. int right = (int)Math.max(dx - left, 0);
  179. Insets insets = new Insets(top, left, bottom, right);
  180. scroll.setViewportBorder(new MatteBorder(insets, Color.BLACK));
  181. scroll.revalidate();
  182. }
  183. public PixelFormat getPreferredPF()
  184. {
  185. return viewport.getPreferredPF();
  186. }
  187. public void setName(String name)
  188. {
  189. setTitle(name);
  190. }
  191. // Copy the areas of the framebuffer that have been changed (damaged)
  192. // to the displayed window.
  193. public void updateWindow()
  194. {
  195. if (firstUpdate) {
  196. pack();
  197. if (fullScreen.getValue())
  198. fullscreen_on();
  199. else
  200. setVisible(true);
  201. if (maximize.getValue())
  202. setExtendedState(JFrame.MAXIMIZED_BOTH);
  203. if (cc.server.supportsSetDesktopSize && !desktopSize.getValue().equals("")) {
  204. // Hack: Wait until we're in the proper mode and position until
  205. // resizing things, otherwise we might send the wrong thing.
  206. if (delayedFullscreen)
  207. delayedDesktopSize = true;
  208. else
  209. handleDesktopSize();
  210. }
  211. firstUpdate = false;
  212. }
  213. viewport.updateWindow();
  214. }
  215. public void resizeFramebuffer(int new_w, int new_h)
  216. {
  217. if ((new_w == viewport.scaledWidth) && (new_h == viewport.scaledHeight))
  218. return;
  219. // If we're letting the viewport match the window perfectly, then
  220. // keep things that way for the new size, otherwise just keep things
  221. // like they are.
  222. int dx = getInsets().left + getInsets().right;
  223. int dy = getInsets().top + getInsets().bottom;
  224. if (!fullscreen_active()) {
  225. if ((w() == viewport.scaledWidth) && (h() == viewport.scaledHeight))
  226. setSize(new_w+dx, new_h+dy);
  227. else {
  228. // Make sure the window isn't too big. We do this manually because
  229. // we have to disable the window size restriction (and it isn't
  230. // entirely trustworthy to begin with).
  231. if ((w() > new_w) || (h() > new_h))
  232. setSize(Math.min(w(), new_w)+dx, Math.min(h(), new_h)+dy);
  233. }
  234. }
  235. viewport.resize(0, 0, new_w, new_h);
  236. // We might not resize the main window, so we need to manually call this
  237. // to make sure the viewport is centered.
  238. repositionViewport();
  239. // repositionViewport() makes sure the scroll widget notices any changes
  240. // in position, but it might be just the size that changes so we also
  241. // need a poke here as well.
  242. validate();
  243. }
  244. public void setCursor(int width, int height, Point hotspot,
  245. byte[] data)
  246. {
  247. viewport.setCursor(width, height, hotspot, data);
  248. }
  249. public void fullscreen_on()
  250. {
  251. fullScreen.setParam(true);
  252. lastState = getExtendedState();
  253. lastBounds = getBounds();
  254. dispose();
  255. // Screen bounds calculation affected by maximized window?
  256. setExtendedState(JFrame.NORMAL);
  257. setUndecorated(true);
  258. setVisible(true);
  259. setBounds(getScreenBounds());
  260. }
  261. public void fullscreen_off()
  262. {
  263. fullScreen.setParam(false);
  264. dispose();
  265. setUndecorated(false);
  266. setExtendedState(lastState);
  267. setBounds(lastBounds);
  268. setVisible(true);
  269. }
  270. public boolean fullscreen_active()
  271. {
  272. return isUndecorated();
  273. }
  274. private void handleDesktopSize()
  275. {
  276. if (!desktopSize.getValue().equals("")) {
  277. int width, height;
  278. // An explicit size has been requested
  279. if (desktopSize.getValue().split("x").length != 2)
  280. return;
  281. width = Integer.parseInt(desktopSize.getValue().split("x")[0]);
  282. height = Integer.parseInt(desktopSize.getValue().split("x")[1]);
  283. remoteResize(width, height);
  284. } else if (remoteResize.getValue()) {
  285. // No explicit size, but remote resizing is on so make sure it
  286. // matches whatever size the window ended up being
  287. remoteResize(w(), h());
  288. }
  289. }
  290. public void handleResizeTimeout()
  291. {
  292. DesktopWindow self = (DesktopWindow)this;
  293. assert(self != null);
  294. self.remoteResize(self.w(), self.h());
  295. }
  296. private void remoteResize(int width, int height)
  297. {
  298. ScreenSet layout;
  299. ListIterator<Screen> iter;
  300. if (!fullscreen_active() || (width > w()) || (height > h())) {
  301. // In windowed mode (or the framebuffer is so large that we need
  302. // to scroll) we just report a single virtual screen that covers
  303. // the entire framebuffer.
  304. layout = cc.server.screenLayout();
  305. // Not sure why we have no screens, but adding a new one should be
  306. // safe as there is nothing to conflict with...
  307. if (layout.num_screens() == 0)
  308. layout.add_screen(new Screen());
  309. else if (layout.num_screens() != 1) {
  310. // More than one screen. Remove all but the first (which we
  311. // assume is the "primary").
  312. while (true) {
  313. iter = layout.begin();
  314. Screen screen = iter.next();
  315. if (iter == layout.end())
  316. break;
  317. layout.remove_screen(screen.id);
  318. }
  319. }
  320. // Resize the remaining single screen to the complete framebuffer
  321. ((Screen)layout.begin().next()).dimensions.tl.x = 0;
  322. ((Screen)layout.begin().next()).dimensions.tl.y = 0;
  323. ((Screen)layout.begin().next()).dimensions.br.x = width;
  324. ((Screen)layout.begin().next()).dimensions.br.y = height;
  325. } else {
  326. layout = new ScreenSet();
  327. int id;
  328. int sx, sy, sw, sh;
  329. Rect viewport_rect = new Rect();
  330. Rect screen_rect = new Rect();
  331. // In full screen we report all screens that are fully covered.
  332. viewport_rect.setXYWH(x() + (w() - width)/2, y() + (h() - height)/2,
  333. width, height);
  334. // If we can find a matching screen in the existing set, we use
  335. // that, otherwise we create a brand new screen.
  336. //
  337. // FIXME: We should really track screens better so we can handle
  338. // a resized one.
  339. //
  340. GraphicsEnvironment ge =
  341. GraphicsEnvironment.getLocalGraphicsEnvironment();
  342. for (GraphicsDevice gd : ge.getScreenDevices()) {
  343. for (GraphicsConfiguration gc : gd.getConfigurations()) {
  344. Rectangle bounds = gc.getBounds();
  345. sx = bounds.x;
  346. sy = bounds.y;
  347. sw = bounds.width;
  348. sh = bounds.height;
  349. // Check that the screen is fully inside the framebuffer
  350. screen_rect.setXYWH(sx, sy, sw, sh);
  351. if (!screen_rect.enclosed_by(viewport_rect))
  352. continue;
  353. // Adjust the coordinates so they are relative to our viewport
  354. sx -= viewport_rect.tl.x;
  355. sy -= viewport_rect.tl.y;
  356. // Look for perfectly matching existing screen...
  357. for (iter = cc.server.screenLayout().begin();
  358. iter != cc.server.screenLayout().end(); iter.next()) {
  359. Screen screen = iter.next(); iter.previous();
  360. if ((screen.dimensions.tl.x == sx) &&
  361. (screen.dimensions.tl.y == sy) &&
  362. (screen.dimensions.width() == sw) &&
  363. (screen.dimensions.height() == sh))
  364. break;
  365. }
  366. // Found it?
  367. if (iter != cc.server.screenLayout().end()) {
  368. layout.add_screen(iter.next());
  369. continue;
  370. }
  371. // Need to add a new one, which means we need to find an unused id
  372. Random rng = new Random();
  373. while (true) {
  374. id = rng.nextInt();
  375. for (iter = cc.server.screenLayout().begin();
  376. iter != cc.server.screenLayout().end(); iter.next()) {
  377. Screen screen = iter.next(); iter.previous();
  378. if (screen.id == id)
  379. break;
  380. }
  381. if (iter == cc.server.screenLayout().end())
  382. break;
  383. }
  384. layout.add_screen(new Screen(id, sx, sy, sw, sh, 0));
  385. }
  386. // If the viewport doesn't match a physical screen, then we might
  387. // end up with no screens in the layout. Add a fake one...
  388. if (layout.num_screens() == 0)
  389. layout.add_screen(new Screen(0, 0, 0, width, height, 0));
  390. }
  391. }
  392. // Do we actually change anything?
  393. if ((width == cc.server.width()) &&
  394. (height == cc.server.height()) &&
  395. (layout == cc.server.screenLayout()))
  396. return;
  397. String buffer;
  398. vlog.debug(String.format("Requesting framebuffer resize from %dx%d to %dx%d",
  399. cc.server.width(), cc.server.height(), width, height));
  400. layout.debug_print();
  401. if (!layout.validate(width, height)) {
  402. vlog.error("Invalid screen layout computed for resize request!");
  403. return;
  404. }
  405. cc.writer().writeSetDesktopSize(width, height, layout);
  406. }
  407. boolean lionFSSupported() { return canDoLionFS; }
  408. private int x() { return getContentPane().getX(); }
  409. private int y() { return getContentPane().getY(); }
  410. private int w() { return getContentPane().getWidth(); }
  411. private int h() { return getContentPane().getHeight(); }
  412. void enableLionFS() {
  413. try {
  414. String version = System.getProperty("os.version");
  415. int firstDot = version.indexOf('.');
  416. int lastDot = version.lastIndexOf('.');
  417. if (lastDot > firstDot && lastDot >= 0) {
  418. version = version.substring(0, version.indexOf('.', firstDot + 1));
  419. }
  420. double v = Double.parseDouble(version);
  421. if (v < 10.7)
  422. throw new Exception("Operating system version is " + v);
  423. Class fsuClass = Class.forName("com.apple.eawt.FullScreenUtilities");
  424. Class argClasses[] = new Class[]{Window.class, Boolean.TYPE};
  425. Method setWindowCanFullScreen =
  426. fsuClass.getMethod("setWindowCanFullScreen", argClasses);
  427. setWindowCanFullScreen.invoke(fsuClass, this, true);
  428. canDoLionFS = true;
  429. } catch (Exception e) {
  430. vlog.debug("Could not enable OS X 10.7+ full-screen mode: " +
  431. e.getMessage());
  432. }
  433. }
  434. public void toggleLionFS() {
  435. try {
  436. Class appClass = Class.forName("com.apple.eawt.Application");
  437. Method getApplication = appClass.getMethod("getApplication",
  438. (Class[])null);
  439. Object app = getApplication.invoke(appClass);
  440. Method requestToggleFullScreen =
  441. appClass.getMethod("requestToggleFullScreen", Window.class);
  442. requestToggleFullScreen.invoke(app, this);
  443. } catch (Exception e) {
  444. vlog.debug("Could not toggle OS X 10.7+ full-screen mode: " +
  445. e.getMessage());
  446. }
  447. }
  448. public boolean isMaximized()
  449. {
  450. int state = getExtendedState();
  451. return ((state & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH);
  452. }
  453. public Dimension getScreenSize() {
  454. return getScreenBounds().getSize();
  455. }
  456. public Rectangle getScreenBounds() {
  457. GraphicsEnvironment ge =
  458. GraphicsEnvironment.getLocalGraphicsEnvironment();
  459. Rectangle r = new Rectangle();
  460. if (fullScreenAllMonitors.getValue()) {
  461. for (GraphicsDevice gd : ge.getScreenDevices())
  462. for (GraphicsConfiguration gc : gd.getConfigurations())
  463. r = r.union(gc.getBounds());
  464. } else {
  465. GraphicsConfiguration gc = getGraphicsConfiguration();
  466. r = gc.getBounds();
  467. }
  468. return r;
  469. }
  470. public static Window getFullScreenWindow() {
  471. GraphicsEnvironment ge =
  472. GraphicsEnvironment.getLocalGraphicsEnvironment();
  473. for (GraphicsDevice gd : ge.getScreenDevices()) {
  474. Window fullScreenWindow = gd.getFullScreenWindow();
  475. if (fullScreenWindow != null)
  476. return fullScreenWindow;
  477. }
  478. return null;
  479. }
  480. public static void setFullScreenWindow(Window fullScreenWindow) {
  481. GraphicsEnvironment ge =
  482. GraphicsEnvironment.getLocalGraphicsEnvironment();
  483. if (fullScreenAllMonitors.getValue()) {
  484. for (GraphicsDevice gd : ge.getScreenDevices())
  485. gd.setFullScreenWindow(fullScreenWindow);
  486. } else {
  487. GraphicsDevice gd = ge.getDefaultScreenDevice();
  488. gd.setFullScreenWindow(fullScreenWindow);
  489. }
  490. }
  491. public void handleOptions()
  492. {
  493. if (fullScreen.getValue() && !fullscreen_active())
  494. fullscreen_on();
  495. else if (!fullScreen.getValue() && fullscreen_active())
  496. fullscreen_off();
  497. if (remoteResize.getValue()) {
  498. scroll.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED);
  499. scroll.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_AS_NEEDED);
  500. remoteResize(w(), h());
  501. } else {
  502. String scaleString = scalingFactor.getValue();
  503. if (!scaleString.equals(lastScaleFactor)) {
  504. if (scaleString.matches("^[0-9]+$")) {
  505. scroll.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED);
  506. scroll.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_AS_NEEDED);
  507. viewport.setScaledSize(cc.server.width(), cc.server.height());
  508. } else {
  509. scroll.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_NEVER);
  510. scroll.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_NEVER);
  511. viewport.setScaledSize(w(), h());
  512. }
  513. if (isMaximized() || fullscreen_active()) {
  514. repositionViewport();
  515. } else {
  516. int dx = getInsets().left + getInsets().right;
  517. int dy = getInsets().top + getInsets().bottom;
  518. setSize(viewport.scaledWidth+dx, viewport.scaledHeight+dy);
  519. }
  520. repositionViewport();
  521. lastScaleFactor = scaleString;
  522. }
  523. }
  524. if (isVisible()) {
  525. toFront();
  526. requestFocus();
  527. }
  528. }
  529. public void handleFullscreenTimeout()
  530. {
  531. DesktopWindow self = (DesktopWindow)this;
  532. assert(self != null);
  533. self.delayedFullscreen = false;
  534. if (self.delayedDesktopSize) {
  535. self.handleDesktopSize();
  536. self.delayedDesktopSize = false;
  537. }
  538. }
  539. private CConn cc;
  540. private JScrollPane scroll;
  541. public Viewport viewport;
  542. private boolean firstUpdate;
  543. private boolean delayedFullscreen;
  544. private boolean delayedDesktopSize;
  545. private boolean canDoLionFS;
  546. private String lastScaleFactor;
  547. private Rectangle lastBounds;
  548. private int lastState;
  549. private Timer timer;
  550. }