forked from dorkbox/SystemTray
Code polish for Gtk Checkbox items and ubuntu
This commit is contained in:
parent
15029d0256
commit
7673edae4b
@ -28,10 +28,10 @@ import dorkbox.systemTray.Menu;
|
|||||||
import dorkbox.systemTray.MenuItem;
|
import dorkbox.systemTray.MenuItem;
|
||||||
import dorkbox.systemTray.Separator;
|
import dorkbox.systemTray.Separator;
|
||||||
import dorkbox.systemTray.Status;
|
import dorkbox.systemTray.Status;
|
||||||
import dorkbox.systemTray.SystemTray;
|
|
||||||
import dorkbox.systemTray.jna.linux.Gtk;
|
import dorkbox.systemTray.jna.linux.Gtk;
|
||||||
import dorkbox.systemTray.peer.MenuPeer;
|
import dorkbox.systemTray.peer.MenuPeer;
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
class GtkMenu extends GtkBaseMenuItem implements 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
|
// 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 LinkedList<GtkBaseMenuItem>();
|
private final List<GtkBaseMenuItem> menuEntries = new LinkedList<GtkBaseMenuItem>();
|
||||||
@ -59,6 +59,7 @@ class GtkMenu extends GtkBaseMenuItem implements MenuPeer {
|
|||||||
|
|
||||||
// This is NOT a copy constructor!
|
// This is NOT a copy constructor!
|
||||||
@SuppressWarnings("IncompleteCopyConstructor")
|
@SuppressWarnings("IncompleteCopyConstructor")
|
||||||
|
private
|
||||||
GtkMenu(final GtkMenu parent) {
|
GtkMenu(final GtkMenu parent) {
|
||||||
super(Gtk.gtk_image_menu_item_new_with_mnemonic("")); // is what is added to the parent menu (so images work)
|
super(Gtk.gtk_image_menu_item_new_with_mnemonic("")); // is what is added to the parent menu (so images work)
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
@ -99,6 +100,7 @@ class GtkMenu extends GtkBaseMenuItem implements MenuPeer {
|
|||||||
*
|
*
|
||||||
* ALWAYS CALLED ON EDT
|
* ALWAYS CALLED ON EDT
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("ForLoopReplaceableByForEach")
|
||||||
private
|
private
|
||||||
void deleteMenu() {
|
void deleteMenu() {
|
||||||
if (obliterateInProgress.get()) {
|
if (obliterateInProgress.get()) {
|
||||||
@ -135,6 +137,7 @@ class GtkMenu extends GtkBaseMenuItem implements MenuPeer {
|
|||||||
*
|
*
|
||||||
* ALWAYS CALLED ON THE EDT
|
* ALWAYS CALLED ON THE EDT
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("ForLoopReplaceableByForEach")
|
||||||
private
|
private
|
||||||
void createMenu() {
|
void createMenu() {
|
||||||
if (obliterateInProgress.get()) {
|
if (obliterateInProgress.get()) {
|
||||||
@ -176,6 +179,7 @@ class GtkMenu extends GtkBaseMenuItem implements MenuPeer {
|
|||||||
*
|
*
|
||||||
* ALWAYS CALLED ON THE EDT
|
* ALWAYS CALLED ON THE EDT
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("ForLoopReplaceableByForEach")
|
||||||
private
|
private
|
||||||
void obliterateMenu() {
|
void obliterateMenu() {
|
||||||
if (_nativeMenu != null && !obliterateInProgress.get()) {
|
if (_nativeMenu != null && !obliterateInProgress.get()) {
|
||||||
@ -226,11 +230,7 @@ class GtkMenu extends GtkBaseMenuItem implements MenuPeer {
|
|||||||
entry.bind(item, parentMenu, parentMenu.getSystemTray());
|
entry.bind(item, parentMenu, parentMenu.getSystemTray());
|
||||||
}
|
}
|
||||||
else if (entry instanceof Checkbox) {
|
else if (entry instanceof Checkbox) {
|
||||||
// Additionally, we can ask the SystemTray WHAT KIND of tray it is, since it will know by this point in time.
|
GtkMenuItemCheckbox item = new GtkMenuItemCheckbox(GtkMenu.this);
|
||||||
// necessary because of bad layout decisions by AppIndicators for checkbox items
|
|
||||||
|
|
||||||
boolean isAppIndicator = SystemTray.get().getMenu() instanceof _AppIndicatorNativeTray;
|
|
||||||
GtkMenuItemCheckbox item = new GtkMenuItemCheckbox(GtkMenu.this, isAppIndicator);
|
|
||||||
add(item, index);
|
add(item, index);
|
||||||
((Checkbox) entry).bind(item, parentMenu, parentMenu.getSystemTray());
|
((Checkbox) entry).bind(item, parentMenu, parentMenu.getSystemTray());
|
||||||
}
|
}
|
||||||
|
@ -32,14 +32,38 @@ import dorkbox.systemTray.SystemTray;
|
|||||||
import dorkbox.systemTray.jna.linux.GCallback;
|
import dorkbox.systemTray.jna.linux.GCallback;
|
||||||
import dorkbox.systemTray.jna.linux.Gobject;
|
import dorkbox.systemTray.jna.linux.Gobject;
|
||||||
import dorkbox.systemTray.jna.linux.Gtk;
|
import dorkbox.systemTray.jna.linux.Gtk;
|
||||||
|
import dorkbox.systemTray.jna.linux.GtkTheme;
|
||||||
import dorkbox.systemTray.peer.CheckboxPeer;
|
import dorkbox.systemTray.peer.CheckboxPeer;
|
||||||
|
import dorkbox.systemTray.util.HeavyCheckMark;
|
||||||
import dorkbox.systemTray.util.ImageResizeUtil;
|
import dorkbox.systemTray.util.ImageResizeUtil;
|
||||||
|
import dorkbox.util.OSUtil;
|
||||||
import dorkbox.util.SwingUtil;
|
import dorkbox.util.SwingUtil;
|
||||||
|
|
||||||
// ElementaryOS shows the checkbox on the right, everyone else is on the left. With eOS, we CANNOT show the spacer image. It does not work
|
@SuppressWarnings("deprecation")
|
||||||
class GtkMenuItemCheckbox extends GtkBaseMenuItem implements CheckboxPeer, GCallback {
|
class GtkMenuItemCheckbox extends GtkBaseMenuItem implements CheckboxPeer, GCallback {
|
||||||
private static String checkedFile;
|
private static volatile String checkedFile;
|
||||||
private static String uncheckedFile;
|
|
||||||
|
// here, it doesn't matter what size the image is, as long as there is an image, the text in the menu will be shifted correctly
|
||||||
|
private static final String uncheckedFile = ImageResizeUtil.getTransparentImage().getAbsolutePath();
|
||||||
|
|
||||||
|
// Note: So far, ONLY Ubuntu has managed to fail at rendering (bad layouts) checkbox menu items.
|
||||||
|
// If there are OTHER OSes that fail, checks for them should be added here
|
||||||
|
private static final boolean useFakeCheckMark;
|
||||||
|
static {
|
||||||
|
// this class is initialized on the GTK dispatch thread.
|
||||||
|
|
||||||
|
if (SystemTray.AUTO_FIX_INCONSISTENCIES &&
|
||||||
|
(SystemTray.get().getMenu() instanceof _AppIndicatorNativeTray) &&
|
||||||
|
OSUtil.Linux.isUbuntu()) {
|
||||||
|
useFakeCheckMark = true;
|
||||||
|
} else {
|
||||||
|
useFakeCheckMark = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SystemTray.DEBUG) {
|
||||||
|
SystemTray.logger.info("Using Fake CheckMark: " + useFakeCheckMark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final GtkMenu parent;
|
private final GtkMenu parent;
|
||||||
|
|
||||||
@ -54,73 +78,59 @@ class GtkMenuItemCheckbox extends GtkBaseMenuItem implements CheckboxPeer, GCall
|
|||||||
// GtkStatusIconTray will show on mouse+keyboard movement
|
// GtkStatusIconTray will show on mouse+keyboard movement
|
||||||
private volatile char mnemonicKey = 0;
|
private volatile char mnemonicKey = 0;
|
||||||
private final long handlerId;
|
private final long handlerId;
|
||||||
private final boolean isAppIndicator;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it!
|
* called from inside GTK 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
|
* this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref
|
||||||
*
|
*
|
||||||
* Because AppIndicator checkbox's DO NOT align correctly (on ubuntu), we use an image_menu_item (instead of a check_menu_item), so that
|
* Because Ubuntu AppIndicator checkbox's DO NOT align correctly, we use an image_menu_item (instead of a check_menu_item),
|
||||||
* the alignment is correct for the menu item (with a check_menu_item, they are shifted left - which looks pretty bad)
|
* so that the alignment is correct for the menu item (with a check_menu_item, they are shifted left - which looks pretty bad)
|
||||||
*
|
*
|
||||||
* For AppIndicators, this is not possible to fix, because we cannot control how the menu's are rendered (this is by design)
|
* For AppIndicators, this is not possible to fix, because we cannot control how the menu's are rendered (this is by design)
|
||||||
* Specifically, since it's implementation was copied from GTK, GtkCheckButton and GtkRadioButton allocate only the minimum size
|
* Specifically, since it's implementation was copied from GTK, GtkCheckButton and GtkRadioButton allocate only the minimum size
|
||||||
* necessary for its child. This causes the child alignment to fail. There is no fix we can apply - so we don't use them.
|
* necessary for its child. This causes the child alignment to fail. There is no fix we can apply - so we don't use them.
|
||||||
|
*
|
||||||
|
* Again, this is ONLY noticed on UBUNTU. For example, ElementaryOS is OK (it is also with a checkbox on the right).
|
||||||
|
* ElementaryOS shows the checkbox on the right, everyone else is on the left. With eOS, we CANNOT show the spacer image, so we MUST
|
||||||
|
* show this as a GTK Status Icon (not an AppIndicator), this way the "proper" checkbox is shown.
|
||||||
*/
|
*/
|
||||||
GtkMenuItemCheckbox(final GtkMenu parent, final boolean isAppIndicator) {
|
GtkMenuItemCheckbox(final GtkMenu parent) {
|
||||||
super(isAppIndicator ? Gtk.gtk_image_menu_item_new_with_mnemonic("") : Gtk.gtk_check_menu_item_new_with_mnemonic(""));
|
super(useFakeCheckMark ? Gtk.gtk_image_menu_item_new_with_mnemonic("") : Gtk.gtk_check_menu_item_new_with_mnemonic(""));
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.isAppIndicator = isAppIndicator;
|
|
||||||
|
|
||||||
handlerId = Gobject.g_signal_connect_object(_native, "activate", this, null, 0);
|
handlerId = Gobject.g_signal_connect_object(_native, "activate", this, null, 0);
|
||||||
|
|
||||||
if (checkedFile == null) {
|
if (useFakeCheckMark) {
|
||||||
Color color = Gtk.getCurrentThemeTextColor();
|
if (checkedFile == null) {
|
||||||
|
// on GTK thread
|
||||||
|
final Color color = GtkTheme.getCurrentThemeTextColor();
|
||||||
|
|
||||||
try {
|
// have to invoke Swing components on the Swing thread! In some cases, when the UI Manager is the native one (and
|
||||||
int iconSize = 32;
|
// thus is GTK), the swing thread is combined with the GTK thread. This causes problems!
|
||||||
final File newFile = new File(ImageResizeUtil.TEMP_DIR, iconSize + "_checkMark_" + color.getRGB() + ".png").getAbsoluteFile();
|
SwingUtil.invokeLater(new Runnable() {
|
||||||
|
@Override
|
||||||
if (!newFile.canRead()) {
|
public
|
||||||
JMenuItem jMenuItem = new JMenuItem();
|
void run() {
|
||||||
|
if (checkedFile == null) {
|
||||||
// do the same modifications that would also happen (if specified) for the actual displayed menu items
|
checkedFile = getCheckedFile(color);
|
||||||
if (SystemTray.SWING_UI != null) {
|
|
||||||
jMenuItem.setUI(SystemTray.SWING_UI.getItemUI(jMenuItem, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure the directory exists
|
|
||||||
if (!newFile.getParentFile()
|
|
||||||
.isDirectory()) {
|
|
||||||
|
|
||||||
boolean mkdirs = newFile.getParentFile()
|
|
||||||
.mkdirs();
|
|
||||||
|
|
||||||
if (!mkdirs) {
|
|
||||||
SystemTray.logger.error("Unable to create directory for check-mark image at: {}", newFile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// now that the swing part is finished, we can conclude with the GTK part
|
||||||
|
Gtk.dispatch(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
setCheckedIconForFakeCheckMarks();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
// get the largest font (based on the orig font) that can be used WITHOUT changing the size of the JMenuItem
|
} else {
|
||||||
Font fontForSpecificHeight = SwingUtil.getFontForSpecificHeight(jMenuItem.getFont(), iconSize);
|
setCheckedIconForFakeCheckMarks();
|
||||||
|
|
||||||
BufferedImage fontAsImage = SwingUtil.getFontAsImage(fontForSpecificHeight, "✔", color); // or "✓"
|
|
||||||
ImageIO.write(fontAsImage, "png", newFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
checkedFile = newFile.getAbsolutePath();
|
|
||||||
|
|
||||||
// here, it doesn't matter what size the image is, as long as there is an image, the text in the menu will be shifted correctly
|
|
||||||
uncheckedFile = ImageResizeUtil.getTransparentImage().getAbsolutePath();
|
|
||||||
} catch(Exception e) {
|
|
||||||
SystemTray.logger.error("Error creating check-mark image.", e);
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if (isAppIndicator) {
|
|
||||||
setCheckedIconForAppIndicators();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Gobject.g_signal_handler_block(_native, handlerId);
|
Gobject.g_signal_handler_block(_native, handlerId);
|
||||||
Gtk.gtk_check_menu_item_set_active(_native, false);
|
Gtk.gtk_check_menu_item_set_active(_native, false);
|
||||||
Gobject.g_signal_handler_unblock(_native, handlerId);
|
Gobject.g_signal_handler_unblock(_native, handlerId);
|
||||||
@ -235,7 +245,9 @@ class GtkMenuItemCheckbox extends GtkBaseMenuItem implements CheckboxPeer, GCall
|
|||||||
@Override
|
@Override
|
||||||
public
|
public
|
||||||
void run() {
|
void run() {
|
||||||
if (!isAppIndicator) {
|
if (useFakeCheckMark) {
|
||||||
|
setCheckedIconForFakeCheckMarks();
|
||||||
|
} else {
|
||||||
// note: this will trigger "activate", which will then trigger the callback.
|
// note: this will trigger "activate", which will then trigger the callback.
|
||||||
// we assume this is consistent across ALL versions and variants of GTK
|
// we assume this is consistent across ALL versions and variants of GTK
|
||||||
// https://github.com/GNOME/gtk/blob/master/gtk/gtkcheckmenuitem.c#L317
|
// https://github.com/GNOME/gtk/blob/master/gtk/gtkcheckmenuitem.c#L317
|
||||||
@ -243,16 +255,15 @@ class GtkMenuItemCheckbox extends GtkBaseMenuItem implements CheckboxPeer, GCall
|
|||||||
Gobject.g_signal_handler_block(_native, handlerId);
|
Gobject.g_signal_handler_block(_native, handlerId);
|
||||||
Gtk.gtk_check_menu_item_set_active(_native, isChecked);
|
Gtk.gtk_check_menu_item_set_active(_native, isChecked);
|
||||||
Gobject.g_signal_handler_unblock(_native, handlerId);
|
Gobject.g_signal_handler_unblock(_native, handlerId);
|
||||||
} else {
|
|
||||||
setCheckedIconForAppIndicators();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this is pretty much ONLY for Ubuntu AppIndicators
|
||||||
private
|
private
|
||||||
void setCheckedIconForAppIndicators() {
|
void setCheckedIconForFakeCheckMarks() {
|
||||||
if (checkedImage != null) {
|
if (checkedImage != null) {
|
||||||
Gtk.gtk_container_remove(_native, checkedImage); // will automatically get destroyed if no other references to it
|
Gtk.gtk_container_remove(_native, checkedImage); // will automatically get destroyed if no other references to it
|
||||||
checkedImage = null;
|
checkedImage = null;
|
||||||
@ -303,4 +314,50 @@ class GtkMenuItemCheckbox extends GtkBaseMenuItem implements CheckboxPeer, GCall
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This must always occur on the SWING thread (because stuff here is SWING related...)
|
||||||
|
*
|
||||||
|
* This saves a scalable CheckMark to a correctly sized PNG file.
|
||||||
|
*
|
||||||
|
* @param color the color of the CheckMark
|
||||||
|
*/
|
||||||
|
private static
|
||||||
|
String getCheckedFile(Color color) {
|
||||||
|
final int iconSize = 32;
|
||||||
|
String name = iconSize + "_checkMark" + HeavyCheckMark.VERSION + "_" + color.getRGB() + ".png";
|
||||||
|
final File newFile = new File(ImageResizeUtil.TEMP_DIR, name).getAbsoluteFile();
|
||||||
|
|
||||||
|
if (newFile.canRead() || newFile.length() == 0) {
|
||||||
|
try {
|
||||||
|
JMenuItem jMenuItem = new JMenuItem();
|
||||||
|
// do the same modifications that would also happen (if specified) for the actual displayed menu items
|
||||||
|
if (SystemTray.SWING_UI != null) {
|
||||||
|
jMenuItem.setUI(SystemTray.SWING_UI.getItemUI(jMenuItem, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure the directory exists
|
||||||
|
if (!newFile.getParentFile()
|
||||||
|
.isDirectory()) {
|
||||||
|
|
||||||
|
boolean mkdirs = newFile.getParentFile()
|
||||||
|
.mkdirs();
|
||||||
|
|
||||||
|
if (!mkdirs) {
|
||||||
|
SystemTray.logger.error("Unable to create directory for check-mark image at: {}", newFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the largest font (based on the orig font) that can be used WITHOUT changing the size of the JMenuItem
|
||||||
|
Font fontForSpecificHeight = SwingUtil.getFontForSpecificHeight(jMenuItem.getFont(), iconSize);
|
||||||
|
|
||||||
|
BufferedImage img = HeavyCheckMark.draw(fontForSpecificHeight, color);
|
||||||
|
ImageIO.write(img, "png", newFile);
|
||||||
|
} catch (Exception e) {
|
||||||
|
SystemTray.logger.error("Error creating check-mark image.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newFile.getAbsolutePath();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user