diff --git a/Makefile b/Makefile index 52d1691..d232d48 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ update: echo "Aktualisiert." fi $(MAKE) --no-print-directory sync-config reload - echo "Neuer Code wirkt nach dem nächsten Ab- und Anmelden." + echo "Neuer Code ist aktiv. Nur extension.js selbst wirkt erst nach dem nächsten Anmelden." # UUID in die aktiven Extensions eintragen, falls sie fehlt enable: diff --git a/android/app/src/main/kotlin/de/marha/planer/ui/NotesScreen.kt b/android/app/src/main/kotlin/de/marha/planer/ui/NotesScreen.kt index 795be3e..6e4a33e 100644 --- a/android/app/src/main/kotlin/de/marha/planer/ui/NotesScreen.kt +++ b/android/app/src/main/kotlin/de/marha/planer/ui/NotesScreen.kt @@ -81,7 +81,7 @@ fun NotesScreen(store: Store, modifier: Modifier = Modifier) { val opened = open?.let { store.note(it) } if (opened != null) { - NoteView(store, opened.id, onClose = { open = null }) + NoteView(store, opened.id, modifier, onClose = { open = null }) return } @@ -180,7 +180,7 @@ private fun TreeRow(row: Row, onClick: () -> Unit, onLongClick: () -> Unit) { // Eine Notiz: formatiert anzeigen, Tipp in den Text bearbeitet. Gespeichert wird alle 5 Sekunden und beim Verlassen. @Composable -fun NoteView(store: Store, id: String, onClose: () -> Unit) { +fun NoteView(store: Store, id: String, modifier: Modifier = Modifier, onClose: () -> Unit) { store.version.intValue val note = store.note(id) var editing by rememberSaveable { mutableStateOf(false) } @@ -211,7 +211,7 @@ fun NoteView(store: Store, id: String, onClose: () -> Unit) { } } - Column(Modifier.fillMaxSize()) { + Column(modifier.fillMaxSize()) { Row(Modifier.fillMaxWidth().padding(4.dp), verticalAlignment = Alignment.CenterVertically) { IconButton(onClick = { if (editing) stopEdit() else onClose() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") } Text(note?.name ?: "", style = MaterialTheme.typography.titleLarge, modifier = Modifier.weight(1f)) diff --git a/desktop@marha.de/app.js b/desktop@marha.de/app.js new file mode 100644 index 0000000..8345710 --- /dev/null +++ b/desktop@marha.de/app.js @@ -0,0 +1,74 @@ +import Meta from 'gi://Meta'; +import St from 'gi://St'; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; + +import {CalendarPanel} from './calendar.js'; +import {NotesPanel} from './notes.js'; +import {Store} from './store.js'; +import {Sync} from './sync.js'; +import {TodoPanel} from './todos.js'; + +// Die eigentliche Extension. extension.js lädt dieses Modul bei jedem Einschalten frisch, +// der Zustand liegt auf dem Extension-Objekt (ext), damit Tests und D-Bus ihn dort finden. + +const MARGIN = 24; + +export function enable(ext) { + ext._store = new Store(); + + // Nur die Panels sind reaktiv. Der Rest lässt Klicks zum Desktop durch. + ext._root = new St.BoxLayout({style_class: 'desktop-root', reactive: false}); + ext._notes = new NotesPanel(ext._store); + ext._root.add_child(ext._notes); + // Rechte Spalte füllt den Rest, der Abstand dazwischen entspricht dem Außenrand. + const right = new St.BoxLayout({vertical: true, style_class: 'desktop-right', reactive: false, x_expand: true}); + right.add_child(new CalendarPanel(ext._store)); + right.add_child(new TodoPanel(ext._store)); + ext._root.add_child(right); + + global.window_group.add_child(ext._root); + ext._restackId = global.display.connect('restacked', () => restack(ext)); + ext._mapId = global.window_manager.connect('map', () => restack(ext)); + ext._workareaId = global.display.connect('workareas-changed', () => layout(ext)); + layout(ext); + restack(ext); + + ext._sync = new Sync(ext._store); + ext._sync.start(); +} + +export function disable(ext) { + ext._sync.stop(); + ext._sync = null; + global.display.disconnect(ext._restackId); + global.window_manager.disconnect(ext._mapId); + global.display.disconnect(ext._workareaId); + ext._root.destroy(); + ext._root = null; + ext._notes = null; + ext._store = null; +} + +// Füllt den Arbeitsbereich des Hauptmonitors (ohne Topbar und Dock). Notizen nehmen die halbe Bildschirmbreite. +function layout(ext) { + const area = Main.layoutManager.getWorkAreaForMonitor(Main.layoutManager.primaryIndex); + ext._root.set_position(area.x + MARGIN, area.y + MARGIN); + ext._root.set_size(area.width - 2 * MARGIN, area.height - 2 * MARGIN); + ext._notes.set_width(Math.round(Main.layoutManager.primaryMonitor.width / 2)); +} + +// Liegt direkt über Desktop-Fenstern (z.B. Desktop Icons), sonst direkt über dem Wallpaper. +// Mutter sortiert die Fensterebene bei jedem Restack neu, daher hier nachziehen. +function restack(ext) { + let below = Main.layoutManager._backgroundGroup; + for (const actor of global.window_group.get_children()) { + if (!(actor instanceof Meta.WindowActor)) + continue; + const window = actor.meta_window; + const isDing = window.customJS_ding || window.title?.startsWith('Desktop Icons'); + if (isDing || window.window_type === Meta.WindowType.DESKTOP) + below = actor; + } + global.window_group.set_child_above_sibling(ext._root, below); +} diff --git a/desktop@marha.de/extension.js b/desktop@marha.de/extension.js index 9a67820..63ec4b1 100644 --- a/desktop@marha.de/extension.js +++ b/desktop@marha.de/extension.js @@ -1,74 +1,67 @@ -import Meta from 'gi://Meta'; -import St from 'gi://St'; +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js'; -import * as Main from 'resource:///org/gnome/shell/ui/main.js'; - -import {CalendarPanel} from './calendar.js'; -import {NotesPanel} from './notes.js'; -import {Store} from './store.js'; -import {Sync} from './sync.js'; -import {TodoPanel} from './todos.js'; - -const MARGIN = 24; +// GNOME lädt Extension-Code nur einmal pro Sitzung. Deshalb ist diese Datei nur ein Lader: +// Beim Einschalten kopiert er die Module in einen frischen Ordner und importiert sie von dort. +// Neuer Ordner heißt neue Adresse, also lädt GJS wirklich den aktuellen Code. Nach einer +// Änderung reicht so make update (kopiert, schaltet aus und ein), ohne Ab- und Anmelden. +// Nur diese Datei selbst wirkt erst nach dem nächsten Anmelden. export default class DesktopExtension extends Extension { + _generation = 0; + enable() { - this._store = new Store(); - - // Nur die Panels sind reaktiv. Der Rest lässt Klicks zum Desktop durch. - this._root = new St.BoxLayout({style_class: 'desktop-root', reactive: false}); - this._notes = new NotesPanel(this._store); - this._root.add_child(this._notes); - // Rechte Spalte füllt den Rest, der Abstand dazwischen entspricht dem Außenrand. - const right = new St.BoxLayout({vertical: true, style_class: 'desktop-right', reactive: false, x_expand: true}); - right.add_child(new CalendarPanel(this._store)); - right.add_child(new TodoPanel(this._store)); - this._root.add_child(right); - - global.window_group.add_child(this._root); - this._restackId = global.display.connect('restacked', () => this._restack()); - this._mapId = global.window_manager.connect('map', () => this._restack()); - this._workareaId = global.display.connect('workareas-changed', () => this._layout()); - this._layout(); - this._restack(); - - this._sync = new Sync(this._store); - this._sync.start(); + const generation = ++this._generation; + const dir = copyModules(this.dir); + import(`${dir.get_uri()}/app.js`) + .then(app => { + // Inzwischen aus- oder erneut eingeschaltet: dieser Stand ist überholt + if (this._generation !== generation) + return; + this._app = app; + app.enable(this); + }) + .catch(e => console.error(`desktop@marha.de: Laden fehlgeschlagen: ${e.message}\n${e.stack}`)); } disable() { - this._sync.stop(); - this._sync = null; - global.display.disconnect(this._restackId); - global.window_manager.disconnect(this._mapId); - global.display.disconnect(this._workareaId); - this._root.destroy(); - this._root = null; - this._notes = null; - this._store = null; - } - - // Füllt den Arbeitsbereich des Hauptmonitors (ohne Topbar und Dock). Notizen nehmen die halbe Bildschirmbreite. - _layout() { - const area = Main.layoutManager.getWorkAreaForMonitor(Main.layoutManager.primaryIndex); - this._root.set_position(area.x + MARGIN, area.y + MARGIN); - this._root.set_size(area.width - 2 * MARGIN, area.height - 2 * MARGIN); - this._notes.set_width(Math.round(Main.layoutManager.primaryMonitor.width / 2)); - } - - // Liegt direkt über Desktop-Fenstern (z.B. Desktop Icons), sonst direkt über dem Wallpaper. - // Mutter sortiert die Fensterebene bei jedem Restack neu, daher hier nachziehen. - _restack() { - let below = Main.layoutManager._backgroundGroup; - for (const actor of global.window_group.get_children()) { - if (!(actor instanceof Meta.WindowActor)) - continue; - const window = actor.meta_window; - const isDing = window.customJS_ding || window.title?.startsWith('Desktop Icons'); - if (isDing || window.window_type === Meta.WindowType.DESKTOP) - below = actor; - } - global.window_group.set_child_above_sibling(this._root, below); + this._generation++; + this._app?.disable(this); + this._app = null; } } + +// Alle Module außer dieser Datei nach ~/.cache/desktop-app/live- kopieren. Ältere Kopien +// sind schon geladen und werden gelöscht. +function copyModules(source) { + const base = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_cache_dir(), 'desktop-app'])); + GLib.mkdir_with_parents(base.get_path(), 0o700); + for (const name of listNames(base)) { + if (name.startsWith('live-')) + removeDir(base.get_child(name)); + } + const dest = base.get_child(`live-${GLib.get_real_time()}`); + dest.make_directory(null); + for (const name of listNames(source)) { + if (name.endsWith('.js') && name !== 'extension.js') + source.get_child(name).copy(dest.get_child(name), Gio.FileCopyFlags.OVERWRITE, null, null); + } + return dest; +} + +function listNames(dir) { + const names = []; + const children = dir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null); + let info; + while ((info = children.next_file(null))) + names.push(info.get_name()); + children.close(null); + return names; +} + +function removeDir(dir) { + for (const name of listNames(dir)) + dir.get_child(name).delete(null); + dir.delete(null); +} diff --git a/desktop@marha.de/widgets.js b/desktop@marha.de/widgets.js index 23d5d42..946218c 100644 --- a/desktop@marha.de/widgets.js +++ b/desktop@marha.de/widgets.js @@ -1,4 +1,5 @@ import Clutter from 'gi://Clutter'; +import GObject from 'gi://GObject'; import Pango from 'gi://Pango'; import St from 'gi://St'; @@ -50,8 +51,19 @@ export function scrollBox(styleClass) { // Mehrzeiliges Eingabefeld in einem Scrollbereich. Der Scrollbereich ist der sichtbare Rahmen, // ein Klick in die freie Fläche darunter setzt den Cursor ans Ende. Der Cursor bleibt beim Tippen sichtbar. +// Ein mehrzeiliges Feld ist so hoch wie sein Text. Themes wie Yaru geben StEntry eine min-height. +// Die ersetzt die Mindesthöhe, der Scrollbereich richtet sich aber genau danach: er hielte das +// Feld für schrumpfbar und ließe nichts zu scrollen. +const MultilineEntry = GObject.registerClass( +class MultilineEntry extends St.Entry { + vfunc_get_preferred_height(forWidth) { + const [, natural] = super.vfunc_get_preferred_height(forWidth); + return [natural, natural]; + } +}); + export function multilineEntry(text, styleClass, scrollClass, hint = '') { - const entry = new St.Entry({can_focus: true, text, hint_text: hint, style_class: `${styleClass} multiline`, x_expand: true}); + const entry = new MultilineEntry({can_focus: true, text, hint_text: hint, style_class: `${styleClass} multiline`, x_expand: true}); const clutterText = entry.clutter_text; clutterText.single_line_mode = false; clutterText.line_wrap = true;