Moved GtkTheme to SystemTray -- it is only useful there, and is explicitly for system tray info.

master
Robinson 2023-11-20 21:04:19 +01:00
parent 3db250915d
commit 6e1aaf6b85
No known key found for this signature in database
GPG Key ID: 8E7DB78588BD6F5C
2 changed files with 0 additions and 546 deletions

View File

@ -1,33 +0,0 @@
package dorkbox.jna
import java.io.File
import java.util.concurrent.*
internal object KotlinUtils {
/**
* Reads the contents of the supplied input stream into a list of lines.
*
* @return Always returns a list, even if the file does not exist, or there are errors reading it.
*/
fun readLines(file: File): List<String> {
return file.readLines()
}
fun toInteger(string: String?): Int? {
return string?.toIntOrNull()
}
/**
* Executes the given command and returns its output.
*
* This is based on an aggregate of the answers provided here: [https://stackoverflow.com/questions/35421699/how-to-invoke-external-command-from-within-kotlin-code]
*/
fun execute(vararg args: String): String {
return ProcessBuilder(args.toList())
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectError(ProcessBuilder.Redirect.PIPE)
.start()
.apply { waitFor(60, TimeUnit.SECONDS) }
.inputStream.bufferedReader().readText().trim()
}
}

View File

@ -1,513 +0,0 @@
/*
* Copyright 2017 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.jna.linux;
import static dorkbox.jna.linux.Gtk.Gtk3;
import static dorkbox.jna.linux.Gtk2.Gtk2;
import static dorkbox.jna.linux.Gtk2.isGtk3;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.io.File;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.LoggerFactory;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.PointerByReference;
import dorkbox.jna.KotlinUtils;
import dorkbox.jna.linux.structs.GtkRequisition;
import dorkbox.jna.linux.structs.GtkStyle;
import dorkbox.jna.linux.structs.PangoRectangle;
import dorkbox.os.OS;
/**
* Class to contain all of the various methods needed to get information set by a GTK theme.
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public
class GtkTheme {
/** Fallback for an unknown tray image size. */
public static volatile int TRAY_IMAGE_SIZE_FALLBACK = OS.INSTANCE.getInt(GtkTheme.class.getCanonicalName() + ".TRAY_IMAGE_SIZE_FALLBACK", 24);
/** Fallback for an unknown tray menu image size. */
public static volatile int TRAY_MENU_IMAGE_SIZE_FALLBACK = OS.INSTANCE.getInt(GtkTheme.class.getCanonicalName() + ".TRAY_MENU_IMAGE_SIZE_FALLBACK", 16);
public static
Rectangle getPixelTextHeight(String text) {
// the following method requires an offscreen widget to get the size of text (for the checkmark size) via pango
// don't forget to destroy everything!
Pointer menu = null;
Pointer item = null;
try {
menu = Gtk2.gtk_menu_new();
item = Gtk2.gtk_image_menu_item_new_with_mnemonic(text);
Gtk2.gtk_container_add(menu, item);
Gtk2.gtk_widget_realize(menu);
Gtk2.gtk_widget_realize(item);
Gtk2.gtk_widget_show_all(menu);
// get the text widget (GtkAccelLabel/GtkLabel) from inside the GtkMenuItem
Pointer textLabel = Gtk2.gtk_bin_get_child(item);
Pointer pangoLayout = Gtk2.gtk_label_get_layout(textLabel);
// ink pixel size is how much exact space it takes on the screen
PangoRectangle ink = new PangoRectangle();
Gtk2.pango_layout_get_pixel_extents(pangoLayout, ink.getPointer(), null);
ink.read();
return new Rectangle(ink.width, ink.height);
} finally {
Gtk2.gtk_widget_destroy(item);
Gtk2.gtk_widget_destroy(menu);
}
}
/**
* @return the size of the GTK menu entry's IMAGE, as best as we can tell, for determining how large of icons to use for the menu entry
*/
public static
int getMenuEntryImageSize() {
final AtomicReference<Integer> imageHeight = new AtomicReference<>();
GtkEventDispatch.dispatchAndWait(()->{
Pointer offscreen = Gtk2.gtk_offscreen_window_new();
// get the default icon size for the "paste" icon.
Pointer item = null;
try {
item = Gtk2.gtk_image_menu_item_new_from_stock("gtk-paste", null);
// make sure the image is shown (sometimes it's not always shown, then height is 0)
Gtk2.gtk_image_menu_item_set_always_show_image(item, true);
Gtk2.gtk_container_add(offscreen, item);
Gtk2.gtk_widget_realize(offscreen);
Gtk2.gtk_widget_realize(item);
Gtk2.gtk_widget_show_all(item);
PointerByReference r = new PointerByReference();
GObject.g_object_get(item, "image", r.getPointer(), null);
Pointer imageWidget = r.getValue();
GtkRequisition gtkRequisition = new GtkRequisition();
Gtk2.gtk_widget_size_request(imageWidget, gtkRequisition.getPointer());
gtkRequisition.read();
imageHeight.set(gtkRequisition.height);
} finally {
if (item != null) {
Gtk2.gtk_widget_destroy(item);
Gtk2.gtk_widget_destroy(offscreen);
}
}
});
int height = imageHeight.get();
if (height > 0) {
return height;
}
LoggerFactory.getLogger(GtkTheme.class).warn("Unable to get tray menu image size. Using fallback: " + TRAY_MENU_IMAGE_SIZE_FALLBACK);
return TRAY_MENU_IMAGE_SIZE_FALLBACK;
}
/**
* Gets the system tray indicator size.
* - AppIndicator: will properly scale the image if it's not the correct size
* - GtkStatusIndicator: ??
*/
public static
int getIndicatorSize() {
// Linux is similar enough, that it just uses this method
// https://wiki.archlinux.org/index.php/HiDPI
// 96 DPI is the default
final double defaultDPI = 96.0;
final AtomicReference<Double> screenScale = new AtomicReference<>();
final AtomicInteger screenDPI = new AtomicInteger();
screenScale.set(0D);
screenDPI.set(0);
GtkEventDispatch.dispatchAndWait(()->{
// screen DPI
Pointer screen = Gtk2.gdk_screen_get_default();
if (screen != null) {
// this call makes NO SENSE, but reading the documentation shows it is the CORRECT call.
screenDPI.set((int) Gtk2.gdk_screen_get_resolution(screen));
}
if (isGtk3) {
Pointer window = Gtk2.gdk_get_default_root_window();
if (window != null) {
double scale = Gtk3.gdk_window_get_scale_factor(window);
screenScale.set(scale);
}
}
});
// fallback
if (screenDPI.get() <= 0) {
// GET THE DPI IN LINUX
// https://wiki.archlinux.org/index.php/Talk:GNOME
Object detectedValue = Toolkit.getDefaultToolkit().getDesktopProperty("gnome.Xft/DPI");
if (detectedValue instanceof Integer) {
int asInteger = (Integer) detectedValue;
// reasonably safe check
if (asInteger > 1024) {
int dpi = asInteger / 1024;
screenDPI.set(dpi);
}
}
}
// 50 dpi is the minimum value gnome allows, and assume something screwed up. We apply this for ALL environments!
if (screenDPI.get() < 50) {
screenDPI.set((int) defaultDPI);
}
// check system ENV variables.
if (screenScale.get() == 0) {
String envVar = System.getenv("QT_AUTO_SCREEN_SCALE_FACTOR");
if (envVar != null) {
try {
screenScale.set(Double.parseDouble(envVar));
} catch (Exception ignored) {
}
}
}
// check system ENV variables.
if (screenScale.get() == 0) {
String envVar = System.getenv("QT_SCALE_FACTOR");
if (envVar != null) {
try {
screenScale.set(Double.parseDouble(envVar));
} catch (Exception ignored) {
}
}
}
// check system ENV variables.
if (screenScale.get() == 0) {
String envVar = System.getenv("GDK_SCALE");
if (envVar != null) {
try {
screenScale.set(Double.parseDouble(envVar));
} catch (Exception ignored) {
}
}
}
// check system ENV variables.
if (screenScale.get() == 0) {
String envVar = System.getenv("ELM_SCALE");
if (envVar != null) {
try {
screenScale.set(Double.parseDouble(envVar));
} catch (Exception ignored) {
}
}
}
OS.DesktopEnv.Env env = OS.DesktopEnv.INSTANCE.getEnv();
// sometimes the scaling-factor is set. If we have gsettings, great! otherwise try KDE
try {
// gsettings get org.gnome.desktop.interface scaling-factor
String output = KotlinUtils.INSTANCE.execute("gsettings", "get", "org.gnome.desktop.interface", "scaling-factor");
if (!output.isEmpty() && !output.endsWith("not found")) {
// DEFAULT icon size is 16. HiDpi changes this scale, so we should use it as well.
// should be: uint32 0 or something
if (output.contains("uint32")) {
String value = output.substring(output.indexOf("uint") + 7);
// 0 is disabled (no scaling)
// 1 is enabled (default scale)
// 2 is 2x scale
// 3 is 3x scale
// etc
double scalingFactor = Double.parseDouble(value);
if (scalingFactor >= 1) {
screenScale.set(scalingFactor);
}
// A setting of 2, 3, etc, which is all you can do with scaling-factor
// To enable HiDPI, use gsettings:
// gsettings set org.gnome.desktop.interface scaling-factor 2
}
}
} catch (Throwable ignore) {
}
if (OS.DesktopEnv.INSTANCE.isKDE()) {
// check the custom KDE override file
try {
File customSettings = new File("/usr/bin/startkde-custom");
if (customSettings.canRead()) {
List<String> lines = KotlinUtils.INSTANCE.readLines(customSettings);
for (String line : lines) {
String str = "export GDK_SCALE=";
int i = line.indexOf(str);
if (i > -1) {
String scale = line.substring(i + str.length());
double scalingFactor = Double.parseDouble(scale);
if (scalingFactor >= 1) {
screenScale.set(scalingFactor);
break;
}
}
}
}
} catch (Exception ignored) {
}
// System.err.println("screen scale: " + screenScale.get());
// System.err.println("screen DPI: " + screenDPI.get());
if (screenScale.get() == 0) {
/*
*
* Looking in plasma-framework/src/declarativeimports/core/units.cpp:
// Scale the icon sizes up using the devicePixelRatio
// This function returns the next stepping icon size
// and multiplies the global settings with the dpi ratio.
const qreal ratio = devicePixelRatio();
if (ratio < 1.5) {
return size;
} else if (ratio < 2.0) {
return size * 1.5;
} else if (ratio < 2.5) {
return size * 2.0;
} else if (ratio < 3.0) {
return size * 2.5;
} else if (ratio < 3.5) {
return size * 3.0;
} else {
return size * ratio;
}
My ratio is 1.47674, that means I have no scaling at all when there is a 1.5 factor existing. Is it reasonable? Wouldn't it make more sense to use the factor the closest to the ratio rather than what is done here?
*/
File mainFile = new File("/usr/share/plasma/plasmoids/org.kde.plasma.private.systemtray/contents/config/main.xml");
if (mainFile.canRead()) {
List<String> lines = KotlinUtils.INSTANCE.readLines(mainFile);
boolean found = false;
int index;
for (final String line : lines) {
if (line.contains("<entry name=\"iconSize\" type=\"Int\">")) {
found = true;
// have to get the "default" line value
}
String str = "<default>";
if (found && (index = line.indexOf(str)) > -1) {
// this is our line. now get the value.
String substring = line.substring(index + str.length(), line.indexOf("</default>", index));
// Default icon size for the systray icons, it's an enum which values mean,
// Small, SmallMedium, Medium, Large, Huge, Enormous respectively.
// On low DPI systems they correspond to :
// 16, 22, 32, 48, 64, 128 pixels.
// On high DPI systems those values would be scaled up, depending on the DPI.
int imageSize = 0;
Integer imageSizeEnum = KotlinUtils.INSTANCE.toInteger(substring);
if (imageSizeEnum != null) {
switch (imageSizeEnum) {
case 0:
imageSize = 16;
break;
case 1:
imageSize = 22;
break;
case 2:
imageSize = 32;
break;
case 3:
imageSize = 48;
break;
case 4:
imageSize = 64;
break;
case 5:
imageSize = 128;
break;
}
if (imageSize > 0) {
double scaleRatio = screenDPI.get() / defaultDPI;
return (int) (scaleRatio * imageSize);
}
}
}
}
}
}
}
if (OS.Linux.INSTANCE.isUbuntu() && OS.DesktopEnv.INSTANCE.isUnity(env)) {
// if we measure on ubuntu unity using a screen shot (using swing, so....) , the max size was 24, HOWEVER this goes from
// the top->bottom of the indicator bar -- and since it was swing, it uses a different rendering method and it (honestly)
// looks weird, because there is no padding for the icon. The official AppIndicator size is hardcoded...
// http://bazaar.launchpad.net/~indicator-applet-developers/libindicator/trunk.16.10/view/head:/libindicator/indicator-image-helper.c
return 22;
}
if (env == OS.DesktopEnv.Env.XFCE) {
// xfce is easy, because it's not a GTK setting for the size (xfce notification area maximum icon size)
String properties = OS.DesktopEnv.INSTANCE.queryXfce("xfce4-panel", null);
String[] propertiesAsList = properties.split(OS.INSTANCE.getLINE_SEPARATOR());
for (String prop : propertiesAsList) {
if (prop.startsWith("/plugins/") && prop.endsWith("/size-max")) {
// this is the property we are looking for (we just don't know which panel it's on)
// note: trim() is required because it will strip new-line
// xfconf-query -c xfce4-panel -p /plugins/plugin-14 (this will say 'systray' or 'tasklist' or whatever)
// find the 'systray' plugin
String panelString = prop.substring(0, prop.indexOf("/size-max"));
String panelName = OS.DesktopEnv.INSTANCE.queryXfce("xfce4-panel", panelString).trim();
if (panelName.equals("systray")) {
String size = OS.DesktopEnv.INSTANCE.queryXfce("xfce4-panel", prop).trim();
try {
return Integer.parseInt(size);
} catch (Exception e) {
LoggerFactory.getLogger(GtkTheme.class)
.error("Unable to get XFCE notification panel size for channel '{}', property '{}'",
"xfce4-panel", prop, e);
}
}
}
}
}
// try to use GTK to get the tray icon size
final AtomicInteger traySize = new AtomicInteger();
GtkEventDispatch.dispatchAndWait(()->{
Pointer screen = Gtk2.gdk_screen_get_default();
Pointer settings = null;
if (screen != null) {
settings = Gtk2.gtk_settings_get_for_screen(screen);
}
if (settings != null) {
PointerByReference pointer = new PointerByReference();
// https://wiki.archlinux.org/index.php/GTK%2B
// To use smaller icons, use a line like this:
// gtk-icon-sizes = "panel-menu=16,16:panel=16,16:gtk-menu=16,16:gtk-large-toolbar=16,16:gtk-small-toolbar=16,16:gtk-button=16,16"
// this gets icon sizes. On XFCE, ubuntu, it returns "panel-menu-bar=24,24"
// NOTE: gtk-icon-sizes is deprecated and ignored since GTK+ 3.10.
// A list of icon sizes. The list is separated by colons, and item has the form: size-name = width , height
GObject.g_object_get(settings, "gtk-icon-sizes", pointer.getPointer(), null);
Pointer value = pointer.getValue();
if (value != null) {
// this might be null for later versions of GTK!
String iconSizes = value.getString(0);
String[] strings = new String[] {"panel-menu-bar=", "panel=", "gtk-large-toolbar=", "gtk-small-toolbar="};
for (String var : strings) {
int i = iconSizes.indexOf(var);
if (i >= 0) {
String size = iconSizes.substring(i + var.length(), iconSizes.indexOf(",", i));
Integer sizeInt = KotlinUtils.INSTANCE.toInteger(size);
if (sizeInt != null) {
traySize.set(sizeInt);
return;
}
}
}
}
}
});
int i = traySize.get();
if (i != 0) {
return i;
}
// sane default
LoggerFactory.getLogger(GtkTheme.class).warn("Unable to get tray image size. Using fallback: " + TRAY_IMAGE_SIZE_FALLBACK);
return TRAY_IMAGE_SIZE_FALLBACK;
}
/**
* @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 getTextColor() {
final AtomicReference<Color> color = new AtomicReference<>(null);
GtkEventDispatch.dispatchAndWait(()->{
Color c;
// the following method requires an offscreen widget to get the style information from.
// don't forget to destroy everything!
Pointer menu = null;
Pointer item = null;
try {
menu = Gtk2.gtk_menu_new();
item = Gtk2.gtk_image_menu_item_new_with_mnemonic("a");
Gtk2.gtk_container_add(menu, item);
Gtk2.gtk_widget_realize(menu);
Gtk2.gtk_widget_realize(item);
Gtk2.gtk_widget_show_all(menu);
GtkStyle style = Gtk2.gtk_rc_get_style(item);
style.read();
// this is the same color chromium uses (fg)
// https://chromium.googlesource.com/chromium/src/+/b3ca230ddd7d1238ee96ed26ea23e369f10dd655/chrome/browser/ui/libgtk2ui/gtk2_ui.cc#873
c = style.fg[GtkState.NORMAL].getColor();
color.set(c);
} finally {
Gtk2.gtk_widget_destroy(item);
Gtk2.gtk_widget_destroy(menu);
}
});
return color.get();
}
}