Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
680fa96f32 | ||
|
|
eb0a801921 | ||
|
|
76ecdac081 | ||
|
|
ea9ee6c0d7 | ||
|
|
d1f0b46fac | ||
|
|
82a297f395 |
Generated
+1
-1
@@ -1393,7 +1393,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ink-notes"
|
name = "ink-notes"
|
||||||
version = "0.18.0"
|
version = "0.20.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ab_glyph",
|
"ab_glyph",
|
||||||
"arboard",
|
"arboard",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ink-notes"
|
name = "ink-notes"
|
||||||
version = "0.18.0"
|
version = "0.20.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
|
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
|
||||||
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ Getestet wird vor allem unter Zorin OS 18 / Ubuntu 24.04 (Wayland und X11).
|
|||||||
- **Eigene Farben:** Jede Farbe in der Werkzeugleiste (Stifte, Textmarker, Text) lässt sich anpassen – gewählte Farbe erneut antippen, eine Vorgabe oder mit 🎨 einen freien Farbton wählen. Die Leiste merkt sich die Farben. Am Bildschirm erscheinen sie genauso wie im PDF- und Bildexport.
|
- **Eigene Farben:** Jede Farbe in der Werkzeugleiste (Stifte, Textmarker, Text) lässt sich anpassen – gewählte Farbe erneut antippen, eine Vorgabe oder mit 🎨 einen freien Farbton wählen. Die Leiste merkt sich die Farben. Am Bildschirm erscheinen sie genauso wie im PDF- und Bildexport.
|
||||||
- **Glättung in zwei Stufen:** Während des Schreibens glättet ein 1€-Filter das Zittern, die Linie erscheint sofort. Beim Absetzen wird der Strich nachbearbeitet: Ecken (Knicke über 65°) bleiben erhalten, der Druckverlauf wird geglättet, überflüssige Punkte entfallen, die Enden werden rund.
|
- **Glättung in zwei Stufen:** Während des Schreibens glättet ein 1€-Filter das Zittern, die Linie erscheint sofort. Beim Absetzen wird der Strich nachbearbeitet: Ecken (Knicke über 65°) bleiben erhalten, der Druckverlauf wird geglättet, überflüssige Punkte entfallen, die Enden werden rund.
|
||||||
- **Druckkurve** im ☰-Menü an den eigenen Stift anpassen (weicher/härter, Schwelle gegen Grundrauschen).
|
- **Druckkurve** im ☰-Menü an den eigenen Stift anpassen (weicher/härter, Schwelle gegen Grundrauschen).
|
||||||
- **Radierer** in drei Modi: ganze Striche, nur den überstrichenen Teil (Teilstrich) oder nur Textmarker. Stiftrückseite und Seitentaste des Stifts radieren ebenfalls.
|
- **Tasten am Stift** im ☰-Menü belegen: Radieren (Standard), Auswählen, Textmarker, Stift oder nichts – gilt, solange die Taste gedrückt ist. Jede Taste erscheint dort nach dem ersten Drücken.
|
||||||
|
- **Radierer** in drei Modi: ganze Striche, nur den überstrichenen Teil (Teilstrich) oder nur Textmarker. Die Stiftrückseite radiert ebenfalls.
|
||||||
- **Virtuelles Lineal** (`L`): verschieben, am Griff drehen (rastet bei 15° ein); Striche entlang einer Kante werden gerade.
|
- **Virtuelles Lineal** (`L`): verschieben, am Griff drehen (rastet bei 15° ein); Striche entlang einer Kante werden gerade.
|
||||||
|
|
||||||
### Formerkennung
|
### Formerkennung
|
||||||
@@ -53,7 +54,7 @@ Getestet wird vor allem unter Zorin OS 18 / Ubuntu 24.04 (Wayland und X11).
|
|||||||
|
|
||||||
### Notizbücher und Speichern
|
### Notizbücher und Speichern
|
||||||
- Mehrere **Notizbücher** anlegen, umbenennen, wechseln und löschen (gelöschte landen zunächst im Papierkorb und lassen sich wiederherstellen).
|
- Mehrere **Notizbücher** anlegen, umbenennen, wechseln und löschen (gelöschte landen zunächst im Papierkorb und lassen sich wiederherstellen).
|
||||||
- **Ordner** für Notizbücher, beliebig verschachtelt (z. B. 3. Semester › Mathe): anlegen, umbenennen, löschen (der Inhalt rückt eine Ebene nach oben) und Notizbücher mit 📂 verschieben. Neue Notizbücher landen im gerade geöffneten Ordner.
|
- **Ordner** für Notizbücher, beliebig verschachtelt (z. B. 3. Semester › Mathe): anlegen, umbenennen, löschen (der Inhalt rückt eine Ebene nach oben) und Notizbücher wie Ordner (samt Inhalt) mit 📂 verschieben. Neue Notizbücher landen im gerade geöffneten Ordner.
|
||||||
- **Automatisches Speichern** im Hintergrund: 1,5 Sekunden nach der letzten Änderung, bei ununterbrochenem Schreiben spätestens alle 10 Sekunden. Zeichnen wird dabei nicht unterbrochen.
|
- **Automatisches Speichern** im Hintergrund: 1,5 Sekunden nach der letzten Änderung, bei ununterbrochenem Schreiben spätestens alle 10 Sekunden. Zeichnen wird dabei nicht unterbrochen.
|
||||||
- Jede Datei wird erst vollständig in eine temporäre Datei geschrieben und dann ersetzt – ein Absturz beim Speichern hinterlässt keine halbe Datei.
|
- Jede Datei wird erst vollständig in eine temporäre Datei geschrieben und dann ersetzt – ein Absturz beim Speichern hinterlässt keine halbe Datei.
|
||||||
- **Beim Schließen** (Fenster oder `Strg + Q`) und beim Wechsel des Notizbuchs wird alles Ausstehende geschrieben, bevor es weitergeht. Schlägt das fehl, bleibt die App offen und bietet Wiederholen oder eine Notfall-Sicherung an.
|
- **Beim Schließen** (Fenster oder `Strg + Q`) und beim Wechsel des Notizbuchs wird alles Ausstehende geschrieben, bevor es weitergeht. Schlägt das fehl, bleibt die App offen und bietet Wiederholen oder eine Notfall-Sicherung an.
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ Dieses Dokument beschreibt, wo und wie ink-notes Notizbücher speichert, und die
|
|||||||
- Alle Dateien werden mit Rechten `0600`, Ordner mit `0700` angelegt.
|
- Alle Dateien werden mit Rechten `0600`, Ordner mit `0700` angelegt.
|
||||||
|
|
||||||
### Ordner (`library.json`)
|
### Ordner (`library.json`)
|
||||||
Ordner wie „3. Semester › Mathe“ sind rein logisch: Die Notizbücher bleiben in ihren Verzeichnissen, `library.json` hält nur fest, welcher Ordner welches Notizbuch enthält. Umbenennen, Verschieben und Löschen von Ordnern ändern deshalb nie eine Notizbuch-Datei.
|
Ordner wie „3. Semester › Mathe“ sind rein logisch: Die Notizbücher bleiben in ihren Verzeichnissen, `library.json` hält nur fest, welcher Ordner welches Notizbuch enthält. Umbenennen, Verschieben (samt Inhalt) und Löschen von Ordnern ändern deshalb nie eine Notizbuch-Datei.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
Notizbuch-App für Stift, Touch und Maus unter Linux (getestet mit Zorin OS 18 / Ubuntu 24.04).
|
Notizbuch-App für Stift, Touch und Maus unter Linux (getestet mit Zorin OS 18 / Ubuntu 24.04).
|
||||||
|
|
||||||
### Highlights:
|
### Highlights:
|
||||||
|
- **Fehlerbehebung Stift**: Das erste Antippen mit dem Stift trifft jetzt – Knöpfe und Notizbücher reagieren sofort statt erst beim zweiten Tippen; Striche beginnen nicht mehr mit einer Linie von der vorherigen Stiftposition
|
||||||
|
- **Tasten am Stift belegen**: Im ☰-Menü unter Eingabe jede Stifttaste mit Radieren, Auswählen, Textmarker, Stift oder Nichts belegen (gilt, solange die Taste gedrückt ist); Tasten erscheinen nach dem ersten Drücken und leuchten beim Drücken grün
|
||||||
|
- **Ordner verschieben**: Ordner lassen sich jetzt wie Notizbücher mit 📂 samt Inhalt in andere Ordner oder auf die oberste Ebene verschieben
|
||||||
- **Ordner für Notizbücher**: Beliebig verschachtelte Ordner (z. B. 3. Semester › Mathe) im Dokumenten-Manager anlegen, umbenennen und löschen, Notizbücher mit 📂 verschieben; neue Notizbücher landen im geöffneten Ordner. Die Notizbuch-Dateien bleiben unverändert (Zuordnung in library.json)
|
- **Ordner für Notizbücher**: Beliebig verschachtelte Ordner (z. B. 3. Semester › Mathe) im Dokumenten-Manager anlegen, umbenennen und löschen, Notizbücher mit 📂 verschieben; neue Notizbücher landen im geöffneten Ordner. Die Notizbuch-Dateien bleiben unverändert (Zuordnung in library.json)
|
||||||
- **Einfügen per Finger**: Lange (½ s) mit dem Finger aufs Blatt drücken öffnet ein Menü „Einfügen“ – Screenshot, kopierter Text oder kopierte Objekte landen genau an dieser Stelle (wie in Rnote)
|
- **Einfügen per Finger**: Lange (½ s) mit dem Finger aufs Blatt drücken öffnet ein Menü „Einfügen“ – Screenshot, kopierter Text oder kopierte Objekte landen genau an dieser Stelle (wie in Rnote)
|
||||||
- **Eigene Farben**: Jede Farbe der Werkzeugleiste (Stifte, Textmarker, Text) lässt sich anpassen – erneut antippen, Vorgabe oder mit 🎨 frei wählen; die Leiste merkt sich die Farben. Farben am Bildschirm wie im PDF-/Bildexport (Schwarz ist schwarz, Textmarker mischt wie im PDF); Karos und Linien im PDF so hell wie am Bildschirm
|
- **Eigene Farben**: Jede Farbe der Werkzeugleiste (Stifte, Textmarker, Text) lässt sich anpassen – erneut antippen, Vorgabe oder mit 🎨 frei wählen; die Leiste merkt sich die Farben. Farben am Bildschirm wie im PDF-/Bildexport (Schwarz ist schwarz, Textmarker mischt wie im PDF); Karos und Linien im PDF so hell wie am Bildschirm
|
||||||
|
|||||||
@@ -236,6 +236,39 @@ impl Library {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verschiebt einen Ordner samt Inhalt nach `new_parent` (oberste Ebene: [`ROOT`]) und
|
||||||
|
/// gibt seinen neuen Pfad zurück. Nicht in sich selbst oder einen eigenen Unterordner.
|
||||||
|
pub fn move_folder(&mut self, path: &str, new_parent: &str) -> Result<String, String> {
|
||||||
|
if !self.folders.contains(path) {
|
||||||
|
return Err("Den Ordner gibt es nicht mehr".to_string());
|
||||||
|
}
|
||||||
|
if !new_parent.is_empty() && !self.folders.contains(new_parent) {
|
||||||
|
return Err("Den Zielordner gibt es nicht mehr".to_string());
|
||||||
|
}
|
||||||
|
if is_within(new_parent, path) {
|
||||||
|
return Err("Ein Ordner kann nicht in sich selbst verschoben werden".to_string());
|
||||||
|
}
|
||||||
|
let name = folder_name(path);
|
||||||
|
let new_path = join(new_parent, name);
|
||||||
|
if new_path == path {
|
||||||
|
return Ok(new_path);
|
||||||
|
}
|
||||||
|
if self.folders.contains(&new_path) {
|
||||||
|
return Err(format!("Im Zielordner gibt es schon einen Ordner „{name}“"));
|
||||||
|
}
|
||||||
|
self.move_contents(path, &new_path);
|
||||||
|
Ok(new_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mögliche Ziele für das Verschieben des Ordners `path`: alle Ordner außer ihm selbst und
|
||||||
|
/// seinen Unterordnern (die oberste Ebene ist immer ein Ziel).
|
||||||
|
pub fn folder_move_targets(&self, path: &str) -> Vec<&str> {
|
||||||
|
self.all_folders()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|f| !is_within(f, path))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Legt das Notizbuch in `folder` (oberste Ebene: [`ROOT`]).
|
/// Legt das Notizbuch in `folder` (oberste Ebene: [`ROOT`]).
|
||||||
pub fn move_notebook(&mut self, notebook_id: &str, folder: &str) -> Result<(), String> {
|
pub fn move_notebook(&mut self, notebook_id: &str, folder: &str) -> Result<(), String> {
|
||||||
if folder.is_empty() {
|
if folder.is_empty() {
|
||||||
@@ -338,6 +371,38 @@ mod tests {
|
|||||||
assert_eq!(lib.subfolders(ROOT), ["Mathe", "Physik"]);
|
assert_eq!(lib.subfolders(ROOT), ["Mathe", "Physik"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_move_folder() {
|
||||||
|
let mut lib = library();
|
||||||
|
lib.create_folder(ROOT, "Mathe-Sammlung").unwrap();
|
||||||
|
// Ordner samt Inhalt in einen anderen Ordner schieben
|
||||||
|
let new = lib
|
||||||
|
.move_folder("3. Semester/Mathe", "Mathe-Sammlung")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(new, "Mathe-Sammlung/Mathe");
|
||||||
|
assert_eq!(lib.folder_of("analysis"), "Mathe-Sammlung/Mathe");
|
||||||
|
assert!(!lib.folders.contains("3. Semester/Mathe"));
|
||||||
|
// Auf die oberste Ebene und zurück
|
||||||
|
assert_eq!(
|
||||||
|
lib.move_folder("Mathe-Sammlung/Mathe", ROOT).unwrap(),
|
||||||
|
"Mathe"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
lib.move_folder("Mathe", "3. Semester").unwrap(),
|
||||||
|
"3. Semester/Mathe"
|
||||||
|
);
|
||||||
|
// Nicht in sich selbst oder in einen eigenen Unterordner
|
||||||
|
assert!(lib.move_folder("3. Semester", "3. Semester").is_err());
|
||||||
|
assert!(lib.move_folder("3. Semester", "3. Semester/Mathe").is_err());
|
||||||
|
assert_eq!(lib.folder_move_targets("3. Semester"), ["Mathe-Sammlung"]);
|
||||||
|
// Namenskollision am Ziel
|
||||||
|
lib.create_folder("Mathe-Sammlung", "Physik").unwrap();
|
||||||
|
assert!(lib
|
||||||
|
.move_folder("3. Semester/Physik", "Mathe-Sammlung")
|
||||||
|
.is_err());
|
||||||
|
assert_eq!(lib.folder_of("uebung"), "3. Semester");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_move_notebook() {
|
fn test_move_notebook() {
|
||||||
let mut lib = library();
|
let mut lib = library();
|
||||||
|
|||||||
+123
-33
@@ -30,11 +30,39 @@ use ink_notes::platform::{self, DisplayBackend};
|
|||||||
use ink_notes::selection::{
|
use ink_notes::selection::{
|
||||||
render_selection_overlay, DragMode, SelectionHandleHit, SelectionState,
|
render_selection_overlay, DragMode, SelectionHandleHit, SelectionState,
|
||||||
};
|
};
|
||||||
use ink_notes::settings::{SidebarSide, UiSettings};
|
use ink_notes::settings::{
|
||||||
|
SidebarSide, StylusAction, StylusButton, UiSettings, MAX_STYLUS_BUTTONS,
|
||||||
|
};
|
||||||
use ink_notes::storage::{self, NotebookManifest};
|
use ink_notes::storage::{self, NotebookManifest};
|
||||||
use ink_notes::ui::{self, ActiveTool, UiAction, UiState};
|
use ink_notes::ui::{self, ActiveTool, UiAction, UiState};
|
||||||
use ink_notes::updater::UpdaterController;
|
use ink_notes::updater::UpdaterController;
|
||||||
|
|
||||||
|
/// Wählt `tool` und stellt Farbe und Stärke des Live-Strichs passend ein.
|
||||||
|
fn activate_tool(tool: ActiveTool, ui_state: &mut UiState, canvas: &mut CanvasState) {
|
||||||
|
ui_state.active_tool = tool;
|
||||||
|
match tool {
|
||||||
|
ActiveTool::Pen | ActiveTool::Shape => {
|
||||||
|
canvas.set_stroke_color(ui_state.pen_color);
|
||||||
|
canvas.set_stroke_width_mm(ui_state.pen_stroke_width_mm);
|
||||||
|
}
|
||||||
|
ActiveTool::Highlighter => {
|
||||||
|
canvas.set_stroke_color(ui_state.highlighter_color);
|
||||||
|
canvas.set_stroke_width_mm(ui_state.highlighter_stroke_width_mm);
|
||||||
|
}
|
||||||
|
ActiveTool::Eraser | ActiveTool::Select | ActiveTool::Text => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kennung einer Stifttaste, die über Programmstarts hinweg gleich bleibt (die Tablet-
|
||||||
|
/// Schnittstelle liefert nur eine undurchsichtige, aber vergleichbare Kennung).
|
||||||
|
fn stylus_button_key(id: &octotablet::tool::ButtonID) -> u64 {
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
// DefaultHasher::new() verwendet feste Schlüssel: gleiche Taste, gleicher Wert
|
||||||
|
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
id.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
/// Liest Notizbücher und Ordner für den Dokumenten-Manager neu ein.
|
/// Liest Notizbücher und Ordner für den Dokumenten-Manager neu ein.
|
||||||
fn refresh_notebooks(base_dir: &Path, ui_state: &mut UiState) {
|
fn refresh_notebooks(base_dir: &Path, ui_state: &mut UiState) {
|
||||||
ui_state.available_notebooks = storage::list_notebooks(base_dir);
|
ui_state.available_notebooks = storage::list_notebooks(base_dir);
|
||||||
@@ -1107,6 +1135,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
ui_state.pressure_curve = ui_settings.pressure_curve;
|
ui_state.pressure_curve = ui_settings.pressure_curve;
|
||||||
ui_state.continuous_scroll = ui_settings.continuous_scroll;
|
ui_state.continuous_scroll = ui_settings.continuous_scroll;
|
||||||
ui_state.pen_presets = ui_settings.pen_presets;
|
ui_state.pen_presets = ui_settings.pen_presets;
|
||||||
|
ui_state.stylus_buttons = ui_settings.stylus_buttons.clone();
|
||||||
ui_state.select_pen(ui_settings.active_pen);
|
ui_state.select_pen(ui_settings.active_pen);
|
||||||
ui_state.highlighter_colors = ui_settings.highlighter_colors;
|
ui_state.highlighter_colors = ui_settings.highlighter_colors;
|
||||||
ui_state.select_highlighter_color(ui_settings.active_highlighter);
|
ui_state.select_highlighter_color(ui_settings.active_highlighter);
|
||||||
@@ -1186,7 +1215,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let mut is_stylus_selecting = false;
|
let mut is_stylus_selecting = false;
|
||||||
let mut is_stylus_in_proximity = false;
|
let mut is_stylus_in_proximity = false;
|
||||||
let mut stylus_tool_id: Option<octotablet::tool::ID> = None;
|
let mut stylus_tool_id: Option<octotablet::tool::ID> = None;
|
||||||
let mut stylus_barrel_pressed = false;
|
// Gedrückte Stifttaste (Kennung, Belegung) und das Werkzeug, das während eines Strichs mit
|
||||||
|
// einer Werkzeug-Taste (Auswählen, Textmarker, Stift) vertreten wird
|
||||||
|
let mut stylus_held: Option<(u64, StylusAction)> = None;
|
||||||
|
let mut tool_before_button: Option<ActiveTool> = None;
|
||||||
let mut stylus_screen_pos = (0.0_f32, 0.0_f32);
|
let mut stylus_screen_pos = (0.0_f32, 0.0_f32);
|
||||||
// Stift hat auf der UI aufgesetzt: alle Ereignisse bis zum Abheben gehen an egui
|
// Stift hat auf der UI aufgesetzt: alle Ereignisse bis zum Abheben gehen an egui
|
||||||
let mut stylus_ui_capture = false;
|
let mut stylus_ui_capture = false;
|
||||||
@@ -1307,6 +1339,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if let Ok(tablet_events) = mgr.pump() {
|
if let Ok(tablet_events) = mgr.pump() {
|
||||||
let scale = window.scale_factor() as f32;
|
let scale = window.scale_factor() as f32;
|
||||||
let now = start_time.elapsed().as_secs_f64();
|
let now = start_time.elapsed().as_secs_f64();
|
||||||
|
// Aufsetzen erst mit der Position desselben Frames verarbeiten
|
||||||
|
let tablet_events = ink_notes::platform::down_after_pose(
|
||||||
|
tablet_events,
|
||||||
|
|e| matches!(e, octotablet::events::Event::Tool { event: octotablet::events::ToolEvent::Down, .. }),
|
||||||
|
|e| matches!(e, octotablet::events::Event::Tool { event: octotablet::events::ToolEvent::Pose(_), .. }),
|
||||||
|
);
|
||||||
|
|
||||||
for t_event in tablet_events {
|
for t_event in tablet_events {
|
||||||
if let octotablet::events::Event::Tool { tool, event } = t_event {
|
if let octotablet::events::Event::Tool { tool, event } = t_event {
|
||||||
@@ -1357,23 +1395,45 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state);
|
selection_release(false, &mut notebook, &mut manifest, &mut autosave, &mut selection_state);
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
if let Some(tool) = tool_before_button.take() {
|
||||||
|
activate_tool(tool, &mut ui_state, &mut canvas);
|
||||||
|
}
|
||||||
|
stylus_held = None;
|
||||||
|
ui_state.pressed_stylus_button = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
octotablet::events::ToolEvent::Button { pressed, .. } => {
|
octotablet::events::ToolEvent::Button { button_id, pressed } => {
|
||||||
stylus_barrel_pressed = pressed;
|
// Tasten werden beim ersten Drücken gelernt (Standard: Radieren)
|
||||||
if pressed && is_stylus_drawing {
|
let key = stylus_button_key(&button_id);
|
||||||
is_stylus_drawing = false;
|
let mut index = ui_state.stylus_buttons.iter().position(|b| b.id == key);
|
||||||
is_drawing = false;
|
if index.is_none() && pressed && ui_state.stylus_buttons.len() < MAX_STYLUS_BUTTONS {
|
||||||
canvas.cancel_stroke();
|
ui_state.stylus_buttons.push(StylusButton { id: key, action: StylusAction::Erase });
|
||||||
is_erasing = true;
|
index = Some(ui_state.stylus_buttons.len() - 1);
|
||||||
window.request_redraw();
|
ui_settings.stylus_buttons = ui_state.stylus_buttons.clone();
|
||||||
} else if !pressed
|
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
||||||
&& is_erasing
|
|
||||||
&& !tool_is_hardware_eraser
|
|
||||||
&& ui_state.active_tool != ActiveTool::Eraser
|
|
||||||
{
|
|
||||||
is_erasing = false;
|
|
||||||
}
|
}
|
||||||
|
let action = index.map_or(StylusAction::Erase, |i| ui_state.stylus_buttons[i].action);
|
||||||
|
if pressed {
|
||||||
|
stylus_held = Some((key, action));
|
||||||
|
ui_state.pressed_stylus_button = index;
|
||||||
|
if action == StylusAction::Erase && is_stylus_drawing {
|
||||||
|
is_stylus_drawing = false;
|
||||||
|
is_drawing = false;
|
||||||
|
canvas.cancel_stroke();
|
||||||
|
is_erasing = true;
|
||||||
|
}
|
||||||
|
} else if stylus_held.is_some_and(|(held, _)| held == key) {
|
||||||
|
stylus_held = None;
|
||||||
|
ui_state.pressed_stylus_button = None;
|
||||||
|
if action == StylusAction::Erase
|
||||||
|
&& is_erasing
|
||||||
|
&& !tool_is_hardware_eraser
|
||||||
|
&& ui_state.active_tool != ActiveTool::Eraser
|
||||||
|
{
|
||||||
|
is_erasing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
octotablet::events::ToolEvent::Pose(pose) => {
|
octotablet::events::ToolEvent::Pose(pose) => {
|
||||||
is_stylus_in_proximity = true;
|
is_stylus_in_proximity = true;
|
||||||
@@ -1471,8 +1531,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
focus_page_at!((sx, sy));
|
focus_page_at!((sx, sy));
|
||||||
|
// Werkzeug-Taste gedrückt: für diesen Strich dieses Werkzeug
|
||||||
|
let button_action = stylus_held.map(|(_, action)| action);
|
||||||
|
let button_tool = match button_action {
|
||||||
|
Some(StylusAction::Select) => Some(ActiveTool::Select),
|
||||||
|
Some(StylusAction::Highlighter) => Some(ActiveTool::Highlighter),
|
||||||
|
Some(StylusAction::Pen) => Some(ActiveTool::Pen),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(tool) = button_tool
|
||||||
|
.filter(|&t| !tool_is_hardware_eraser && t != ui_state.active_tool)
|
||||||
|
{
|
||||||
|
tool_before_button = Some(ui_state.active_tool);
|
||||||
|
activate_tool(tool, &mut ui_state, &mut canvas);
|
||||||
|
}
|
||||||
let should_erase = tool_is_hardware_eraser
|
let should_erase = tool_is_hardware_eraser
|
||||||
|| stylus_barrel_pressed
|
|| button_action == Some(StylusAction::Erase)
|
||||||
|| ui_state.active_tool == ActiveTool::Eraser;
|
|| ui_state.active_tool == ActiveTool::Eraser;
|
||||||
|
|
||||||
if !should_erase && ui_state.active_tool == ActiveTool::Text {
|
if !should_erase && ui_state.active_tool == ActiveTool::Text {
|
||||||
@@ -1550,6 +1624,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let restore_tool = tool_before_button.take();
|
||||||
|
|
||||||
if is_stylus_drawing {
|
if is_stylus_drawing {
|
||||||
is_stylus_drawing = false;
|
is_stylus_drawing = false;
|
||||||
@@ -1584,6 +1659,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
snapped_shape,
|
snapped_shape,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Nach einem Strich mit Werkzeug-Taste wieder das gewählte Werkzeug
|
||||||
|
if let Some(tool) = restore_tool {
|
||||||
|
activate_tool(tool, &mut ui_state, &mut canvas);
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -2731,22 +2811,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
for action in actions {
|
for action in actions {
|
||||||
match action {
|
match action {
|
||||||
UiAction::SetTool(tool) => {
|
UiAction::SetTool(tool) => {
|
||||||
ui_state.active_tool = tool;
|
activate_tool(tool, &mut ui_state, &mut canvas);
|
||||||
match tool {
|
|
||||||
ActiveTool::Pen => {
|
|
||||||
canvas.set_stroke_color(ui_state.pen_color);
|
|
||||||
canvas.set_stroke_width_mm(ui_state.pen_stroke_width_mm);
|
|
||||||
}
|
|
||||||
ActiveTool::Highlighter => {
|
|
||||||
canvas.set_stroke_color(ui_state.highlighter_color);
|
|
||||||
canvas.set_stroke_width_mm(ui_state.highlighter_stroke_width_mm);
|
|
||||||
}
|
|
||||||
ActiveTool::Shape => {
|
|
||||||
canvas.set_stroke_color(ui_state.pen_color);
|
|
||||||
canvas.set_stroke_width_mm(ui_state.pen_stroke_width_mm);
|
|
||||||
}
|
|
||||||
ActiveTool::Eraser | ActiveTool::Select | ActiveTool::Text => {}
|
|
||||||
}
|
|
||||||
update_window_title(&window, ¬ebook, &manifest, tool, autosave.status());
|
update_window_title(&window, ¬ebook, &manifest, tool, autosave.status());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -3045,6 +3110,27 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
UiAction::MoveFolder { path, new_parent } => {
|
||||||
|
if let Some(new_path) = change_library(&storage_base_dir, &mut ui_state, |library| {
|
||||||
|
library.move_folder(&path, &new_parent)
|
||||||
|
}) {
|
||||||
|
// Der gezeigte Ordner kann darin liegen
|
||||||
|
let shown = &ui_state.notebook_folder;
|
||||||
|
if shown == &path || shown.starts_with(&format!("{path}/")) {
|
||||||
|
ui_state.notebook_folder = format!("{new_path}{}", &shown[path.len()..]);
|
||||||
|
}
|
||||||
|
let target = if new_parent.is_empty() {
|
||||||
|
"die oberste Ebene".to_string()
|
||||||
|
} else {
|
||||||
|
format!("„{}“", ink_notes::library::folder_name(&new_parent))
|
||||||
|
};
|
||||||
|
ui_state.set_export_message(format!(
|
||||||
|
"Ordner „{}“ verschoben in {target}",
|
||||||
|
ink_notes::library::folder_name(&path)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
UiAction::MoveNotebook { id, folder } => {
|
UiAction::MoveNotebook { id, folder } => {
|
||||||
if change_library(&storage_base_dir, &mut ui_state, |library| {
|
if change_library(&storage_base_dir, &mut ui_state, |library| {
|
||||||
library.move_notebook(&id, &folder)
|
library.move_notebook(&id, &folder)
|
||||||
@@ -3412,6 +3498,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
UiAction::SaveStylusButtons => {
|
||||||
|
ui_settings.stylus_buttons = ui_state.stylus_buttons.clone();
|
||||||
|
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
||||||
|
}
|
||||||
UiAction::SavePressureCurve(curve) => {
|
UiAction::SavePressureCurve(curve) => {
|
||||||
ui_settings.pressure_curve = curve;
|
ui_settings.pressure_curve = curve;
|
||||||
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
||||||
|
|||||||
@@ -58,10 +58,62 @@ pub fn display_backend_from_env() -> DisplayBackend {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bringt die Ereignisse eines Tablet-Frames in eine brauchbare Reihenfolge: octotablet meldet
|
||||||
|
/// das Aufsetzen des Stifts (`Down`) vor der Position desselben Frames (`Pose`). Bis dahin
|
||||||
|
/// gälte noch die alte Position – kam der Stift schnell heran (Annähern und Aufsetzen im selben
|
||||||
|
/// Frame), läge sie ganz woanders: Ein Tippen auf einen Knopf ginge daneben, ein Strich begänne
|
||||||
|
/// mit einer Linie von der alten Stelle. Hier folgt jedes `Down` auf die nächste `Pose`, spätestens
|
||||||
|
/// vor dem nächsten anderen Ereignis.
|
||||||
|
pub fn down_after_pose<T>(
|
||||||
|
events: impl IntoIterator<Item = T>,
|
||||||
|
is_down: impl Fn(&T) -> bool,
|
||||||
|
is_pose: impl Fn(&T) -> bool,
|
||||||
|
) -> Vec<T> {
|
||||||
|
let mut ordered = Vec::new();
|
||||||
|
let mut deferred = None;
|
||||||
|
for event in events {
|
||||||
|
if is_down(&event) {
|
||||||
|
if let Some(down) = deferred.replace(event) {
|
||||||
|
ordered.push(down);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let pose = is_pose(&event);
|
||||||
|
if !pose {
|
||||||
|
ordered.extend(deferred.take());
|
||||||
|
}
|
||||||
|
ordered.push(event);
|
||||||
|
if pose {
|
||||||
|
ordered.extend(deferred.take());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ordered.extend(deferred);
|
||||||
|
ordered
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_down_follows_pose_of_its_frame() {
|
||||||
|
let order = |events: &str| {
|
||||||
|
down_after_pose(events.chars(), |c| *c == 'D', |c| *c == 'P')
|
||||||
|
.into_iter()
|
||||||
|
.collect::<String>()
|
||||||
|
};
|
||||||
|
// In, Down, Pose, Button, Frame -> Down erst mit der neuen Position
|
||||||
|
assert_eq!(order("IDPBF"), "IPDBF");
|
||||||
|
// Down ohne Pose im Frame: vor dem Frame-Ende
|
||||||
|
assert_eq!(order("DF"), "DF");
|
||||||
|
assert_eq!(order("DBF"), "DBF");
|
||||||
|
// Am Ende ohne weiteres Ereignis
|
||||||
|
assert_eq!(order("PD"), "PD");
|
||||||
|
// Andere Ereignisse bleiben unverändert
|
||||||
|
assert_eq!(order("PFPFUF"), "PFPFUF");
|
||||||
|
assert_eq!(order("IDPFPFUF"), "IPDFPFUF");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_choose_display_backend() {
|
fn test_choose_display_backend() {
|
||||||
// WSL mit X11-Display: X11 (Wayland unter WSLg bricht ab)
|
// WSL mit X11-Display: X11 (Wayland unter WSLg bricht ab)
|
||||||
|
|||||||
@@ -150,6 +150,53 @@ fn sanitized_color(color: [f32; 4], fallback: [f32; 4]) -> [f32; 4] {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Was eine Taste am Stift bewirkt, solange sie gedrückt ist.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
pub enum StylusAction {
|
||||||
|
/// Radieren (Standard, wie bisher)
|
||||||
|
#[default]
|
||||||
|
Erase,
|
||||||
|
/// Auswählen und verschieben
|
||||||
|
Select,
|
||||||
|
/// Mit dem Textmarker schreiben
|
||||||
|
Highlighter,
|
||||||
|
/// Mit dem gewählten Stift schreiben (z. B. während der Textmarker aktiv ist)
|
||||||
|
Pen,
|
||||||
|
/// Taste nicht verwenden
|
||||||
|
Nothing,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StylusAction {
|
||||||
|
pub const ALL: [StylusAction; 5] = [
|
||||||
|
StylusAction::Erase,
|
||||||
|
StylusAction::Select,
|
||||||
|
StylusAction::Highlighter,
|
||||||
|
StylusAction::Pen,
|
||||||
|
StylusAction::Nothing,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
StylusAction::Erase => "Radieren",
|
||||||
|
StylusAction::Select => "Auswählen",
|
||||||
|
StylusAction::Highlighter => "Textmarker",
|
||||||
|
StylusAction::Pen => "Stift",
|
||||||
|
StylusAction::Nothing => "Nichts",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eine Taste am Stift: Kennung (aus der Tablet-Schnittstelle, beim ersten Drücken gelernt)
|
||||||
|
/// und ihre Belegung.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct StylusButton {
|
||||||
|
pub id: u64,
|
||||||
|
pub action: StylusAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Höchstens so viele Stifttasten werden gemerkt.
|
||||||
|
pub const MAX_STYLUS_BUTTONS: usize = 4;
|
||||||
|
|
||||||
/// Benutzereinstellungen der Oberfläche.
|
/// Benutzereinstellungen der Oberfläche.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -173,6 +220,8 @@ pub struct UiSettings {
|
|||||||
pub active_highlighter: usize,
|
pub active_highlighter: usize,
|
||||||
pub text_colors: [[f32; 4]; 5],
|
pub text_colors: [[f32; 4]; 5],
|
||||||
pub active_text_color: usize,
|
pub active_text_color: usize,
|
||||||
|
/// Tasten am Stift in der Reihenfolge, in der sie zuerst gedrückt wurden
|
||||||
|
pub stylus_buttons: Vec<StylusButton>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UiSettings {
|
impl Default for UiSettings {
|
||||||
@@ -190,6 +239,7 @@ impl Default for UiSettings {
|
|||||||
active_highlighter: 0,
|
active_highlighter: 0,
|
||||||
text_colors: DEFAULT_TEXT_COLORS,
|
text_colors: DEFAULT_TEXT_COLORS,
|
||||||
active_text_color: 0,
|
active_text_color: 0,
|
||||||
|
stylus_buttons: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,6 +287,7 @@ impl UiSettings {
|
|||||||
settings.active_text_color = settings
|
settings.active_text_color = settings
|
||||||
.active_text_color
|
.active_text_color
|
||||||
.min(settings.text_colors.len() - 1);
|
.min(settings.text_colors.len() - 1);
|
||||||
|
settings.stylus_buttons.truncate(MAX_STYLUS_BUTTONS);
|
||||||
settings
|
settings
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,6 +328,16 @@ mod tests {
|
|||||||
active_highlighter: 3,
|
active_highlighter: 3,
|
||||||
text_colors: [[0.3, 0.1, 0.6, 1.0]; 5],
|
text_colors: [[0.3, 0.1, 0.6, 1.0]; 5],
|
||||||
active_text_color: 4,
|
active_text_color: 4,
|
||||||
|
stylus_buttons: vec![
|
||||||
|
StylusButton {
|
||||||
|
id: 17,
|
||||||
|
action: StylusAction::Select,
|
||||||
|
},
|
||||||
|
StylusButton {
|
||||||
|
id: 42,
|
||||||
|
action: StylusAction::Nothing,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
settings.save_to(&path).unwrap();
|
settings.save_to(&path).unwrap();
|
||||||
assert_eq!(UiSettings::load_from(&path), settings);
|
assert_eq!(UiSettings::load_from(&path), settings);
|
||||||
|
|||||||
@@ -171,6 +171,14 @@ impl LongPress {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Was im Dokumenten-Manager gerade verschoben wird.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum MoveSubject {
|
||||||
|
Notebook(String),
|
||||||
|
/// Pfad des Ordners
|
||||||
|
Folder(String),
|
||||||
|
}
|
||||||
|
|
||||||
/// Kontextmenü nach langem Drücken auf das Blatt.
|
/// Kontextmenü nach langem Drücken auf das Blatt.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct ContextMenu {
|
pub struct ContextMenu {
|
||||||
@@ -248,6 +256,11 @@ pub enum UiAction {
|
|||||||
id: String,
|
id: String,
|
||||||
folder: String,
|
folder: String,
|
||||||
},
|
},
|
||||||
|
/// Ordner samt Inhalt nach `new_parent` verschieben
|
||||||
|
MoveFolder {
|
||||||
|
path: String,
|
||||||
|
new_parent: String,
|
||||||
|
},
|
||||||
RenameNotebook {
|
RenameNotebook {
|
||||||
id: String,
|
id: String,
|
||||||
new_title: String,
|
new_title: String,
|
||||||
@@ -275,6 +288,8 @@ pub enum UiAction {
|
|||||||
theme: PaperTheme,
|
theme: PaperTheme,
|
||||||
adapt_ink: bool,
|
adapt_ink: bool,
|
||||||
},
|
},
|
||||||
|
/// Belegung der Stifttasten speichern (gilt sofort über `UiState::stylus_buttons`)
|
||||||
|
SaveStylusButtons,
|
||||||
/// Druckkurve speichern (gilt während des Ziehens bereits über `UiState::pressure_curve`)
|
/// Druckkurve speichern (gilt während des Ziehens bereits über `UiState::pressure_curve`)
|
||||||
SavePressureCurve(PressureCurve),
|
SavePressureCurve(PressureCurve),
|
||||||
/// Seiten durchgehend untereinander statt einzeln anzeigen (wird gespeichert)
|
/// Seiten durchgehend untereinander statt einzeln anzeigen (wird gespeichert)
|
||||||
@@ -331,8 +346,8 @@ pub struct UiState {
|
|||||||
/// Ordner, der gerade umbenannt wird, bzw. dessen Löschen bestätigt werden soll
|
/// Ordner, der gerade umbenannt wird, bzw. dessen Löschen bestätigt werden soll
|
||||||
pub renaming_folder: Option<String>,
|
pub renaming_folder: Option<String>,
|
||||||
pub confirm_delete_folder: Option<String>,
|
pub confirm_delete_folder: Option<String>,
|
||||||
/// Notizbuch, für das gerade ein Zielordner gewählt wird
|
/// Notizbuch oder Ordner, für das gerade ein Zielordner gewählt wird
|
||||||
pub moving_notebook_id: Option<String>,
|
pub moving: Option<MoveSubject>,
|
||||||
pub renaming_title_input: String,
|
pub renaming_title_input: String,
|
||||||
pub document_modal_rect: Option<egui::Rect>,
|
pub document_modal_rect: Option<egui::Rect>,
|
||||||
/// Gitea Release- & Update-Manager
|
/// Gitea Release- & Update-Manager
|
||||||
@@ -363,6 +378,9 @@ pub struct UiState {
|
|||||||
pub show_color_picker: bool,
|
pub show_color_picker: bool,
|
||||||
/// Kontextmenü nach langem Drücken (Einfügen an dieser Stelle)
|
/// Kontextmenü nach langem Drücken (Einfügen an dieser Stelle)
|
||||||
pub context_menu: Option<ContextMenu>,
|
pub context_menu: Option<ContextMenu>,
|
||||||
|
/// Tasten am Stift und ihre Belegung; die gerade gedrückte (Index) wird hervorgehoben
|
||||||
|
pub stylus_buttons: Vec<crate::settings::StylusButton>,
|
||||||
|
pub pressed_stylus_button: Option<usize>,
|
||||||
/// Rückfrage „Seite löschen?“ im ☰-Menü ist offen
|
/// Rückfrage „Seite löschen?“ im ☰-Menü ist offen
|
||||||
pub confirm_delete_page: bool,
|
pub confirm_delete_page: bool,
|
||||||
/// Seitenzahl der zuletzt gelöschten Seite, solange sie wiederhergestellt werden kann
|
/// Seitenzahl der zuletzt gelöschten Seite, solange sie wiederhergestellt werden kann
|
||||||
@@ -439,7 +457,7 @@ impl Default for UiState {
|
|||||||
notebook_folder: String::new(),
|
notebook_folder: String::new(),
|
||||||
renaming_folder: None,
|
renaming_folder: None,
|
||||||
confirm_delete_folder: None,
|
confirm_delete_folder: None,
|
||||||
moving_notebook_id: None,
|
moving: None,
|
||||||
renaming_title_input: String::new(),
|
renaming_title_input: String::new(),
|
||||||
document_modal_rect: None,
|
document_modal_rect: None,
|
||||||
is_update_dialog_open: false,
|
is_update_dialog_open: false,
|
||||||
@@ -458,6 +476,8 @@ impl Default for UiState {
|
|||||||
is_text_color_menu_open: false,
|
is_text_color_menu_open: false,
|
||||||
show_color_picker: false,
|
show_color_picker: false,
|
||||||
context_menu: None,
|
context_menu: None,
|
||||||
|
stylus_buttons: Vec::new(),
|
||||||
|
pressed_stylus_button: None,
|
||||||
confirm_delete_page: false,
|
confirm_delete_page: false,
|
||||||
restorable_page: None,
|
restorable_page: None,
|
||||||
shutdown_error: None,
|
shutdown_error: None,
|
||||||
@@ -1622,6 +1642,70 @@ fn pattern_color_button(
|
|||||||
/// Schieberegler für die Druckkurve mit Vorschau der Kennlinie. Änderungen gelten sofort
|
/// Schieberegler für die Druckkurve mit Vorschau der Kennlinie. Änderungen gelten sofort
|
||||||
/// (`state.pressure_curve`); zurückgegeben wird die Kurve, sobald sie gespeichert werden soll
|
/// (`state.pressure_curve`); zurückgegeben wird die Kurve, sobald sie gespeichert werden soll
|
||||||
/// (Regler losgelassen oder per Tastatur geändert).
|
/// (Regler losgelassen oder per Tastatur geändert).
|
||||||
|
/// Belegung der Tasten am Stift. Tasten erscheinen, sobald sie einmal gedrückt wurden; die
|
||||||
|
/// gerade gedrückte ist markiert. `true`, wenn sich etwas geändert hat.
|
||||||
|
fn stylus_buttons_editor(ui: &mut egui::Ui, state: &mut UiState) -> bool {
|
||||||
|
use crate::settings::StylusAction;
|
||||||
|
let mut changed = false;
|
||||||
|
ui.add_space(4.0);
|
||||||
|
ui.label(egui::RichText::new("Tasten am Stift").color(egui::Color32::from_gray(200)));
|
||||||
|
if state.stylus_buttons.is_empty() {
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(
|
||||||
|
"Drücke eine Taste am Stift (über dem Bildschirm) – sie erscheint dann hier. \
|
||||||
|
Bis dahin radieren alle Tasten.",
|
||||||
|
)
|
||||||
|
.size(11.0)
|
||||||
|
.color(egui::Color32::from_gray(150)),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut remove = None;
|
||||||
|
for i in 0..state.stylus_buttons.len() {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let pressed = state.pressed_stylus_button == Some(i);
|
||||||
|
let label = egui::RichText::new(format!("Taste {}", i + 1));
|
||||||
|
ui.label(if pressed {
|
||||||
|
label.strong().color(egui::Color32::from_rgb(46, 204, 113))
|
||||||
|
} else {
|
||||||
|
label
|
||||||
|
})
|
||||||
|
.on_hover_text("Leuchtet grün, solange die Taste gedrückt ist");
|
||||||
|
let current = state.stylus_buttons[i].action;
|
||||||
|
egui::ComboBox::from_id_source(("stylus_button", i))
|
||||||
|
.selected_text(current.label())
|
||||||
|
.width(130.0)
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for action in StylusAction::ALL {
|
||||||
|
if ui
|
||||||
|
.selectable_label(action == current, action.label())
|
||||||
|
.clicked()
|
||||||
|
&& action != current
|
||||||
|
{
|
||||||
|
state.stylus_buttons[i].action = action;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if ui
|
||||||
|
.small_button("🗙")
|
||||||
|
.on_hover_text(
|
||||||
|
"Vergessen (erscheint beim nächsten Drücken wieder, dann mit Radieren)",
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
remove = Some(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(i) = remove {
|
||||||
|
state.stylus_buttons.remove(i);
|
||||||
|
state.pressed_stylus_button = None;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
fn pressure_curve_editor(ui: &mut egui::Ui, state: &mut UiState) -> Option<PressureCurve> {
|
fn pressure_curve_editor(ui: &mut egui::Ui, state: &mut UiState) -> Option<PressureCurve> {
|
||||||
let mut save = false;
|
let mut save = false;
|
||||||
let mut track = |response: egui::Response| {
|
let mut track = |response: egui::Response| {
|
||||||
@@ -1819,6 +1903,13 @@ fn folder_row(ui: &mut egui::Ui, state: &mut UiState, path: &str, actions: &mut
|
|||||||
{
|
{
|
||||||
state.confirm_delete_folder = Some(path.to_string());
|
state.confirm_delete_folder = Some(path.to_string());
|
||||||
}
|
}
|
||||||
|
if ui
|
||||||
|
.button(egui::RichText::new("📂").size(16.0))
|
||||||
|
.on_hover_text("Ordner samt Inhalt in einen anderen Ordner verschieben")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
state.moving = Some(MoveSubject::Folder(path.to_string()));
|
||||||
|
}
|
||||||
if ui
|
if ui
|
||||||
.button(egui::RichText::new("✏").size(16.0))
|
.button(egui::RichText::new("✏").size(16.0))
|
||||||
.on_hover_text("Ordner umbenennen")
|
.on_hover_text("Ordner umbenennen")
|
||||||
@@ -1860,24 +1951,35 @@ fn folder_row(ui: &mut egui::Ui, state: &mut UiState, path: &str, actions: &mut
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Zielordner für das Verschieben eines Notizbuchs: oberste Ebene und alle Ordner als Baum.
|
/// Zielordner für das Verschieben eines Notizbuchs oder Ordners: oberste Ebene und alle
|
||||||
|
/// Ordner als Baum (ein Ordner nicht in sich selbst oder seine Unterordner).
|
||||||
fn move_target_list(
|
fn move_target_list(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
state: &mut UiState,
|
state: &mut UiState,
|
||||||
notebook_id: &str,
|
subject: &MoveSubject,
|
||||||
actions: &mut Vec<UiAction>,
|
actions: &mut Vec<UiAction>,
|
||||||
) {
|
) {
|
||||||
let title = state
|
let (title, current, folders) = match subject {
|
||||||
.available_notebooks
|
MoveSubject::Notebook(id) => (
|
||||||
.iter()
|
state
|
||||||
.find(|s| s.id == notebook_id)
|
.available_notebooks
|
||||||
.map_or(notebook_id, |s| s.title.as_str())
|
.iter()
|
||||||
.to_string();
|
.find(|s| &s.id == id)
|
||||||
let current = state.library.folder_of(notebook_id).to_string();
|
.map_or(id.as_str(), |s| s.title.as_str())
|
||||||
|
.to_string(),
|
||||||
|
state.library.folder_of(id).to_string(),
|
||||||
|
state.library.all_folders(),
|
||||||
|
),
|
||||||
|
MoveSubject::Folder(path) => (
|
||||||
|
format!("📁 {}", crate::library::folder_name(path)),
|
||||||
|
crate::library::parent_folder(path).to_string(),
|
||||||
|
state.library.folder_move_targets(path),
|
||||||
|
),
|
||||||
|
};
|
||||||
ui.label(egui::RichText::new(format!("„{title}“ verschieben nach:")).strong());
|
ui.label(egui::RichText::new(format!("„{title}“ verschieben nach:")).strong());
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
let mut targets = vec![(String::new(), "📚 Oberste Ebene".to_string(), 0)];
|
let mut targets = vec![(String::new(), "📚 Oberste Ebene".to_string(), 0)];
|
||||||
for path in state.library.all_folders() {
|
for path in folders {
|
||||||
let depth = path.matches('/').count() + 1;
|
let depth = path.matches('/').count() + 1;
|
||||||
targets.push((
|
targets.push((
|
||||||
path.to_string(),
|
path.to_string(),
|
||||||
@@ -1897,18 +1999,24 @@ fn move_target_list(
|
|||||||
.on_disabled_hover_text("Liegt bereits hier")
|
.on_disabled_hover_text("Liegt bereits hier")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
actions.push(UiAction::MoveNotebook {
|
actions.push(match subject {
|
||||||
id: notebook_id.to_string(),
|
MoveSubject::Notebook(id) => UiAction::MoveNotebook {
|
||||||
folder: path.clone(),
|
id: id.clone(),
|
||||||
|
folder: path.clone(),
|
||||||
|
},
|
||||||
|
MoveSubject::Folder(folder) => UiAction::MoveFolder {
|
||||||
|
path: folder.clone(),
|
||||||
|
new_parent: path.clone(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
state.moving_notebook_id = None;
|
state.moving = None;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
if ui.button("Abbrechen").clicked() {
|
if ui.button("Abbrechen").clicked() {
|
||||||
state.moving_notebook_id = None;
|
state.moving = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2537,7 +2645,7 @@ pub fn render_ui(
|
|||||||
.to_string();
|
.to_string();
|
||||||
state.renaming_folder = None;
|
state.renaming_folder = None;
|
||||||
state.confirm_delete_folder = None;
|
state.confirm_delete_folder = None;
|
||||||
state.moving_notebook_id = None;
|
state.moving = None;
|
||||||
actions.push(UiAction::RefreshNotebooks);
|
actions.push(UiAction::RefreshNotebooks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3523,6 +3631,9 @@ pub fn render_ui(
|
|||||||
if let Some(curve) = pressure_curve_editor(ui, state) {
|
if let Some(curve) = pressure_curve_editor(ui, state) {
|
||||||
actions.push(UiAction::SavePressureCurve(curve));
|
actions.push(UiAction::SavePressureCurve(curve));
|
||||||
}
|
}
|
||||||
|
if stylus_buttons_editor(ui, state) {
|
||||||
|
actions.push(UiAction::SaveStylusButtons);
|
||||||
|
}
|
||||||
|
|
||||||
section(ui, "WERKZEUGLEISTE");
|
section(ui, "WERKZEUGLEISTE");
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
@@ -3804,7 +3915,7 @@ pub fn render_ui(
|
|||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
|
|
||||||
// Zielordner für „Verschieben“ wählen
|
// Zielordner für „Verschieben“ wählen
|
||||||
if let Some(moving) = state.moving_notebook_id.clone() {
|
if let Some(moving) = state.moving.clone() {
|
||||||
move_target_list(ui, state, &moving, &mut actions);
|
move_target_list(ui, state, &moving, &mut actions);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3917,7 +4028,7 @@ pub fn render_ui(
|
|||||||
.on_hover_text("In einen anderen Ordner verschieben")
|
.on_hover_text("In einen anderen Ordner verschieben")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
state.moving_notebook_id = Some(summary.id.clone());
|
state.moving = Some(MoveSubject::Notebook(summary.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.button(egui::RichText::new("✏").size(16.0))
|
if ui.button(egui::RichText::new("✏").size(16.0))
|
||||||
@@ -5258,6 +5369,43 @@ mod tests {
|
|||||||
actions
|
actions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stylus_buttons_in_menu() {
|
||||||
|
use crate::settings::{StylusAction, StylusButton};
|
||||||
|
let notebook = Notebook::new();
|
||||||
|
let mut state = UiState::new();
|
||||||
|
state.is_more_menu_open = true;
|
||||||
|
// Ohne gelernte Tasten: nur der Hinweis, keine Aktion
|
||||||
|
let ctx = egui::Context::default();
|
||||||
|
let updater = crate::updater::UpdaterController::new();
|
||||||
|
let shapes = ctx
|
||||||
|
.run(egui::RawInput::default(), |ctx| {
|
||||||
|
let _ = render_ui(ctx, &mut state, ¬ebook, AutosaveStatus::Saved, &updater);
|
||||||
|
})
|
||||||
|
.shapes
|
||||||
|
.len();
|
||||||
|
assert!(shapes > 0);
|
||||||
|
|
||||||
|
// Mit zwei Tasten: beide Zeilen, die gedrückte hervorgehoben, ohne Fehler
|
||||||
|
state.stylus_buttons = vec![
|
||||||
|
StylusButton {
|
||||||
|
id: 1,
|
||||||
|
action: StylusAction::Erase,
|
||||||
|
},
|
||||||
|
StylusButton {
|
||||||
|
id: 2,
|
||||||
|
action: StylusAction::Select,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
state.pressed_stylus_button = Some(1);
|
||||||
|
let output = ctx.run(egui::RawInput::default(), |ctx| {
|
||||||
|
let actions = render_ui(ctx, &mut state, ¬ebook, AutosaveStatus::Saved, &updater);
|
||||||
|
assert!(!actions.contains(&UiAction::SaveStylusButtons));
|
||||||
|
});
|
||||||
|
assert!(!output.shapes.is_empty());
|
||||||
|
assert_eq!(state.stylus_buttons.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_long_press_detection() {
|
fn test_long_press_detection() {
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
|
|||||||
Reference in New Issue
Block a user