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