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