diff options
Diffstat (limited to 'src/net/java/sip/communicator/impl/osdependent/systemtray/appindicator')
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); +} |