408 lines
14 KiB
Java
408 lines
14 KiB
Java
/*
|
|
* Copyright 2021 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.ui.gtk;
|
|
|
|
import static dorkbox.jna.linux.Gtk.Gtk2;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
import com.sun.jna.Pointer;
|
|
|
|
import dorkbox.jna.linux.GtkEventDispatch;
|
|
import dorkbox.systemTray.Checkbox;
|
|
import dorkbox.systemTray.Entry;
|
|
import dorkbox.systemTray.Menu;
|
|
import dorkbox.systemTray.MenuItem;
|
|
import dorkbox.systemTray.Separator;
|
|
import dorkbox.systemTray.Status;
|
|
import dorkbox.systemTray.peer.MenuPeer;
|
|
|
|
class GtkMenu extends GtkBaseMenuItem implements MenuPeer {
|
|
// this is a list (that mirrors the actual list) BECAUSE we have to create/delete the entire menu in GTK every time something is changed
|
|
private final List<GtkBaseMenuItem> menuEntries = new ArrayList<>();
|
|
|
|
private final GtkMenu parent; // null when we are the main menu attached to the tray icon
|
|
|
|
volatile Pointer _nativeMenu; // must ONLY be created at the end of delete!
|
|
|
|
private volatile Pointer image;
|
|
|
|
// The mnemonic will ONLY show-up once a menu entry is selected. IT WILL NOT show up before then!
|
|
// AppIndicators will only show if you use the keyboard to navigate
|
|
// GtkStatusIconTray will show on mouse+keyboard movement
|
|
private volatile char mnemonicKey = 0;
|
|
|
|
// have to make sure no other methods can call obliterate, delete, or create menu once it's already started
|
|
private final AtomicBoolean obliterateInProgress = new AtomicBoolean(false);
|
|
|
|
// called by the system tray constructors
|
|
// This is NOT a copy constructor!
|
|
@SuppressWarnings("IncompleteCopyConstructor")
|
|
GtkMenu() {
|
|
super(null);
|
|
this.parent = null;
|
|
}
|
|
|
|
// This is NOT a copy constructor!
|
|
@SuppressWarnings("IncompleteCopyConstructor")
|
|
private
|
|
GtkMenu(final GtkMenu parent) {
|
|
super(Gtk2.gtk_image_menu_item_new_with_mnemonic("")); // is what is added to the parent menu (so images work)
|
|
this.parent = parent;
|
|
}
|
|
|
|
GtkMenu getParent() {
|
|
return parent;
|
|
}
|
|
|
|
/**
|
|
* Called inside the gdk_threads block
|
|
*
|
|
* ALWAYS CALLED ON THE EDT
|
|
*/
|
|
protected
|
|
void onMenuAdded(final Pointer menu) {
|
|
// only needed for AppIndicator
|
|
}
|
|
|
|
|
|
/**
|
|
* Deletes the menu, and unreferences everything in it. ALSO recreates ONLY the menu object.
|
|
*
|
|
* 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.
|
|
*
|
|
* ALWAYS CALLED ON EDT
|
|
*/
|
|
@SuppressWarnings("ForLoopReplaceableByForEach")
|
|
private
|
|
void deleteMenu(boolean recursiveDeleteParentMenu) {
|
|
if (obliterateInProgress.get()) {
|
|
return;
|
|
}
|
|
|
|
if (_nativeMenu != null) {
|
|
// have to remove all other menu entries
|
|
for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) {
|
|
final GtkBaseMenuItem menuEntry__ = menuEntries.get(i);
|
|
menuEntry__.onDeleteMenu(_nativeMenu);
|
|
}
|
|
|
|
Gtk2.gtk_widget_destroy(_nativeMenu);
|
|
}
|
|
|
|
if (parent != null && recursiveDeleteParentMenu) {
|
|
parent.deleteMenu(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* ALWAYS CALLED ON THE EDT
|
|
*/
|
|
@SuppressWarnings("ForLoopReplaceableByForEach")
|
|
private
|
|
void createMenu(boolean recursiveCreateParentMenu) {
|
|
if (obliterateInProgress.get()) {
|
|
return;
|
|
}
|
|
|
|
// makes a new one
|
|
_nativeMenu = Gtk2.gtk_menu_new();
|
|
|
|
// binds sub-menu to entry (if it exists! it does not for the root menu)
|
|
if (parent != null) {
|
|
Gtk2.gtk_menu_item_set_submenu(_native, _nativeMenu);
|
|
}
|
|
|
|
if (parent != null && recursiveCreateParentMenu) {
|
|
parent.createMenu(true);
|
|
}
|
|
|
|
// now add back other menu entries
|
|
boolean hasImages = false;
|
|
|
|
for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) {
|
|
final GtkBaseMenuItem menuEntry__ = menuEntries.get(i);
|
|
hasImages |= menuEntry__.hasImage();
|
|
}
|
|
|
|
for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) {
|
|
// the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images
|
|
final GtkBaseMenuItem menuEntry__ = menuEntries.get(i);
|
|
menuEntry__.onCreateMenu(_nativeMenu, hasImages);
|
|
|
|
if (menuEntry__ instanceof GtkMenu) {
|
|
GtkMenu subMenu = (GtkMenu) menuEntry__;
|
|
if (subMenu.getParent() != GtkMenu.this) {
|
|
// we don't want to "createMenu" on our sub-menu that is assigned to us directly, as they are already doing it
|
|
subMenu.createMenu(recursiveCreateParentMenu);
|
|
}
|
|
}
|
|
}
|
|
|
|
onMenuAdded(_nativeMenu);
|
|
}
|
|
|
|
/**
|
|
* Completely obliterates the menu, no possible way to reconstruct it.
|
|
*
|
|
* ALWAYS CALLED ON THE EDT
|
|
*/
|
|
@SuppressWarnings("ForLoopReplaceableByForEach")
|
|
private
|
|
void obliterateMenu() {
|
|
if (_nativeMenu != null && !obliterateInProgress.get()) {
|
|
obliterateInProgress.set(true);
|
|
|
|
// have to remove all other menu entries
|
|
|
|
// a copy is made because sub-menus remove themselves from parents when .remove() is called. If we don't
|
|
// do this, errors will be had because indices don't line up anymore.
|
|
ArrayList<GtkBaseMenuItem> menuEntriesCopy = new ArrayList<>(menuEntries);
|
|
menuEntries.clear();
|
|
|
|
for (int i = 0, menuEntriesSize = menuEntriesCopy.size(); i < menuEntriesSize; i++) {
|
|
final GtkBaseMenuItem menuEntry__ = menuEntriesCopy.get(i);
|
|
menuEntry__.remove();
|
|
}
|
|
menuEntriesCopy.clear();
|
|
|
|
Gtk2.gtk_widget_destroy(_nativeMenu);
|
|
_nativeMenu = null;
|
|
|
|
obliterateInProgress.set(false);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public
|
|
void add(final Menu parentMenu, final Entry entry, final int index) {
|
|
// must always be called on the GTK dispatch. This must be dispatchAndWait() so it will properly executed immediately
|
|
GtkEventDispatch.dispatchAndWait(()->{
|
|
// 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.
|
|
|
|
// when adding/removing menus DURING the `add` operation for a menu, we DO NOT want to recursively add/remove menus!
|
|
deleteMenu(false);
|
|
|
|
GtkBaseMenuItem item = null;
|
|
|
|
if (entry instanceof Menu) {
|
|
// some implementations of appindicator, do NOT like having a menu added, which has no menu items yet.
|
|
// see: https://bugs.launchpad.net/glipper/+bug/1203888
|
|
item = new GtkMenu(GtkMenu.this);
|
|
menuEntries.add(index, item);
|
|
}
|
|
else if (entry instanceof Separator) {
|
|
item = new GtkMenuItemSeparator(GtkMenu.this);
|
|
menuEntries.add(index, item);
|
|
}
|
|
else if (entry instanceof Checkbox) {
|
|
item = new GtkMenuItemCheckbox(GtkMenu.this);
|
|
menuEntries.add(index, item);
|
|
}
|
|
else if (entry instanceof Status) {
|
|
item = new GtkMenuItemStatus(GtkMenu.this);
|
|
menuEntries.add(index, item);
|
|
}
|
|
else if (entry instanceof MenuItem) {
|
|
item = new GtkMenuItem(GtkMenu.this);
|
|
menuEntries.add(index, item);
|
|
}
|
|
|
|
|
|
// we must create the menu BEFORE binding the menu, otherwise the menus' children's GTK element can be added before
|
|
// their parent GTK elements are added (and the menu won't show up)
|
|
if (entry instanceof Menu) {
|
|
((Menu) entry).bind((GtkMenu) item, parentMenu, parentMenu.getImageResizeUtil());
|
|
}
|
|
else if (entry instanceof Separator) {
|
|
((Separator)entry).bind((GtkMenuItemSeparator) item, parentMenu, parentMenu.getImageResizeUtil());
|
|
}
|
|
else if (entry instanceof Checkbox) {
|
|
((Checkbox) entry).bind((GtkMenuItemCheckbox) item, parentMenu, parentMenu.getImageResizeUtil());
|
|
}
|
|
else if (entry instanceof Status) {
|
|
((Status) entry).bind((GtkMenuItemStatus) item, parentMenu, parentMenu.getImageResizeUtil());
|
|
}
|
|
else if (entry instanceof MenuItem) {
|
|
((MenuItem) entry).bind((GtkMenuItem) item, parentMenu, parentMenu.getImageResizeUtil());
|
|
}
|
|
|
|
// when adding/removing menus DURING the `add` operation for a menu, we DO NOT want to recursively add/remove menus!
|
|
createMenu(false);
|
|
|
|
// only call show on the ROOT menu!
|
|
if (parent == null) {
|
|
Gtk2.gtk_widget_show_all(_nativeMenu);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// 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
|
|
|
|
// is overridden in tray impl
|
|
@SuppressWarnings("Duplicates")
|
|
@Override
|
|
public
|
|
void setImage(final MenuItem menuItem) {
|
|
// is overridden by system tray
|
|
setLegitImage(menuItem.getImage() != null);
|
|
|
|
GtkEventDispatch.dispatch(()->{
|
|
if (image != null) {
|
|
Gtk2.gtk_container_remove(_native, image); // will automatically get destroyed if no other references to it
|
|
image = null;
|
|
}
|
|
|
|
if (menuItem.getImage() != null) {
|
|
image = Gtk2.gtk_image_new_from_file(menuItem.getImage().getAbsolutePath());
|
|
Gtk2.gtk_image_menu_item_set_image(_native, image);
|
|
|
|
// must always re-set always-show after setting the image
|
|
Gtk2.gtk_image_menu_item_set_always_show_image(_native, true);
|
|
}
|
|
|
|
Gtk2.gtk_widget_show_all(_native);
|
|
});
|
|
}
|
|
|
|
// is overridden in tray impl
|
|
@Override
|
|
public
|
|
void setEnabled(final MenuItem menuItem) {
|
|
// is overridden by system tray
|
|
GtkEventDispatch.dispatch(()->Gtk2.gtk_widget_set_sensitive(_native, menuItem.getEnabled()));
|
|
}
|
|
|
|
// is overridden in tray impl
|
|
@SuppressWarnings("Duplicates")
|
|
@Override
|
|
public
|
|
void setText(final MenuItem menuItem) {
|
|
// is overridden by system tray
|
|
final String textWithMnemonic;
|
|
|
|
if (mnemonicKey != 0) {
|
|
String text = menuItem.getText();
|
|
|
|
if (text != null) {
|
|
// they are CASE INSENSITIVE!
|
|
int i = text.toLowerCase()
|
|
.indexOf(mnemonicKey);
|
|
|
|
if (i >= 0) {
|
|
textWithMnemonic = text.substring(0, i) + "_" + text.substring(i);
|
|
}
|
|
else {
|
|
textWithMnemonic = menuItem.getText();
|
|
}
|
|
} else {
|
|
textWithMnemonic = null;
|
|
}
|
|
}
|
|
else {
|
|
textWithMnemonic = menuItem.getText();
|
|
}
|
|
|
|
GtkEventDispatch.dispatch(()->{
|
|
Gtk2.gtk_menu_item_set_label(_native, textWithMnemonic);
|
|
Gtk2.gtk_widget_show_all(_native);
|
|
});
|
|
}
|
|
|
|
// is overridden in tray impl
|
|
@Override
|
|
public
|
|
void setCallback(final MenuItem menuItem) {
|
|
// can't have a callback for menus!
|
|
}
|
|
|
|
// is overridden in tray impl
|
|
@Override
|
|
public
|
|
void setShortcut(final MenuItem menuItem) {
|
|
char shortcut = menuItem.getShortcut();
|
|
|
|
if (shortcut != 0) {
|
|
this.mnemonicKey = Character.toLowerCase(shortcut);
|
|
} else {
|
|
this.mnemonicKey = 0;
|
|
}
|
|
|
|
setText(menuItem);
|
|
}
|
|
|
|
@Override
|
|
public
|
|
void setTooltip(final MenuItem menuItem) {
|
|
GtkEventDispatch.dispatch(()->{
|
|
// NOTE: this will not work for AppIndicator tray types!
|
|
// null will remove the tooltip
|
|
Gtk2.gtk_widget_set_tooltip_text(_native, menuItem.getTooltip());
|
|
});
|
|
}
|
|
|
|
/**
|
|
* called when a child removes itself from the parent menu. Does not work for sub-menus
|
|
*
|
|
* ALWAYS CALLED ON THE EDT
|
|
*/
|
|
public
|
|
void remove(final GtkBaseMenuItem item) {
|
|
menuEntries.remove(item);
|
|
|
|
// have to rebuild the menu now...
|
|
deleteMenu(true); // must be on EDT
|
|
createMenu(true); // must be on EDT
|
|
}
|
|
|
|
// a child will always remove itself from the parent.
|
|
@Override
|
|
public
|
|
void remove() {
|
|
GtkEventDispatch.dispatch(()->{
|
|
GtkMenu parent = getParent();
|
|
|
|
if (parent != null) {
|
|
// have to remove from the parent.menuEntries first
|
|
parent.menuEntries.remove(GtkMenu.this);
|
|
}
|
|
|
|
// delete all of the children of this submenu (must happen before the menuEntry is removed)
|
|
obliterateMenu(); // must be on EDT
|
|
|
|
if (parent != null) {
|
|
// remove the gtk entry item from our menu NATIVE components
|
|
Gtk2.gtk_menu_item_set_submenu(_native, null);
|
|
|
|
// have to rebuild the menu now...
|
|
parent.deleteMenu(true); // must be on EDT
|
|
parent.createMenu(true); // must be on EDT
|
|
}
|
|
});
|
|
}
|
|
}
|