diff options
author | Ingo Bauersachs <ingo@jitsi.org> | 2017-01-10 02:04:00 +0100 |
---|---|---|
committer | Ingo Bauersachs <ingo@jitsi.org> | 2017-01-10 02:05:03 +0100 |
commit | 11d0c16f315806ed7cce19dfe235a1cd7823214f (patch) | |
tree | 070f25f7ece6f5555e2240599d5fc2624ef5faf1 /src | |
parent | 0b278bf6748cb32b74c1731b83ebe1c0158bfaa7 (diff) | |
download | jitsi-11d0c16f315806ed7cce19dfe235a1cd7823214f.zip jitsi-11d0c16f315806ed7cce19dfe235a1cd7823214f.tar.gz jitsi-11d0c16f315806ed7cce19dfe235a1cd7823214f.tar.bz2 |
Add support for AppIndicators
See #192
Doesn't properly work on Debian because of outdated/mismatching GTK
dependencies. Fixed in Ubuntu Launchpad bug #1203888, but not imported
in Debian. See bug #850769.
Diffstat (limited to 'src')
13 files changed, 1147 insertions, 61 deletions
diff --git a/src/net/java/sip/communicator/impl/osdependent/jdic/StatusSubMenu.java b/src/net/java/sip/communicator/impl/osdependent/jdic/StatusSubMenu.java index 2d77621..92088fa 100644 --- a/src/net/java/sip/communicator/impl/osdependent/jdic/StatusSubMenu.java +++ b/src/net/java/sip/communicator/impl/osdependent/jdic/StatusSubMenu.java @@ -66,7 +66,7 @@ public class StatusSubMenu * @param swing <tt>true</tt> to represent this instance with a Swing * <tt>JMenu</tt>; <tt>false</tt> to use an AWT <tt>Menu</tt> */ - public StatusSubMenu(boolean swing) + public StatusSubMenu(boolean swing, boolean accountMenuSupported) { String text = Resources.getString("impl.systray.SET_STATUS"); @@ -86,6 +86,7 @@ public class StatusSubMenu this.menu = new Menu(text); } + if (accountMenuSupported) { String hideAccountStatusSelectorsProperty = "impl.gui.HIDE_ACCOUNT_STATUS_SELECTORS"; @@ -103,6 +104,10 @@ public class StatusSubMenu hideAccountStatusSelectorsProperty, hideAccountStatusSelectors); } + else + { + hideAccountStatusSelectors = true; + } PresenceStatus offlineStatus = null; // creates menu item entry for every global status @@ -116,9 +121,11 @@ public class StatusSubMenu // initially it is offline selectItemFromStatus(offlineStatus.getStatus()); - this.addSeparator(); - - addMenuItem(menu, new GlobalStatusMessageMenu(swing).getMenu()); + if (accountMenuSupported) + { + this.addSeparator(); + addMenuItem(menu, new GlobalStatusMessageMenu(swing).getMenu()); + } if(!hideAccountStatusSelectors) this.addSeparator(); diff --git a/src/net/java/sip/communicator/impl/osdependent/jdic/SystrayServiceJdicImpl.java b/src/net/java/sip/communicator/impl/osdependent/jdic/SystrayServiceJdicImpl.java index 6fd9d8c..fd9d43e 100644 --- a/src/net/java/sip/communicator/impl/osdependent/jdic/SystrayServiceJdicImpl.java +++ b/src/net/java/sip/communicator/impl/osdependent/jdic/SystrayServiceJdicImpl.java @@ -35,6 +35,7 @@ import net.java.sip.communicator.service.systray.*; import net.java.sip.communicator.service.systray.event.*; import net.java.sip.communicator.util.Logger; +import org.apache.commons.lang3.tuple.Pair; import org.jitsi.util.*; import org.osgi.framework.*; @@ -185,47 +186,34 @@ public class SystrayServiceJdicImpl return; } - menu = TrayMenuFactory.createTrayMenu(this, systray.useSwingPopupMenu()); + Pair<Object, Object> createdMenu = TrayMenuFactory.createTrayMenu( + this, + systray.useSwingPopupMenu(), + systray.supportsDynamicMenu()); + menu = createdMenu.getLeft(); boolean isMac = OSUtils.IS_MAC; - // If we're running under Windows, we use a special icon without - // background. - if (OSUtils.IS_WINDOWS) - { - logoIcon = Resources.getImage("service.systray.TRAY_ICON_WINDOWS"); - logoIconOffline = Resources.getImage( - "service.systray.TRAY_ICON_WINDOWS_OFFLINE"); - logoIconAway = Resources.getImage( - "service.systray.TRAY_ICON_WINDOWS_AWAY"); - logoIconExtendedAway = Resources.getImage( - "service.systray.TRAY_ICON_WINDOWS_EXTENDED_AWAY"); - logoIconFFC = Resources.getImage( - "service.systray.TRAY_ICON_WINDOWS_FFC"); - logoIconDND = Resources.getImage( - "service.systray.TRAY_ICON_WINDOWS_DND"); - } - /* - * If we're running under Mac OS X, we use special black and white icons - * without background. - */ - else if (isMac) + logoIcon = Resources.getImage("service.systray.TRAY_ICON_WINDOWS"); + logoIconOffline = Resources.getImage( + "service.systray.TRAY_ICON_WINDOWS_OFFLINE"); + logoIconAway = Resources.getImage( + "service.systray.TRAY_ICON_WINDOWS_AWAY"); + logoIconExtendedAway = Resources.getImage( + "service.systray.TRAY_ICON_WINDOWS_EXTENDED_AWAY"); + logoIconFFC = Resources.getImage( + "service.systray.TRAY_ICON_WINDOWS_FFC"); + logoIconDND = Resources.getImage( + "service.systray.TRAY_ICON_WINDOWS_DND"); + + // If we're running under Mac OS X, we use special black and white + // icons without background. + if (isMac) { logoIcon = Resources.getImage("service.systray.TRAY_ICON_MACOSX"); logoIconWhite = Resources.getImage( "service.systray.TRAY_ICON_MACOSX_WHITE"); } - else - { - logoIcon = Resources.getImage("service.systray.TRAY_ICON"); - logoIconOffline = Resources.getImage( - "service.systray.TRAY_ICON_OFFLINE"); - logoIconAway = Resources.getImage("service.systray.TRAY_ICON_AWAY"); - logoIconExtendedAway = Resources.getImage( - "service.systray.TRAY_ICON_EXTENDED_AWAY"); - logoIconFFC = Resources.getImage("service.systray.TRAY_ICON_FFC"); - logoIconDND = Resources.getImage("service.systray.TRAY_ICON_DND"); - } /* * Default to set offline , if any protocols become online will set it @@ -259,21 +247,15 @@ public class SystrayServiceJdicImpl } //Show/hide the contact list when user clicks on the systray. - trayIcon.addActionListener( - new ActionListener() - { - public void actionPerformed(ActionEvent e) - { - UIService uiService - = OsDependentActivator.getUIService(); - ExportedWindow mainWindow - = uiService.getExportedWindow( - ExportedWindow.MAIN_WINDOW); - boolean setIsVisible = !mainWindow.isVisible(); - - uiService.setVisible(setIsVisible); - } - }); + final Object defaultActionItem; + if (systray.useSwingPopupMenu()) + { + defaultActionItem = ((JMenuItem) createdMenu.getRight()); + } + else + { + defaultActionItem = ((MenuItem) createdMenu.getRight()); + } /* * Change the Mac OS X icon with the white one when the pop-up menu @@ -336,6 +318,7 @@ public class SystrayServiceJdicImpl public void run() { systray.addTrayIcon(trayIcon); + trayIcon.setDefaultAction(defaultActionItem); } }); diff --git a/src/net/java/sip/communicator/impl/osdependent/jdic/TrayMenuFactory.java b/src/net/java/sip/communicator/impl/osdependent/jdic/TrayMenuFactory.java index c13a505..05fe771 100644 --- a/src/net/java/sip/communicator/impl/osdependent/jdic/TrayMenuFactory.java +++ b/src/net/java/sip/communicator/impl/osdependent/jdic/TrayMenuFactory.java @@ -26,6 +26,7 @@ import javax.swing.event.*; import net.java.sip.communicator.impl.osdependent.*; import net.java.sip.communicator.service.gui.*; +import org.apache.commons.lang3.tuple.*; import org.jitsi.util.*; /** @@ -139,12 +140,16 @@ public final class TrayMenuFactory * * @param tray the system tray for which we're creating a menu * @param swing indicates if we should create a Swing or an AWT menu - * @return a tray menu for the given system tray + * @return a tray menu for the given system tray (first) and the default + * menu item (second) */ - public static Object createTrayMenu(SystrayServiceJdicImpl tray, - boolean swing) + public static Pair<Object, Object> createTrayMenu( + SystrayServiceJdicImpl tray, + boolean swing, + boolean accountMenuSupported + ) { - Object trayMenu = swing ? new JPopupMenu() : new PopupMenu(); + final Object trayMenu = swing ? new JPopupMenu() : new PopupMenu(); ActionListener listener = new ActionListener() { public void actionPerformed(ActionEvent event) @@ -182,7 +187,9 @@ public final class TrayMenuFactory if (!chatPresenceDisabled.booleanValue()) { - add(trayMenu, new StatusSubMenu(swing).getMenu()); + add( + trayMenu, + new StatusSubMenu(swing, accountMenuSupported).getMenu()); addSeparator(trayMenu); } @@ -197,9 +204,11 @@ public final class TrayMenuFactory showHideIconId = "service.gui.icons.SEARCH_ICON_16x16"; } else + { showHideName = "service.gui.SHOW"; showHideTextId = "service.gui.SHOW"; showHideIconId = "service.gui.icons.SEARCH_ICON_16x16"; + } final Object showHideMenuItem = createTrayMenuItem( showHideName, showHideTextId, @@ -241,7 +250,7 @@ public final class TrayMenuFactory } }); - return trayMenu; + return Pair.of(trayMenu, showHideMenuItem); } /** diff --git a/src/net/java/sip/communicator/impl/osdependent/osdependent.manifest.mf b/src/net/java/sip/communicator/impl/osdependent/osdependent.manifest.mf index bc56371..91f2791 100644 --- a/src/net/java/sip/communicator/impl/osdependent/osdependent.manifest.mf +++ b/src/net/java/sip/communicator/impl/osdependent/osdependent.manifest.mf @@ -27,6 +27,7 @@ Import-Package: com.apple.cocoa.application, net.java.sip.communicator.plugin.desktoputil.presence,
javax.accessibility,
javax.imageio,
+ javax.imageio.stream,
javax.swing,
javax.swing.border,
javax.swing.event,
@@ -39,6 +40,7 @@ Import-Package: com.apple.cocoa.application, javax.swing.text.html,
javax.swing.tree,
javax.swing.undo,
+ org.apache.commons.lang3.tuple,
org.jitsi.service.configuration,
org.jitsi.service.resources,
org.jitsi.util,
diff --git a/src/net/java/sip/communicator/impl/osdependent/systemtray/SystemTray.java b/src/net/java/sip/communicator/impl/osdependent/systemtray/SystemTray.java index 125a190..eeffd29 100644 --- a/src/net/java/sip/communicator/impl/osdependent/systemtray/SystemTray.java +++ b/src/net/java/sip/communicator/impl/osdependent/systemtray/SystemTray.java @@ -19,13 +19,22 @@ package net.java.sip.communicator.impl.osdependent.systemtray; import javax.swing.*; +import org.jitsi.util.*; + +import net.java.sip.communicator.impl.osdependent.*; +import net.java.sip.communicator.impl.osdependent.systemtray.appindicator.*; import net.java.sip.communicator.impl.osdependent.systemtray.awt.*; +import net.java.sip.communicator.util.Logger; /** * Base class for all wrappers of <tt>SystemTray</tt> implementations. */ public abstract class SystemTray { + private static final String PNMAE_DISABLE_TRY = + "net.java.sip.communicator.osdependent.systemtray.DISABLE"; + + private static final Logger logger = Logger.getLogger(SystemTray.class); private static SystemTray systemTray; /** @@ -34,8 +43,28 @@ public abstract class SystemTray */ public final static SystemTray getSystemTray() { + boolean disable = OsDependentActivator.getConfigurationService() + .getBoolean(PNMAE_DISABLE_TRY, false); + if (disable) + { + return null; + } + if (systemTray == null) { + if (OSUtils.IS_LINUX) + { + try + { + systemTray = new AppIndicatorTray(); + return systemTray; + } + catch(Exception ex) + { + logger.info(ex.getMessage()); + } + } + if (java.awt.SystemTray.isSupported()) { systemTray = new AWTSystemTray(); @@ -75,4 +104,11 @@ public abstract class SystemTray * <tt>PopupMenu</tt> */ public abstract boolean useSwingPopupMenu(); + + /** + * Determines if the tray icon supports dynamic menus. + * + * @return True if the menu can be changed while running, false otherwise. + */ + public abstract boolean supportsDynamicMenu(); } diff --git a/src/net/java/sip/communicator/impl/osdependent/systemtray/TrayIcon.java b/src/net/java/sip/communicator/impl/osdependent/systemtray/TrayIcon.java index 39fa6bd..78df44c 100644 --- a/src/net/java/sip/communicator/impl/osdependent/systemtray/TrayIcon.java +++ b/src/net/java/sip/communicator/impl/osdependent/systemtray/TrayIcon.java @@ -29,7 +29,7 @@ import javax.swing.*; */ public interface TrayIcon { - public void addActionListener(ActionListener listener); + public void setDefaultAction(Object menuItem); public void addBalloonActionListener(ActionListener listener); diff --git a/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/AppIndicator1.java b/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/AppIndicator1.java new file mode 100644 index 0000000..85fbd73 --- /dev/null +++ b/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/AppIndicator1.java @@ -0,0 +1,189 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.java.sip.communicator.impl.osdependent.systemtray.appindicator; + +import java.util.Arrays; +import java.util.List; + +import com.sun.jna.*; + +/** + * JNA mappings for libappindicator1. + * + * @author Ingo Bauersachs + */ +interface AppIndicator1 extends Library +{ + static final AppIndicator1 INSTANCE = + (AppIndicator1) Native.loadLibrary("appindicator", AppIndicator1.class); + + static final String APP_INDICATOR_SIGNAL_NEW_ICON = "new-icon"; + static final String APP_INDICATOR_SIGNAL_NEW_ATTENTION_ICON = "new-attention-icon"; + static final String APP_INDICATOR_SIGNAL_NEW_STATUS = "new-status"; + static final String APP_INDICATOR_SIGNAL_NEW_LABEL = "new-label"; + static final String APP_INDICATOR_SIGNAL_CONNECTION_CHANGED = "connection-changed"; + static final String APP_INDICATOR_SIGNAL_NEW_ICON_THEME_PATH = "new-icon-theme-path"; + static final String APP_INDICATOR_SIGNAL_SCROLL_EVENT = "scroll-event"; + + /** + * The category provides grouping for the indicators so that users can find + * indicators that are similar together. + */ + enum APP_INDICATOR_CATEGORY + { + /** The indicator is used to display the status of the application. */ + APPLICATION_STATUS, + + /** The application is used for communication with other people. */ + COMMUNICATIONS, + + /** A system indicator relating to something in the user's system. */ + SYSTEM_SERVICES, + + /** An indicator relating to the user's hardware. */ + HARDWARE, + + /** + * Something not defined in this enum, please don't use unless you + * really need it. + */ + OTHER + } + + /** + * These are the states that the indicator can be on in the user's panel. + * The indicator by default starts in the state {@link #PASSIVE} and can be + * shown by setting it to {@link #ACTIVE}. + */ + enum APP_INDICATOR_STATUS + { + /** The indicator should not be shown to the user. */ + PASSIVE, + + /** The indicator should be shown in it's default state. */ + ACTIVE, + + /** The indicator should show it's attention icon. */ + ATTENTION + } + + class AppIndicatorClass extends Structure + { + // Parent + public /*Gobject.GObjectClass*/ Pointer parent_class; + + // DBus Signals + public Pointer new_icon; + public Pointer new_attention_icon; + public Pointer new_status; + public Pointer new_icon_theme_path; + public Pointer new_label; + + // Local Signals + public Pointer connection_changed; + public Pointer scroll_event; + public Pointer app_indicator_reserved_ats; + + // Overridable Functions + public Pointer fallback; + public Pointer unfallback; + + // Reserved + public Pointer app_indicator_reserved_1; + public Pointer app_indicator_reserved_2; + public Pointer app_indicator_reserved_3; + public Pointer app_indicator_reserved_4; + public Pointer app_indicator_reserved_5; + public Pointer app_indicator_reserved_6; + + @Override + protected List getFieldOrder() { + return Arrays.asList( + "parent_class", + "new_icon", + "new_attention_icon", + "new_status", + "new_icon_theme_path", + "new_label", + + "connection_changed", + "scroll_event", + "app_indicator_reserved_ats", + + "fallback", + "unfallback", + + "app_indicator_reserved_1", + "app_indicator_reserved_2", + "app_indicator_reserved_3", + "app_indicator_reserved_4", + "app_indicator_reserved_5", + "app_indicator_reserved_6"); + } + } + + class AppIndicator extends Structure + { + public /*Gobject.GObject*/ Pointer parent; + public Pointer priv; + + @Override + protected List getFieldOrder() + { + return Arrays.asList("parent", "priv"); + } + } + + // GObject Stuff + NativeLong app_indicator_get_type(); + AppIndicator app_indicator_new(String id, String icon_name, int category); + AppIndicator app_indicator_new_with_path(String id, String icon_name, int category, String icon_theme_path); + + // Set properties + void app_indicator_set_status(AppIndicator self, int status); + void app_indicator_set_attention_icon(AppIndicator self, String icon_name); + void app_indicator_set_attention_icon_full(AppIndicator self, String name, String icon_desc); + void app_indicator_set_menu(AppIndicator self, Pointer menu); + void app_indicator_set_icon(AppIndicator self, String icon_name); + void app_indicator_set_icon_full(AppIndicator self, String icon_name, String icon_desc); + void app_indicator_set_label(AppIndicator self, String label, String guide); + void app_indicator_set_icon_theme_path(AppIndicator self, String icon_theme_path); + void app_indicator_set_ordering_index(AppIndicator self, int ordering_index); + void app_indicator_set_secondary_activate_target(AppIndicator self, Pointer menuitem); + void app_indicator_set_title(AppIndicator self, String title); + + // Get properties + String app_indicator_get_id(AppIndicator self); + int app_indicator_get_category(AppIndicator self); + int app_indicator_get_status(AppIndicator self); + String app_indicator_get_icon(AppIndicator self); + String app_indicator_get_icon_desc(AppIndicator self); + String app_indicator_get_icon_theme_path(AppIndicator self); + String app_indicator_get_attention_icon(AppIndicator self); + String app_indicator_get_attention_icon_desc(AppIndicator self); + String app_indicator_get_title(AppIndicator self); + + Pointer app_indicator_get_menu(AppIndicator self); + String app_indicator_get_label(AppIndicator self); + String app_indicator_get_label_guide(AppIndicator self); + int app_indicator_get_ordering_index(AppIndicator self); + Pointer app_indicator_get_secondary_activate_target(AppIndicator self, Pointer widget); + + // Helpers + void app_indicator_build_menu_from_desktop(AppIndicator self, String desktop_file, String destkop_profile); +}
\ No newline at end of file diff --git a/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/AppIndicatorTray.java b/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/AppIndicatorTray.java new file mode 100644 index 0000000..3f4e267 --- /dev/null +++ b/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/AppIndicatorTray.java @@ -0,0 +1,94 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.java.sip.communicator.impl.osdependent.systemtray.appindicator; + +import javax.swing.*; + +import org.jitsi.util.*; + +import net.java.sip.communicator.impl.osdependent.*; +import net.java.sip.communicator.impl.osdependent.systemtray.*; +import net.java.sip.communicator.util.*; + +/** + * Jitsi system tray abstraction for libappindicator. + * + * @author Ingo Bauersachs + */ +public class AppIndicatorTray extends SystemTray +{ + private static final String PNMAE_APPINDICATOR_DISABLED = + "net.java.sip.communicator.osdependent.systemtray.appindicator.DISABLED"; + private static final String PNMAE_APPINDICATOR_DYNAMIC_MENU = + "net.java.sip.communicator.osdependent.systemtray.appindicator.DYNAMIC_MENU"; + + public AppIndicatorTray() throws Exception + { + boolean disable = OsDependentActivator.getConfigurationService() + .getBoolean(PNMAE_APPINDICATOR_DISABLED, false); + if (disable) + { + throw new Exception("AppIndicator is disabled"); + } + + if (!OSUtils.IS_LINUX) + { + throw new Exception("Not running Linux, AppIndicator1 is not available"); + } + + try + { + // pre-initialize the JNA libraries before attempting to use them + AppIndicator1.INSTANCE.toString(); + Gtk.INSTANCE.toString(); + Gobject.INSTANCE.toString(); + Gtk.INSTANCE.gtk_init(0, null); + } + catch (Throwable t) + { + throw new Exception("AppIndicator1 tray icon not available", t); + } + } + + @Override + public void addTrayIcon(TrayIcon trayIcon) + { + ((AppIndicatorTrayIcon) trayIcon).createTray(); + } + + @Override + public TrayIcon createTrayIcon(ImageIcon icon, String tooltip, Object popup) + { + + return new AppIndicatorTrayIcon(icon, tooltip, (JPopupMenu) popup); + } + + @Override + public boolean useSwingPopupMenu() + { + // we want icons + return true; + } + + @Override + public boolean supportsDynamicMenu() + { + return OsDependentActivator.getConfigurationService() + .getBoolean(PNMAE_APPINDICATOR_DYNAMIC_MENU, true); + } +} diff --git a/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/AppIndicatorTrayIcon.java b/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/AppIndicatorTrayIcon.java new file mode 100644 index 0000000..8779312 --- /dev/null +++ b/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/AppIndicatorTrayIcon.java @@ -0,0 +1,596 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.java.sip.communicator.impl.osdependent.systemtray.appindicator; + +import java.awt.*; +import java.awt.TrayIcon.*; +import java.awt.event.*; +import java.awt.image.BufferedImage; +import java.beans.*; +import java.io.*; +import java.net.*; +import java.nio.file.*; +import java.util.*; +import java.util.List; +import java.util.Timer; + +import javax.accessibility.*; +import javax.imageio.*; +import javax.imageio.stream.*; +import javax.print.attribute.standard.*; +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import org.jitsi.util.*; +import org.jitsi.util.Logger; + +import com.sun.jna.*; + +import net.java.sip.communicator.impl.osdependent.*; +import net.java.sip.communicator.impl.osdependent.systemtray.*; +import net.java.sip.communicator.impl.osdependent.systemtray.TrayIcon; +import net.java.sip.communicator.impl.osdependent.systemtray.appindicator.Gobject.*; +import net.java.sip.communicator.util.*; + +/** + * System tray icon implementation based on libappindicator1. + * + * @author Ingo Bauersachs + */ +class AppIndicatorTrayIcon implements TrayIcon +{ + private static final Logger logger = + Logger.getLogger(AppIndicatorTrayIcon.class); + + // shortcuts + private static Gobject gobject = Gobject.INSTANCE; + private static Gtk gtk = Gtk.INSTANCE; + private static AppIndicator1 ai = AppIndicator1.INSTANCE; + + // references to the root menu and the native icon + private ImageIcon mainIcon; + private String title; + private JPopupMenu popup; + private Map<String, String> extractedFiles = new HashMap<>(); + private PopupMenuPeer popupPeer; + private AppIndicator1.AppIndicator appIndicator; + + public AppIndicatorTrayIcon(ImageIcon mainIcon, String title, + JPopupMenu popup) + { + this.mainIcon = mainIcon; + this.title = title; + this.popup = popup; + this.popupPeer = null; + } + + /** + * Combines the references of each swing menu item with the GTK counterpart + */ + private class PopupMenuPeer implements ContainerListener + { + public PopupMenuPeer(PopupMenuPeer parent, Component em) + { + menuItem = em; + + // if this menu item is a submenu, add ourselves as listener to + // add or remove the native counterpart + if (em instanceof JMenu) + { + ((JMenu)em).getPopupMenu().addContainerListener(this); + ((JMenu)em).addContainerListener(this); + } + } + + public List<PopupMenuPeer> children = new ArrayList<>(); + public Pointer gtkMenuItem; + public Pointer gtkMenu; + public Pointer gtkImage; + public Memory gtkImageBuffer; + public Pointer gtkPixbuf; + public Component menuItem; + public MenuItemSignalHandler signalHandler; + + @Override + public void componentAdded(ContainerEvent e) + { + AppIndicatorTrayIcon.this.printMenu(popup.getComponents(), 1); + gtk.gdk_threads_enter(); + createGtkMenuItems(this, new Component[]{e.getChild()}); + gtk.gtk_widget_show_all(popupPeer.gtkMenu); + gtk.gdk_threads_leave(); + } + + @Override + public void componentRemoved(ContainerEvent e) + { + AppIndicatorTrayIcon.this.printMenu(popup.getComponents(), 1); + for (PopupMenuPeer c : children) + { + if (c.menuItem == e.getChild()) + { + gtk.gdk_threads_enter(); + gtk.gtk_widget_destroy(c.gtkMenuItem); + gtk.gdk_threads_leave(); + cleanMenu(c); + break; + } + } + } + } + + public void createTray() + { + appIndicator = ai.app_indicator_new( + "jitsi", + "indicator-messages-new", + AppIndicator1.APP_INDICATOR_CATEGORY.COMMUNICATIONS.ordinal()); + + ai.app_indicator_set_title(appIndicator, title); + setupGtkMenu(); + + String path = imageIconToPath(mainIcon); + if (path != null) + { + ai.app_indicator_set_icon_full(appIndicator, path, "Jitsi"); + } + + ai.app_indicator_set_status( + appIndicator, + AppIndicator1.APP_INDICATOR_STATUS.ACTIVE.ordinal()); + + new Thread() + { + public void run() + { + gtk.gtk_main(); + } + }.start(); + } + + private void setupGtkMenu() + { + // create root menu + popupPeer = new PopupMenuPeer(null, popup); + popupPeer.gtkMenu = gtk.gtk_menu_new(); + + // transfer everything in the swing menu to the gtk menu + createGtkMenuItems(popupPeer, popup.getComponents()); + gtk.gtk_widget_show_all(popupPeer.gtkMenu); + + // attach the menu to the indicator + ai.app_indicator_set_menu(appIndicator, popupPeer.gtkMenu); + } + + private void cleanMenu(PopupMenuPeer peer) + { + for (PopupMenuPeer p : peer.children) + { + cleanMenu(p); + } + + // - the root menu is released when it's unset from the indicator + // - gtk auto-frees menu item, submenu, image, and pixbuf + // - the imagebuffer was jna allocated, GC should take care of freeing + peer.gtkImageBuffer = null; + removeListeners(peer); + } + + private void removeListeners(PopupMenuPeer peer) + { + if (peer.menuItem instanceof JMenu) + { + ((JMenu)peer.menuItem).removeContainerListener(peer); + ((JMenu)peer.menuItem).getPopupMenu().removeContainerListener(peer); + } + + for (PopupMenuPeer p : peer.children) + { + removeListeners(p); + } + } + + private void createGtkMenuItems( + PopupMenuPeer parent, + Component[] components) + { + for (Component em : components) + { + logger.debug("Creating item for " + em.getClass().getName()); + PopupMenuPeer peer = new PopupMenuPeer(parent, em); + if (em instanceof JPopupMenu.Separator) + { + peer.gtkMenuItem = gtk.gtk_separator_menu_item_new(); + } + + if (em instanceof JMenuItem) + { + JMenuItem m = (JMenuItem)em; + logger.debug(" title: " + m.getText()); + createGtkMenuItem(peer); + } + + if (em instanceof JMenu && peer.gtkMenuItem != null) + { + JMenu m = (JMenu)em; + logger.debug("Creating submenu on " + m.getText()); + peer.gtkMenu = gtk.gtk_menu_new(); + createGtkMenuItems(peer, m.getMenuComponents()); + gtk.gtk_menu_item_set_submenu(peer.gtkMenuItem, peer.gtkMenu); + } + + if (peer.gtkMenuItem != null) + { + parent.children.add(peer); + gtk.gtk_menu_shell_append(parent.gtkMenu, peer.gtkMenuItem); + } + } + } + + private void createGtkMenuItem(PopupMenuPeer peer) + { + JMenuItem m = (JMenuItem)peer.menuItem; + if (m instanceof JCheckBoxMenuItem) + { + peer.gtkMenuItem = gtk.gtk_check_menu_item_new_with_label( + m.getText()); + JCheckBoxMenuItem cb = (JCheckBoxMenuItem)m; + gtk.gtk_check_menu_item_set_active( + peer.gtkMenuItem, + cb.isSelected() ? 1 : 0); + } + else + { + peer.gtkMenuItem = gtk.gtk_image_menu_item_new_with_label( + m.getText()); + if (m.getIcon() instanceof ImageIcon) + { + ImageIcon ii = ((ImageIcon) m.getIcon()); + imageIconToGtkWidget(peer, ii); + if (peer.gtkImage != null) + { + gtk.gtk_image_menu_item_set_image( + peer.gtkMenuItem, + peer.gtkImage); + gtk.gtk_image_menu_item_set_always_show_image( + peer.gtkMenuItem, + 1); + } + } + } + + if (peer.gtkMenuItem == null) + { + logger.debug("Could not create menu item for " + m.getText()); + return; + } + + MenuItemChangeListener micl = new MenuItemChangeListener(peer); + m.addPropertyChangeListener(micl); + m.addChangeListener(micl); + + // skip GTK events if it's a submenu + if (!(m instanceof JMenu)) + { + gtk.gtk_widget_set_sensitive( + peer.gtkMenuItem, + m.isEnabled() ? 1 : 0); + peer.signalHandler = new MenuItemSignalHandler(peer); + gobject.g_signal_connect_data( + peer.gtkMenuItem, + "activate", + peer.signalHandler, + null, + null, + 0); + } + } + + private String imageIconToPath(ImageIcon ii) + { + if (ii.getDescription() != null) + { + String path = extractedFiles.get(ii.getDescription()); + if (path != null) + { + return path; + } + } + + try + { + File f = File.createTempFile("jitsi-appindicator", ".png"); + f.deleteOnExit(); + try (FileImageOutputStream fios = new FileImageOutputStream(f)) + { + if (!ImageIO.write(getBufferedImage(ii), "png", fios)) + { + return null; + } + + if (ii.getDescription() != null) + { + extractedFiles.put( + ii.getDescription(), + f.getAbsolutePath()); + } + + return f.getAbsolutePath(); + } + } + catch (IOException e) + { + logger.debug("Failed to extract image: " + ii.getDescription(), e); + } + + return null; + } + + BufferedImage getBufferedImage(ImageIcon ii) + { + Image img = ii.getImage(); + if (img == null) + { + return null; + } + + if (img instanceof BufferedImage) + { + return (BufferedImage) img; + } + + BufferedImage bi = new BufferedImage( + img.getWidth(null), + img.getHeight(null), + BufferedImage.TYPE_INT_ARGB); + Graphics g = bi.createGraphics(); + g.drawImage(img, 0, 0, null); + g.dispose(); + return bi; + } + + private void imageIconToGtkWidget(PopupMenuPeer peer, ImageIcon ii) + { + BufferedImage bi = getBufferedImage(ii); + if (bi == null) + { + return; + } + + int[] pixels = bi.getRGB( + 0, + 0, + bi.getWidth(), + bi.getHeight(), + null, + 0, + bi.getWidth()); + + peer.gtkImageBuffer = new Memory(pixels.length * 4); + for (int i = 0; i < pixels.length; i++) + { + // convert from argb (big endian) -> rgba (little endian) => abgr + peer.gtkImageBuffer.setInt(i * 4, (pixels[i] & 0xFF000000) | + (pixels[i] << 16) | + (pixels[i] & 0xFF00) | + (pixels[i] >>> 16 & 0xFF)); + } + + peer.gtkPixbuf = gtk.gdk_pixbuf_new_from_data( + peer.gtkImageBuffer, + 0, + 1, + 8, + bi.getWidth(), + bi.getHeight(), + bi.getWidth() * 4, + null, + null); + peer.gtkImage = gtk.gtk_image_new_from_pixbuf(peer.gtkPixbuf); + + // Now that the image ref's the buffer, we can release our own ref and + // the buffer will be free'd along with the image + gobject.g_object_unref(peer.gtkPixbuf); + } + + private static class MenuItemChangeListener + implements PropertyChangeListener, ChangeListener + { + private PopupMenuPeer peer; + private JMenuItem menu; + + public MenuItemChangeListener(PopupMenuPeer peer) + { + this.peer = peer; + this.menu = (JMenuItem)peer.menuItem; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) + { + if (logger.isDebugEnabled()) + { + logger.debug(menu.getText() + "::" + evt); + } + + switch (evt.getPropertyName()) + { + case JMenuItem.TEXT_CHANGED_PROPERTY: + gtk.gdk_threads_enter(); + gtk.gtk_menu_item_set_label( + peer.gtkMenuItem, + evt.getNewValue().toString()); + gtk.gdk_threads_leave(); + break; +// case JMenuItem.ICON_CHANGED_PROPERTY: +// gtk.gtk_image_menu_item_set_image(gtkMenuItem, image); +// break; + case AccessibleContext.ACCESSIBLE_STATE_PROPERTY: + gtk.gtk_widget_set_sensitive( + peer.gtkMenuItem, + AccessibleState.ENABLED.equals(evt.getNewValue()) ? 1 : 0); + break; + } + } + + @Override + public void stateChanged(ChangeEvent e) + { + logger.debug(menu.getText() + " -> " + menu.isSelected()); + gtk.gdk_threads_enter(); + gtk.gtk_check_menu_item_set_active( + peer.gtkMenuItem, + menu.isSelected() ? 1 : 0); + gtk.gdk_threads_leave(); + } + } + + private static class MenuItemSignalHandler + implements SignalHandler, Runnable + { + private PopupMenuPeer peer; + + MenuItemSignalHandler(PopupMenuPeer peer) + { + this.peer = peer; + } + + @Override + public void signal(Pointer widget, Pointer data) + { + SwingUtilities.invokeLater(this); + } + + @Override + public void run() + { + JMenuItem menu = (JMenuItem)peer.menuItem; + if (menu instanceof JCheckBoxMenuItem) + { + // Ignore GTK callback events if the menu state is + // already the same. Setting the selected state on the + // GTK sends the "activate" event, and would cause + // a loop + if (menu.isSelected() == isGtkSelected()) + { + return; + } + } + + for (ActionListener l : menu.getActionListeners()) + { + logger.debug("Invoking " + l + " on " + menu.getText()); + l.actionPerformed(new ActionEvent(menu, 0, "activate")); + } + } + + private boolean isGtkSelected() + { + return (gtk.gtk_check_menu_item_get_active(peer.gtkMenuItem) == 1); + } + } + + @Override + public void setDefaultAction(Object menuItem) + { + Pointer gtkMenuItem = findMenuItem(popupPeer, menuItem); + if (gtkMenuItem != null) + { + ai.app_indicator_set_secondary_activate_target( + appIndicator, + gtkMenuItem); + } + } + + private Pointer findMenuItem(PopupMenuPeer peer, Object menuItem) + { + if (peer.menuItem == menuItem) + { + return peer.gtkMenuItem; + } + + for (PopupMenuPeer p : peer.children) + { + Pointer found = findMenuItem(p, menuItem); + if (found != null) + { + return found; + } + } + + return null; + } + + @Override + public void addBalloonActionListener(ActionListener listener) + { + // not supported + } + + @Override + public void displayMessage(String caption, String text, + MessageType messageType) + { + // not supported + } + + @Override + public void setIcon(ImageIcon icon) throws NullPointerException + { + ai.app_indicator_set_icon_full( + appIndicator, + imageIconToPath(icon), + "Jitsi"); + } + + @Override + public void setIconAutoSize(boolean autoSize) + { + // nothing to do + } + + private void printMenu(Component[] components, int indent) + { + if (!logger.isDebugEnabled()) + { + return; + } + + String p = String.format("%0" + indent * 4 + "d", 0).replace('0', ' '); + for (Component em : components) + { + if (em instanceof JPopupMenu.Separator) + { + logger.debug(p + "-----------------------"); + } + + if (em instanceof JMenuItem) + { + JMenuItem m = (JMenuItem) em; + logger.debug(p + em.getClass().getName() + ": " + m.getText()); + } + + if (em instanceof JMenu) + { + JMenu m = (JMenu) em; + printMenu(m.getMenuComponents(), indent + 1); + } + } + } +};
\ No newline at end of file diff --git a/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/Gobject.java b/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/Gobject.java new file mode 100644 index 0000000..8a2349e --- /dev/null +++ b/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/Gobject.java @@ -0,0 +1,68 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.java.sip.communicator.impl.osdependent.systemtray.appindicator; + +import java.util.*; + +import com.sun.jna.*; + +/** + * JNA mappings for GTK GObject types that are required for the tray menu. + * + * @author Ingo Bauersachs + */ +interface Gobject extends Library +{ + static final Gobject INSTANCE = + (Gobject) Native.loadLibrary("gobject-2.0", Gobject.class); + + interface SignalHandler extends Callback + { + void signal(Pointer widget, Pointer data); + } + + /** + * Connects a GCallback function to a signal for a particular object. + * Similar to g_signal_connect(), but allows to provide a GClosureNotify for + * the data which will be called when the signal handler is disconnected and + * no longer used. Specify connect_flags if you need ..._after() or + * ..._swapped() variants of this function. + * + * @param instance the instance to connect to. + * @param detailed_signal a string of the form "signal-name::detail". + * @param c_handler the GCallback to connect. + * @param data data to pass to c_handler calls. + * @param destroy_data a GClosureNotify for data. + * @param connect_flags a combination of GConnectFlags. + */ + void g_signal_connect_data(Pointer instance, String detailed_signal, + SignalHandler c_handler, Pointer data, Pointer destroy_data, + int connect_flags); + + /** + * Decreases the reference count of object. When its reference count drops + * to 0, the object is finalized (i.e. its memory is freed). If the pointer + * to the GObject may be reused in future (for example, if it is an instance + * variable of another object), it is recommended to clear the pointer to + * NULL rather than retain a dangling pointer to a potentially invalid + * GObject instance. Use g_clear_object() for this. + * + * @param object a GObject. + */ + void g_object_unref(Pointer object); +} diff --git a/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/Gtk.java b/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/Gtk.java new file mode 100644 index 0000000..7b74428 --- /dev/null +++ b/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/Gtk.java @@ -0,0 +1,79 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.java.sip.communicator.impl.osdependent.systemtray.appindicator; + +import com.sun.jna.*; + +/** + * JNA mappings for the gtk2 library. Only functions required for the try menu + * are defined. + * + * @author Ingo Bauersachs + */ +interface Gtk extends Library +{ + static final Gtk INSTANCE = + (Gtk) Native.loadLibrary("gtk-x11-2.0", Gtk.class); + + public enum GtkIconSize + { + INVALID, + MENU, + SMALL_TOOLBAR, + LARGE_TOOLBAR, + BUTTON, + DND, + DIALOG + } + + void gtk_init(int argc, String[] argv); + void gtk_main(); + Pointer gtk_menu_new(); + Pointer gtk_menu_item_new(); + Pointer gtk_menu_item_new_with_label(String label); + Pointer gtk_image_menu_item_new_with_mnemonic(String label); + Pointer gtk_image_menu_item_new_with_label(String label); + Pointer gtk_image_new_from_gicon(Pointer icon, int size); + Pointer gtk_image_new_from_stock(String stock_id, int size); + Pointer gtk_image_new_from_icon_name(String icon_name, int size); + Pointer gtk_image_new_from_file(String filename); + Pointer gtk_separator_menu_item_new(); + void gtk_menu_item_set_submenu(Pointer menu_item, Pointer submenu); + void gtk_image_menu_item_set_image(Pointer image_menu_item, Pointer image); + void gtk_image_menu_item_set_always_show_image(Pointer image_menu_item, int always_show); + void gtk_menu_item_set_label(Pointer menu_item, String label); + void gtk_menu_shell_append(Pointer menu_shell, Pointer child); + void gtk_widget_set_sensitive(Pointer widget, int sesitive); + void gtk_widget_show_all(Pointer widget); + //void gtk_container_remove(Pointer container, Pointer widget); + void gtk_widget_destroy(Pointer widget); + void gtk_widget_show(Pointer widget); + Pointer gtk_check_menu_item_new_with_label(String label); + int gtk_check_menu_item_get_active(Pointer check_menu_item); + void gtk_check_menu_item_set_active(Pointer check_menu_item, int is_active); + + Pointer g_file_new_for_uri(String uri); + Pointer g_file_icon_new(Pointer file); + void gdk_threads_enter(); + void gdk_threads_leave(); + + Pointer gdk_pixbuf_new_from_data(Pointer data, int colorspace, int has_alpha, + int bits_per_sample, int width, int height, int rowstride, + Pointer destroy_fn, Pointer destroy_fn_data); + Pointer gtk_image_new_from_pixbuf(Pointer pixbuf); +} diff --git a/src/net/java/sip/communicator/impl/osdependent/systemtray/awt/AWTSystemTray.java b/src/net/java/sip/communicator/impl/osdependent/systemtray/awt/AWTSystemTray.java index 2fa25b3..1f211c7 100644 --- a/src/net/java/sip/communicator/impl/osdependent/systemtray/awt/AWTSystemTray.java +++ b/src/net/java/sip/communicator/impl/osdependent/systemtray/awt/AWTSystemTray.java @@ -69,4 +69,10 @@ public class AWTSystemTray // enable swing for Java 1.6 except for the mac version return !OSUtils.IS_MAC; } + + @Override + public boolean supportsDynamicMenu() + { + return true; + } }
\ No newline at end of file diff --git a/src/net/java/sip/communicator/impl/osdependent/systemtray/awt/AWTTrayIcon.java b/src/net/java/sip/communicator/impl/osdependent/systemtray/awt/AWTTrayIcon.java index 54d97ec..0aef71b 100644 --- a/src/net/java/sip/communicator/impl/osdependent/systemtray/awt/AWTTrayIcon.java +++ b/src/net/java/sip/communicator/impl/osdependent/systemtray/awt/AWTTrayIcon.java @@ -63,9 +63,26 @@ public class AWTTrayIcon } } - public void addActionListener(ActionListener listener) + public void setDefaultAction(Object menuItem) { - impl.addActionListener(listener); + ActionListener[] listeners; + if (menuItem instanceof JMenuItem) + { + listeners = ((JMenuItem) menuItem).getActionListeners(); + } + else if (menuItem instanceof MenuItem) + { + listeners = ((MenuItem) menuItem).getActionListeners(); + } + else + { + return; + } + + for (ActionListener l : listeners) + { + impl.addActionListener(l); + } } public void addBalloonActionListener(ActionListener listener) |