View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2009  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program 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 program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  package davmail.ui.tray;
20  
21  import davmail.BundleMessage;
22  import davmail.DavGateway;
23  import davmail.Settings;
24  import davmail.ui.AboutFrame;
25  import davmail.ui.SettingsFrame;
26  import davmail.util.IOUtil;
27  import org.apache.log4j.Level;
28  import org.apache.log4j.Logger;
29  import org.eclipse.swt.SWT;
30  import org.eclipse.swt.graphics.DeviceData;
31  import org.eclipse.swt.graphics.GC;
32  import org.eclipse.swt.graphics.Image;
33  import org.eclipse.swt.graphics.ImageData;
34  import org.eclipse.swt.internal.gtk.OS;
35  import org.eclipse.swt.widgets.Display;
36  import org.eclipse.swt.widgets.Menu;
37  import org.eclipse.swt.widgets.MenuItem;
38  import org.eclipse.swt.widgets.Shell;
39  import org.eclipse.swt.widgets.ToolTip;
40  import org.eclipse.swt.widgets.Tray;
41  import org.eclipse.swt.widgets.TrayItem;
42  
43  import javax.swing.*;
44  import java.io.ByteArrayInputStream;
45  import java.io.IOException;
46  import java.net.URL;
47  import java.util.ArrayList;
48  
49  /**
50   * Tray icon handler based on SWT
51   */
52  public class SwtGatewayTray implements DavGatewayTrayInterface {
53      private static final Logger LOGGER = Logger.getLogger(SwtGatewayTray.class);
54  
55      private static final Object LOCK = new Object();
56  
57      protected SwtGatewayTray() {
58      }
59  
60      SettingsFrame settingsFrame;
61      AboutFrame aboutFrame;
62  
63      private static TrayItem trayItem;
64      private static ArrayList<java.awt.Image> frameIcons;
65  
66      private static Image image;
67      private static Image image2;
68      private static Image inactiveImage;
69      private static Display display;
70      private static Shell shell;
71      private boolean isActive = true;
72      private boolean isReady;
73      private Error error;
74      private boolean firstMessage = true;
75  
76      /**
77       * Return AWT Image icon for frame title.
78       *
79       * @return frame icon
80       */
81      @Override
82      public java.util.List<java.awt.Image> getFrameIcons() {
83          return frameIcons;
84      }
85  
86      /**
87       * Switch tray icon between active and standby icon.
88       */
89      public void switchIcon() {
90          isActive = true;
91          display.syncExec(() -> {
92              Image currentImage = trayItem.getImage();
93              if (currentImage != null && currentImage.equals(image)) {
94                  trayItem.setImage(image2);
95              } else {
96                  trayItem.setImage(image);
97              }
98          });
99  
100     }
101 
102     /**
103      * Set tray icon to inactive (network down)
104      */
105     public void resetIcon() {
106         display.syncExec(() -> trayItem.setImage(image));
107     }
108 
109     /**
110      * Set tray icon to inactive (network down)
111      */
112     public void inactiveIcon() {
113         isActive = false;
114         display.syncExec(() -> trayItem.setImage(inactiveImage));
115     }
116 
117     /**
118      * Check if current tray status is inactive (network down).
119      *
120      * @return true if inactive
121      */
122     public boolean isActive() {
123         return isActive;
124     }
125 
126     /**
127      * Log and display balloon message according to log level.
128      *
129      * @param message text message
130      * @param level   log level
131      */
132     public void displayMessage(final String message, final Level level) {
133         if (trayItem != null) {
134             display.asyncExec(() -> {
135                 int messageType = 0;
136                 if (level.equals(Level.INFO)) {
137                     messageType = SWT.ICON_INFORMATION;
138                 } else if (level.equals(Level.WARN)) {
139                     messageType = SWT.ICON_WARNING;
140                 } else if (level.equals(Level.ERROR)) {
141                     messageType = SWT.ICON_ERROR;
142                 }
143                 if (messageType != 0) {
144                     final ToolTip toolTip = new ToolTip(shell, SWT.BALLOON | messageType);
145                     toolTip.setText(BundleMessage.format("UI_DAVMAIL_GATEWAY"));
146                     toolTip.setMessage(message);
147                     trayItem.setToolTip(toolTip);
148                     // Wait for tray init 1 second on first message
149                     if (firstMessage) {
150                         firstMessage = false;
151                         try {
152                             Thread.sleep(1000);
153                         } catch (InterruptedException e) {
154                             Thread.currentThread().interrupt();
155                         }
156                     }
157                     toolTip.setVisible(true);
158                 }
159                 trayItem.setToolTipText(BundleMessage.format("UI_DAVMAIL_GATEWAY") + '\n' + message);
160             });
161         }
162     }
163 
164     /**
165      * Load image with current class loader.
166      *
167      * @param fileName image resource file name
168      * @return image
169      */
170     public static Image loadSwtImage(String fileName) {
171         Image result = null;
172         try {
173             ClassLoader classloader = DavGatewayTray.class.getClassLoader();
174             URL imageUrl = classloader.getResource(fileName);
175             if (imageUrl == null) {
176                 throw new IOException(fileName);
177             }
178             byte[] imageContent = IOUtil.readFully(imageUrl.openStream());
179             Image tempImage = new Image(display, new ByteArrayInputStream(imageContent));
180             Image backgroundImage = new Image(null, 24, 24);
181             ImageData imageData = backgroundImage.getImageData();
182             imageData.transparentPixel = imageData.getPixel(0, 0);
183             backgroundImage.dispose();
184             result = new Image(null, imageData);
185 
186             GC gc = new GC(result);
187             gc.drawImage(tempImage, 4, 4);
188             tempImage.dispose();
189 
190         } catch (IOException e) {
191             DavGatewayTray.warn(new BundleMessage("LOG_UNABLE_TO_LOAD_IMAGE"), e);
192         }
193         return result;
194     }
195 
196     /**
197      * Create tray icon and register frame listeners.
198      */
199     public void init() {
200         boolean isGTK3;
201         // SWT 4.9 and later
202         try {
203             Class gtk = Class.forName("org.eclipse.swt.internal.gtk.GTK");
204             isGTK3 = (Boolean) gtk.getDeclaredField("GTK3").get(null);
205             LOGGER.debug("org.eclipse.swt.internal.gtk.GTK.GTK3="+isGTK3);
206             if (isGTK3) {
207                 LOGGER.warn("GTK 3 not supported, please set SWT_GTK3=0");
208             }
209         } catch (Throwable e) {
210             // ignore
211         }
212         try {
213             Class gdk = Class.forName("org.eclipse.swt.internal.gtk.GDK");
214             //noinspection unchecked
215             gdk.getDeclaredMethod("gdk_error_trap_push").invoke(null);
216             LOGGER.debug("Called org.eclipse.swt.internal.gtk.GDK.gdk_error_trap_push");
217         } catch (Throwable e) {
218             // ignore
219         }
220 
221         try {
222             //noinspection JavaReflectionMemberAccess
223             OS.class.getDeclaredMethod("gdk_error_trap_push").invoke(null);
224             LOGGER.debug("Called org.eclipse.swt.internal.gtk.OS.gdk_error_trap_push");
225         } catch (Exception e) {
226             // ignore
227         }
228         try {
229             // workaround for bug when SWT and AWT both try to access Gtk
230             UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
231         } catch (Exception e) {
232             DavGatewayTray.warn(new BundleMessage("LOG_UNABLE_TO_SET_LOOK_AND_FEEL"));
233         }
234 
235         new Thread("SWT") {
236             @Override
237             public void run() {
238                 try {
239                     DeviceData data = new DeviceData();
240                     data.debug = true;
241                     display = new Display(data);
242                     shell = new Shell(display);
243 
244                     final Tray tray = display.getSystemTray();
245                     if (tray != null) {
246 
247                         trayItem = new TrayItem(tray, SWT.NONE);
248                         trayItem.setToolTipText(BundleMessage.format("UI_DAVMAIL_GATEWAY"));
249 
250                         frameIcons = new ArrayList<>();
251                         frameIcons.add(DavGatewayTray.loadImage(AwtGatewayTray.TRAY128_PNG));
252                         frameIcons.add(DavGatewayTray.loadImage(AwtGatewayTray.TRAY_PNG));
253 
254                         image = loadSwtImage(AwtGatewayTray.TRAY_PNG);
255                         image2 = loadSwtImage(AwtGatewayTray.TRAY_ACTIVE_PNG);
256                         inactiveImage = loadSwtImage(AwtGatewayTray.TRAY_INACTIVE_PNG);
257 
258                         trayItem.setImage(image);
259                         trayItem.addDisposeListener(e -> {
260                             if (image != null && !image.isDisposed()) {
261                                 image.dispose();
262                             }
263                             if (image2 != null && !image2.isDisposed()) {
264                                 image2.dispose();
265                             }
266                             if (inactiveImage != null && !inactiveImage.isDisposed()) {
267                                 inactiveImage.dispose();
268                             }
269                         });
270 
271                         // create a popup menu
272                         final Menu popup = new Menu(shell, SWT.POP_UP);
273                         trayItem.addListener(SWT.MenuDetect, event -> display.asyncExec(
274                                 () -> popup.setVisible(true)));
275 
276                         MenuItem aboutItem = new MenuItem(popup, SWT.PUSH);
277                         aboutItem.setText(BundleMessage.format("UI_ABOUT"));
278                         aboutItem.addListener(SWT.Selection, event -> SwingUtilities.invokeLater(
279                                 () -> {
280                                     if (aboutFrame == null) {
281                                         aboutFrame = new AboutFrame();
282                                     }
283                                     aboutFrame.update();
284                                     aboutFrame.setVisible(true);
285                                     aboutFrame.toFront();
286                                     aboutFrame.requestFocus();
287                                 }));
288 
289                         // create menu item for the default action
290                         trayItem.addListener(SWT.DefaultSelection, event -> SwingUtilities.invokeLater(
291                                 () -> openSettingsFrame()));
292 
293                         MenuItem defaultItem = new MenuItem(popup, SWT.PUSH);
294                         defaultItem.setText(BundleMessage.format("UI_SETTINGS"));
295                         defaultItem.addListener(SWT.Selection, event -> SwingUtilities.invokeLater(
296                                 () -> openSettingsFrame()));
297 
298                         MenuItem logItem = new MenuItem(popup, SWT.PUSH);
299                         logItem.setText(BundleMessage.format("UI_SHOW_LOGS"));
300                         logItem.addListener(SWT.Selection, event -> SwingUtilities.invokeLater(DavGatewayTray::showLogs));
301 
302                         MenuItem exitItem = new MenuItem(popup, SWT.PUSH);
303                         exitItem.setText(BundleMessage.format("UI_EXIT"));
304                         exitItem.addListener(SWT.Selection, event -> DavGateway.stop());
305 
306                         // display settings frame on first start
307                         if (Settings.isFirstStart()) {
308                             SwingUtilities.invokeLater(() -> {
309                                 // create frame on first call
310                                 if (settingsFrame == null) {
311                                     settingsFrame = new SettingsFrame();
312                                 }
313                                 settingsFrame.setVisible(true);
314                                 settingsFrame.toFront();
315                                 settingsFrame.requestFocus();
316                             });
317 
318                         }
319 
320                         synchronized (LOCK) {
321                             // ready
322                             isReady = true;
323                             LOCK.notifyAll();
324                         }
325 
326                         while (!shell.isDisposed()) {
327                             if (!display.readAndDispatch()) {
328                                 display.sleep();
329                             }
330                         }
331                     }
332                     // dispose AWT frames
333                     if (settingsFrame != null) {
334                         settingsFrame.dispose();
335                     }
336                     if (aboutFrame != null) {
337                         aboutFrame.dispose();
338                     }
339                 } catch (Exception exc) {
340                     DavGatewayTray.error(exc);
341                 } catch (Error exc) {
342                     error = exc;
343                     throw exc;
344                 }
345                 // make sure we do exit
346                 System.exit(0);
347             }
348         }.start();
349         while (true) {
350             // wait for SWT init
351             try {
352                 synchronized (LOCK) {
353                     if (error != null) {
354                         throw error;
355                     }
356                     if (isReady) {
357                         break;
358                     }
359                     LOCK.wait(1000);
360                 }
361             } catch (InterruptedException e) {
362                 DavGatewayTray.error(new BundleMessage("LOG_ERROR_WAITING_FOR_SWT_INIT"), e);
363                 Thread.currentThread().interrupt();
364             }
365         }
366     }
367 
368     private void openSettingsFrame() {
369         // create frame on first call
370         if (settingsFrame == null) {
371             settingsFrame = new SettingsFrame();
372         }
373         settingsFrame.reload();
374         settingsFrame.setVisible(true);
375         settingsFrame.toFront();
376         settingsFrame.requestFocus();
377     }
378 
379     public void dispose() {
380         shell.dispose();
381     }
382 
383 }