This commit is contained in:
team3
2026-09-19 22:50:49 +02:00
parent 9bb3a30783
commit e0c615c73a
9 changed files with 98 additions and 92 deletions

View File

@@ -5,7 +5,7 @@ import Pango from 'gi://Pango';
import St from 'gi://St';
import * as D from './dates.js';
import {editTodo, todoMenu, todoRow, todoTitle} from './todos.js';
import {editTodo, todoClick, todoRow, todoTitle} from './todos.js';
import {button, contextMenu, dropdown, openDatePicker, openTimePicker, Popover, scrollBox, textButton} from './widgets.js';
// Rote Linie für die aktuelle Uhrzeit wandert einmal pro Minute
@@ -77,16 +77,15 @@ function assignLanes(items) {
// Menü an der Mausposition: Termin oder Todo für Tag und optional Uhrzeit anlegen.
function createMenu(store, day, time = null) {
const [x, y] = global.get_pointer();
contextMenu([
{label: 'Termin erstellen', onSelect: () => editEvent(store, {date: day, start: time}, {x, y})},
{label: 'Todo erstellen', onSelect: () => editTodo(store, {date: day, time}, {x, y})},
{label: 'Termin erstellen', onSelect: () => editEvent(store, {date: day, start: time})},
{label: 'Todo erstellen', onSelect: () => editTodo(store, {date: day, time})},
]);
}
// Popover mit Titel sowie Von und Bis (Datum und Uhrzeit). Enter speichert.
export function editEvent(store, event, anchor) {
const popover = new Popover(anchor);
// Popover am Mauszeiger mit Titel sowie Von und Bis (Datum und Uhrzeit). Enter speichert.
export function editEvent(store, event) {
const popover = new Popover();
popover.addLabel(event.id ? 'Termin bearbeiten' : 'Neuer Termin', 'popover-title');
const title = popover.addEntry(event.title ?? '', 'Titel');
@@ -148,6 +147,14 @@ export function eventMenu(store, event) {
contextMenu([{label: 'Löschen', onSelect: () => store.removeEvent(event.id)}]);
}
// Gilt überall, wo ein Termin steht: links bearbeiten, rechts Menü.
function eventClick(store, event, mouseButton) {
if (mouseButton === 3)
eventMenu(store, event);
else
editEvent(store, event);
}
export const CalendarPanel = GObject.registerClass(
class CalendarPanel extends St.BoxLayout {
_init(store) {
@@ -237,22 +244,12 @@ class CalendarPanel extends St.BoxLayout {
_eventEntry(event, styleClass) {
const singleDay = endDateOf(event) === event.date;
const text = event.start && singleDay ? `${event.start} ${event.title}` : event.title;
return textButton(text, (b, mouseButton) => {
if (mouseButton === 3)
eventMenu(this._store, event);
else
editEvent(this._store, event, b);
}, styleClass);
return textButton(text, (_b, mouseButton) => eventClick(this._store, event, mouseButton), styleClass);
}
_todoEntry(todo, styleClass) {
const text = `${todo.done ? '☑' : '☐'} ${todo.time ? `${todo.time} ` : ''}${todoTitle(todo)}`;
return textButton(text, (b, mouseButton) => {
if (mouseButton === 3)
todoMenu(this._store, todo);
else
editTodo(this._store, todo, b);
}, `${styleClass} todo`);
return textButton(text, (_b, mouseButton) => todoClick(this._store, todo, mouseButton), `${styleClass} todo`);
}
// ---- Monat ----
@@ -453,12 +450,7 @@ class CalendarPanel extends St.BoxLayout {
clip_to_allocation: true,
button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
});
block.connect('clicked', (_b, mouseButton) => {
if (mouseButton === 3)
eventMenu(this._store, event);
else
editEvent(this._store, event, block);
});
block.connect('clicked', (_b, mouseButton) => eventClick(this._store, event, mouseButton));
return block;
}
@@ -472,12 +464,7 @@ class CalendarPanel extends St.BoxLayout {
can_focus: true,
button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
});
block.connect('clicked', (_b, mouseButton) => {
if (mouseButton === 3)
eventMenu(this._store, event);
else
editEvent(this._store, event, block);
});
block.connect('clicked', (_b, mouseButton) => eventClick(this._store, event, mouseButton));
return block;
}
@@ -607,13 +594,11 @@ class CalendarPanel extends St.BoxLayout {
const preset = {date: this._weekDays[first.day]};
if (last.day !== first.day)
preset.endDate = this._weekDays[last.day];
let anchor = this._alldaySlots[last.day];
if (!allDay) {
preset.start = `${pad(first.hour)}:00`;
preset.end = last.hour === 23 ? '23:59' : `${pad(last.hour + 1)}:00`;
anchor = this._slots[last.day][last.hour];
}
const popover = editEvent(this._store, preset, anchor);
const popover = editEvent(this._store, preset);
popover.onClose = () => this._clearHighlight();
}
@@ -643,12 +628,7 @@ class CalendarPanel extends St.BoxLayout {
x_expand: true,
button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
});
b.connect('clicked', (_b, mouseButton) => {
if (mouseButton === 3)
eventMenu(this._store, event);
else
editEvent(this._store, event, b);
});
b.connect('clicked', (_b, mouseButton) => eventClick(this._store, event, mouseButton));
list.add_child(b);
}

View File

@@ -79,7 +79,7 @@ class NotesPanel extends St.BoxLayout {
_addChildren(folder, depth) {
for (const sub of folder.folders) {
const open = this._expanded.has(sub.path);
this._treeRow(`${open ? '▾' : '▸'} ${sub.name}`, depth, 'folder', sub, () => {
this._treeRow(sub.name, depth, 'folder', sub, () => {
if (open)
this._expanded.delete(sub.path);
else
@@ -89,23 +89,20 @@ class NotesPanel extends St.BoxLayout {
if (open)
this._addChildren(sub, depth + 1);
}
// Dünne Linie trennt Ordner von Notizen
if (folder.folders.length && folder.notes.length) {
const separator = new St.Widget({style_class: 'tree-separator'});
separator.set_style(`margin-left: ${8 + depth * 16}px`);
this._tree.add_child(separator);
}
for (const note of folder.notes)
this._treeRow(note.name, depth, 'note', note, () => this._select({type: 'note', path: note.path, name: note.name}));
}
// Links wählt aus, Mitte benennt um, rechts öffnet das Menü.
_treeRow(text, depth, type, node, onClick) {
const row = textButton(text, (b, mouseButton) => {
if (mouseButton === 3)
this._rowMenu(type, node, b);
const row = textButton(text, (_b, mouseButton) => {
if (mouseButton === 2)
this._rename(type, node);
else if (mouseButton === 3)
this._rowMenu(type, node);
else
onClick();
}, `tree-row ${type}`, type === 'folder' ? 'folder-symbolic' : 'text-x-generic-symbolic');
}, `tree-row ${type}`, type === 'folder' ? (this._expanded.has(node.path) ? 'folder-open-symbolic' : 'folder-symbolic') : 'text-x-generic-symbolic');
row.set_style(`padding-left: ${8 + depth * 16}px`);
if (this._selected?.path === node.path)
row.add_style_class_name('selected');
@@ -202,8 +199,9 @@ class NotesPanel extends St.BoxLayout {
// ---- Anlegen, Umbenennen, Löschen ----
_namePopover(anchor, title, value, onSave) {
const popover = new Popover(anchor);
// Popover am Mauszeiger mit einem Namensfeld.
_namePopover(title, value, onSave) {
const popover = new Popover();
popover.addLabel(title, 'popover-title');
const entry = popover.addEntry(value, 'Name');
popover.setSave(() => onSave(entry.get_text().trim()));
@@ -212,9 +210,9 @@ class NotesPanel extends St.BoxLayout {
return popover;
}
_newNote(anchor, folder) {
_newNote(folder) {
let created = false;
const popover = this._namePopover(anchor, 'Neue Notiz', '', name => {
const popover = this._namePopover('Neue Notiz', '', name => {
const path = this._store.createNote(folder, name);
this._expanded.add(folder);
this._select({type: 'note', path, name});
@@ -227,8 +225,8 @@ class NotesPanel extends St.BoxLayout {
};
}
_newFolder(anchor, parent) {
this._namePopover(anchor, 'Neuer Ordner', '', name => {
_newFolder(parent) {
this._namePopover('Neuer Ordner', '', name => {
const path = this._store.createFolder(parent, name);
this._expanded.add(parent);
this._expanded.add(path);
@@ -237,10 +235,9 @@ class NotesPanel extends St.BoxLayout {
}
_createItems(folder) {
const [x, y] = global.get_pointer();
return [
{label: 'Notiz erstellen', onSelect: () => this._newNote({x, y}, folder)},
{label: 'Ordner erstellen', onSelect: () => this._newFolder({x, y}, folder)},
{label: 'Notiz erstellen', onSelect: () => this._newNote(folder)},
{label: 'Ordner erstellen', onSelect: () => this._newFolder(folder)},
];
}
@@ -248,18 +245,18 @@ class NotesPanel extends St.BoxLayout {
contextMenu(this._createItems(folder));
}
_rowMenu(type, node, anchor) {
_rowMenu(type, node) {
const isFolder = type === 'folder';
const empty = isFolder ? this._store.folderEmpty(node.path) : true;
contextMenu([
...isFolder ? this._createItems(node.path) : [],
{label: 'Umbenennen', onSelect: () => this._rename(type, node, anchor)},
{label: 'Umbenennen', onSelect: () => this._rename(type, node)},
{label: empty ? 'Löschen' : 'Löschen (Ordner ist nicht leer)', sensitive: empty, onSelect: () => this._delete(type, node)},
]);
}
_rename(type, node, anchor) {
this._namePopover(anchor, 'Umbenennen', node.name, name => {
_rename(type, node) {
this._namePopover('Umbenennen', node.name, name => {
if (name === node.name)
return;
const path = type === 'note' ? this._store.renameNote(node.path, name) : this._store.renameFolder(node.path, name);

View File

@@ -114,7 +114,6 @@
.tree-row.folder { font-weight: bold; }
.icon-row { spacing: 6px; }
.tree-row StIcon { color: #aaa; }
.tree-separator { height: 1px; background-color: rgba(255, 255, 255, 0.15); margin: 4px 8px; }
.notes-content-column { spacing: 8px; padding-left: 12px; }
.note-content { font-size: 10.5pt; }
/* Der Scrollbereich ist der sichtbare Rahmen, das Feld darin bleibt unsichtbar. */

View File

@@ -18,9 +18,9 @@ export function todoTitle(todo) {
return todo.title.split('\n')[0];
}
// Popover mit Textfeld sowie Auswahl für Datum und Uhrzeit. Enter speichert.
export function editTodo(store, todo, anchor) {
const popover = new Popover(anchor);
// Popover am Mauszeiger mit Textfeld sowie Auswahl für Datum und Uhrzeit. Enter speichert.
export function editTodo(store, todo) {
const popover = new Popover();
popover.addLabel(todo.id ? 'Todo bearbeiten' : 'Neues Todo', 'popover-title');
const text = popover.addEntry(todo.title ?? '', 'Was ist zu tun?');
@@ -63,7 +63,17 @@ export function todoMenu(store, todo) {
contextMenu([{label: 'Löschen', onSelect: () => store.removeTodo(todo.id)}]);
}
// Zeile mit Häkchen, Titel und Termin. Klick bearbeitet, Rechtsklick öffnet das Menü.
// Gilt überall, wo ein Todo steht: links bearbeiten, Mitte abhaken, rechts Menü.
export function todoClick(store, todo, mouseButton) {
if (mouseButton === 2)
store.toggleTodo(todo.id);
else if (mouseButton === 3)
todoMenu(store, todo);
else
editTodo(store, todo);
}
// Zeile mit Häkchen, Titel und Termin. Links bearbeitet, Mitte hakt ab, rechts öffnet das Menü.
export function todoRow(store, todo) {
const row = new St.BoxLayout({style_class: 'todo-row', x_expand: true});
@@ -75,12 +85,8 @@ export function todoRow(store, todo) {
check.connect('clicked', () => store.toggleTodo(todo.id));
row.add_child(check);
const title = textButton(todoTitle(todo), (b, mouseButton) => {
if (mouseButton === 3)
todoMenu(store, todo);
else
editTodo(store, todo, b);
}, todo.done ? 'todo-title done' : 'todo-title');
const title = textButton(todoTitle(todo), (_b, mouseButton) => todoClick(store, todo, mouseButton),
todo.done ? 'todo-title done' : 'todo-title');
if (todo.done)
title.child.clutter_text.set_markup(`<s>${escapeMarkup(todoTitle(todo))}</s>`);
row.add_child(title);
@@ -109,8 +115,7 @@ class TodoPanel extends St.BoxLayout {
this.connect('button-press-event', (_actor, event) => {
if (event.get_button() !== Clutter.BUTTON_SECONDARY)
return Clutter.EVENT_PROPAGATE;
const [x, y] = global.get_pointer();
contextMenu([{label: 'Erstellen', onSelect: () => editTodo(store, {}, {x, y})}]);
contextMenu([{label: 'Erstellen', onSelect: () => editTodo(store, {})}]);
return Clutter.EVENT_STOP;
});

View File

@@ -12,7 +12,7 @@ export function button(label, onClick, styleClass = 'desktop-button') {
return b;
}
// Button mit linksbündigem, gekürztem Text und optionalem Icon. onClick(button, mausTaste): 1 = links, 3 = rechts.
// Button mit linksbündigem, gekürztem Text und optionalem Icon. onClick(button, mausTaste): 1 = links, 2 = Mitte, 3 = rechts.
export function textButton(text, onClick, styleClass, iconName = null) {
const label = new St.Label({text, x_expand: true, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER});
label.clutter_text.ellipsize = Pango.EllipsizeMode.END;
@@ -28,7 +28,7 @@ export function textButton(text, onClick, styleClass, iconName = null) {
can_focus: true,
x_expand: true,
x_align: Clutter.ActorAlign.FILL,
button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO | St.ButtonMask.THREE,
});
b.connect('clicked', onClick);
b.label_actor = label;
@@ -77,13 +77,18 @@ export function multilineEntry(text, styleClass, scrollClass, hint = '') {
return [scroll, entry];
}
// Kleines Fenster neben einem Anker (Actor oder {x, y}). Hält Maus und Tastatur.
// Kleines Fenster am Mauszeiger oder neben einem Anker (Actor oder {x, y}). Hält Maus und Tastatur.
// Enter in einem Feld speichert, Klick daneben oder Esc bricht ab. Keine Buttons.
// Mehrere Popover stapeln sich, das oberste hält den Grab.
// Bei aktivem Grab laufen Events nur durch den Grab-Actor, daher hängt captured-event an der Box.
export class Popover {
// Ohne Anker öffnet das Popover am Mauszeiger.
// below: unter dem Anker statt daneben (für Auswahllisten unter einem Button)
constructor(anchor, styleClass = 'form', {below = false} = {}) {
constructor(anchor = null, styleClass = 'form', {below = false} = {}) {
if (!anchor) {
const [x, y] = global.get_pointer();
anchor = {x, y};
}
this._anchor = anchor;
this._below = below;
this.box = new St.BoxLayout({vertical: true, style_class: `desktop-popover ${styleClass}`, reactive: true});
@@ -214,8 +219,7 @@ export class Popover {
// Kontextmenü am Mauszeiger. items: [{label, onSelect, sensitive = true}]
export function contextMenu(items) {
const [x, y] = global.get_pointer();
const menu = new Popover({x, y}, 'menu');
const menu = new Popover(null, 'menu');
for (const item of items) {
const row = textButton(item.label, () => {
menu.close();