diff --git a/src/dorkbox/systemTray/MenuSpacer.java b/src/dorkbox/systemTray/MenuSpacer.java new file mode 100644 index 0000000..d1f9f7b --- /dev/null +++ b/src/dorkbox/systemTray/MenuSpacer.java @@ -0,0 +1,25 @@ +/* + * Copyright 2015 dorkbox, llc + * + * 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 dorkbox.systemTray; + +/** + * This represents a common menu-spacer entry, that is cross platform in nature + */ +public +interface MenuSpacer { + +} diff --git a/src/dorkbox/systemTray/MenuStatus.java b/src/dorkbox/systemTray/MenuStatus.java new file mode 100644 index 0000000..45db01c --- /dev/null +++ b/src/dorkbox/systemTray/MenuStatus.java @@ -0,0 +1,25 @@ +/* + * Copyright 2015 dorkbox, llc + * + * 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 dorkbox.systemTray; + +/** + * This represents a common menu-status entry, that is cross platform in nature + */ +public +interface MenuStatus { + +} diff --git a/src/dorkbox/systemTray/SystemTray.java b/src/dorkbox/systemTray/SystemTray.java index 663609e..de8787e 100644 --- a/src/dorkbox/systemTray/SystemTray.java +++ b/src/dorkbox/systemTray/SystemTray.java @@ -697,9 +697,16 @@ class SystemTray { * Must be wrapped in a synchronized block for object visibility */ protected - MenuEntry getMenuEntry(String menuText) { + MenuEntry getMenuEntry(final String menuText) { + if (menuText == null) { + return null; + } + for (MenuEntry entry : menuEntries) { - if (entry.getText().equals(menuText)) { + String text = entry.getText(); + + // text can be null + if (menuText.equals(text)) { return entry; } } @@ -832,7 +839,6 @@ class SystemTray { * @param imageStream the InputStream of the image to use. If null, no image will be used * @param callback callback that will be executed when this menu entry is clicked */ - @Deprecated public abstract void addMenuEntry(String menuText, InputStream imageStream, SystemTrayMenuAction callback); @@ -1169,8 +1175,6 @@ class SystemTray { throw new NullPointerException("No menu entry exists for menuEntry"); } - final String label = menuEntry.getText(); - // have to wait for the value final CountDownLatch countDownLatch = new CountDownLatch(1); final AtomicBoolean hasValue = new AtomicBoolean(false); @@ -1183,8 +1187,7 @@ class SystemTray { synchronized (menuEntries) { for (Iterator iterator = menuEntries.iterator(); iterator.hasNext(); ) { final MenuEntry entry = iterator.next(); - if (entry.getText() - .equals(label)) { + if (entry == menuEntry) { iterator.remove(); // this will also reset the menu @@ -1193,6 +1196,19 @@ class SystemTray { break; } } + + // now check to see if a spacer is at the top/bottom of the list (and remove it if so. This is a recursive function. + if (!menuEntries.isEmpty()) { + if (menuEntries.get(0) instanceof MenuSpacer) { + removeMenuEntry(menuEntries.get(0)); + } + } + // now check to see if a spacer is at the top/bottom of the list (and remove it if so. This is a recursive function. + if (!menuEntries.isEmpty()) { + if (menuEntries.get(menuEntries.size()-1) instanceof MenuSpacer) { + removeMenuEntry(menuEntries.get(menuEntries.size()-1)); + } + } } } catch (Exception e) { logger.error("Error removing menu entry from list.", e); @@ -1209,11 +1225,11 @@ class SystemTray { } } catch (InterruptedException e) { - logger.error("Error removing menu entry: {}", label); + logger.error("Error removing menu entry: {}", menuEntry.getText()); } if (!hasValue.get()) { - throw new NullPointerException("Menu entry '" + label + "'not found in list while trying to remove it."); + throw new NullPointerException("Menu entry '" + menuEntry.getText() + "'not found in list while trying to remove it."); } } @@ -1261,5 +1277,21 @@ class SystemTray { throw new NullPointerException("No menu entry exists for string '" + menuText + "'"); } } + + + /** + * Adds a spacer to the dropdown menu. When menu entries are removed, any menu spacer that ends up at the top/bottom of the drop-down + * menu, will also be removed. For example: + * + * Original Entry3 deleted Result + * + * + * Entry1 Entry1 Entry1 + * Entry2 -> Entry2 -> Entry2 + * (deleted) + * Entry3 (deleted) + */ + public abstract + void addMenuSpacer(); } diff --git a/src/dorkbox/systemTray/linux/GtkMenuEntry.java b/src/dorkbox/systemTray/linux/GtkMenuEntry.java index f7994d0..372f78a 100644 --- a/src/dorkbox/systemTray/linux/GtkMenuEntry.java +++ b/src/dorkbox/systemTray/linux/GtkMenuEntry.java @@ -18,108 +18,49 @@ package dorkbox.systemTray.linux; import java.io.File; import java.io.InputStream; import java.net.URL; -import java.util.concurrent.atomic.AtomicInteger; -import com.sun.jna.NativeLong; import com.sun.jna.Pointer; import dorkbox.systemTray.MenuEntry; -import dorkbox.systemTray.SystemTrayMenuAction; -import dorkbox.systemTray.linux.jna.GCallback; -import dorkbox.systemTray.linux.jna.Gobject; import dorkbox.systemTray.linux.jna.Gtk; import dorkbox.systemTray.util.ImageUtils; -class GtkMenuEntry implements MenuEntry, GCallback { - private static final AtomicInteger ID_COUNTER = new AtomicInteger(); - private final int id = ID_COUNTER.getAndIncrement(); +abstract +class GtkMenuEntry implements MenuEntry { + private final int id = GtkTypeSystemTray.MENU_ID_COUNTER.getAndIncrement(); final Pointer menuItem; - final GtkTypeSystemTray parent; + final GtkTypeSystemTray systemTray; - @SuppressWarnings({"FieldCanBeLocal", "unused"}) - private final NativeLong nativeLong; - - // these have to be volatile, because they can be changed from any thread + // this have to be volatile, because they can be changed from any thread private volatile String text; - private volatile SystemTrayMenuAction callback; - private volatile Pointer image; - - // these are necessary BECAUSE GTK menus look funky as hell when there are some menu entries WITH icons and some WITHOUT - private volatile boolean hasLegitIcon = true; - private static File transparentIcon = null; /** * called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it! * this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref */ - GtkMenuEntry(final String label, final File imagePath, final SystemTrayMenuAction callback, final GtkTypeSystemTray parent) { - this.parent = parent; - this.text = label; - this.callback = callback; - - menuItem = Gtk.gtk_image_menu_item_new_with_label(label); - - if (transparentIcon == null) { - transparentIcon = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE); - } - - hasLegitIcon = imagePath != null; - if (hasLegitIcon) { - // NOTE: XFCE used to use appindicator3, which DOES NOT support images in the menu. This change was reverted. - // see: https://ask.fedoraproject.org/en/question/23116/how-to-fix-missing-icons-in-program-menus-and-context-menus/ - // see: https://git.gnome.org/browse/gtk+/commit/?id=627a03683f5f41efbfc86cc0f10e1b7c11e9bb25 - image = Gtk.gtk_image_new_from_file(imagePath.getAbsolutePath()); - - Gtk.gtk_image_menu_item_set_image(menuItem, image); - - // must always re-set always-show after setting the image - Gtk.gtk_image_menu_item_set_always_show_image(menuItem, Gtk.TRUE); - } - - nativeLong = Gobject.g_signal_connect_object(menuItem, "activate", this, null, 0); + GtkMenuEntry(Pointer menuItem, final GtkTypeSystemTray systemTray) { + this.systemTray = systemTray; + this.menuItem = menuItem; } /** * the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images * - * called on the DISPATCH thread + * always called on the DISPATCH thread */ - void setSpacerImage(final boolean everyoneElseHasImages) { - if (hasLegitIcon) { - // we have a legit icon, so there is nothing else we can do. - return; - } + abstract + void setSpacerImage(final boolean everyoneElseHasImages); - if (image != null) { - Gtk.gtk_widget_destroy(image); - image = null; - Gtk.gtk_widget_show_all(menuItem); - } + /** + * must always be called in the GTK thread + */ + abstract + void renderText(final String text); - if (everyoneElseHasImages) { - image = Gtk.gtk_image_new_from_file(transparentIcon.getAbsolutePath()); + abstract + void setImage_(final File imageFile); - Gtk.gtk_image_menu_item_set_image(menuItem, image); - - // must always re-set always-show after setting the image - Gtk.gtk_image_menu_item_set_always_show_image(menuItem, Gtk.TRUE); - } - - Gtk.gtk_widget_show_all(menuItem); - } - - // called by native code - @Override - public - int callback(final Pointer instance, final Pointer data) { - final SystemTrayMenuAction cb = this.callback; - if (cb != null) { - Gtk.proxyClick(cb, parent, GtkMenuEntry.this); - } - - return Gtk.TRUE; - } @Override public @@ -128,50 +69,21 @@ class GtkMenuEntry implements MenuEntry, GCallback { } @Override - public + public final void setText(final String newText) { - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - text = newText; - Gtk.gtk_menu_item_set_label(menuItem, newText); - - Gtk.gtk_widget_show_all(menuItem); - } - }); - } - - private - void setImage_(final File imageFile) { - hasLegitIcon = imageFile != null; + text = newText; Gtk.dispatch(new Runnable() { @Override public void run() { - if (image != null) { - Gtk.gtk_widget_destroy(image); - image = null; - Gtk.gtk_widget_show_all(menuItem); - } - - if (hasLegitIcon) { - image = Gtk.gtk_image_new_from_file(imageFile.getAbsolutePath()); - Gtk.gtk_image_menu_item_set_image(menuItem, image); - Gobject.g_object_ref_sink(image); - - // must always re-set always-show after setting the image - Gtk.gtk_image_menu_item_set_always_show_image(menuItem, Gtk.TRUE); - } - - Gtk.gtk_widget_show_all(menuItem); + renderText(text); } }); } @Override - public + public final void setImage(final String imagePath) { if (imagePath == null) { setImage_(null); @@ -182,7 +94,7 @@ class GtkMenuEntry implements MenuEntry, GCallback { } @Override - public + public final void setImage(final URL imageUrl) { if (imageUrl == null) { setImage_(null); @@ -193,7 +105,7 @@ class GtkMenuEntry implements MenuEntry, GCallback { } @Override - public + public final void setImage(final String cacheName, final InputStream imageStream) { if (imageStream == null) { setImage_(null); @@ -204,7 +116,7 @@ class GtkMenuEntry implements MenuEntry, GCallback { } @Override - public + public final void setImage(final InputStream imageStream) { if (imageStream == null) { setImage_(null); @@ -214,56 +126,41 @@ class GtkMenuEntry implements MenuEntry, GCallback { } } - @Override - public - boolean hasImage() { - return hasLegitIcon; - } - - @Override - public - void setCallback(final SystemTrayMenuAction callback) { - this.callback = callback; - } - /** * This is ONLY called via systray.menuEntry.remove() !! */ - public + public final void remove() { Gtk.dispatch(new Runnable() { @Override public void run() { + Gtk.gtk_container_remove(systemTray.getMenu(), menuItem); + Gtk.gtk_menu_shell_deactivate(systemTray.getMenu(), menuItem); + Gtk.gtk_widget_destroy(menuItem); + removePrivate(); // have to rebuild the menu now... - parent.deleteMenu(); - parent.createMenu(); + systemTray.deleteMenu(); + systemTray.createMenu(); } }); } - void removePrivate() { - callback = null; - Gtk.gtk_menu_shell_deactivate(parent.getMenu(), menuItem); - - if (image != null) { - Gtk.gtk_widget_destroy(image); - } - - Gtk.gtk_widget_destroy(menuItem); - } + // called when this item is removed. Necessary to cleanup/remove itself + abstract + void removePrivate(); @Override - public + public final int hashCode() { return id; } @Override - public + public final boolean equals(Object obj) { if (this == obj) { return true; diff --git a/src/dorkbox/systemTray/linux/GtkMenuEntryItem.java b/src/dorkbox/systemTray/linux/GtkMenuEntryItem.java new file mode 100644 index 0000000..17237bf --- /dev/null +++ b/src/dorkbox/systemTray/linux/GtkMenuEntryItem.java @@ -0,0 +1,160 @@ +/* + * Copyright 2014 dorkbox, llc + * + * 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 dorkbox.systemTray.linux; + +import java.io.File; + +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; + +import dorkbox.systemTray.SystemTrayMenuAction; +import dorkbox.systemTray.linux.jna.GCallback; +import dorkbox.systemTray.linux.jna.Gobject; +import dorkbox.systemTray.linux.jna.Gtk; +import dorkbox.systemTray.util.ImageUtils; + +class GtkMenuEntryItem extends GtkMenuEntry implements GCallback { + private static File transparentIcon = null; + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final NativeLong nativeLong; + // these have to be volatile, because they can be changed from any thread + private volatile SystemTrayMenuAction callback; + private volatile Pointer image; + // these are necessary BECAUSE GTK menus look funky as hell when there are some menu entries WITH icons and some WITHOUT + private volatile boolean hasLegitIcon = true; + + /** + * called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it! + * this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref + */ + GtkMenuEntryItem(final String label, final File image, final SystemTrayMenuAction callback, final GtkTypeSystemTray systemTray) { + super(Gtk.gtk_image_menu_item_new_with_label(""), systemTray); + this.callback = callback; + setText(label); + + if (transparentIcon == null) { + transparentIcon = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE); + } + + setImage_(image); + + if (callback != null) { + nativeLong = Gobject.g_signal_connect_object(menuItem, "activate", this, null, 0); + } + else { + nativeLong = null; + } + } + + @Override + public + void setCallback(final SystemTrayMenuAction callback) { + this.callback = callback; + } + + // called by native code + @Override + public + int callback(final Pointer instance, final Pointer data) { + final SystemTrayMenuAction cb = this.callback; + if (cb != null) { + Gtk.proxyClick(cb, systemTray, GtkMenuEntryItem.this); + } + + return Gtk.TRUE; + } + + @Override + public + boolean hasImage() { + return hasLegitIcon; + } + + /** + * the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images + *

+ * called on the DISPATCH thread + */ + void setSpacerImage(final boolean everyoneElseHasImages) { + if (hasLegitIcon) { + // we have a legit icon, so there is nothing else we can do. + return; + } + + if (image != null) { + Gtk.gtk_widget_destroy(image); + image = null; + Gtk.gtk_widget_show_all(menuItem); + } + + if (everyoneElseHasImages) { + image = Gtk.gtk_image_new_from_file(transparentIcon.getAbsolutePath()); + + Gtk.gtk_image_menu_item_set_image(menuItem, image); + + // must always re-set always-show after setting the image + Gtk.gtk_image_menu_item_set_always_show_image(menuItem, Gtk.TRUE); + } + + Gtk.gtk_widget_show_all(menuItem); + } + + /** + * must always be called in the GTK thread + */ + void renderText(final String text) { + Gtk.gtk_menu_item_set_label(menuItem, text); + Gtk.gtk_widget_show_all(menuItem); + } + + // NOTE: XFCE used to use appindicator3, which DOES NOT support images in the menu. This change was reverted. + // see: https://ask.fedoraproject.org/en/question/23116/how-to-fix-missing-icons-in-program-menus-and-context-menus/ + // see: https://git.gnome.org/browse/gtk+/commit/?id=627a03683f5f41efbfc86cc0f10e1b7c11e9bb25 + void setImage_(final File imageFile) { + hasLegitIcon = imageFile != null; + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + if (image != null) { + Gtk.gtk_widget_destroy(image); + image = null; + Gtk.gtk_widget_show_all(menuItem); + } + + if (imageFile != null) { + image = Gtk.gtk_image_new_from_file(imageFile.getAbsolutePath()); + Gtk.gtk_image_menu_item_set_image(menuItem, image); + Gobject.g_object_ref_sink(image); + + // must always re-set always-show after setting the image + Gtk.gtk_image_menu_item_set_always_show_image(menuItem, Gtk.TRUE); + } + + Gtk.gtk_widget_show_all(menuItem); + } + }); + } + + void removePrivate() { + callback = null; + + if (image != null) { + Gtk.gtk_widget_destroy(image); + } + } +} diff --git a/src/dorkbox/systemTray/linux/GtkMenuEntrySpacer.java b/src/dorkbox/systemTray/linux/GtkMenuEntrySpacer.java new file mode 100644 index 0000000..6fd0e0c --- /dev/null +++ b/src/dorkbox/systemTray/linux/GtkMenuEntrySpacer.java @@ -0,0 +1,62 @@ +/* + * Copyright 2014 dorkbox, llc + * + * 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 dorkbox.systemTray.linux; + +import java.io.File; + +import dorkbox.systemTray.MenuSpacer; +import dorkbox.systemTray.SystemTrayMenuAction; +import dorkbox.systemTray.linux.jna.Gtk; + +class GtkMenuEntrySpacer extends GtkMenuEntry implements MenuSpacer { + + /** + * called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it! + * this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref + */ + GtkMenuEntrySpacer(final GtkTypeSystemTray parent) { + super(Gtk.gtk_separator_menu_item_new(), parent); + } + + @Override + void setSpacerImage(final boolean everyoneElseHasImages) { + } + + // called in the GTK thread + @Override + void renderText(final String text) { + } + + @Override + void setImage_(final File imageFile) { + } + + @Override + void removePrivate() { + } + + @Override + public + boolean hasImage() { + return false; + } + + @Override + public + void setCallback(final SystemTrayMenuAction callback) { + + } +} diff --git a/src/dorkbox/systemTray/linux/GtkMenuEntryStatus.java b/src/dorkbox/systemTray/linux/GtkMenuEntryStatus.java new file mode 100644 index 0000000..13e8bb4 --- /dev/null +++ b/src/dorkbox/systemTray/linux/GtkMenuEntryStatus.java @@ -0,0 +1,74 @@ +/* + * Copyright 2014 dorkbox, llc + * + * 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 dorkbox.systemTray.linux; + +import java.io.File; + +import com.sun.jna.Pointer; + +import dorkbox.systemTray.MenuStatus; +import dorkbox.systemTray.SystemTrayMenuAction; +import dorkbox.systemTray.linux.jna.Gobject; +import dorkbox.systemTray.linux.jna.Gtk; + +class GtkMenuEntryStatus extends GtkMenuEntry implements MenuStatus { + + /** + * called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it! + * this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref + */ + GtkMenuEntryStatus(final String label, final GtkTypeSystemTray parent) { + super(Gtk.gtk_menu_item_new_with_label(""), parent); + setText(label); + } + + @Override + void setSpacerImage(final boolean everyoneElseHasImages) { + } + + // called in the GTK thread + @Override + void renderText(final String text) { + // evil hacks abound... + Pointer label = Gtk.gtk_bin_get_child(menuItem); + Gtk.gtk_label_set_use_markup(label, Gtk.TRUE); + Pointer markup = Gobject.g_markup_printf_escaped("%s", text); + Gtk.gtk_label_set_markup(label, markup); + Gobject.g_free(markup); + + Gtk.gtk_widget_set_sensitive(menuItem, Gtk.FALSE); + } + + @Override + void setImage_(final File imageFile) { + } + + @Override + void removePrivate() { + } + + @Override + public + boolean hasImage() { + return false; + } + + @Override + public + void setCallback(final SystemTrayMenuAction callback) { + + } +} diff --git a/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java b/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java index 2f314cd..ad43709 100644 --- a/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java +++ b/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java @@ -19,9 +19,11 @@ package dorkbox.systemTray.linux; import java.io.File; import java.io.InputStream; import java.net.URL; +import java.util.concurrent.atomic.AtomicInteger; import com.sun.jna.Pointer; +import dorkbox.systemTray.MenuEntry; import dorkbox.systemTray.SystemTray; import dorkbox.systemTray.SystemTrayMenuAction; import dorkbox.systemTray.linux.jna.Gobject; @@ -34,10 +36,9 @@ import dorkbox.systemTray.util.ImageUtils; */ public abstract class GtkTypeSystemTray extends SystemTray { - private volatile Pointer menu; + static final AtomicInteger MENU_ID_COUNTER = new AtomicInteger(); - private volatile Pointer connectionStatusItem; - private volatile String statusText = null; + private volatile Pointer menu; @Override protected @@ -62,63 +63,77 @@ class GtkTypeSystemTray extends SystemTray { @Override public String getStatus() { - return statusText; + synchronized (menuEntries) { + MenuEntry menuEntry = menuEntries.get(0); + if (menuEntry instanceof GtkMenuEntryStatus) { + return menuEntry.getText(); + } + } + + return null; } @Override public void setStatus(final String statusText) { - this.statusText = statusText; - Gtk.dispatch(new Runnable() { @Override public void run() { // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. // To work around this issue, we destroy then recreate the menu every time something is changed. - if (connectionStatusItem == null && statusText != null && !statusText.isEmpty()) { + synchronized (menuEntries) { + // status is ALWAYS at 0 index... + GtkMenuEntry menuEntry = null; + if (!menuEntries.isEmpty()) { + menuEntry = (GtkMenuEntry) menuEntries.get(0); + } + + if (menuEntry instanceof GtkMenuEntryStatus) { + // always delete... + removeMenuEntry(menuEntry); + } + + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. deleteMenu(); - connectionStatusItem = Gtk.gtk_menu_item_new_with_label(""); - - // evil hacks abound... - Pointer label = Gtk.gtk_bin_get_child(connectionStatusItem); - Gtk.gtk_label_set_use_markup(label, Gtk.TRUE); - Pointer markup = Gobject.g_markup_printf_escaped("%s", statusText); - Gtk.gtk_label_set_markup(label, markup); - Gobject.g_free(markup); - - Gtk.gtk_widget_set_sensitive(connectionStatusItem, Gtk.FALSE); + if (menuEntry == null) { + menuEntry = new GtkMenuEntryStatus(statusText, GtkTypeSystemTray.this); + // status is ALWAYS at 0 index... + menuEntries.add(0, menuEntry); + } else if (menuEntry instanceof GtkMenuEntryStatus) { + // change the text? + if (statusText != null) { + menuEntry = new GtkMenuEntryStatus(statusText, GtkTypeSystemTray.this); + menuEntries.add(0, menuEntry); + } + } createMenu(); } - else { - if (statusText == null || statusText.isEmpty()) { - // this means the status text already exists, and we are removing it + } + }); + } - Gtk.gtk_container_remove(menu, connectionStatusItem); - connectionStatusItem = null; // because we manually delete it + @Override + public + void addMenuSpacer() { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. + synchronized (menuEntries) { + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. + deleteMenu(); - Gtk.gtk_widget_show_all(menu); + GtkMenuEntry menuEntry = new GtkMenuEntrySpacer(GtkTypeSystemTray.this); + menuEntries.add(menuEntry); - deleteMenu(); - createMenu(); - } - else { - // here we set the text only. it already exists - - // set bold instead - // libgtk.gtk_menu_item_set_label(this.connectionStatusItem, statusText); - - // evil hacks abound... - Pointer label = Gtk.gtk_bin_get_child(connectionStatusItem); - Gtk.gtk_label_set_use_markup(label, Gtk.TRUE); - Pointer markup = Gobject.g_markup_printf_escaped("%s", statusText); - Gtk.gtk_label_set_markup(label, markup); - Gobject.g_free(markup); - - Gtk.gtk_widget_show_all(menu); - } + createMenu(); } } }); @@ -131,12 +146,6 @@ class GtkTypeSystemTray extends SystemTray { */ void deleteMenu() { if (menu != null) { - // have to remove status from menu (but not destroy the object) - if (connectionStatusItem != null) { - Gobject.g_object_force_floating(connectionStatusItem); - Gtk.gtk_container_remove(menu, connectionStatusItem); - } - // have to remove all other menu entries synchronized (menuEntries) { for (int i = 0; i < menuEntries.size(); i++) { @@ -157,19 +166,12 @@ class GtkTypeSystemTray extends SystemTray { // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. // To work around this issue, we destroy then recreate the menu every time something is changed. void createMenu() { - // now add status - if (connectionStatusItem != null) { - Gtk.gtk_menu_shell_append(this.menu, this.connectionStatusItem); - Gobject.g_object_ref_sink(connectionStatusItem); - } - boolean hasImages = false; // now add back other menu entries synchronized (menuEntries) { for (int i = 0; i < menuEntries.size(); i++) { - GtkMenuEntry menuEntry__ = (GtkMenuEntry) menuEntries.get(i); - + MenuEntry menuEntry__ = menuEntries.get(i); hasImages |= menuEntry__.hasImage(); } @@ -189,23 +191,23 @@ class GtkTypeSystemTray extends SystemTray { } } + /** + * Called inside the gdk_threads block + */ + void onMenuAdded(final Pointer menu) { + // only needed for AppIndicator + } + /** * Completely obliterates the menu, no possible way to reconstruct it. */ private void obliterateMenu() { if (menu != null) { - // have to remove status from menu - if (connectionStatusItem != null) { - Gtk.gtk_widget_destroy(connectionStatusItem); - connectionStatusItem = null; - } - // have to remove all other menu entries synchronized (menuEntries) { for (int i = 0; i < menuEntries.size(); i++) { GtkMenuEntry menuEntry__ = (GtkMenuEntry) menuEntries.get(i); - menuEntry__.removePrivate(); } menuEntries.clear(); @@ -215,12 +217,6 @@ class GtkTypeSystemTray extends SystemTray { } } - /** - * Called inside the gdk_threads block - */ - protected - void onMenuAdded(final Pointer menu) {} - protected Pointer getMenu() { return menu; @@ -240,14 +236,13 @@ class GtkTypeSystemTray extends SystemTray { public void run() { synchronized (menuEntries) { - GtkMenuEntry menuEntry = (GtkMenuEntry) getMenuEntry(menuText); - + MenuEntry menuEntry = getMenuEntry(menuText); if (menuEntry == null) { // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. // To work around this issue, we destroy then recreate the menu every time something is changed. deleteMenu(); - menuEntry = new GtkMenuEntry(menuText, imagePath, callback, GtkTypeSystemTray.this); + menuEntry = new GtkMenuEntryItem(menuText, imagePath, callback, GtkTypeSystemTray.this); menuEntries.add(menuEntry); createMenu(); @@ -291,7 +286,6 @@ class GtkTypeSystemTray extends SystemTray { } @Override - @Deprecated public void addMenuEntry(final String menuText, final InputStream imageStream, final SystemTrayMenuAction callback) { if (imageStream == null) { diff --git a/src/dorkbox/systemTray/linux/jna/Gtk.java b/src/dorkbox/systemTray/linux/jna/Gtk.java index e7ab7ac..865f4c4 100644 --- a/src/dorkbox/systemTray/linux/jna/Gtk.java +++ b/src/dorkbox/systemTray/linux/jna/Gtk.java @@ -317,6 +317,8 @@ class Gtk { */ public static void dispatch(final Runnable runnable) { + // FIXME: on mac, check -XstartOnFirstThread.. there are issues with javaFX (possibly SWT as well) + if (alreadyRunningGTK) { // SWT/JavaFX if (SystemTray.isJavaFxLoaded) { @@ -443,6 +445,8 @@ class Gtk { public static native Pointer gtk_menu_item_new_with_label(String label); + public static native Pointer gtk_separator_menu_item_new(); + // to create a menu entry WITH an icon. public static native Pointer gtk_image_new_from_file(String iconPath); diff --git a/src/dorkbox/systemTray/swing/SwingMenuEntry.java b/src/dorkbox/systemTray/swing/SwingMenuEntry.java index 8de4de8..a75b2bd 100644 --- a/src/dorkbox/systemTray/swing/SwingMenuEntry.java +++ b/src/dorkbox/systemTray/swing/SwingMenuEntry.java @@ -16,66 +16,43 @@ package dorkbox.systemTray.swing; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.io.File; import java.io.InputStream; import java.net.URL; -import javax.swing.ImageIcon; -import javax.swing.JMenuItem; +import javax.swing.JComponent; import dorkbox.systemTray.MenuEntry; -import dorkbox.systemTray.SystemTray; -import dorkbox.systemTray.SystemTrayMenuAction; import dorkbox.systemTray.util.ImageUtils; import dorkbox.util.SwingUtil; +abstract class SwingMenuEntry implements MenuEntry { - private final SwingSystemTrayMenuPopup parent; - private final SystemTray systemTray; - private final JMenuItem menuItem; - private final ActionListener swingCallback; + private final int id = SwingSystemTray.MENU_ID_COUNTER.getAndIncrement(); - private volatile boolean hasLegitIcon = false; + final SwingSystemTray systemTray; + final JComponent menuItem; + + // this have to be volatile, because they can be changed from any thread private volatile String text; - private volatile SystemTrayMenuAction callback; // this is ALWAYS called on the EDT. - SwingMenuEntry(final SwingSystemTrayMenuPopup parentMenu, final String label, final File imagePath, final SystemTrayMenuAction callback, - final SystemTray systemTray) { - this.parent = parentMenu; - this.text = label; - this.callback = callback; + SwingMenuEntry(JComponent menuItem, final SwingSystemTray systemTray) { + this.menuItem = menuItem; this.systemTray = systemTray; - swingCallback = new ActionListener() { - @Override - public - void actionPerformed(ActionEvent e) { - // we want it to run on the EDT - handle(); - } - }; - - menuItem = new JMenuItem(label); - menuItem.addActionListener(swingCallback); - - if (imagePath != null) { - hasLegitIcon = true; - setImageIcon(imagePath); - } - - parentMenu.add(menuItem); + systemTray.getMenu().add(menuItem); } - private - void handle() { - SystemTrayMenuAction cb = this.callback; - if (cb != null) { - cb.onClick(systemTray, this); - } - } + /** + * must always be called in the GTK thread + */ + abstract + void renderText(final String text); + + abstract + void setImage_(final File imageFile); + @Override public @@ -92,38 +69,13 @@ class SwingMenuEntry implements MenuEntry { @Override public void run() { - menuItem.setText(newText); + renderText(newText); } }); } - private - void setImage_(final File imagePath) { - SwingUtil.invokeLater(new Runnable() { - @Override - public - void run() { - setImageIcon(imagePath); - } - }); - } - - // always called on the EDT - private - void setImageIcon(final File imagePath) { - if (imagePath != null) { - hasLegitIcon = true; - ImageIcon origIcon = new ImageIcon(imagePath.getAbsolutePath()); - menuItem.setIcon(origIcon); - } - else { - hasLegitIcon = false; - menuItem.setIcon(null); - } - } - @Override - public + public final void setImage(final String imagePath) { if (imagePath == null) { setImage_(null); @@ -134,7 +86,7 @@ class SwingMenuEntry implements MenuEntry { } @Override - public + public final void setImage(final URL imageUrl) { if (imageUrl == null) { setImage_(null); @@ -145,7 +97,7 @@ class SwingMenuEntry implements MenuEntry { } @Override - public + public final void setImage(final String cacheName, final InputStream imageStream) { if (imageStream == null) { setImage_(null); @@ -156,7 +108,7 @@ class SwingMenuEntry implements MenuEntry { } @Override - public + public final void setImage(final InputStream imageStream) { if (imageStream == null) { setImage_(null); @@ -167,27 +119,43 @@ class SwingMenuEntry implements MenuEntry { } @Override - public - boolean hasImage() { - return hasLegitIcon; - } - - @Override - public - void setCallback(final SystemTrayMenuAction callback) { - this.callback = callback; - } - - @Override - public + public final void remove() { SwingUtil.invokeAndWait(new Runnable() { @Override public void run() { - menuItem.removeActionListener(swingCallback); - parent.remove(menuItem); + removePrivate(); + systemTray.getMenu().remove(menuItem); } }); } + + // called when this item is removed. Necessary to cleanup/remove itself + abstract + void removePrivate(); + + @Override + public final + int hashCode() { + return id; + } + + + @Override + public final + boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + SwingMenuEntry other = (SwingMenuEntry) obj; + return this.id == other.id; + } } diff --git a/src/dorkbox/systemTray/swing/SwingMenuEntryItem.java b/src/dorkbox/systemTray/swing/SwingMenuEntryItem.java new file mode 100644 index 0000000..2252448 --- /dev/null +++ b/src/dorkbox/systemTray/swing/SwingMenuEntryItem.java @@ -0,0 +1,105 @@ +/* + * Copyright 2014 dorkbox, llc + * + * 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 dorkbox.systemTray.swing; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; + +import javax.swing.ImageIcon; +import javax.swing.JMenuItem; + +import dorkbox.systemTray.SystemTrayMenuAction; +import dorkbox.util.SwingUtil; + +class SwingMenuEntryItem extends SwingMenuEntry { + private final ActionListener swingCallback; + + private volatile boolean hasLegitIcon = false; + private volatile SystemTrayMenuAction callback; + + // this is ALWAYS called on the EDT. + SwingMenuEntryItem(final String label, final File image, final SystemTrayMenuAction callback, final SwingSystemTray systemTray) { + super(new JMenuItem(label), systemTray); + + setText(label); + + this.callback = callback; + swingCallback = new ActionListener() { + @Override + public + void actionPerformed(ActionEvent e) { + // we want it to run on the EDT + handle(); + } + }; + + ((JMenuItem) menuItem).addActionListener(swingCallback); + + setImage_(image); + } + + @Override + public + void setCallback(final SystemTrayMenuAction callback) { + this.callback = callback; + } + + private + void handle() { + SystemTrayMenuAction cb = this.callback; + if (cb != null) { + cb.onClick(systemTray, this); + } + } + + @Override + public + boolean hasImage() { + return hasLegitIcon; + } + + @Override + void removePrivate() { + ((JMenuItem) menuItem).removeActionListener(swingCallback); + } + + // always called in the EDT + @Override + void renderText(final String text) { + ((JMenuItem) menuItem).setText(text); + } + + @Override + void setImage_(final File imageFile) { + hasLegitIcon = imageFile != null; + + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + if (imageFile != null) { + ImageIcon origIcon = new ImageIcon(imageFile.getAbsolutePath()); + ((JMenuItem) menuItem).setIcon(origIcon); + } + else { + ((JMenuItem) menuItem).setIcon(null); + } + } + }); + } +} diff --git a/src/dorkbox/systemTray/swing/SwingMenuEntrySpacer.java b/src/dorkbox/systemTray/swing/SwingMenuEntrySpacer.java new file mode 100644 index 0000000..1ca86d8 --- /dev/null +++ b/src/dorkbox/systemTray/swing/SwingMenuEntrySpacer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2014 dorkbox, llc + * + * 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 dorkbox.systemTray.swing; + +import java.io.File; + +import javax.swing.JSeparator; + +import dorkbox.systemTray.MenuSpacer; +import dorkbox.systemTray.SystemTrayMenuAction; + +class SwingMenuEntrySpacer extends SwingMenuEntry implements MenuSpacer { + + // this is ALWAYS called on the EDT. + SwingMenuEntrySpacer(final SwingSystemTray parent) { + super(new JSeparator(JSeparator.HORIZONTAL), parent); + } + + // called in the EDT thread + @Override + void renderText(final String text) { + } + + @Override + void setImage_(final File imageFile) { + } + + @Override + void removePrivate() { + } + + @Override + public + boolean hasImage() { + return false; + } + + @Override + public + void setCallback(final SystemTrayMenuAction callback) { + + } +} diff --git a/src/dorkbox/systemTray/swing/SwingMenuEntryStatus.java b/src/dorkbox/systemTray/swing/SwingMenuEntryStatus.java new file mode 100644 index 0000000..cc16c53 --- /dev/null +++ b/src/dorkbox/systemTray/swing/SwingMenuEntryStatus.java @@ -0,0 +1,64 @@ +/* + * Copyright 2014 dorkbox, llc + * + * 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 dorkbox.systemTray.swing; + +import java.awt.Font; +import java.io.File; + +import javax.swing.JMenuItem; + +import dorkbox.systemTray.MenuStatus; +import dorkbox.systemTray.SystemTrayMenuAction; + +class SwingMenuEntryStatus extends SwingMenuEntry implements MenuStatus { + + // this is ALWAYS called on the EDT. + SwingMenuEntryStatus(final String label, final SwingSystemTray parent) { + super(new JMenuItem(""), parent); + setText(label); + } + + // called in the EDT thread + @Override + void renderText(final String text) { + ((JMenuItem) menuItem).setText(text); + Font font = menuItem.getFont(); + Font font1 = font.deriveFont(Font.BOLD); + menuItem.setFont(font1); + + menuItem.setEnabled(false); + } + + @Override + void setImage_(final File imageFile) { + } + + @Override + void removePrivate() { + } + + @Override + public + boolean hasImage() { + return false; + } + + @Override + public + void setCallback(final SystemTrayMenuAction callback) { + + } +} diff --git a/src/dorkbox/systemTray/swing/SwingSystemTray.java b/src/dorkbox/systemTray/swing/SwingSystemTray.java index 08be48a..d2a3f86 100644 --- a/src/dorkbox/systemTray/swing/SwingSystemTray.java +++ b/src/dorkbox/systemTray/swing/SwingSystemTray.java @@ -17,7 +17,6 @@ package dorkbox.systemTray.swing; import java.awt.AWTException; import java.awt.Dimension; -import java.awt.Font; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; @@ -28,9 +27,9 @@ import java.awt.event.MouseEvent; import java.io.File; import java.io.InputStream; import java.net.URL; +import java.util.concurrent.atomic.AtomicInteger; import javax.swing.ImageIcon; -import javax.swing.JMenuItem; import dorkbox.systemTray.MenuEntry; import dorkbox.systemTray.SystemTrayMenuAction; @@ -49,10 +48,9 @@ import dorkbox.util.SwingUtil; @SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "WeakerAccess"}) public class SwingSystemTray extends dorkbox.systemTray.SystemTray { - volatile SwingSystemTrayMenuPopup menu; + static final AtomicInteger MENU_ID_COUNTER = new AtomicInteger(); - volatile JMenuItem connectionStatusItem; - private volatile String statusText = null; + volatile SwingSystemTrayMenuPopup menu; volatile SystemTray tray; volatile TrayIcon trayIcon; @@ -96,17 +94,27 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray { menuEntry.remove(); } tray.menuEntries.clear(); - - tray.connectionStatusItem = null; } } }); } + protected SwingSystemTrayMenuPopup getMenu() { + return menu; + } + + @Override public String getStatus() { - return this.statusText; + synchronized (menuEntries) { + MenuEntry menuEntry = menuEntries.get(0); + if (menuEntry instanceof SwingMenuEntryStatus) { + return menuEntry.getText(); + } + } + + return null; } protected @@ -118,33 +126,65 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray { @Override public void setStatus(final String statusText) { - this.statusText = statusText; - dispatch(new Runnable() { @Override public void run() { SwingSystemTray tray = SwingSystemTray.this; synchronized (tray) { - if (tray.connectionStatusItem == null) { - final JMenuItem connectionStatusItem = new JMenuItem(statusText); - Font font = connectionStatusItem.getFont(); - Font font1 = font.deriveFont(Font.BOLD); - connectionStatusItem.setFont(font1); + synchronized (menuEntries) { + // status is ALWAYS at 0 index... + SwingMenuEntry menuEntry = null; + if (!menuEntries.isEmpty()) { + menuEntry = (SwingMenuEntry) menuEntries.get(0); + } - connectionStatusItem.setEnabled(false); - tray.menu.add(connectionStatusItem); + if (menuEntry instanceof SwingMenuEntryStatus) { + // set the text or delete... - tray.connectionStatusItem = connectionStatusItem; - } - else { - tray.connectionStatusItem.setText(statusText); + if (statusText == null) { + // delete + removeMenuEntry(menuEntry); + } + else { + // set text + menuEntry.setText(statusText); + } + + } else { + // create a new one + menuEntry = new SwingMenuEntryStatus(statusText, tray); + // status is ALWAYS at 0 index... + menuEntries.add(0, menuEntry); + } } } } }); } + + @Override + public + void addMenuSpacer() { + dispatch(new Runnable() { + @Override + public + void run() { + SwingSystemTray tray = SwingSystemTray.this; + synchronized (tray) { + synchronized (menuEntries) { + synchronized (menuEntries) { + MenuEntry menuEntry = new SwingMenuEntrySpacer(tray); + menuEntries.add(menuEntry); + } + } + } + } + }); + } + + @Override protected void setIcon_(final File iconFile) { @@ -244,7 +284,7 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray { throw new IllegalArgumentException("Menu entry already exists for given label '" + menuText + "'"); } else { - menuEntry = new SwingMenuEntry(menu, menuText, imagePath, callback, tray); + menuEntry = new SwingMenuEntryItem(menuText, imagePath, callback, tray); menuEntries.add(menuEntry); } } @@ -287,7 +327,6 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray { } @Override - @Deprecated public void addMenuEntry(final String menuText, final InputStream imageStream, final SystemTrayMenuAction callback) { if (imageStream == null) { diff --git a/test/dorkbox/TestTray.java b/test/dorkbox/TestTray.java index a401eea..6ad7c79 100644 --- a/test/dorkbox/TestTray.java +++ b/test/dorkbox/TestTray.java @@ -81,6 +81,7 @@ class TestTray { }; this.systemTray.addMenuEntry("Green Mail", GREEN_MAIL, callbackGreen); + this.systemTray.addMenuSpacer(); systemTray.addMenuEntry("Quit", new SystemTrayMenuAction() { @Override diff --git a/test/dorkbox/TestTrayJavaFX.java b/test/dorkbox/TestTrayJavaFX.java index 416ae38..45e7941 100644 --- a/test/dorkbox/TestTrayJavaFX.java +++ b/test/dorkbox/TestTrayJavaFX.java @@ -115,6 +115,7 @@ class TestTrayJavaFX extends Application { }; this.systemTray.addMenuEntry("Green Mail", GREEN_MAIL, callbackGreen); + this.systemTray.addMenuSpacer(); systemTray.addMenuEntry("Quit", new SystemTrayMenuAction() { @Override diff --git a/test/dorkbox/TestTraySwt.java b/test/dorkbox/TestTraySwt.java index 8cf886c..044a029 100644 --- a/test/dorkbox/TestTraySwt.java +++ b/test/dorkbox/TestTraySwt.java @@ -99,6 +99,7 @@ class TestTraySwt { }; this.systemTray.addMenuEntry("Green Mail", GREEN_MAIL, callbackGreen); + this.systemTray.addMenuSpacer(); systemTray.addMenuEntry("Quit", new SystemTrayMenuAction() { @Override