Desktop Widgets in Java

vorhergehende Artikel in: Java Linux Komponenten
28.10.2023

Ich begann ein Projekt umzusetzen, das als Fingerübung dienen sollte: Ich wollte ein Widget-Framework bauen, das anderen – wie etwa Screenlets oder Plasmoids nachempfunden sein sollte. Da Java ein Konstrukt ist, das den Entwickler beinahe komplett vom unterliegenden Betriebssystem und vor allem der Implementierung des Windows-Systems abkoppelt, wusste ich vorher, dass ich auf das eine oder andere Problem stoßen würde.

Eines der Probleme war die Anforderung, dass die Widgets auf dem Hintergrund gezeichnet sein sollten und andere Anwendungsfenster immer vor diesen dargestellt werden sollte. Das war nicht einfach mit Java zu erreichen und es existieren immer noch Situationen, in denen es dazu kommen kann, dass Widgets vor Anwendungsfenstern gezeichnet werden.

Das zweite Problem war wesentlich schwieriger zu lösen und mit Java alleine ist es unlösbar: Solche Widgets haben entweder die Eigenschaft oder es ist zumindest konfigurierbar, dass sie auf allen Virtuellen Desktops angezeigt werden. Normalerweise sind Anwendungsfenster einem Virtuellen Desktop zugewiesen und wenn ein anderer angezeigt wird, verschwinden sie. Sogenannte Sticky Windows sind dagegen sichtbar, egal welcher Virtual Desktop gerade aktiv ist. Für Widgets eigentlich eine prima Sache. Leider ist das Konzept Virtueller Desktop in Standard Java AWT / Swing nicht abgebildet: Weder weiß man, welcher Virtuelle Desktop gerade aktiv ist, noch wie viele es gibt und logischerweise kann man keine Events abonnieren, die einen Wechsel des aktuellen Virtuellen Desktop anzeigen.

Ich fand eine – sehr krude – Variante zur Lösung dieses Problems (unter Linux und wahrscheinlich nur unter Xorg): Folgendes Kommando liest die Nummer des aktuellen Virtuellen Desktops aus:

xprop -root _NET_CURRENT_DESKTOP

Man könnte also im Java-Code während der Repaint-Methode von Swing (rund 10x pro Sekunde) dieses Kommando ausführen und auf eine Änderung des Ergebniswerts reagieren. Zur Verlagerung eines Fensters von einem nicht sichtbaren auf den aktuellen Virtuellen Desktop genügt es, das Java-Fenster mittels setVisible(false) zu verbergen und es anschließend mittels setVisible(true) wieder anzuzeigen. Allerdings ist die Ausführung eines Betriebssystemprozesses 10 mal in der Sekunde ein gewaltiger Overhead für so eine simple Aufgabe.

Daher begann ich weiterzusuchen und ich wurde fündig: Es existiert Beispielcode im Internet, der erklärt, wie ich über entsprechende Events informiert werden kann: Ich fand welchen auf Anhieb für C und Python. Der C-Code ist hier zu sehen:

//Unter Ubuntu 22.04 zu übersetzen mittels : gcc VirtualScreenMonitor.c -oVirtualScreenMonitor `pkg-config --cflags --libs gtk+-3.0` -lXrandr -lXext -lX11
//https://stackoverflow.com/questions/2641766/python-x11-find-out-if-user-switches-virtual-desktops
 <gtk/gtk.h>
 <gdk/gdkx.h>

GdkFilterReturn propertyChangeFilter(GdkXEvent* xevent, GdkEvent*e, gpointer data) { const XPropertyEvent*const propEvt=(const XPropertyEvent*)xevent;

if(propEvt->type!=PropertyNotify) return GDK_FILTER_CONTINUE; if(propEvt->state!=PropertyNewValue) return GDK_FILTER_CONTINUE; const Atom NET_CURRENT_DESKTOP=(Atom)data; if(propEvt->atom!=NET_CURRENT_DESKTOP) return GDK_FILTER_CONTINUE;

fprintf(stderr, "Desktop change detected\n"); Atom actualType; int actualFormat; unsigned long nitems, remainingBytes; unsigned char* prop; if(XGetWindowProperty(propEvt->display, propEvt->window, propEvt->atom, 0, 1, False, AnyPropertyType, &actualType, &actualFormat, &nitems, &remainingBytes, &prop) != Success) { fprintf(stderr, "Failed to get current desktop number\n"); return GDK_FILTER_CONTINUE; } if(nitems!=1 || remainingBytes!=0 || actualFormat!=32) { XFree(prop); fprintf(stderr, "Unexpected number of items (%lu) or remaining bytes (%lu)" " or format (%d)\n", nitems, remainingBytes, actualFormat); return GDK_FILTER_CONTINUE; } guint32 value; memcpy(&value, prop, sizeof value); XFree(prop); fprintf(stderr, "Current desktop: %u\n", value);

return GDK_FILTER_CONTINUE; }

int main(int argc, char** argv) { gtk_init(&argc,&argv);

GdkDisplay*const gdkDisplay=gdk_display_get_default(); Display*const display=gdk_x11_display_get_xdisplay(gdkDisplay); const Atom atom=XInternAtom(display, "_NET_CURRENT_DESKTOP", True); GdkWindow*const root=gdk_get_default_root_window(); gdk_window_set_events(root, GDK_PROPERTY_CHANGE_MASK); gdk_window_add_filter(root, propertyChangeFilter, (gpointer)atom);

gtk_main(); }

Mein Plan war nun, eine solche minimale Anwendung zu schreiben und sie, nachdem sie gestartet ist mit dem Java-Code kommunizieren zu lassen. Meine erste Idee dafür war, den externen Prozess bei Auftreten eines solchen Events in eine Pipe schreiben zu lassen an deren anderem Ende der Java-Code lauscht.

Da aber hier auch Python als Möglichkeit auftauchte, dachte ich, dass es portabler wäre, einfach aus Java heraus ein entsprechendes Python-Skript zu starten, das als Resource vorliegen könnte. Der Vorteil bei der Umsetzung mit Python ist, dass ich dem Java-Code die Aufgabe übertragen kann, den Python-Teil zu starten und es dazu möglich ist, den Python-Code als Ressource im Java-Code zu hinterlegen. Damit sieht die Anwendung für den Anwender wieder wie aus einem Guss aus und er bemerkt nichts von der komplexen Mechanik im Inneren. Eine stärkere Integration - wie etwa die Ausführung mittels Jython wählte ich ab, da Jython lediglich Python 2.x unterstützt.

Der dazu benötigte Python-Code sieht wie folgt aus:

#!/usr/bin/env python3
# Based on code by Stephan Sokolow
# MIT-licensed
# Source: https://gist.github.com/ssokolow/e7c9aae63fb7973e4d64cff969a78ae8
"""python-xlib example which reacts to changing the active Desktop.

Requires: - Python - python-xlib

Tested with Python 3.x

Design: -------

Any modern window manager that isn't horrendously broken maintains an X11 property on the root window named _NET_ACTIVE_WINDOW.

Any modern application toolkit presents the window title via a property named _NET_WM_NAME.

This listens for changes to both of them and then hides duplicate events so it only reacts to title changes once. """

# pylint: disable=unused-import import sys from contextlib import contextmanager from typing import Any, Dict, Optional, Tuple, Union # noqa

from Xlib import X from Xlib.display import Display from Xlib.error import XError, BadWindow from Xlib.xobject.drawable import Window from Xlib.protocol.rq import Event

# Connect to the X server and get the root window disp = Display() root = disp.screen().root

# Prepare the property names we use so they can be fed into X11 APIs NET_CURRENT_DESKTOP = disp.intern_atom('_NET_CURRENT_DESKTOP') # Legacy encoding

def handle_xevent(event: Event): """Handler for X events which ignores anything but focus/title change""" if event.type != X.PropertyNotify: return

if event.atom == NET_CURRENT_DESKTOP: response = root.get_full_property(NET_CURRENT_DESKTOP, X.AnyPropertyType) vd=root.get_full_property(NET_CURRENT_DESKTOP, 0) print("Current desktop: {0}".format(vd.value[0])) sys.stdout.flush() # else: # print(event.atom)

if __name__ == '__main__': print("started") # Listen for _NET_ACTIVE_WINDOW changes root.change_attributes(event_mask=X.PropertyChangeMask)

while True: # next_event() sleeps until we get an event handle_xevent(disp.next_event())

Nachdem ich das geklärt hatte, überlegte ich, wie ich auf einfache Art und Weise weitere Widgets anbieten könnte und kam auf die Darstellung von Webseiten. Dabei sollte es zum Beispiel auch möglich sein, Single Page Apps zu rendern. Hierdurch wurde die implizite Anforderung hinzugefügt, dass Interaktivität möglich und damit Javascript ausgeführt werden sollte. Damit wäre es dann zum Beispiel möglich, solche

Uhren

auf dem Desktop nutzen zu können. Idealerweise würde man die Javascript-Ausführung pro Widget-Instanz an- und abschalten können.

Die Unterstützung der JavaScript enabled WebView Widgets erfolgt durch Nutzung der entsprechenden JavaFX Komponente, die ich dazu in Swing eingebettet habe.

Alle Artikel rss Wochenübersicht Monatsübersicht Github Repositories Gitlab Repositories Mastodon Über mich home xmpp


Vor 5 Jahren hier im Blog

  • Mandelbrot-Sets mittels Shadern berechnen

    17.05.2019

    Nachdem ich in den letzten verregneten Tagen auf Youtube in den Videos von Numberphile versunken bin, hat mich eines davon angestachelt, mich selbst mit dem Mandelbrotset zu beschäftigen. Als ich dann noch Code fand, der behauptete, das auf einer Graphikkarte mittels Shadern berechnen zu können, war es um mich geschehen...

    Weiterlesen...

Neueste Artikel

  • Erste Vor-Version eines Gis-Plugin für die sQLshell

    Wie bereits in einem früheren Artikel erwähnt plane ich, demnächst ein Plugin für die sQLshell anzubieten, das eine Visualisierung von Daten mit räumlichem Bezug im Stil eines Geoinformationssystems erlaubt.

    Weiterlesen...
  • bad-certificates Version 2.1.0

    Das bereits vorgestellte Projekt zur automatisierten Erzeugung von Zertifikaten mit allen möglichen Fehlern hat eine Erweiterung erfahren und verfügt über ein Partnerprojekt - beide sind nunmehr in der Version 2.1.0 freigegeben

    Weiterlesen...
  • SQLite als Geodatenbank

    Wie bereits in einem früheren Artikel beschrieben treibe ich derzeit Anstrengungen voran, die sQLshell attraktiver für Nutzer zu machen, die mit Geodatenbanken arbeiten.

    Weiterlesen...

Manche nennen es Blog, manche Web-Seite - ich schreibe hier hin und wieder über meine Erlebnisse, Rückschläge und Erleuchtungen bei meinen Hobbies.

Wer daran teilhaben und eventuell sogar davon profitieren möchte, muß damit leben, daß ich hin und wieder kleine Ausflüge in Bereiche mache, die nichts mit IT, Administration oder Softwareentwicklung zu tun haben.

Ich wünsche allen Lesern viel Spaß und hin und wieder einen kleinen AHA!-Effekt...

PS: Meine öffentlichen GitHub-Repositories findet man hier - meine öffentlichen GitLab-Repositories finden sich dagegen hier.