diff --git a/src/dorkbox/systemTray/jna/linux/GdkColor.java b/src/dorkbox/systemTray/jna/linux/GdkColor.java index 8bb0503..7dfd475 100644 --- a/src/dorkbox/systemTray/jna/linux/GdkColor.java +++ b/src/dorkbox/systemTray/jna/linux/GdkColor.java @@ -1,5 +1,6 @@ package dorkbox.systemTray.jna.linux; +import java.awt.Color; import java.util.Arrays; import java.util.List; @@ -13,11 +14,48 @@ import com.sun.jna.Structure; public class GdkColor extends Structure { + /* The color type. + * A color consists of red, green and blue values in the + * range 0-65535 and a pixel value. The pixel value is highly + * dependent on the depth and colormap which this color will + * be used to draw into. Therefore, sharing colors between + * colormaps is a bad idea. + */ public int pixel; public short red; public short green; public short blue; + /** + * Convert to positive int (value between 0 and 65535, these are 16 bits per pixel) that is from 0-255 + */ + private static int convert(int inputColor) { + return (inputColor & 0x0000FFFF >> 8) & 0xFF; + } + + public int red() { + return convert(red); + } + + public int green() { + return convert(green); + } + + public int blue() { + return convert(blue); + } + + public + Color getColor() { + return new Color(red(), green(), blue()); + } + + @Override + public + String toString() { + return "[r=" + red() + ",g=" + green() + ",b=" + blue() + "]"; + } + @Override protected List getFieldOrder() { diff --git a/src/dorkbox/systemTray/jna/linux/Gtk.java b/src/dorkbox/systemTray/jna/linux/Gtk.java index 2e2693d..434fcde 100644 --- a/src/dorkbox/systemTray/jna/linux/Gtk.java +++ b/src/dorkbox/systemTray/jna/linux/Gtk.java @@ -17,25 +17,17 @@ package dorkbox.systemTray.jna.linux; import static dorkbox.systemTray.SystemTray.logger; -import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.util.ArrayList; import java.util.LinkedList; -import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; import com.sun.jna.Function; import com.sun.jna.Pointer; -import com.sun.jna.ptr.PointerByReference; import dorkbox.systemTray.Entry; import dorkbox.systemTray.SystemTray; -import dorkbox.systemTray.Tray; -import dorkbox.systemTray.nativeUI._AppIndicatorNativeTray; -import dorkbox.systemTray.nativeUI._GtkStatusIconNativeTray; import dorkbox.systemTray.util.JavaFX; import dorkbox.systemTray.util.Swt; import dorkbox.util.OS; @@ -43,11 +35,7 @@ import dorkbox.util.jna.JnaHelper; /** * bindings for GTK+ 2. Bindings that are exclusively for GTK+ 3 are in that respective class - * - * note: gtk2/3 loading is SENSITIVE, and which AppIndicator symbols are loaded depends on this being loaded first - * Additionally, gtk3 has deprecated some methods -- which, fortunately for us, means it will be another 25 years before they are - * removed; forcing us to have completely separate gtk2/3 bindings. - * + *

* Direct-mapping, See: https://github.com/java-native-access/jna/blob/master/www/DirectMapping.md */ @SuppressWarnings({"Duplicates", "SameParameterValue", "DanglingJavadoc", "DeprecatedIsStillUsed"}) @@ -56,18 +44,50 @@ class Gtk { // For funsies to look at, SyncThing did a LOT of work on compatibility in python (unfortunate for us, but interesting). // https://github.com/syncthing/syncthing-gtk/blob/b7a3bc00e3bb6d62365ae62b5395370f3dcc7f55/syncthing_gtk/statusicon.py + + @SuppressWarnings("unused") + public static class State { + public static final int NORMAL = 0x0; // normal state. + public static final int ACTIVE = 0x1; // pressed-in or activated; e.g. buttons while the mouse button is held down. + public static final int PRELIGHT = 0x2; // color when the mouse is over an activatable widget. + public static final int SELECTED = 0x3; // color when something is selected, e.g. when selecting some text to cut/copy. + public static final int INSENSITIVE = 0x4; // color when the mouse is over an activatable widget. + + public static final int FLAG_NORMAL = 0; + public static final int FLAG_ACTIVE = 1 << 0; + public static final int FLAG_PRELIGHT = 1 << 1; + public static final int FLAG_SELECTED = 1 << 2; + public static final int FLAG_INSENSITIVE = 1 << 3; + public static final int FLAG_INCONSISTENT = 1 << 4; + public static final int FLAG_FOCUSED = 1 << 5; + public static final int FLAG_BACKDROP = 1 << 6; + } + + + // NOTE: AppIndicator uses this info to figure out WHAT VERSION OF appindicator to use: GTK2 -> appindicator1, GTK3 -> appindicator3 public static final boolean isGtk2; public static final boolean isGtk3; public static final boolean isLoaded; - public static Function gtk_status_icon_position_menu = null; + // objdump -T /usr/lib/x86_64-linux-gnu/libgtk-x11-2.0.so.0 | grep gtk + // objdump -T /usr/lib/x86_64-linux-gnu/libgtk-3.so.0 | grep gtk + + // objdump -T /usr/local/lib/libgtk-3.so.0 | grep gtk + + @SuppressWarnings("WeakerAccess") + public static final int FALSE = 0; + public static final int TRUE = 1; private static final boolean alreadyRunningGTK; - // when debugging the EDT, we need a longer timeout. private static final boolean debugEDT = true; - + // timeout is in seconds + private static final int TIMEOUT = debugEDT ? 10000000 : 2; + // have to save these in a field to prevent GC on the objects (since they go out-of-scope from java) + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + private static final LinkedList gtkCallbacks = new LinkedList(); + public static Function gtk_status_icon_position_menu = null; // This is required because the EDT needs to have it's own value for this boolean, that is a different value than the main thread private static ThreadLocal isDispatch = new ThreadLocal() { @Override @@ -76,14 +96,9 @@ class Gtk { return false; } }; - - // timeout is in seconds - private static final int TIMEOUT = debugEDT ? 10000000 : 2; - - // objdump -T /usr/lib/x86_64-linux-gnu/libgtk-x11-2.0.so.0 | grep gtk - // objdump -T /usr/lib/x86_64-linux-gnu/libgtk-3.so.0 | grep gtk - - // objdump -T /usr/local/lib/libgtk-3.so.0 | grep gtk + private static volatile boolean started = false; + @SuppressWarnings("FieldCanBeLocal") + private static Thread gtkUpdateThread = null; /** * We can have GTK v3 or v2. @@ -217,20 +232,6 @@ class Gtk { } } - private static volatile boolean started = false; - - // have to save these in a field to prevent GC on the objects (since they go out-of-scope from java) - @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") - private static final LinkedList gtkCallbacks = new LinkedList(); - - @SuppressWarnings("FieldCanBeLocal") - private static Thread gtkUpdateThread = null; - - @SuppressWarnings("WeakerAccess") - public static final int FALSE = 0; - public static final int TRUE = 1; - - public static void startGui() { // only permit one startup per JVM instance @@ -247,10 +248,15 @@ class Gtk { @Override public void run() { + Glib.GLogFunc orig = null; if (SystemTray.DEBUG) { logger.debug("Running GTK Native Event Loop"); + } else { + // NOTE: This can output warnings, so we suppress them + orig = Glib.g_log_set_default_handler(Glib.nullLogFunc, null); } + // prep for the event loop. // GThread.g_thread_init(null); would be needed for g_idle_add() @@ -263,6 +269,10 @@ class Gtk { // gdk_threads_enter(); would be needed for g_idle_add() + if (orig != null) { + Glib.g_log_set_default_handler(orig, null); + } + // blocks unit quit gtk_main(); @@ -277,6 +287,20 @@ class Gtk { } } + /** + * This would NORMALLY have a 2nd argument that is a String[] -- however JNA direct-mapping DOES NOT support this. We are lucky + * enough that we just pass 'null' as the second argument, therefore, we don't have to define that parameter here. + */ + private static native + boolean gtk_init_check(int argc); + + /** + * Runs the main loop until gtk_main_quit() is called. You can nest calls to gtk_main(). In that case gtk_main_quit() will make the + * innermost invocation of the main loop return. + */ + private static native + void gtk_main(); + /** * Waits for the GUI to finish loading */ @@ -432,6 +456,33 @@ class Gtk { } } + /** + * Adds a function to be called whenever there are no higher priority events pending. If the function returns FALSE it is automatically + * removed from the list of event sources and will not be called again. + *

+ * This variant of g_idle_add_full() calls function with the GDK lock held. It can be thought of a MT-safe version for GTK+ widgets + * for the following use case, where you have to worry about idle_callback() running in thread A and accessing self after it has + * been finalized in thread B. + */ + public static native + int gdk_threads_add_idle_full(int priority, FuncCallback function, Pointer data, Pointer notify); + + public static + void shutdownGui() { + dispatchAndWait(new Runnable() { + @Override + public + void run() { + // If JavaFX/SWT is used, this is UNNECESSARY (and will break SWT/JavaFX shutdown) + if (!alreadyRunningGTK) { + gtk_main_quit(); + } + + started = false; + } + }); + } + public static void dispatchAndWait(final Runnable runnable) { if (isDispatch.get()) { @@ -475,21 +526,12 @@ class Gtk { } } - public static - void shutdownGui() { - dispatchAndWait(new Runnable() { - @Override - public - void run() { - // If JavaFX/SWT is used, this is UNNECESSARY (and will break SWT/JavaFX shutdown) - if (!alreadyRunningGTK) { - gtk_main_quit(); - } - - started = false; - } - }); - } + /** + * Makes the innermost invocation of the main loop return when it regains control. ONLY CALL FROM THE GtkSupport class, UNLESS you know + * what you're doing! + */ + private static native + void gtk_main_quit(); /** * required to properly setup the dispatch flag when using native menus @@ -514,44 +556,12 @@ class Gtk { } } - /** - * This would NORMALLY have a 2nd argument that is a String[] -- however JNA direct-mapping DOES NOT support this. We are lucky - * enough that we just pass 'null' as the second argument, therefore, we don't have to define that parameter here. - */ - private static native - boolean gtk_init_check(int argc); - - /** - * Runs the main loop until gtk_main_quit() is called. You can nest calls to gtk_main(). In that case gtk_main_quit() will make the - * innermost invocation of the main loop return. - */ - private static native - void gtk_main(); - /** * aks for the current nesting level of the main loop. Useful to determine (at startup) if GTK is already running */ private static native int gtk_main_level(); - /** - * Makes the innermost invocation of the main loop return when it regains control. ONLY CALL FROM THE GtkSupport class, UNLESS you know - * what you're doing! - */ - private static native - void gtk_main_quit(); - - /** - * Adds a function to be called whenever there are no higher priority events pending. If the function returns FALSE it is automatically - * removed from the list of event sources and will not be called again. - *

- * This variant of g_idle_add_full() calls function with the GDK lock held. It can be thought of a MT-safe version for GTK+ widgets - * for the following use case, where you have to worry about idle_callback() running in thread A and accessing self after it has - * been finalized in thread B. - */ - public static native - int gdk_threads_add_idle_full(int priority, FuncCallback function, Pointer data, Pointer notify); - /** * Creates a new GtkMenu */ @@ -592,9 +602,9 @@ class Gtk { /** * Creates a new GtkImageMenuItem containing a label. The label will be created using gtk_label_new_with_mnemonic(), so underscores * in label indicate the mnemonic for the menu item. - * + *

* uses '_' to define which key is the mnemonic - * + *

* gtk_image_menu_item_new_with_mnemonic has been deprecated since version 3.10 and should not be used in newly-written code. * NOTE: Use gtk_menu_item_new_with_mnemonic() instead. */ @@ -608,7 +618,7 @@ class Gtk { /** * Sets the image of image_menu_item to the given widget. Note that it depends on the show-menu-images setting whether the image * will be displayed or not. - * + *

* gtk_image_menu_item_set_image has been deprecated since version 3.10 and should not be used in newly-written code. */ @Deprecated @@ -627,7 +637,7 @@ class Gtk { /** * Creates an empty status icon object. - * + *

* gtk_status_icon_new has been deprecated since version 3.14 and should not be used in newly-written code. * Use notifications */ @@ -639,16 +649,17 @@ class Gtk { * Gets the size in pixels that is available for the image. Stock icons and named icons adapt their size automatically if the size of * the notification area changes. For other storage types, the size-changed signal can be used to react to size changes. * Note that the returned size is only meaningful while the status icon is embedded (see gtk_status_icon_is_embedded()). - * + *

* gtk_status_icon_get_size has been deprecated since version 3.14 and should not be used in newly-written code. * Use notifications */ @Deprecated - public static native int gtk_status_icon_get_size(Pointer status_icon); + public static native + int gtk_status_icon_get_size(Pointer status_icon); /** * Makes status_icon display the file filename . See gtk_status_icon_new_from_file() for details. - * + *

* gtk_status_icon_set_from_file has been deprecated since version 3.14 and should not be used in newly-written code. * Use notifications */ @@ -658,7 +669,7 @@ class Gtk { /** * Shows or hides a status icon. - * + *

* gtk_status_icon_set_visible has been deprecated since version 3.14 and should not be used in newly-written code. * Use notifications */ @@ -676,14 +687,13 @@ class Gtk { * * gtk_status_icon_set_tooltip_text has been deprecated since version 3.14 and should not be used in newly-written code. * Use notifications - */ - public static native + */ public static native void gtk_status_icon_set_tooltip_text(Pointer widget, String tooltipText); /** * Sets the title of this tray icon. This should be a short, human-readable, localized string describing the tray icon. It may be used * by tools like screen readers to render the tray icon. - * + *

* gtk_status_icon_set_title has been deprecated since version 3.14 and should not be used in newly-written code. * Use notifications */ @@ -694,7 +704,7 @@ class Gtk { /** * Sets the name of this tray icon. This should be a string identifying this icon. It is may be used for sorting the icons in the * tray and will not be shown to the user. - * + *

* gtk_status_icon_set_name has been deprecated since version 3.14 and should not be used in newly-written code. * Use notifications */ @@ -704,7 +714,7 @@ class Gtk { /** * Displays a menu and makes it available for selection. - * + *

* gtk_menu_popup has been deprecated since version 3.22 and should not be used in newly-written code. * NOTE: Please use gtk_menu_popup_at_widget(), gtk_menu_popup_at_pointer(). or gtk_menu_popup_at_rect() instead */ @@ -777,18 +787,27 @@ class Gtk { public static native Pointer gtk_settings_get_default(); + + /** + * Simply an accessor function that returns @widget->style. + */ + public static native + GtkStyle.ByReference gtk_widget_get_style(Pointer widget); + /** * Finds all matching RC styles for a given widget, composites them together, and then creates a GtkStyle representing the composite * appearance. (GTK+ actually keeps a cache of previously created styles, so a new style may not be created.) */ - public static native Pointer gtk_rc_get_style(Pointer widget); + public static native + Pointer gtk_rc_get_style(Pointer widget); /** * Creates a toplevel container widget that is used to retrieve snapshots of widgets without showing them on the screen. * * @since 2.20 */ - public static native Pointer gtk_offscreen_window_new(); + public static native + Pointer gtk_offscreen_window_new(); /** * Looks up color_name in the style’s logical color mappings, filling in color and returning TRUE if found, otherwise returning @@ -796,8 +815,8 @@ class Gtk { * * @since 2.10 */ - public static native boolean gtk_style_lookup_color(Pointer widgetStyle, String color_name, Pointer color); - + public static native + boolean gtk_style_lookup_color(Pointer widgetStyle, String color_name, Pointer color); /** * Adds widget to container . Typically used for simple containers such as GtkWindow, GtkFrame, or GtkButton; for more complicated @@ -805,576 +824,7 @@ class Gtk { * consider functions such as gtk_box_pack_start() and gtk_table_attach() as an alternative to gtk_container_add() in those cases. * A widget may be added to only one container at a time; you can't place the same widget inside two different containers. */ - public static native void gtk_container_add(Pointer offscreen, Pointer widget); - - - /** - * @return the widget color of text for the current theme, or black. It is important that this is called AFTER GTK has been initialized. - */ - public static - Color getCurrentThemeTextColor() { - /* - * There are several 'directives' to change the attributes of a widget. - * fg - Sets the foreground color of a widget. - * bg - Sets the background color of a widget. - * text - Sets the foreground color for widgets that have editable text. - * base - Sets the background color for widgets that have editable text. - * bg_pixmap - Sets the background of a widget to a tiled pixmap. - * font_name - Sets the font to be used with the given widget. - * xthickness - Sets the left and right border width. This is not what you might think; it sets the borders of children(?) - * ythickness - similar to above but for the top and bottom. - * - * There are several states a widget can be in, and you can set different colors, pixmaps and fonts for each state. These states are: - * NORMAL - The normal state of a widget. Ie the mouse is not over it, and it is not being pressed, etc. - * PRELIGHT - When the mouse is over top of the widget, colors defined using this state will be in effect. - * ACTIVE - When the widget is pressed or clicked it will be active, and the attributes assigned by this tag will be in effect. - * INSENSITIVE - This is the state when a widget is 'greyed out'. It is not active, and cannot be clicked on. - * SELECTED - When an object is selected, it takes these attributes. - */ - - // NOTE: when getting CSS, we redirect STDERR to null, so that we won't spam the console if there are parse errors. - // this is a horrid hack, but the only way to work around the errors we have no control over. The parse errors, if bad enough - // just mean that we are unable to get the CSS as we want. - - // these methods are from most accurate (but limited in support) to compatible across Linux OSes.. Strangely enough, GTK makes - // this information insanely difficult to get. - final AtomicReference color = new AtomicReference(null); - Gtk.dispatchAndWait(new Runnable() { - - @Override - public - void run() { - // see if we can get the info via CSS properties (> GTK+ 3.2) - if (Gtk.isGtk3) { - Color c = getFromCss(); - if (c != null) { - System.err.println("Got from CSS"); - color.set(c); - return; - } - } - - // we got here because it's not possible to get the info via raw-CSS... - - - // try to get via the color scheme - Color c = getFromColorScheme(); - if (c != null) { - System.err.println("Got from color scheme"); - color.set(c); - return; - } - - - // the following methods all require an offscreen widget to get the style information from - - - // create an off-screen widget (don't forget to destroy everything!) - Pointer offscreen = Gtk.gtk_offscreen_window_new(); - final Pointer item = Gtk.gtk_image_menu_item_new_with_mnemonic("asd"); - Gtk.gtk_container_add (offscreen, item); - Gtk.gtk_widget_show_all(item); - - - - // Try to get via RC style... Sometimes this works (sometimes it does not...) - { - Pointer style = Gtk.gtk_rc_get_style(item); - - GdkColor gdkColor = new GdkColor(); - boolean success = Gtk.gtk_style_lookup_color(style, "menu_fg_color", gdkColor.getPointer()); - if (!success) { - success = Gtk.gtk_style_lookup_color(style, "text_color", gdkColor.getPointer()); - } - if (!success) { - success = Gtk.gtk_style_lookup_color(style, "theme_text_color", gdkColor.getPointer()); - } - if (success) { - // Have to convert to positive int (value between 0 and 65535, these are 16 bits per pixel) that is from 0-255 - int red = gdkColor.red & 0x0000FFFF; - int green = gdkColor.green & 0x0000FFFF; - int blue = gdkColor.blue & 0x0000FFFF; - - red = (red >> 8) & 0xFF; - green = (green >> 8) & 0xFF; - blue = (blue >> 8) & 0xFF; - - color.set(new Color(red, green, blue)); - - Gtk.gtk_widget_destroy(item); - return; - } - } - - if (Gtk.isGtk3) { - Pointer context = Gtk3.gtk_widget_get_style_context(item); - int state = Gtk3.gtk_style_context_get_state(context); - - GdkRGBAColor gdkColor = new GdkRGBAColor(); - boolean success = Gtk3.gtk_style_context_lookup_color(context, "fg_color", gdkColor.getPointer()); - if (!success) { - success = Gtk3.gtk_style_context_lookup_color(context, "text_color", gdkColor.getPointer()); - } - if (!success) { - success = Gtk3.gtk_style_context_lookup_color(context, "menu_fg_color", gdkColor.getPointer()); - } - - if (!success) { - success = Gtk3.gtk_style_context_lookup_color(context, "color", gdkColor.getPointer()); - } - - if (success) { - color.set(new Color((float) gdkColor.red, (float) gdkColor.green, (float) gdkColor.blue, (float) gdkColor.alpha)); - } else { - // fall back in case nothing else works - Gtk3.gtk_style_context_get_color(context, state, gdkColor.getPointer()); - if ((gdkColor.red == 0.0 && gdkColor.green == 0.0 && gdkColor.blue == 0.0) || gdkColor.alpha == 0.0) { - // have nothing here, check something else? - - } else { - // if we have something that is not all 0's - color.set(new Color((float) gdkColor.red, (float) gdkColor.green, (float) gdkColor.blue, (float) gdkColor.alpha)); - } - } - } - - Gtk.gtk_widget_destroy(item); - } - }); - - - Color color1 = color.get(); - if (color1 != null) { - System.err.println("COLOR: " + color1); - return color1; - } - - SystemTray.logger.error("Unable to determine the text color in use by your system. Please create an issue and include your " + - "full OS configuration and desktop environment (including theme and color variant) details."); - - // who knows WHAT the color is supposed to be. This is just a "best guess" default value. - return Color.BLACK; - } - - /** - * get the color we are interested in via raw CSS parsing. This is specifically to get the color of the text of the - * appindicator/gtk-status-icon menu. - * - * @return the color string, parsed from CSS/ - */ - private static Color getFromCss() { - String css = getGtkThemeCss(); - if (css != null) { -// System.err.println("css: " + css); - - String[] nodes; - Tray tray = (Tray) SystemTray.get() - .getMenu(); - - - // we care about the following CSS head nodes, and account for multiple versions, in order of preference. - if (tray instanceof _GtkStatusIconNativeTray) { - nodes = new String[] {"GtkPopover", "gnome-panel-menu-bar", "unity-panel", "PanelMenuBar", ".check"}; - } - else if (tray instanceof _AppIndicatorNativeTray) { - nodes = new String[] {"GtkPopover", "unity-panel", "gnome-panel-menu-bar", "PanelMenuBar", ".check"}; - } - else { - // not supported for other types - return null; - } - - // collect a list of all of the sections that have what we are interested in - List sections = new ArrayList(); - - String colorString = null; - - // now check the css nodes to see if they contain a combination of what we are looking for. - colorCheck: - for (String node : nodes) { - int i = 0; - while (i != -1) { - i = css.indexOf(node, i); - if (i > -1) { - int endOfNodeLabels = css.indexOf("{", i); - int endOfSection = css.indexOf("}", endOfNodeLabels + 1) + 1; - int endOfSectionTest = css.indexOf("}", i) + 1; - - // this makes sure that weird parsing errors don't happen as a result of node keywords appearing in node sections - if (endOfSection != endOfSectionTest) { - // advance the index - i = endOfSection; - continue; - } - - String nodeLabel = css.substring(i, endOfNodeLabels); - String nodeSection = css.substring(endOfNodeLabels, endOfSection); - -// if (nodeSection.contains("menu_fg_color")) { -// System.err.println(nodeSection); -// } - - int j = nodeSection.indexOf(" color"); - if (j > -1) { - sections.add(nodeLabel + " " + nodeSection); - } - - // advance the index - i = endOfSection; - } - } - } - -// for (String section : sections) { -// System.err.println("--------------"); -// System.err.println(section); -// System.err.println("--------------"); -// } - - if (!sections.isEmpty()) { - String section = sections.get(0); - int start = section.indexOf("{"); - int colorIndex = section.indexOf(" color", start); - - int startOfColorDef = section.indexOf(":", colorIndex) + 1; - int endOfColorDef = section.indexOf(";", startOfColorDef); - colorString = section.substring(startOfColorDef, endOfColorDef) - .trim(); - } - - // hopefully we found it. - if (colorString != null) { - if (colorString.startsWith("@")) { - // it's a color definition - colorString = colorString.substring(1); - - // have to setup the "define color" section - String colorDefine = "@define-color"; - int start = css.indexOf(colorDefine); - int end = css.lastIndexOf(colorDefine); - end = css.lastIndexOf(";", end) + 1; // include the ; - String colorDefines = css.substring(start, end); - -// System.err.println("+++++++++++++++++++++++"); -// System.err.println(colorDefines); -// System.err.println("+++++++++++++++++++++++"); - - // since it's a color definition, it will start a very specific way. - String newColorString = colorDefine + " " + colorString; - - int i = 0; - while (i != -1) { - i = colorDefines.indexOf(newColorString); - - if (i >= 0) { - try { - int startIndex = i + newColorString.length(); - int endIndex = colorDefines.indexOf(";", i); - - String colorSubString = colorDefines.substring(startIndex, endIndex) - .trim(); - - if (colorSubString.startsWith("@")) { - // have to recursively get the defined color - newColorString = colorDefine + " " + colorSubString.substring(1); - i = 0; - continue; - } - - return parseColor(colorSubString); - } catch (Exception ignored) { - } - } - } - } else { - return parseColor(colorString); - } - } - } - - return null; - } - - // this works for GtkStatusIcon menus. - private static Color getFromColorScheme() { - Pointer settings = Gtk.gtk_settings_get_default(); - if (settings != null) { - // see if we can get the info we want the EASY way (likely only when GTK+ 2 is used, but can be < GTK+ 3.2)... - - // been deprecated since version 3.8 - PointerByReference pointer = new PointerByReference(); - Gobject.g_object_get(settings, "gtk-color-scheme", pointer, null); - - // A palette of named colors for use in themes. The format of the string is - // name1: color1 - // name2: color2 - // - // Color names must be acceptable as identifiers in the gtkrc syntax, and color specifications must be in the format - // accepted by gdk_color_parse(). - // - // Note that due to the way the color tables from different sources are merged, color specifications will be converted - // to hexadecimal form when getting this property. - // - // Starting with GTK+ 2.12, the entries can alternatively be separated by ';' instead of newlines: - // name1: color1; name2: color2; ... - // - // GtkSettings:gtk-color-scheme has been deprecated since version 3.8 and should not be used in newly-written code. - // Color scheme support was dropped and is no longer supported. You can still set this property, but it will be ignored. - - - Pointer value = pointer.getValue(); - if (value != null) { - String s = value.getString(0); - if (!s.isEmpty()) { - // System.out.println("\t string: " + s); - - // Note: these are the values on my system when forcing GTK+ 2 (XUbuntu 16.04) with GtkStatusIcon - // bg_color_dark: #686868686868 - // fg_color: #3c3c3c3c3c3c - // fm_color: #f7f7f7f7f7f7 - // selected_fg_color: #ffffffffffff - // panel_bg: #686868686868 - // text_color: #212121212121 - // text_color_dark: #ffffffffffff - // tooltip_bg_color: #000000000000 - // link_color: #2d2d7171b8b8 - // tooltip_fg_color: #e1e1e1e1e1e1 - // base_color: #fcfcfcfcfcfc - // bg_color: #cececececece - // selected_bg_color: #39398e8ee7e7 - - - String textColor = "text_color"; // also theme_text_color - int i = s.indexOf(textColor); - if (i >= 0) { - try { - // the color will ALWAYS be in hex notation - - // it is also possible to be separated by ; instead of newline - int endIndex = s.indexOf(';', i); - if (endIndex == -1) { - endIndex = s.indexOf('\n', i); - } - - int startIndex = s.indexOf('#', i); - String colorString = s.substring(startIndex, endIndex).trim(); - - return parseColor(colorString); - } catch (Exception ignored) { - } - } - } - } - } - - return null; - } - - - /** - * Parses out the color from a color: - * - * - the word "transparent" - * - hex 12 digit #ffffaaaaffff - * - hex 6 digit #ffaaff - * - hex 3 digit #faf - * - rgb(r, g, b) rgb(33, 33, 33) - * - rgb(r, g, b) rgb(.6, .3, .3) - * - rgb(r%, g%, b%) rgb(10%, 20%, 30%) - * - rgba(r, g, b, a) rgb(33, 33, 33, 0.53) - * - rgba(r, g, b, a) rgb(.33, .33, .33, 0.53) - * - rgba(r, g, b, a) rgb(10%, 20%, 30%, 0.53) - * - * Notes: - * - rgb(), when an int, is between 0-255 - * - rgb(), when a float, is between 0.0-1.0 - * - rgb(), when a percent, is between 0-100 - * - alpha is always a float - * - * @return the parsed color - */ - private static - Color parseColor(String colorString) { - int red = 0; - int green = 0; - int blue = 0; - int alpha = 255; - - if (colorString.startsWith("#")) { - colorString = colorString.substring(1); - - if (colorString.length() > 11) { - red = Integer.parseInt(colorString.substring(0, 4), 16); - green = Integer.parseInt(colorString.substring(4, 8), 16); - blue = Integer.parseInt(colorString.substring(8), 16); - - // Have to convert to positive int (value between 0 and 65535, these are 16 bits per pixel) that is from 0-255 - red = red & 0x0000FFFF; - green = green & 0x0000FFFF; - blue = blue & 0x0000FFFF; - - red = (red >> 8) & 0xFF; - green = (green >> 8) & 0xFF; - blue = (blue >> 8) & 0xFF; - } - else if (colorString.length() > 5) { - red = Integer.parseInt(colorString.substring(0, 2), 16); - green = Integer.parseInt(colorString.substring(2, 4), 16); - blue = Integer.parseInt(colorString.substring(4), 16); - } - else { - red = Integer.parseInt(colorString.substring(0, 1), 16); - green = Integer.parseInt(colorString.substring(1, 2), 16); - blue = Integer.parseInt(colorString.substring(2), 16); - } - } - else if (colorString.startsWith("rgba")) { - colorString = colorString.substring(colorString.indexOf('(') + 1, colorString.indexOf(')')); - String[] split = colorString.split(","); - - String trim1 = split[0].trim(); - String trim2 = split[1].trim(); - String trim3 = split[2].trim(); - String trim4 = split[3].trim(); - - if (colorString.contains("%")) { - trim1 = trim1.replace("%", ""); - trim2 = trim2.replace("%", ""); - trim3 = trim3.replace("%", ""); - - red = Integer.parseInt(trim1) * 255; - green = Integer.parseInt(trim2) * 255; - blue = Integer.parseInt(trim3) * 255; - } else if (colorString.contains(".")) { - red = (int) (Float.parseFloat(trim1) * 255); - green = (int) (Float.parseFloat(trim2) * 255); - blue = (int) (Float.parseFloat(trim3) * 255); - } else { - red = Integer.parseInt(trim1); - green = Integer.parseInt(trim2); - blue = Integer.parseInt(trim3); - } - - float alphaF = Float.parseFloat(trim4); - alpha = (int) (alphaF * 255); - } - else if (colorString.startsWith("rgb")) { - colorString = colorString.substring(colorString.indexOf('(') + 1, colorString.indexOf(')')); - String[] split = colorString.split(","); - - String trim1 = split[0].trim(); - String trim2 = split[1].trim(); - String trim3 = split[2].trim(); - - if (colorString.contains("%")) { - trim1 = trim1.replace("%", ""); - trim2 = trim2.replace("%", ""); - trim3 = trim3.replace("%", ""); - - red = Integer.parseInt(trim1) * 255; - green = Integer.parseInt(trim2) * 255; - blue = Integer.parseInt(trim3) * 255; - } else if (colorString.contains(".")) { - red = (int) (Float.parseFloat(trim1) * 255); - green = (int) (Float.parseFloat(trim2) * 255); - blue = (int) (Float.parseFloat(trim3) * 255); - } else { - red = Integer.parseInt(trim1); - green = Integer.parseInt(trim2); - blue = Integer.parseInt(trim3); - } - } - else if (colorString.contains("transparent")) { - alpha = 0; - } - else { - int index = colorString.indexOf(";"); - if (index > 0) { - colorString = colorString.substring(0, index); - } - colorString = colorString.replaceAll("\"", ""); - colorString = colorString.replaceAll("'", ""); - - // maybe it's just a "color" description, such as "red"? - try { - return Color.decode(colorString); - } catch (Exception e) { - return null; - } - } - - return new Color(red, green, blue, alpha); - } - - /** - * @return the CSS for the current theme or null. It is important that this is called AFTER GTK has been initialized. - */ - public static - String getGtkThemeCss() { - final AtomicReference css = new AtomicReference(null); - - Gtk.dispatchAndWait(new Runnable() { - @Override - public - void run() { - Pointer settings = Gtk.gtk_settings_get_default(); - if (settings != null) { - PointerByReference pointer = new PointerByReference(); - Gobject.g_object_get(settings, "gtk-theme-name", pointer, null); - - // https://wiki.archlinux.org/index.php/GTK%2B - // - // gets the name of the currently loaded theme (can be used to get colors?) - // GTK+ 2: - // ~/.gtkrc-2.0 - // gtk-icon-theme-name = "Adwaita" - // gtk-theme-name = "Adwaita" - // gtk-font-name = "DejaVu Sans 11" - // - // GTK+ 3: - // $XDG_CONFIG_HOME/gtk-3.0/settings.ini - // [Settings] - // gtk-icon-theme-name = Adwaita - // gtk-theme-name = Adwaita - // gtk-font-name = DejaVu Sans 11 - // Note: The icon theme name is the name defined in the theme's index file, not the name of its directory. - - String themeName = null; - Pointer value = pointer.getValue(); - if (value != null) { - themeName = value.getString(0); - } - - if (themeName != null) { - value = Gtk3.gtk_css_provider_get_named(themeName, null); - if (value != null) { - // we have the css provider! - // NOTE: This can output warnings if the theme doesn't parse correctly by GTK, so we suppress them - Glib.GLogFunc orig = Glib.g_log_set_default_handler(Glib.nullLogFunc, null); - - css.set(Gtk3.gtk_css_provider_to_string(value)); - - Glib.g_log_set_default_handler(orig, null); - } - } - } - else { - Pointer value = Gtk3.gtk_css_provider_get_default(); - if (value != null) { - // we have the css provider! - // NOTE: This can output warnings if the theme doesn't parse correctly by GTK, so we suppress them - Glib.GLogFunc orig = Glib.g_log_set_default_handler(Glib.nullLogFunc, null); - - css.set(Gtk3.gtk_css_provider_to_string(value)); - - Glib.g_log_set_default_handler(orig, null); - } - } - } - }); - - // will be either the string, or null. - return css.get(); - } + public static native + void gtk_container_add(Pointer offscreen, Pointer widget); } diff --git a/src/dorkbox/systemTray/jna/linux/GtkStyle.java b/src/dorkbox/systemTray/jna/linux/GtkStyle.java new file mode 100644 index 0000000..e65d4e5 --- /dev/null +++ b/src/dorkbox/systemTray/jna/linux/GtkStyle.java @@ -0,0 +1,83 @@ +package dorkbox.systemTray.jna.linux; + +import java.util.Arrays; +import java.util.List; + +import com.sun.jna.Pointer; +import com.sun.jna.Structure; + +import dorkbox.util.Keep; + +@Keep +public +class GtkStyle extends Structure { + /* + * There are several 'directives' to change the attributes of a widget. + * fg - Sets the foreground color of a widget. + * bg - Sets the background color of a widget. + * text - Sets the foreground color for widgets that have editable text. + * base - Sets the background color for widgets that have editable text. + * bg_pixmap - Sets the background of a widget to a tiled pixmap. + * font_name - Sets the font to be used with the given widget. + * xthickness - Sets the left and right border width. This is not what you might think; it sets the borders of children(?) + * ythickness - similar to above but for the top and bottom. + * + * There are several states a widget can be in, and you can set different colors, pixmaps and fonts for each state. These states are: + * NORMAL - The normal state of a widget. Ie the mouse is not over it, and it is not being pressed, etc. + * PRELIGHT - When the mouse is over top of the widget, colors defined using this state will be in effect. + * ACTIVE - When the widget is pressed or clicked it will be active, and the attributes assigned by this tag will be in effect. + * INSENSITIVE - This is the state when a widget is 'greyed out'. It is not active, and cannot be clicked on. + * SELECTED - When an object is selected, it takes these attributes. + */ + + public static + class ByReference extends GtkStyle implements Structure.ByReference {} + + public + class ByValue extends GtkStyle implements Structure.ByValue {} + + // only list public fields + + /** fg: foreground for drawing GtkLabel */ + public GdkColor fg[] = new GdkColor[5]; + + /** bg: the usual background color, gray by default */ + public GdkColor bg[] = new GdkColor[5]; + public GdkColor light[] = new GdkColor[5]; + public GdkColor dark[] = new GdkColor[5]; + public GdkColor mid[] = new GdkColor[5]; + + /** + * text: text for entries and text widgets (although in GTK 1.2 sometimes fg gets used, this is more or less a bug and fixed in GTK 2.0). + */ + public GdkColor text[] = new GdkColor[5]; + + /** base: background when using text, colored white in the default theme. */ + public GdkColor base[] = new GdkColor[5]; + public GdkColor text_aa[] = new GdkColor[5]; /* Halfway between text/base */ + public GdkColor black; + public GdkColor white; + public Pointer /*PangoFontDescription*/ font_desc; + public int xthickness; + public int ythickness; + public Pointer /*cairo_pattern_t*/ background[] = new Pointer[5]; + + @Override + protected + List getFieldOrder() { + return Arrays.asList("fg", + "bg", + "light", + "dark", + "mid", + "text", + "base", + "text_aa", + "black", + "white", + "font_desc", + "xthickness", + "ythickness", + "background"); + } +} diff --git a/src/dorkbox/systemTray/jna/linux/GtkTheme.java b/src/dorkbox/systemTray/jna/linux/GtkTheme.java new file mode 100644 index 0000000..695cecc --- /dev/null +++ b/src/dorkbox/systemTray/jna/linux/GtkTheme.java @@ -0,0 +1,974 @@ +package dorkbox.systemTray.jna.linux; + +import java.awt.Color; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import com.sun.jna.Pointer; +import com.sun.jna.ptr.PointerByReference; + +import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.Tray; +import dorkbox.systemTray.nativeUI._AppIndicatorNativeTray; +import dorkbox.systemTray.nativeUI._GtkStatusIconNativeTray; +import dorkbox.util.FileUtil; + +/** + * Class to contain all of the methods needed to get the text color from the AppIndicator/GtkStatusIcon menu entry. This is primarily + * used to get the color needed for the checkmark icon. In GTK, the checkmark icon can be defined to be it's OWN color and + * shape, however getting/parsing that would be even significantly more difficult -- so we decided to make the icon the same color + * as the text. + *

+ * Additionally, CUSTOM, user theme modifications in ~/.gtkrc-2.0 (for example), will be ignored. + */ +@SuppressWarnings("deprecation") +public +class GtkTheme { + private static final boolean DEBUG = false; + private static final boolean DEBUG_SHOW_CSS = false; + private static final boolean DEBUG_VERBOSE = false; + + /** + * @return the widget color of text for the current theme, or black. It is important that this is called AFTER GTK has been initialized. + */ + public static + Color getCurrentThemeTextColor() { + // NOTE: when getting CSS, we redirect STDERR to null (via GTK), so that we won't spam the console if there are parse errors. + // this is a horrid hack, but the only way to work around the errors we have no control over. The parse errors, if bad enough + // just mean that we are unable to get the CSS as we want. + + // these methods are from most accurate (but limited in support) to compatible across Linux OSes.. Strangely enough, GTK makes + // this information insanely difficult to get. + final AtomicReference color = new AtomicReference(null); + Gtk.dispatchAndWait(new Runnable() { + + @Override + public + void run() { + // see if we can get the info via CSS properties (> GTK+ 3.2 uses an API, GTK2 gets it from disk) + Color c = getFromCss(); + if (c != null) { + if (DEBUG) { + System.err.println("Got from CSS"); + } + color.set(c); + return; + } + + // we got here because it's not possible to get the info via raw-CSS + + // try to get via the color scheme. A bit more accurate than parsing the raw theme file + c = getFromColorScheme(); + if (c != null) { + if (DEBUG) { + System.err.println("Got from color scheme"); + } + color.set(c); + return; + } + + + // if we get here, this means that there was NO "gtk-color-scheme" value in the theme file. + // This usually happens when the theme does not have @fg_color (for example), but instead has each color explicitly + // defined for every widget instance in the theme file. Old/bizzare themes tend to do it this way... + if (Gtk.isGtk2) { + c = getFromGtk2ThemeText(); + if (c != null) { + if (DEBUG) { + System.err.println("Got from gtk2 color theme file"); + } + color.set(c); + return; + } + } + + + // the following methods all require an offscreen widget to get the style information from. This rarely is correct for + // some bizzare reason. + + + // create an off-screen widget (don't forget to destroy everything!) + Pointer menu = Gtk.gtk_menu_new(); + Pointer item = Gtk.gtk_image_menu_item_new_with_mnemonic("a"); + + Gtk.gtk_container_add(menu, item); + Gtk.gtk_widget_show_all(item); + + // Try to get via RC style... Sometimes this works (sometimes it does not...) + { + Pointer style = Gtk.gtk_rc_get_style(item); + + GdkColor gdkColor = new GdkColor(); + boolean success; + + success = Gtk.gtk_style_lookup_color(style, "menu_fg_color", gdkColor.getPointer()); + if (!success) { + success = Gtk.gtk_style_lookup_color(style, "text_color", gdkColor.getPointer()); + } + if (!success) { + success = Gtk.gtk_style_lookup_color(style, "theme_text_color", gdkColor.getPointer()); + } + if (success) { + color.set(gdkColor.getColor()); + + Gtk.gtk_widget_destroy(item); + return; + } + } + + if (Gtk.isGtk3) { + Pointer context = Gtk3.gtk_widget_get_style_context(item); + int state = Gtk3.gtk_style_context_get_state(context); + + GdkRGBAColor gdkColor = new GdkRGBAColor(); + boolean success = Gtk3.gtk_style_context_lookup_color(context, "fg_color", gdkColor.getPointer()); + if (!success) { + success = Gtk3.gtk_style_context_lookup_color(context, "text_color", gdkColor.getPointer()); + } + if (!success) { + success = Gtk3.gtk_style_context_lookup_color(context, "menu_fg_color", gdkColor.getPointer()); + } + + if (!success) { + success = Gtk3.gtk_style_context_lookup_color(context, "color", gdkColor.getPointer()); + } + + if (success) { + color.set(new Color((float) gdkColor.red, (float) gdkColor.green, (float) gdkColor.blue, (float) gdkColor.alpha)); + } + else { + // fall back in case nothing else works + Gtk3.gtk_style_context_get_color(context, state, gdkColor.getPointer()); + if (gdkColor.red == 0.0 && gdkColor.green == 0.0 && gdkColor.blue == 0.0 && gdkColor.alpha == 0.0) { + // have nothing here, check something else... + if (DEBUG) { + System.err.println("No valid output from gtk_style_context_get_color"); + } + } + else { + // if we have something that is not all 0's + color.set(new Color((float) gdkColor.red, + (float) gdkColor.green, + (float) gdkColor.blue, + (float) gdkColor.alpha)); + } + } + } + + // this also doesn't always work... + GtkStyle.ByReference style = Gtk.gtk_widget_get_style(item); + color.set(style.text[Gtk.State.NORMAL].getColor()); + + Gtk.gtk_widget_destroy(item); + } + }); + + + Color color1 = color.get(); + if (color1 != null) { + if (DEBUG) { + System.err.println("COLOR FOUND: " + color1); + } + return color1; + } + + SystemTray.logger.error("Unable to determine the text color in use by your system. Please create an issue and include your " + + "full OS configuration and desktop environment, including theme details, such as the theme name, color " + + "variant, and custom theme options (if any)."); + + // who knows WHAT the color is supposed to be. This is just a "best guess" default value. + return Color.BLACK; + } + + /** + * get the color we are interested in via raw CSS parsing. This is specifically to get the color of the text of the + * appindicator/gtk-status-icon menu. + *

+ * > GTK+ 3.2 uses an API, GTK2 gets it from disk + * + * @return the color string, parsed from CSS/ + */ + private static + Color getFromCss() { + String css = getGtkThemeCss(); + if (css != null) { + if (DEBUG_SHOW_CSS) { + System.err.println(css); + } + + String[] nodes; + Tray tray = (Tray) SystemTray.get() + .getMenu(); + + + // we care about the following CSS head nodes, and account for multiple versions, in order of preference. + if (tray instanceof _GtkStatusIconNativeTray) { + nodes = new String[] {"GtkPopover", "gnome-panel-menu-bar", "unity-panel", "PanelMenuBar", ".check"}; + } + else if (tray instanceof _AppIndicatorNativeTray) { + nodes = new String[] {"GtkPopover", "unity-panel", "gnome-panel-menu-bar", "PanelMenuBar", ".check"}; + } + else { + // not supported for other types + return null; + } + + // collect a list of all of the sections that have what we are interested in + List sections = new ArrayList(); + + String colorString = null; + + // now check the css nodes to see if they contain a combination of what we are looking for. + colorCheck: + for (String node : nodes) { + int i = 0; + while (i != -1) { + i = css.indexOf(node, i); + if (i > -1) { + int endOfNodeLabels = css.indexOf("{", i); + int endOfSection = css.indexOf("}", endOfNodeLabels + 1) + 1; + int endOfSectionTest = css.indexOf("}", i) + 1; + + // this makes sure that weird parsing errors don't happen as a result of node keywords appearing in node sections + if (endOfSection != endOfSectionTest) { + // advance the index + i = endOfSection; + continue; + } + + String nodeLabel = css.substring(i, endOfNodeLabels); + String nodeSection = css.substring(endOfNodeLabels, endOfSection); + + int j = nodeSection.indexOf(" color"); + if (j > -1) { + sections.add(nodeLabel + " " + nodeSection); + } + + // advance the index + i = endOfSection; + } + } + } + + if (DEBUG_VERBOSE) { + for (String section : sections) { + System.err.println("--------------"); + System.err.println(section); + System.err.println("--------------"); + } + } + + if (!sections.isEmpty()) { + String section = sections.get(0); + int start = section.indexOf("{"); + int colorIndex = section.indexOf(" color", start); + + int startOfColorDef = section.indexOf(":", colorIndex) + 1; + int endOfColorDef = section.indexOf(";", startOfColorDef); + colorString = section.substring(startOfColorDef, endOfColorDef) + .trim(); + } + + // hopefully we found it. + if (colorString != null) { + if (colorString.startsWith("@")) { + // it's a color definition + colorString = colorString.substring(1); + + // have to setup the "define color" section + String colorDefine = "@define-color"; + int start = css.indexOf(colorDefine); + int end = css.lastIndexOf(colorDefine); + end = css.lastIndexOf(";", end) + 1; // include the ; + String colorDefines = css.substring(start, end); + + if (DEBUG_VERBOSE) { + System.err.println("+++++++++++++++++++++++"); + System.err.println(colorDefines); + System.err.println("+++++++++++++++++++++++"); + } + + // since it's a color definition, it will start a very specific way. + String newColorString = colorDefine + " " + colorString; + + int i = 0; + while (i != -1) { + i = colorDefines.indexOf(newColorString); + + if (i >= 0) { + try { + int startIndex = i + newColorString.length(); + int endIndex = colorDefines.indexOf(";", i); + + String colorSubString = colorDefines.substring(startIndex, endIndex) + .trim(); + + if (colorSubString.startsWith("@")) { + // have to recursively get the defined color + newColorString = colorDefine + " " + colorSubString.substring(1); + i = 0; + continue; + } + + return parseColor(colorSubString); + } catch (Exception ignored) { + } + } + } + } + else { + return parseColor(colorString); + } + } + } + + return null; + } + + /** + * @return the CSS for the current theme or null. It is important that this is called AFTER GTK has been initialized. + */ + public static + String getGtkThemeCss() { + if (Gtk.isGtk3) { + final AtomicReference css = new AtomicReference(null); + + Gtk.dispatchAndWait(new Runnable() { + @Override + public + void run() { + String themeName = getThemeName(); + + if (themeName != null) { + Pointer value = Gtk3.gtk_css_provider_get_named(themeName, null); + if (value != null) { + // we have the css provider! + + // NOTE: This can output warnings if the theme doesn't parse correctly by GTK, so we suppress them + Glib.GLogFunc orig = Glib.g_log_set_default_handler(Glib.nullLogFunc, null); + + css.set(Gtk3.gtk_css_provider_to_string(value)); + + Glib.g_log_set_default_handler(orig, null); + } + } + else { + Pointer value = Gtk3.gtk_css_provider_get_default(); + if (value != null) { + // we have the css provider! + + // NOTE: This can output warnings if the theme doesn't parse correctly by GTK, so we suppress them + Glib.GLogFunc orig = Glib.g_log_set_default_handler(Glib.nullLogFunc, null); + + css.set(Gtk3.gtk_css_provider_to_string(value)); + + Glib.g_log_set_default_handler(orig, null); + } + } + } + }); + + // will be either the string, or null. + return css.get(); + } + else { + // GTK2 has to get the GTK3 theme text a different way (parsing it from disk). SOMETIMES, the app must be GTK2, even though + // the system is GTK3. This works around the API restriction if we are an APP in GTK2 mode. + return getGtk3ThemeCssViaFile(); + } + } + + /** + * this works for GtkStatusIcon menus. + * + * @return the menu_fg/fg/text color from gtk-color-scheme or null + */ + public static + Color getFromColorScheme() { + Pointer settings = Gtk.gtk_settings_get_default(); + if (settings != null) { + // see if we can get the info we want the EASY way (likely only when GTK+ 2 is used, but can be < GTK+ 3.2)... + + // been deprecated since version 3.8 + PointerByReference pointer = new PointerByReference(); + Gobject.g_object_get(settings, "gtk-color-scheme", pointer, null); + + + // A palette of named colors for use in themes. The format of the string is + // name1: color1 + // name2: color2 + // + // Color names must be acceptable as identifiers in the gtkrc syntax, and color specifications must be in the format + // accepted by gdk_color_parse(). + // + // Note that due to the way the color tables from different sources are merged, color specifications will be converted + // to hexadecimal form when getting this property. + // + // Starting with GTK+ 2.12, the entries can alternatively be separated by ';' instead of newlines: + // name1: color1; name2: color2; ... + // + // GtkSettings:gtk-color-scheme has been deprecated since version 3.8 and should not be used in newly-written code. + // Color scheme support was dropped and is no longer supported. You can still set this property, but it will be ignored. + + + Pointer value = pointer.getValue(); + if (value != null) { + String s = value.getString(0); + if (!s.isEmpty()) { + if (DEBUG) { + System.out.println("\t string: " + s); + } + + // Note: these are the values on my system when forcing GTK+ 2 (XUbuntu 16.04) with GtkStatusIcon and Aidwata theme + // bg_color_dark: #686868686868 + // fg_color: #3c3c3c3c3c3c + // fm_color: #f7f7f7f7f7f7 + // selected_fg_color: #ffffffffffff + // panel_bg: #686868686868 + // text_color: #212121212121 + // text_color_dark: #ffffffffffff + // tooltip_bg_color: #000000000000 + // link_color: #2d2d7171b8b8 + // tooltip_fg_color: #e1e1e1e1e1e1 + // base_color: #fcfcfcfcfcfc + // bg_color: #cececececece + // selected_bg_color: #39398e8ee7e7 + + // list of colors, in order of importance, that we want to parse. + String colors[] = new String[] {"menu_fg_color", "fg_color", "text_color"}; + + for (String colorName : colors) { + int i = 0; + while (i != -1) { + i = s.indexOf(colorName, i); + if (i >= 0) { + try { + // the color will ALWAYS be in hex notation + + // it is also possible to be separated by ; instead of newline + int endIndex = s.indexOf(';', i); + if (endIndex == -1) { + endIndex = s.indexOf('\n', i); + } + + if (s.charAt(i - 1) == '_') { + i = endIndex; + continue; + } + + int startIndex = s.indexOf('#', i); + String colorString = s.substring(startIndex, endIndex) + .trim(); + + if (DEBUG) { + System.out.println("Color string: " + colorString); + } + return parseColor(colorString); + } catch (Exception ignored) { + } + } + } + } + } + } + } + + return null; + } + + /** + * Checks in the following locations for the current GTK3 theme. + *

+ * /usr/share/themes + * /opt/gnome/share/themes + */ + private static + String getGtk3ThemeCssViaFile() { + File themeDirectory = getThemeDirectory(true); + + if (themeDirectory == null) { + return null; + } + + File gtkFile = new File(themeDirectory, "gtk.css"); + + try { + StringBuilder stringBuilder = new StringBuilder((int) (gtkFile.length())); + FileUtil.read(gtkFile, stringBuilder); + + removeComments(stringBuilder); + + // only comments in the file + if (stringBuilder.length() < 2) { + return null; + } + + injectAdditionalCss(themeDirectory, stringBuilder); + + return stringBuilder.toString(); + } catch (IOException ignored) { + // cant read the file or something else. + } + + return null; + } + + /** + * @return the discovered fg[NORMAL] or text[NORMAL] color for this theme or null + */ + public static + Color getFromGtk2ThemeText() { + String gtk2ThemeText = getGtk2ThemeText(); + + if (gtk2ThemeText != null) { + String[] colorText = new String[] {"fg[NORMAL]", "text[NORMAL]"}; + for (String text : colorText) { + int i = 0; + while (i != -1) { + i = gtk2ThemeText.indexOf(text, i); + if (i != -1) { + if (i > 0 && gtk2ThemeText.charAt(i - 1) != '_') { + i += text.length(); + continue; + } + + + int j = gtk2ThemeText.indexOf("=", i); + if (j != -1) { + int lineEnd = gtk2ThemeText.indexOf('\n', j); + + if (lineEnd != -1) { + String colorName = gtk2ThemeText.substring(j + 1, lineEnd) + .trim(); + + colorName = colorName.replaceAll("\"", ""); + return parseColor(colorName); + } + } + } + } + } + } + + return null; + } + + + /** + * Checks in the following locations for the current GTK2 theme. + *

+ * /usr/share/themes + * /opt/gnome/share/themes + */ + private static + String getGtk2ThemeText() { + File themeDirectory = getThemeDirectory(false); + + if (themeDirectory == null) { + return null; + } + + + // ie: /usr/share/themes/Numix/gtk-2.0/gtkrc + File gtkFile = new File(themeDirectory, "gtkrc"); + + try { + StringBuilder stringBuilder = new StringBuilder((int) (gtkFile.length())); + FileUtil.read(gtkFile, stringBuilder); + + removeComments(stringBuilder); + + // only comments in the file + if (stringBuilder.length() < 2) { + return null; + } + + return stringBuilder.toString(); + } catch (IOException ignored) { + // cant read the file or something else. + } + + return null; + } + + + /** + * Figures out what the directory is for the specified type of GTK theme files (css/gtkrc/etc) + * + * @param gtk3 true if you want to look for the GTK3 theme dir, false if you want the GTK2 theme dir + * + * @return the directory or null if it cannot be found + */ + public static + File getThemeDirectory(boolean gtk3) { + String themeName = getThemeName(); + + if (themeName == null) { + return null; + } + + String gtkType; + if (gtk3) { + gtkType = "gtk-3.0"; + } + else { + gtkType = "gtk-2.0"; + } + + + String[] dirs = new String[] {"/usr/share/themes", "/opt/gnome/share/themes"}; + + // ie: /usr/share/themes + for (String dirName : dirs) { + File themesDir = new File(dirName); + + File[] themeDirs = themesDir.listFiles(); + if (themeDirs != null) { + // ie: /usr/share/themes/Numix + for (File themeDir : themeDirs) { + File[] files1 = themeDir.listFiles(); + if (files1 != null) { + boolean isCorrectTheme; + + File themeIndexFile = new File(themeDir, "index.theme"); + try { + List read = FileUtil.read(themeIndexFile, false); + for (String s : read) { + if (s.startsWith("GtkTheme=")) { + String calculatedThemeName = s.substring("GtkTheme=".length()); + + isCorrectTheme = calculatedThemeName.equals(themeName); + + if (isCorrectTheme) { + // ie: /usr/share/themes/Numix/gtk-3.0/gtk.css + // the DARK variant is only used by some apps. The dark variant is NOT SYSTEM-WIDE! + return new File(themeDir, gtkType); + } + + break; + } + } + } catch (IOException ignored) { + } + } + } + } + } + + return null; + } + + /** + * Parses out the color from a color: + *

+ * - the word "transparent" + * - hex 12 digit #ffffaaaaffff + * - hex 6 digit #ffaaff + * - hex 3 digit #faf + * - rgb(r, g, b) rgb(33, 33, 33) + * - rgb(r, g, b) rgb(.6, .3, .3) + * - rgb(r%, g%, b%) rgb(10%, 20%, 30%) + * - rgba(r, g, b, a) rgb(33, 33, 33, 0.53) + * - rgba(r, g, b, a) rgb(.33, .33, .33, 0.53) + * - rgba(r, g, b, a) rgb(10%, 20%, 30%, 0.53) + *

+ * Notes: + * - rgb(), when an int, is between 0-255 + * - rgb(), when a float, is between 0.0-1.0 + * - rgb(), when a percent, is between 0-100 + * - alpha is always a float + * + * @return the parsed color + */ + @SuppressWarnings("Duplicates") + private static + Color parseColor(String colorString) { + int red = 0; + int green = 0; + int blue = 0; + int alpha = 255; + + if (colorString.startsWith("#")) { + colorString = colorString.substring(1); + + if (colorString.length() > 11) { + red = Integer.parseInt(colorString.substring(0, 4), 16); + green = Integer.parseInt(colorString.substring(4, 8), 16); + blue = Integer.parseInt(colorString.substring(8), 16); + + // Have to convert to positive int (value between 0 and 65535, these are 16 bits per pixel) that is from 0-255 + red = red & 0x0000FFFF; + green = green & 0x0000FFFF; + blue = blue & 0x0000FFFF; + + red = (red >> 8) & 0xFF; + green = (green >> 8) & 0xFF; + blue = (blue >> 8) & 0xFF; + } + else if (colorString.length() > 5) { + red = Integer.parseInt(colorString.substring(0, 2), 16); + green = Integer.parseInt(colorString.substring(2, 4), 16); + blue = Integer.parseInt(colorString.substring(4), 16); + } + else { + red = Integer.parseInt(colorString.substring(0, 1), 16); + green = Integer.parseInt(colorString.substring(1, 2), 16); + blue = Integer.parseInt(colorString.substring(2), 16); + } + } + else if (colorString.startsWith("rgba")) { + colorString = colorString.substring(colorString.indexOf('(') + 1, colorString.indexOf(')')); + String[] split = colorString.split(","); + + String trim1 = split[0].trim(); + String trim2 = split[1].trim(); + String trim3 = split[2].trim(); + String trim4 = split[3].trim(); + + if (colorString.contains("%")) { + trim1 = trim1.replace("%", ""); + trim2 = trim2.replace("%", ""); + trim3 = trim3.replace("%", ""); + + red = Integer.parseInt(trim1) * 255; + green = Integer.parseInt(trim2) * 255; + blue = Integer.parseInt(trim3) * 255; + } + else if (colorString.contains(".")) { + red = (int) (Float.parseFloat(trim1) * 255); + green = (int) (Float.parseFloat(trim2) * 255); + blue = (int) (Float.parseFloat(trim3) * 255); + } + else { + red = Integer.parseInt(trim1); + green = Integer.parseInt(trim2); + blue = Integer.parseInt(trim3); + } + + float alphaF = Float.parseFloat(trim4); + alpha = (int) (alphaF * 255); + } + else if (colorString.startsWith("rgb")) { + colorString = colorString.substring(colorString.indexOf('(') + 1, colorString.indexOf(')')); + String[] split = colorString.split(","); + + String trim1 = split[0].trim(); + String trim2 = split[1].trim(); + String trim3 = split[2].trim(); + + if (colorString.contains("%")) { + trim1 = trim1.replace("%", ""); + trim2 = trim2.replace("%", ""); + trim3 = trim3.replace("%", ""); + + red = Integer.parseInt(trim1) * 255; + green = Integer.parseInt(trim2) * 255; + blue = Integer.parseInt(trim3) * 255; + } + else if (colorString.contains(".")) { + red = (int) (Float.parseFloat(trim1) * 255); + green = (int) (Float.parseFloat(trim2) * 255); + blue = (int) (Float.parseFloat(trim3) * 255); + } + else { + red = Integer.parseInt(trim1); + green = Integer.parseInt(trim2); + blue = Integer.parseInt(trim3); + } + } + else if (colorString.contains("transparent")) { + alpha = 0; + } + else { + int index = colorString.indexOf(";"); + if (index > 0) { + colorString = colorString.substring(0, index); + } + colorString = colorString.replaceAll("\"", ""); + colorString = colorString.replaceAll("'", ""); + + // maybe it's just a "color" description, such as "red"? + try { + return Color.decode(colorString); + } catch (Exception e) { + return null; + } + } + + return new Color(red, green, blue, alpha); + } + + /** + * https://wiki.archlinux.org/index.php/GTK%2B + *

+ * gets the name of the currently loaded theme + * GTK+ 2: + * ~/.gtkrc-2.0 + * gtk-icon-theme-name = "Adwaita" + * gtk-theme-name = "Adwaita" + * gtk-font-name = "DejaVu Sans 11" + *

+ *

+ * GTK+ 3: + * $XDG_CONFIG_HOME/gtk-3.0/settings.ini + * [Settings] + * gtk-icon-theme-name = Adwaita + * gtk-theme-name = Adwaita + * gtk-font-name = DejaVu Sans 11 + *

+ *

+ * Note: The icon theme name is the name defined in the theme's index file, not the name of its directory. + *

+ * directories: + * /usr/share/themes + * /opt/gnome/share/themes + *

+ * GTK+ 2 user specific: ~/.gtkrc-2.0 + * GTK+ 2 system wide: /etc/gtk-2.0/gtkrc + *

+ * GTK+ 3 user specific: $XDG_CONFIG_HOME/gtk-3.0/settings.ini, or $HOME/.config/gtk-3.0/settings.ini if $XDG_CONFIG_HOME is not set + * GTK+ 3 system wide: /etc/gtk-3.0/settings.ini + * + * @return the theme name, or null if it cannot find it. + */ + public static + String getThemeName() { + String themeName = null; + + Pointer settings = Gtk.gtk_settings_get_default(); + if (settings != null) { + + + PointerByReference pointer = new PointerByReference(); + Gobject.g_object_get(settings, "gtk-theme-name", pointer, null); + + Pointer value = pointer.getValue(); + if (value != null) { + themeName = value.getString(0); + } + } + + if (DEBUG) { + System.err.println("Theme name: " + themeName); + } + + return themeName; + } + + @SuppressWarnings("Duplicates") + private static + void removeComments(final StringBuilder stringBuilder) { + // remove block comments, /* .... */ This can span multiple lines + int start = 0; + while (start != -1) { + // get the start of a comment + start = stringBuilder.indexOf("/*", start); + + if (start != -1) { + // get the end of a comment + int end = stringBuilder.indexOf("*/", start); + if (end != -1) { + stringBuilder.delete(start, end + 2); // 2 is the size of */ + + // sometimes when the comments are removed, there is a trailing newline. remove that too. Works for windows too + if (stringBuilder.charAt(start) == '\n') { + stringBuilder.delete(start, start + 1); + } + else { + start++; + } + } + } + } + + // now remove comments that start with // (line MUST start with //) + start = 0; + while (start != -1) { + // get the start of a comment + start = stringBuilder.indexOf("//", start); + + if (start != -1) { + // the comment is at the start of a line + if (start == 0 || stringBuilder.charAt(start-1) == '\n') { + // get the end of the comment (the end of the line) + int end = stringBuilder.indexOf("\n", start); + if (end != -1) { + stringBuilder.delete(start, end + 1); // 1 is the size of \n + } + } + + // sometimes when the comments are removed, there is a trailing newline. remove that too. Works for windows too + if (stringBuilder.charAt(start) == '\n') { + stringBuilder.delete(start, start + 1); + } + else if (start > 0){ + start++; + } + } + } + + // now remove comments that start with # (line MUST start with #) + start = 0; + while (start != -1) { + // get the start of a comment + start = stringBuilder.indexOf("#", start); + + if (start != -1) { + // the comment is at the start of a line + if (start == 0 || stringBuilder.charAt(start-1) == '\n') { + // get the end of the comment (the end of the line) + int end = stringBuilder.indexOf("\n", start); + if (end != -1) { + stringBuilder.delete(start, end + 1); // 1 is the size of \n + } + } + + // sometimes when the comments are removed, there is a trailing newline. remove that too. Works for windows too + if (stringBuilder.charAt(start) == '\n') { + stringBuilder.delete(start, start + 1); + } + else if (start > 0){ + start++; + } + } + } + } + + private static + void injectAdditionalCss(final File parent, final StringBuilder stringBuilder) { + // not the BEST way to do this because duplicates are not merged at all. + + int start = 0; + while (start != -1) { + // now check if it says: @import url("gtk-main.css") + start = stringBuilder.indexOf("@import url(", start); + + if (start != -1) { + int end = stringBuilder.indexOf("\")", start); + if (end != -1) { + String url = stringBuilder.substring(start + 13, end); + stringBuilder.delete(start, end + 2); // 2 is the size of ") + + if (DEBUG_VERBOSE) { + System.err.println("import url: " + url); + } + try { + // now inject the new file where the import command was. + File file = new File(parent, url); + StringBuilder stringBuilder2 = new StringBuilder((int) (file.length())); + FileUtil.read(file, stringBuilder2); + + removeComments(stringBuilder2); + + stringBuilder.insert(start, stringBuilder2); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + } +}