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 org.apache.log4j.Level;
27  import org.apache.log4j.Logger;
28  import org.eclipse.swt.SWT;
29  import org.eclipse.swt.graphics.GC;
30  import org.eclipse.swt.graphics.Image;
31  import org.eclipse.swt.graphics.ImageData;
32  import org.eclipse.swt.graphics.PaletteData;
33  import org.eclipse.swt.internal.Library;
34  import org.eclipse.swt.widgets.Display;
35  import org.eclipse.swt.widgets.Menu;
36  import org.eclipse.swt.widgets.MenuItem;
37  import org.eclipse.swt.widgets.Shell;
38  import org.eclipse.swt.widgets.ToolTip;
39  import org.eclipse.swt.widgets.Tray;
40  import org.eclipse.swt.widgets.TrayItem;
41  
42  import javax.imageio.ImageIO;
43  import javax.swing.*;
44  import java.awt.image.BufferedImage;
45  import java.io.ByteArrayInputStream;
46  import java.io.ByteArrayOutputStream;
47  import java.io.IOException;
48  import java.io.InputStream;
49  import java.net.URL;
50  import java.util.ArrayList;
51  import java.util.concurrent.locks.Condition;
52  import java.util.concurrent.locks.ReentrantLock;
53  
54  /**
55   * Tray icon handler based on SWT
56   */
57  public class SwtGatewayTray implements DavGatewayTrayInterface {
58      private static final Logger LOGGER = Logger.getLogger(SwtGatewayTray.class);
59  
60      private static final ReentrantLock lock = new ReentrantLock();
61      private static final Condition ready = lock.newCondition();
62  
63      protected SwtGatewayTray() {
64      }
65  
66      static SettingsFrame settingsFrame;
67      static AboutFrame aboutFrame;
68  
69      private static TrayItem trayItem;
70      private static ArrayList<java.awt.Image> frameIcons;
71  
72      private static Image image;
73      private static Image image2;
74      private static Image inactiveImage;
75      private static Display display;
76      private static Shell shell;
77      private boolean isActive = true;
78      private static boolean isReady = false;
79      private static Error error;
80      private boolean firstMessage = true;
81  
82      public static void initDisplay() {
83          if (!isReady) {
84              // ready
85              // start main loop, shell can be null before init
86              // dispose AWT frames
87              Thread swtThread = new Thread("SWT") {
88                  @Override
89                  public void run() {
90                      try {
91                          LOGGER.info("Detected SWT "+ Library.getVersionString()+", init display");
92                          Display.setAppName("davmail");
93                          display = Display.getDefault();
94                          shell = new Shell(display);
95  
96                          // ready, notify waiting thread
97                          lock.lock();
98                          try {
99                              isReady = true;
100                             ready.signalAll();
101                         } finally {
102                             lock.unlock();
103                         }
104 
105                         // start main loop, shell can be null before init
106                         while (!shell.isDisposed()) {
107                             if (!display.readAndDispatch()) {
108                                 display.sleep();
109                             }
110                         }
111                         // dispose AWT frames
112                         if (settingsFrame != null) {
113                             settingsFrame.dispose();
114                         }
115                         if (aboutFrame != null) {
116                             aboutFrame.dispose();
117                         }
118                         System.exit(0);
119                     } catch (Throwable e) {
120                         LOGGER.error("Error in SWT thread", e);
121                         lock.lock();
122                         try {
123                             error = new Error(e);
124                             ready.signalAll();
125                         } finally {
126                             lock.unlock();
127                         }
128                     }
129                 }
130             };
131             swtThread.start();
132 
133             lock.lock();
134             try {
135                 while (!isReady) {
136                     ready.await();
137                     if (error != null) {
138                         throw error;
139                     }
140                 }
141             } catch (InterruptedException e) {
142                 Thread.currentThread().interrupt();
143             } finally {
144                 lock.unlock();
145             }
146         }
147     }
148 
149     /**
150      * Return AWT Image icon for frame title.
151      *
152      * @return frame icon
153      */
154     @Override
155     public java.util.List<java.awt.Image> getFrameIcons() {
156         return frameIcons;
157     }
158 
159     /**
160      * Switch tray icon between active and standby icon.
161      */
162     public void switchIcon() {
163         isActive = true;
164         display.syncExec(() -> {
165             Image currentImage = trayItem.getImage();
166             if (currentImage != null && currentImage.equals(image)) {
167                 trayItem.setImage(image2);
168             } else {
169                 trayItem.setImage(image);
170             }
171         });
172 
173     }
174 
175     /**
176      * Set tray icon to inactive (network down)
177      */
178     public void resetIcon() {
179         display.syncExec(() -> trayItem.setImage(image));
180     }
181 
182     /**
183      * Set tray icon to inactive (network down)
184      */
185     public void inactiveIcon() {
186         isActive = false;
187         display.syncExec(() -> trayItem.setImage(inactiveImage));
188     }
189 
190     /**
191      * Check if current tray status is inactive (network down).
192      *
193      * @return true if inactive
194      */
195     public boolean isActive() {
196         return isActive;
197     }
198 
199     /**
200      * Log and display balloon message according to log level.
201      *
202      * @param message text message
203      * @param level   log level
204      */
205     public void displayMessage(final String message, final Level level) {
206         if (trayItem != null) {
207             display.asyncExec(() -> {
208                 int messageType = 0;
209                 if (level.equals(Level.INFO)) {
210                     messageType = SWT.ICON_INFORMATION;
211                 } else if (level.equals(Level.WARN)) {
212                     messageType = SWT.ICON_WARNING;
213                 } else if (level.equals(Level.ERROR)) {
214                     messageType = SWT.ICON_ERROR;
215                 }
216                 if (messageType != 0) {
217                     final ToolTip toolTip = new ToolTip(shell, SWT.BALLOON | messageType);
218                     toolTip.setText(BundleMessage.format("UI_DAVMAIL_GATEWAY"));
219                     toolTip.setMessage(message);
220                     trayItem.setToolTip(toolTip);
221                     // Wait for tray init 1 second on first message
222                     if (firstMessage) {
223                         firstMessage = false;
224                         try {
225                             Thread.sleep(1000);
226                         } catch (InterruptedException e) {
227                             Thread.currentThread().interrupt();
228                         }
229                     }
230                     toolTip.setVisible(true);
231                 }
232                 trayItem.setToolTipText(BundleMessage.format("UI_DAVMAIL_GATEWAY") + '\n' + message);
233             });
234         }
235     }
236 
237     /**
238      * Load image with current class loader.
239      * No scaling
240      *
241      * @param fileName image resource file name
242      * @return image
243      */
244     public static Image loadSwtImage(String fileName) {
245         Image result = null;
246         try {
247             ClassLoader classloader = DavGatewayTray.class.getClassLoader();
248             URL imageUrl = classloader.getResource(fileName);
249             if (imageUrl == null) {
250                 throw new IOException("fileName");
251             }
252             try (InputStream inputStream = imageUrl.openStream()) {
253                 result = new Image(display, inputStream);
254 
255             }
256 
257         } catch (IOException e) {
258             DavGatewayTray.warn(new BundleMessage("LOG_UNABLE_TO_LOAD_IMAGE"), e);
259         }
260         return result;
261     }
262 
263     /**
264      * Load image with current class loader.
265      * Scale to size
266      *
267      * @param fileName image resource file name
268      * @param targetSize     target image size
269      * @return image
270      */
271     public static Image loadSwtImage(String fileName, int targetSize) {
272         int padding = 0;
273         Image result = null;
274         try {
275             ClassLoader classloader = DavGatewayTray.class.getClassLoader();
276             URL imageUrl = classloader.getResource(fileName);
277             if (imageUrl == null) {
278                 throw new IOException(fileName);
279             }
280             BufferedImage bufferedImage;
281             if (Settings.getBooleanProperty("davmail.trayGrayscale", false)) {
282                 bufferedImage = DavGatewayTray.convertGrayscale(ImageIO.read(imageUrl));
283             } else {
284                 bufferedImage = ImageIO.read(imageUrl);
285             }
286             if (bufferedImage == null) {
287                 throw new IOException(fileName);
288             }
289             if (bufferedImage.getWidth() != targetSize || bufferedImage.getHeight() != targetSize) {
290                 bufferedImage = DavGatewayTray.scaleImage(bufferedImage, targetSize);
291             }
292             byte[] imageBytes;
293             try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
294                 ImageIO.write(bufferedImage, "png", os);
295                 imageBytes = os.toByteArray();
296             }
297 
298             try (InputStream inputStream = new ByteArrayInputStream(imageBytes)) {
299                 Image loadedImage = new Image(display, inputStream);
300 
301                 ImageData resultImageData = new ImageData(targetSize, targetSize, 24, new PaletteData(0xff0000, 0x00ff00, 0x0000ff));
302                 // force init alpha channel
303                 resultImageData.setAlpha(0, 0, 0);
304 
305                 result = new Image(display, resultImageData);
306 
307                 // drow loaded image over transparent image
308                 GC gc = new GC(result);
309                 gc.setAntialias(SWT.ON);
310                 gc.setInterpolation(SWT.HIGH);
311                 gc.drawImage(loadedImage, 0, 0, loadedImage.getBounds().width, loadedImage.getBounds().height,
312                         padding, padding, result.getBounds().width - padding, result.getBounds().height - padding);
313                 gc.dispose();
314                 loadedImage.dispose();
315             }
316 
317         } catch (IOException e) {
318             DavGatewayTray.warn(new BundleMessage("LOG_UNABLE_TO_LOAD_IMAGE"), e);
319         }
320         return result;
321     }
322 
323     protected static void setDefaultIconImage(Image image) {
324         SwtGatewayTray.image = image;
325     }
326 
327     protected static void setActiveIconImage(Image image) {
328         SwtGatewayTray.image2 = image;
329     }
330 
331     protected static void setInactiveIconImage(Image image) {
332         SwtGatewayTray.inactiveImage = image;
333     }
334 
335     /**
336      * Create tray icon and register frame listeners.
337      */
338     public void init() {
339         try {
340             // workaround for bug when SWT and AWT both try to access Gtk
341             UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
342         } catch (Exception e) {
343             DavGatewayTray.warn(new BundleMessage("LOG_UNABLE_TO_SET_LOOK_AND_FEEL"));
344         }
345 
346         initDisplay();
347 
348         display.asyncExec(() -> {
349             try {
350                 final Tray tray = display.getSystemTray();
351                 if (tray != null) {
352 
353                     trayItem = new TrayItem(tray, SWT.NONE);
354                     trayItem.setToolTipText(BundleMessage.format("UI_DAVMAIL_GATEWAY"));
355 
356                     frameIcons = new ArrayList<>();
357                     frameIcons.add(DavGatewayTray.adjustTrayIcon(DavGatewayTray.loadImage(AwtGatewayTray.TRAY128_PNG)));
358                     frameIcons.add(DavGatewayTray.adjustTrayIcon(DavGatewayTray.loadImage(AwtGatewayTray.TRAY_PNG)));
359 
360                     // assume 24 pixels default icon size
361                     int trayIconSize = 24;
362                     if (Settings.isWindows()) {
363                         trayIconSize = 16;
364                     }
365 
366                     SwtGatewayTray.setDefaultIconImage(loadSwtImage("tray128.png", trayIconSize));
367                     SwtGatewayTray.setActiveIconImage(loadSwtImage("tray128active.png", trayIconSize));
368                     SwtGatewayTray.setInactiveIconImage(loadSwtImage("tray128inactive.png", trayIconSize));
369 
370                     trayItem.setImage(image);
371                     trayItem.addDisposeListener(e -> {
372                         if (image != null && !image.isDisposed()) {
373                             image.dispose();
374                         }
375                         if (image2 != null && !image2.isDisposed()) {
376                             image2.dispose();
377                         }
378                         if (inactiveImage != null && !inactiveImage.isDisposed()) {
379                             inactiveImage.dispose();
380                         }
381                     });
382 
383                     // create a popup menu
384                     final Menu popup = new Menu(shell, SWT.POP_UP);
385                     trayItem.addListener(SWT.MenuDetect, event -> display.asyncExec(
386                             () -> popup.setVisible(true)));
387 
388                     MenuItem aboutItem = new MenuItem(popup, SWT.PUSH);
389                     aboutItem.setText(BundleMessage.format("UI_ABOUT"));
390                     aboutItem.addListener(SWT.Selection, event -> SwingUtilities.invokeLater(
391                             () -> {
392                                 if (aboutFrame == null) {
393                                     aboutFrame = new AboutFrame();
394                                 }
395                                 aboutFrame.update();
396                                 aboutFrame.setVisible(true);
397                                 aboutFrame.toFront();
398                                 aboutFrame.requestFocus();
399                             }));
400 
401                     // create menu item for the default action
402                     trayItem.addListener(SWT.DefaultSelection, event -> SwingUtilities.invokeLater(
403                             this::openSettingsFrame));
404 
405                     MenuItem defaultItem = new MenuItem(popup, SWT.PUSH);
406                     defaultItem.setText(BundleMessage.format("UI_SETTINGS"));
407                     defaultItem.addListener(SWT.Selection, event -> SwingUtilities.invokeLater(
408                             this::openSettingsFrame));
409 
410                     MenuItem exitItem = new MenuItem(popup, SWT.PUSH);
411                     exitItem.setText(BundleMessage.format("UI_EXIT"));
412                     exitItem.addListener(SWT.Selection, event -> DavGateway.stop());
413 
414                     // display settings frame on first start
415                     if (Settings.isFirstStart()) {
416                         SwingUtilities.invokeLater(() -> {
417                             // create frame on first call
418                             if (settingsFrame == null) {
419                                 settingsFrame = new SettingsFrame();
420                             }
421                             settingsFrame.setVisible(true);
422                             settingsFrame.toFront();
423                             settingsFrame.requestFocus();
424                         });
425 
426                     }
427 
428                 }
429             } catch (Exception exc) {
430                 DavGatewayTray.error(exc);
431             } catch (Error exc) {
432                 error = exc;
433                 throw exc;
434             }
435         });
436     }
437 
438     private void openSettingsFrame() {
439         // create frame on first call
440         if (settingsFrame == null) {
441             settingsFrame = new SettingsFrame();
442         }
443         settingsFrame.reload();
444         settingsFrame.setVisible(true);
445         settingsFrame.toFront();
446         settingsFrame.requestFocus();
447     }
448 
449     public void dispose() {
450         shell.dispose();
451     }
452 
453 }