/*
* 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();
}
}