org.syncany.gui.tray.DefaultTrayIcon.java Source code

Java tutorial

Introduction

Here is the source code for org.syncany.gui.tray.DefaultTrayIcon.java

Source

/*
 * Syncany, www.syncany.org
 * Copyright (C) 2011-2013 Philipp C. Heckel <philipp.heckel@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.syncany.gui.tray;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.ToolTip;
import org.eclipse.swt.widgets.Tray;
import org.eclipse.swt.widgets.TrayItem;
import org.syncany.gui.util.I18n;
import org.syncany.gui.util.SWTResourceManager;
import org.syncany.util.EnvironmentUtil;

import com.google.common.collect.Maps;

/**
 * The default tray icon uses the default SWT {@link TrayItem}
 * class and the {@link Menu} to display the tray icon.
 *
 * <p>These classes are supported by all operating systems and
 * desktop environment,  except Ubuntu/Unity.
 *
 * @author Philipp C. Heckel <philipp.heckel@gmail.com>
 * @author Vincent Wiencek <vwiencek@gmail.com>
 */
public class DefaultTrayIcon extends TrayIcon {
    private static final String STATUS_TEXT_GLOBAL_IDENTIFIER = "GLOBAL";
    private static final String STATUS_TEXT_FOLDER_FORMAT = (EnvironmentUtil.isWindows()) ? "(%s) %s" : "%s\n%s";

    protected TrayItem trayItem;
    protected String trayImageResourceRoot;

    private Menu menu;
    private MenuItem addFolderMenuItem;
    private MenuItem recentFileChangesItem;
    private MenuItem browseHistoryMenuItem;

    private List<File> watches;
    private Map<String, MenuItem> watchedFolderMenuItems;

    private Map<String, String> statusTexts;
    private Map<String, MenuItem> statusTextItems;

    private Map<TrayIconImage, Image> images;

    public DefaultTrayIcon(final Shell shell, final TrayIconTheme theme) {
        super(shell, theme);

        this.trayItem = null;
        this.menu = null;

        this.addFolderMenuItem = null;

        this.watches = Collections.synchronizedList(new ArrayList<File>());
        this.watchedFolderMenuItems = Maps.newConcurrentMap();

        this.recentFileChangesItem = null;

        this.statusTexts = Maps.newConcurrentMap();
        this.statusTextItems = Maps.newConcurrentMap();

        this.images = null;

        setTrayImageResourcesRoot();
        fillImageCache();
        buildTray();
    }

    protected void setTrayImageResourcesRoot() {
        trayImageResourceRoot = "/" + DefaultTrayIcon.class.getPackage().getName().replace(".", "/") + "/"
                + getTheme().toString().toLowerCase() + "/";
    }

    private void fillImageCache() {
        images = new HashMap<TrayIconImage, Image>();

        for (TrayIconImage trayIconImage : TrayIconImage.values()) {
            String trayImageFileName = trayImageResourceRoot + trayIconImage.getFileName();
            Image trayImage = SWTResourceManager.getImage(trayImageFileName);

            images.put(trayIconImage, trayImage);
        }
    }

    private void buildTray() {
        Tray tray = Display.getDefault().getSystemTray();

        if (tray != null) {
            trayItem = new TrayItem(tray, SWT.NONE);
            setTrayImage(TrayIconImage.TRAY_NO_OVERLAY);

            buildMenuItems(null);
            addMenuListeners();
        }
    }

    private void addMenuListeners() {
        Listener showMenuListener = new Listener() {
            public void handleEvent(Event event) {
                menu.setVisible(true);
            }
        };

        trayItem.addListener(SWT.MenuDetect, showMenuListener);

        if (!EnvironmentUtil.isUnixLikeOperatingSystem()) {
            // Tray icon popup menu positioning in Linux is off,
            // Disable it for now.

            trayItem.addListener(SWT.Selection, showMenuListener);
        }
    }

    private void buildMenuItems(final List<File> newWatches) {
        watches.clear();

        if (newWatches != null) {
            watches.addAll(newWatches);
        }

        if (menu == null) {
            menu = new Menu(trayShell, SWT.POP_UP);
        }

        clearMenuItems();

        buildStatusTextMenuItems();
        buildAddFolderMenuItem();
        buildOrUpdateRecentChangesMenuItems();
        buildWatchMenuItems();
        buildStaticMenuItems();
    }

    private void buildStatusTextMenuItems() {
        // Create per-folder status text item
        for (String root : statusTexts.keySet()) {
            String statusText = statusTexts.get(root);
            updateFolderStatusTextItem(root, statusText);
        }

        // Add or hide global status text
        resetGlobalStatusTextItem();
    }

    private void buildAddFolderMenuItem() {
        new MenuItem(menu, SWT.SEPARATOR);

        MenuItem addFolderMenuItem = new MenuItem(menu, SWT.PUSH);
        addFolderMenuItem.setText(I18n.getText("org.syncany.gui.tray.TrayIcon.menu.new"));
        addFolderMenuItem.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                showNew();
            }
        });

        browseHistoryMenuItem = new MenuItem(menu, SWT.PUSH);
        browseHistoryMenuItem.setText(I18n.getText("org.syncany.gui.tray.TrayIcon.menu.browse"));
        browseHistoryMenuItem.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                showBrowseHistory();
            }
        });
    }

    private synchronized void buildOrUpdateRecentChangesMenuItems() {
        if (recentFileChangesItem != null && !recentFileChangesItem.isDisposed()) {
            updateRecentFileChangesMenuItems();
        } else {
            buildRecentFileChangesMenuItems();
        }
    }

    private void updateRecentFileChangesMenuItems() {
        if (recentFileChanges.size() > 0) {
            Menu recentFileChangesSubMenu = recentFileChangesItem.getMenu();

            // Clear old items from submenu
            for (MenuItem recentFileChangesSubMenuItem : recentFileChangesSubMenu.getItems()) {
                recentFileChangesSubMenuItem.dispose();
            }

            // Add items to old submenu
            updateRecentFileChangesSubMenu(recentFileChangesSubMenu);
        } else {
            recentFileChangesItem.dispose();
        }
    }

    private void buildRecentFileChangesMenuItems() {
        if (recentFileChanges.size() > 0) {
            // Create new 'Recent changes >' item, and submenu
            recentFileChangesItem = new MenuItem(menu, SWT.CASCADE, findAddFolderMenuItemIndex());
            recentFileChangesItem.setText(I18n.getText("org.syncany.gui.tray.TrayIcon.menu.recentChanges"));

            Menu recentChangesSubMenu = new Menu(menu);
            recentFileChangesItem.setMenu(recentChangesSubMenu);

            // Add items to submenu
            updateRecentFileChangesSubMenu(recentChangesSubMenu);
        }
    }

    private void updateRecentFileChangesSubMenu(Menu recentFileChangesSubMenu) {
        for (final File recentFile : recentFileChanges.getRecentFiles()) {
            MenuItem recentFileItem = new MenuItem(recentFileChangesSubMenu, SWT.PUSH);
            recentFileItem.setText(recentFile.getName().replaceAll("&", "&&"));

            recentFileItem.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    showRecentFile(recentFile);
                }
            });
        }
    }

    private int findAddFolderMenuItemIndex() {
        for (int i = 0; i < menu.getItemCount(); i++) {
            MenuItem menuItem = menu.getItem(i);

            if (menuItem.equals(addFolderMenuItem)) {
                return i + 1;
            }
        }

        return 4; // Guessing.
    }

    private void buildWatchMenuItems() {
        new MenuItem(menu, SWT.SEPARATOR);

        if (watches.size() > 0) {
            for (final File folder : watches) {
                if (!watchedFolderMenuItems.containsKey(folder.getAbsolutePath())) {
                    if (folder.exists()) {
                        // Menu item for folder  (with submenu)
                        MenuItem folderMenuItem = new MenuItem(menu, SWT.CASCADE);
                        folderMenuItem.setText(folder.getName());

                        Menu folderSubMenu = new Menu(menu);
                        folderMenuItem.setMenu(folderSubMenu);

                        // Menu item for 'Remove'
                        MenuItem folderOpenMenuItem = new MenuItem(folderSubMenu, SWT.PUSH);
                        folderOpenMenuItem.setText(I18n.getText("org.syncany.gui.tray.TrayIcon.menu.open"));
                        folderOpenMenuItem.addSelectionListener(new SelectionAdapter() {
                            @Override
                            public void widgetSelected(SelectionEvent e) {
                                showFolder(folder);
                            }
                        });

                        // Menu item for 'Copy link'
                        MenuItem folderCopyLinkMenuItem = new MenuItem(folderSubMenu, SWT.PUSH);
                        folderCopyLinkMenuItem.setText(I18n.getText("org.syncany.gui.tray.TrayIcon.menu.copyLink"));
                        folderCopyLinkMenuItem.addSelectionListener(new SelectionAdapter() {
                            @Override
                            public void widgetSelected(SelectionEvent e) {
                                copyLink(folder);
                            }
                        });

                        // Menu item for 'Remove'
                        MenuItem folderRemoveMenuItem = new MenuItem(folderSubMenu, SWT.PUSH);
                        folderRemoveMenuItem.setText(I18n.getText("org.syncany.gui.tray.TrayIcon.menu.remove"));
                        folderRemoveMenuItem.addSelectionListener(new SelectionAdapter() {
                            @Override
                            public void widgetSelected(SelectionEvent e) {
                                removeFolder(folder);
                            }
                        });

                        watchedFolderMenuItems.put(folder.getAbsolutePath(), folderMenuItem);
                    }
                }
            }

            for (String filePath : watchedFolderMenuItems.keySet()) {
                boolean removeFilePath = true;

                for (File file : watches) {
                    if (file.getAbsolutePath().equals(filePath)) {
                        removeFilePath = false;
                    }
                }

                if (removeFilePath) {
                    watchedFolderMenuItems.get(filePath).dispose();
                    watchedFolderMenuItems.keySet().remove(filePath);
                }
            }

            new MenuItem(menu, SWT.SEPARATOR);
        }
    }

    private void buildStaticMenuItems() {
        MenuItem preferencesItem = new MenuItem(menu, SWT.PUSH);
        preferencesItem.setText(I18n.getText("org.syncany.gui.tray.TrayIcon.menu.preferences"));
        preferencesItem.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                showPreferences();
            }
        });

        new MenuItem(menu, SWT.SEPARATOR);

        MenuItem reportIssueItem = new MenuItem(menu, SWT.PUSH);
        reportIssueItem.setText(I18n.getText("org.syncany.gui.tray.TrayIcon.menu.issue"));
        reportIssueItem.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                showReportIssue();
            }
        });

        MenuItem donateItem = new MenuItem(menu, SWT.PUSH);
        donateItem.setText(I18n.getText("org.syncany.gui.tray.TrayIcon.menu.donate"));
        donateItem.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                showDonate();
            }
        });

        MenuItem websiteItem = new MenuItem(menu, SWT.PUSH);
        websiteItem.setText(I18n.getText("org.syncany.gui.tray.TrayIcon.menu.website"));
        websiteItem.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                showWebsite();
            }
        });

        new MenuItem(menu, SWT.SEPARATOR);

        MenuItem exitMenu = new MenuItem(menu, SWT.PUSH);
        exitMenu.setText(I18n.getText("org.syncany.gui.tray.TrayIcon.menu.exit"));
        exitMenu.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                exitApplication();
            }
        });
    }

    private void clearMenuItems() {
        if (menu != null) {
            // Dispose of SWT menu items
            while (menu.getItems().length > 0) {
                MenuItem item = menu.getItem(0);
                item.dispose();
            }

            // Clear menu item cache
            watchedFolderMenuItems.clear();
            statusTextItems.clear();
        }
    }

    @Override
    public void setWatchedFolders(final List<File> folders) {
        Display.getDefault().asyncExec(new Runnable() {
            public void run() {
                buildMenuItems(folders);
            }
        });
    }

    @Override
    public void setStatusText(final String root, final String statusText) {
        Display.getDefault().asyncExec(new Runnable() {
            public void run() {
                logger.log(Level.INFO, "setStatusText(" + root + ", " + statusText + ")");

                if (root != null) {
                    updateFolderStatusTextItem(root, statusText);
                } else {
                    clearStatusTextItems();
                }

                resetGlobalStatusTextItem();
            }
        });
    }

    private void clearStatusTextItems() {
        statusTexts.clear();

        synchronized (statusTextItems) {
            Iterator<String> rootIterator = statusTextItems.keySet().iterator();

            while (rootIterator.hasNext()) {
                String root = rootIterator.next();
                MenuItem statusTextItem = statusTextItems.remove(root);

                statusTextItem.dispose();
            }
        }
    }

    private void updateFolderStatusTextItem(String root, String statusText) {
        String inSyncStatusText = I18n.getText("org.syncany.gui.tray.TrayIcon.insync");

        MenuItem statusTextItem = statusTextItems.get(root);
        boolean watchIsInSync = statusText.equals(inSyncStatusText);

        if (watchIsInSync) {
            statusTexts.remove(root);
            statusTextItems.remove(root);

            if (statusTextItem != null) {
                statusTextItem.dispose();
            }
        } else {
            statusTexts.put(root, statusText);

            String statusTextPrefix = new File(root).getName();
            String fullStatusText = String.format(STATUS_TEXT_FOLDER_FORMAT, statusTextPrefix, statusText);

            if (statusTextItem != null) {
                statusTextItem.setText(fullStatusText);
            } else {
                statusTextItem = new MenuItem(menu, SWT.PUSH, 0);
                statusTextItem.setText(fullStatusText);
                statusTextItem.setEnabled(false);

                statusTextItems.put(root, statusTextItem);
            }
        }
    }

    private void resetGlobalStatusTextItem() {
        MenuItem globalStatusTextItem = statusTextItems.get(STATUS_TEXT_GLOBAL_IDENTIFIER);
        boolean otherStatusTextItemsVisible = statusTexts.size() > 0;

        if (otherStatusTextItemsVisible) {
            if (globalStatusTextItem != null && !globalStatusTextItem.isDisposed()) {
                globalStatusTextItem.dispose();
            }
        } else {
            if (globalStatusTextItem == null || globalStatusTextItem.isDisposed()) {
                MenuItem statusTextItem = new MenuItem(menu, SWT.PUSH, 0);
                statusTextItem.setText(I18n.getText("org.syncany.gui.tray.TrayIcon.insync"));
                statusTextItem.setEnabled(false);

                statusTextItems.put(STATUS_TEXT_GLOBAL_IDENTIFIER, statusTextItem);
            }
        }
    }

    @Override
    protected void setTrayImage(final TrayIconImage trayIconImage) {
        Display.getDefault().asyncExec(new Runnable() {
            public void run() {
                trayItem.setImage(images.get(trayIconImage));
            }
        });
    }

    @Override
    protected void displayNotification(final String subject, final String message) {
        Display.getDefault().asyncExec(new Runnable() {
            public void run() {
                ToolTip toolTip = new ToolTip(trayShell, SWT.BALLOON | SWT.ICON_INFORMATION);

                toolTip.setText(subject);
                toolTip.setMessage(message);

                trayItem.setImage(images.get(TrayIconImage.TRAY_NO_OVERLAY));
                trayItem.setToolTip(toolTip);

                toolTip.setVisible(true);
                toolTip.setAutoHide(true);
            }
        });
    }

    @Override
    protected void setRecentChanges(List<File> newRecentChangesFiles) {
        Display.getDefault().asyncExec(new Runnable() {
            public void run() {
                buildOrUpdateRecentChangesMenuItems();
            }
        });
    }

    @Override
    protected void dispose() {
        trayItem.dispose();
    }
}