Compare commits

...
4 Commits
Author SHA1 Message Date
haraldandClaude Opus 5.5 680fa96f32 chore(release): bump version to 0.20.1
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 8m32s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:25:26 +02:00
haraldandClaude Opus 5.5 eb0a801921 fix: Erstes Antippen mit dem Stift trifft (Issues #24, #25)
octotablet meldet das Aufsetzen des Stifts vor der Position desselben Frames.
Bis dahin galt die alte Position – nähert man den Stift und tippt sofort,
lag sie bei der vorherigen Stelle: Der Druck ging an den alten Ort, beim
Loslassen hatte sich der Zeiger „bewegt“, egui wertete das nicht als Klick
und zeigte nur den Tooltip. Erst das zweite Tippen traf. Auf dem Blatt
konnte ein Strich so mit einer Linie von der alten Stelle beginnen.

Jetzt wird das Aufsetzen erst nach der Position seines Frames verarbeitet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:24:12 +02:00
haraldandClaude Opus 5.5 76ecdac081 chore(release): bump version to 0.20.0
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 8m29s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:05:48 +02:00
haraldandClaude Opus 5.5 ea9ee6c0d7 feat: Tasten am Stift belegen (Issue #23)
- Im ☰-Menü unter Eingabe → „Tasten am Stift“: je Taste Radieren (Standard),
  Auswählen, Textmarker, Stift oder Nichts; gilt, solange die Taste gedrückt ist
- Tasten werden beim ersten Drücken gelernt und in ui.json gemerkt; die gerade
  gedrückte leuchtet im Menü grün
- Mit einer Werkzeug-Taste geschriebene Striche benutzen dieses Werkzeug, danach
  ist wieder das gewählte Werkzeug aktiv

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:05:09 +02:00
8 changed files with 332 additions and 36 deletions

No files matched your search

Generated
+1 -1
View File
@@ -1393,7 +1393,7 @@ dependencies = [
[[package]] [[package]]
name = "ink-notes" name = "ink-notes"
version = "0.19.0" version = "0.20.1"
dependencies = [ dependencies = [
"ab_glyph", "ab_glyph",
"arboard", "arboard",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "ink-notes" name = "ink-notes"
version = "0.19.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"
+2 -1
View File
@@ -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
+2
View File
@@ -3,6 +3,8 @@
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 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)
+102 -33
View File
@@ -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, &notebook, &manifest, tool, autosave.status()); update_window_title(&window, &notebook, &manifest, tool, autosave.status());
window.request_redraw(); window.request_redraw();
} }
@@ -3433,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);
+52
View File
@@ -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)
+61
View File
@@ -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);
+111
View File
@@ -288,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)
@@ -376,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
@@ -471,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,
@@ -1635,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| {
@@ -3560,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| {
@@ -5295,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, &notebook, 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, &notebook, 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();