Moved GTK Theme methods to it's own class (it was a lot). Suppressed GTK

warnings during startup.
This commit is contained in:
nathan 2017-05-29 17:32:00 +02:00
parent 9cfbd7689b
commit 15029d0256
4 changed files with 1218 additions and 673 deletions

View File

@ -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<String> getFieldOrder() {

View File

@ -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.
*
* <p>
* 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<Object> gtkCallbacks = new LinkedList<Object>();
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<Boolean> isDispatch = new ThreadLocal<Boolean>() {
@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<Object> gtkCallbacks = new LinkedList<Object>();
@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.
* <p>
* 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.
* <p>
* 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.
*
* <p>
* uses '_' to define which key is the mnemonic
*
* <p>
* 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.
*
* <p>
* 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.
*
* <p>
* 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()).
*
* <p>
* 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.
*
* <p>
* 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.
*
* <p>
* 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.
*
* <p>
* 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.
*
* <p>
* 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.
*
* <p>
* 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 styles 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> color = new AtomicReference<Color>(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<String> sections = new ArrayList<String>();
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<String> css = new AtomicReference<String>(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);
}

View File

@ -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<String> getFieldOrder() {
return Arrays.asList("fg",
"bg",
"light",
"dark",
"mid",
"text",
"base",
"text_aa",
"black",
"white",
"font_desc",
"xthickness",
"ythickness",
"background");
}
}

View File

@ -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.
* <p>
* 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> color = new AtomicReference<Color>(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.
* <p>
* > 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<String> sections = new ArrayList<String>();
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<String> css = new AtomicReference<String>(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.
* <p>
* /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.
* <p>
* /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<String> 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:
* <p>
* - 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)
* <p>
* 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
* <p>
* 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"
* <p>
* <p>
* 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
* <p>
* <p>
* Note: The icon theme name is the name defined in the theme's index file, not the name of its directory.
* <p>
* directories:
* /usr/share/themes
* /opt/gnome/share/themes
* <p>
* GTK+ 2 user specific: ~/.gtkrc-2.0
* GTK+ 2 system wide: /etc/gtk-2.0/gtkrc
* <p>
* 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();
}
}
}
}
}
}