aboutsummaryrefslogtreecommitdiffstats
path: root/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator
diff options
context:
space:
mode:
authorWolfgang Wiedmeyer <wolfgit@wiedmeyer.de>2017-03-11 22:15:03 +0100
committerWolfgang Wiedmeyer <wolfgit@wiedmeyer.de>2017-03-11 22:15:03 +0100
commit85901329b0794b136b96bf745f4ab1572806fc89 (patch)
treef23da7e97cae727f39d825f0fef8348cffb238e4 /src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator
parent3db2e44f186c59429901b2c899e139ea60117a55 (diff)
parentcf5da997da8820b4050f5b87ee9440a0ede36d1f (diff)
downloadjitsi-master.zip
jitsi-master.tar.gz
jitsi-master.tar.bz2
Merge commit 'cf5da99'HEADmaster
Signed-off-by: Wolfgang Wiedmeyer <wolfgit@wiedmeyer.de>
Diffstat (limited to 'src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator')
-rw-r--r--src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/AppIndicator1.java189
-rw-r--r--src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/AppIndicatorTray.java78
-rw-r--r--src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/AppIndicatorTrayIcon.java695
-rw-r--r--src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/Gobject.java90
-rw-r--r--src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/Gtk.java68
5 files changed, 1120 insertions, 0 deletions
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..90c949a
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/AppIndicatorTray.java
@@ -0,0 +1,78 @@
+/*
+ * 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 boolean dynamicMenu;
+
+ public AppIndicatorTray(boolean dynamicMenu) throws Exception
+ {
+ this.dynamicMenu = dynamicMenu;
+ 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 dynamicMenu;
+ }
+}
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..12334f3
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/AppIndicatorTrayIcon.java
@@ -0,0 +1,695 @@
+/*
+ * 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;
+
+ private PopupMenuPeer defaultMenuPeer;
+
+ 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);
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ super.finalize();
+ if (isDefaultMenuItem)
+ {
+ gobject.g_object_unref(gtkMenuItem);
+ }
+ }
+
+ 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;
+ public long gtkSignalHandler;
+ public boolean isDefaultMenuItem;
+
+ @Override
+ public void componentAdded(ContainerEvent e)
+ {
+ AppIndicatorTrayIcon.this.printMenu(popup.getComponents(), 1);
+ gtk.gdk_threads_enter();
+ try
+ {
+ createGtkMenuItems(this, new Component[]{e.getChild()});
+ gtk.gtk_widget_show_all(popupPeer.gtkMenu);
+ }
+ finally
+ {
+ 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();
+ try
+ {
+ cleanMenu(c);
+ }
+ finally
+ {
+ gtk.gdk_threads_leave();
+ }
+
+ children.remove(c);
+ break;
+ }
+ }
+ }
+ }
+
+ public void createTray()
+ {
+ gtk.gdk_threads_enter();
+ try
+ {
+ setupGtkMenu();
+ }
+ finally
+ {
+ gtk.gdk_threads_leave();
+ }
+
+ new Thread()
+ {
+ public void run()
+ {
+ gtk.gtk_main();
+ }
+ }.start();
+ }
+
+ private void setupGtkMenu()
+ {
+ File iconFile = new File(imageIconToPath(mainIcon));
+ appIndicator = ai.app_indicator_new_with_path(
+ "jitsi",
+ iconFile.getName().replaceFirst("[.][^.]+$", ""),
+ AppIndicator1.APP_INDICATOR_CATEGORY.COMMUNICATIONS.ordinal(),
+ iconFile.getParent());
+
+ ai.app_indicator_set_title(appIndicator, title);
+ ai.app_indicator_set_icon_full(
+ appIndicator,
+ iconFile.getAbsolutePath(),
+ "Jitsi");
+
+ // 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);
+ ai.app_indicator_set_status(
+ appIndicator,
+ AppIndicator1.APP_INDICATOR_STATUS.ACTIVE.ordinal());
+ }
+
+ private void cleanMenu(PopupMenuPeer peer)
+ {
+ assert !peer.isDefaultMenuItem;
+ 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
+ if (peer.gtkSignalHandler > 0)
+ {
+ gobject.g_signal_handler_disconnect(
+ peer.gtkMenuItem,
+ peer.gtkSignalHandler);
+ }
+
+ gtk.gtk_widget_destroy(peer.gtkMenuItem);
+ peer.gtkImageBuffer = null;
+ if (peer.menuItem instanceof JMenu)
+ {
+ ((JMenu)peer.menuItem).removeContainerListener(peer);
+ ((JMenu)peer.menuItem).getPopupMenu().removeContainerListener(peer);
+ }
+ }
+
+ private void createGtkMenuItems(
+ PopupMenuPeer parent,
+ Component[] components)
+ {
+ for (Component em : components)
+ {
+ PopupMenuPeer peer = new PopupMenuPeer(parent, em);
+ if (em instanceof JPopupMenu.Separator)
+ {
+ logger.debug("Creating separator");
+ peer.gtkMenuItem = gtk.gtk_separator_menu_item_new();
+ }
+
+ if (em instanceof JMenuItem)
+ {
+ 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;
+ logger.debug("Creating item for " + m.getClass().getName() + ": "
+ + m.getText());
+ 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);
+ peer.gtkSignalHandler = 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.getPropertyName());
+ }
+
+ switch (evt.getPropertyName())
+ {
+ case JMenuItem.TEXT_CHANGED_PROPERTY:
+ gtk.gdk_threads_enter();
+ try
+ {
+ gtk.gtk_menu_item_set_label(
+ peer.gtkMenuItem,
+ evt.getNewValue().toString());
+ }
+ finally
+ {
+ 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.gdk_threads_enter();
+ try
+ {
+ gtk.gtk_widget_set_sensitive(
+ peer.gtkMenuItem,
+ AccessibleState.ENABLED.equals(
+ evt.getNewValue()) ? 1 : 0);
+ }
+ finally
+ {
+ gtk.gdk_threads_leave();
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void stateChanged(ChangeEvent e)
+ {
+ logger.debug(menu.getText() + " -> " + menu.isSelected());
+ gtk.gdk_threads_enter();
+ try
+ {
+ gtk.gtk_check_menu_item_set_active(
+ peer.gtkMenuItem,
+ menu.isSelected() ? 1 : 0);
+ }
+ finally
+ {
+ 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
+ logger.debug("Checking selected state on: " + menu.getText());
+ 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()
+ {
+ gtk.gdk_threads_enter();
+ try
+ {
+ return gtk.gtk_check_menu_item_get_active(peer.gtkMenuItem) == 1;
+ }
+ finally
+ {
+ gtk.gdk_threads_leave();
+ }
+ }
+ }
+
+ @Override
+ public void setDefaultAction(Object menuItem)
+ {
+ // It shouldn't be necessary that we hold a reference to the
+ // default item, it is contained in the menu. It might even create
+ // a memory leak. But if not set, the indicator loses track of it
+ // (at least on Debian). Unref an existing item, then ref the newly
+ // set
+ if (defaultMenuPeer != null)
+ {
+ gobject.g_object_unref(defaultMenuPeer.gtkMenuItem);
+ }
+
+ PopupMenuPeer peer = findMenuItem(popupPeer, menuItem);
+ if (peer != null && peer.gtkMenuItem != null)
+ {
+ gtk.gdk_threads_enter();
+ try
+ {
+ defaultMenuPeer = peer;
+ gobject.g_object_ref(peer.gtkMenuItem);
+ ai.app_indicator_set_secondary_activate_target(
+ appIndicator,
+ peer.gtkMenuItem);
+ }
+ finally
+ {
+ gtk.gdk_threads_leave();
+ }
+ }
+ }
+
+ private PopupMenuPeer findMenuItem(PopupMenuPeer peer, Object menuItem)
+ {
+ if (peer.menuItem == menuItem)
+ {
+ logger.debug("Setting default action to: "
+ + ((JMenuItem)menuItem).getText()
+ + " @" + peer.gtkMenuItem);
+ return peer;
+ }
+
+ for (PopupMenuPeer p : peer.children)
+ {
+ PopupMenuPeer 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
+ {
+ mainIcon = icon;
+ if (appIndicator != null)
+ {
+ gtk.gdk_threads_enter();
+ try
+ {
+ ai.app_indicator_set_icon(
+ appIndicator,
+ imageIconToPath(icon));
+ }
+ finally
+ {
+ gtk.gdk_threads_leave();
+ }
+ }
+ }
+
+ @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..cfc7805
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/Gobject.java
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ *
+ * @return the handler id (always greater than 0 for successful connections)
+ */
+ long g_signal_connect_data(Pointer instance, String detailed_signal,
+ SignalHandler c_handler, Pointer data, Pointer destroy_data,
+ int connect_flags);
+
+ /**
+ * Disconnects a handler from an instance so it will not be called during
+ * any future or currently ongoing emissions of the signal it has been
+ * connected to. The handler_id becomes invalid and may be reused. The
+ * handler_id has to be a valid signal handler id, connected to a signal of
+ * instance .
+ *
+ * @param instance The instance to remove the signal handler from.
+ * @param handler_id Handler id of the handler to be disconnected.
+ */
+ void g_signal_handler_disconnect(Pointer instance, long handler_id);
+
+ /**
+ * 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);
+
+ /**
+ * Increases the reference count of object.
+ *
+ * @param object a GObject.
+ * @return the same object.
+ */
+ Pointer g_object_ref(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..b66d59f
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator/Gtk.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 com.sun.jna.*;
+
+/**
+ * JNA mappings for the gtk2 library. Only functions required for the tray 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_image_menu_item_new_with_label(String label);
+ 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_widget_destroy(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);
+
+ 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);
+}