From 4b1ef0cf457578c3071f4b9c24c0e2c46be2fabb Mon Sep 17 00:00:00 2001 From: Kneemund Date: Wed, 27 Nov 2024 20:02:46 +0100 Subject: [PATCH] feat: spellcheck correction picker --- crates/rnote-engine/src/engine/mod.rs | 35 ++++++ crates/rnote-engine/src/pens/penholder.rs | 2 +- .../rnote-engine/src/pens/typewriter/mod.rs | 53 +++++++++ crates/rnote-engine/src/strokes/textstroke.rs | 54 ++++++++- .../actions/text-squiggly-symbolic.svg | 2 + crates/rnote-ui/data/meson.build | 1 + crates/rnote-ui/data/resources.gresource.xml | 1 + .../data/ui/penssidebar/typewriterpage.ui | 93 +++++++++++++++ crates/rnote-ui/src/appwindow/imp.rs | 10 ++ .../src/penssidebar/typewriterpage.rs | 111 +++++++++++++++++- crates/rnote-ui/src/settingspanel/mod.rs | 17 ++- 11 files changed, 364 insertions(+), 15 deletions(-) create mode 100644 crates/rnote-ui/data/icons/scalable/actions/text-squiggly-symbolic.svg diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs index 077907d59f..2b7d178b98 100644 --- a/crates/rnote-engine/src/engine/mod.rs +++ b/crates/rnote-engine/src/engine/mod.rs @@ -379,6 +379,41 @@ impl Engine { widget_flags } + pub fn get_spellcheck_corrections(&self) -> Option> { + if let Pen::Typewriter(typewriter) = self.penholder.current_pen_ref() { + return typewriter.get_spellcheck_correction_in_modifying_stroke(&mut EngineView { + tasks_tx: self.tasks_tx.clone(), + pens_config: &self.pens_config, + document: &self.document, + store: &self.store, + camera: &self.camera, + audioplayer: &self.audioplayer, + spellcheck: &self.spellcheck, + }); + } + + None + } + + pub fn apply_spellcheck_correction(&mut self, correction: &str) -> WidgetFlags { + if let Pen::Typewriter(typewriter) = self.penholder.current_pen_mut() { + return typewriter.apply_spellcheck_correction_in_modifying_stroke( + correction, + &mut EngineViewMut { + tasks_tx: self.tasks_tx.clone(), + pens_config: &mut self.pens_config, + document: &mut self.document, + store: &mut self.store, + camera: &mut self.camera, + audioplayer: &mut self.audioplayer, + spellcheck: &mut self.spellcheck, + }, + ); + } + + WidgetFlags::default() + } + pub fn optimize_epd(&self) -> bool { self.optimize_epd } diff --git a/crates/rnote-engine/src/pens/penholder.rs b/crates/rnote-engine/src/pens/penholder.rs index 66952fbe25..2f03a877f8 100644 --- a/crates/rnote-engine/src/pens/penholder.rs +++ b/crates/rnote-engine/src/pens/penholder.rs @@ -140,7 +140,7 @@ impl PenHolder { self.progress } - pub fn current_pen_ref(&mut self) -> &Pen { + pub fn current_pen_ref(&self) -> &Pen { &self.current_pen } diff --git a/crates/rnote-engine/src/pens/typewriter/mod.rs b/crates/rnote-engine/src/pens/typewriter/mod.rs index f749425578..5e767dc5c7 100644 --- a/crates/rnote-engine/src/pens/typewriter/mod.rs +++ b/crates/rnote-engine/src/pens/typewriter/mod.rs @@ -827,6 +827,59 @@ impl Typewriter { } } + pub(crate) fn get_spellcheck_correction_in_modifying_stroke( + &self, + engine_view: &EngineView, + ) -> Option> { + if let TypewriterState::Modifying { + stroke_key, cursor, .. + } = &self.state + { + if let Some(Stroke::TextStroke(textstroke)) = + engine_view.store.get_stroke_ref(*stroke_key) + { + return textstroke.get_spellcheck_corrections_at_index( + engine_view.spellcheck, + cursor.cur_cursor(), + ); + } + } + + None + } + + pub(crate) fn apply_spellcheck_correction_in_modifying_stroke( + &mut self, + correction: &str, + engine_view: &mut EngineViewMut, + ) -> WidgetFlags { + let mut widget_flags = WidgetFlags::default(); + + if let TypewriterState::Modifying { + stroke_key, cursor, .. + } = &mut self.state + { + if let Some(Stroke::TextStroke(textstroke)) = + engine_view.store.get_stroke_mut(*stroke_key) + { + textstroke.apply_spellcheck_correction_at_cursor(cursor, correction); + + engine_view.store.update_geometry_for_stroke(*stroke_key); + engine_view.store.regenerate_rendering_for_stroke( + *stroke_key, + engine_view.camera.viewport(), + engine_view.camera.image_scale(), + ); + + widget_flags |= engine_view.store.record(Instant::now()); + widget_flags.redraw = true; + widget_flags.store_modified = true; + } + } + + widget_flags + } + pub(crate) fn toggle_text_attribute_current_selection( &mut self, text_attribute: TextAttribute, diff --git a/crates/rnote-engine/src/strokes/textstroke.rs b/crates/rnote-engine/src/strokes/textstroke.rs index 5b08615009..2f4773e843 100644 --- a/crates/rnote-engine/src/strokes/textstroke.rs +++ b/crates/rnote-engine/src/strokes/textstroke.rs @@ -415,6 +415,8 @@ impl TextStyle { camera: &Camera, ) { const ERROR_COLOR: piet::Color = color::GNOME_REDS[2]; + const STYLE: piet::StrokeStyle = piet::StrokeStyle::new().line_cap(piet::LineCap::Round); + let scale = 1.0 / camera.total_zoom(); if let Ok(selection_rects) = @@ -433,7 +435,7 @@ impl TextStyle { ); let path = create_wavy_line(origin, width, scale); - cx.stroke(path, &ERROR_COLOR, 1.5 * scale); + cx.stroke_styled(path, &ERROR_COLOR, 1.5 * scale, &STYLE); } } } @@ -685,6 +687,52 @@ impl TextStroke { } } + pub fn get_spellcheck_corrections_at_index( + &self, + spellcheck: &Spellcheck, + index: usize, + ) -> Option> { + let Some(dict) = &spellcheck.dict else { + return None; + }; + + let start_index = self.get_prev_word_start_index(index); + + if let Some(length) = self.spellcheck_result.errors.get(&start_index) { + let word = self.get_text_slice_for_range(start_index..start_index + length); + return Some(dict.suggest(word)); + } + + None + } + + pub fn apply_spellcheck_correction_at_cursor( + &mut self, + cursor: &mut GraphemeCursor, + correction: &str, + ) { + let cur_pos = cursor.cur_cursor(); + let start_index = self.get_prev_word_start_index(cur_pos); + + if let Some(length) = self.spellcheck_result.errors.get(&start_index) { + let old_length = *length; + let new_length = correction.len(); + + self.text + .replace_range(start_index..start_index + old_length, correction); + + self.spellcheck_result.errors.remove(&start_index); + + // translate the text attributes + self.translate_attrs_after_cursor( + start_index + old_length, + (new_length as i32) - (old_length as i32), + ); + + *cursor = GraphemeCursor::new(start_index + new_length, self.text.len(), true); + } + } + pub fn check_spelling_range( &mut self, start_index: usize, @@ -870,7 +918,9 @@ impl TextStroke { }; for (word_start, word_length) in translated_words { - let new_word_start = word_start.saturating_add_signed(offset as isize); + let Some(new_word_start) = word_start.checked_add_signed(offset as isize) else { + continue; + }; if new_word_start >= from_pos { self.spellcheck_result diff --git a/crates/rnote-ui/data/icons/scalable/actions/text-squiggly-symbolic.svg b/crates/rnote-ui/data/icons/scalable/actions/text-squiggly-symbolic.svg new file mode 100644 index 0000000000..b920855b0f --- /dev/null +++ b/crates/rnote-ui/data/icons/scalable/actions/text-squiggly-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/crates/rnote-ui/data/meson.build b/crates/rnote-ui/data/meson.build index b1ae9b76b9..9c901af545 100644 --- a/crates/rnote-ui/data/meson.build +++ b/crates/rnote-ui/data/meson.build @@ -275,6 +275,7 @@ rnote_ui_gresources_icons_files = files( 'icons/scalable/actions/text-indent-less-symbolic.svg', 'icons/scalable/actions/text-indent-more-symbolic.svg', 'icons/scalable/actions/text-italic-symbolic.svg', + 'icons/scalable/actions/text-squiggly-symbolic.svg', 'icons/scalable/actions/text-strikethrough-symbolic.svg', 'icons/scalable/actions/text-underline-symbolic.svg', 'icons/scalable/actions/touch-two-finger-long-press-symbolic.svg', diff --git a/crates/rnote-ui/data/resources.gresource.xml b/crates/rnote-ui/data/resources.gresource.xml index a1deb7af1f..3de974d0ff 100644 --- a/crates/rnote-ui/data/resources.gresource.xml +++ b/crates/rnote-ui/data/resources.gresource.xml @@ -146,6 +146,7 @@ icons/scalable/actions/text-indent-less-symbolic.svg icons/scalable/actions/text-indent-more-symbolic.svg icons/scalable/actions/text-italic-symbolic.svg + icons/scalable/actions/text-squiggly-symbolic.svg icons/scalable/actions/text-strikethrough-symbolic.svg icons/scalable/actions/text-underline-symbolic.svg icons/scalable/actions/touch-two-finger-long-press-symbolic.svg diff --git a/crates/rnote-ui/data/ui/penssidebar/typewriterpage.ui b/crates/rnote-ui/data/ui/penssidebar/typewriterpage.ui index a3d2685ea4..a0d4027973 100644 --- a/crates/rnote-ui/data/ui/penssidebar/typewriterpage.ui +++ b/crates/rnote-ui/data/ui/penssidebar/typewriterpage.ui @@ -51,6 +51,99 @@ right + + + left + Spellcheck Corrections + text-squiggly-symbolic + + + + + + + true + + + + + + + + vertical + 12 + 18 + 18 + 18 + 18 + + + text-squiggly-symbolic + 64 + + + + + + center + No Corrections +Available + + + + + + center + Move cursor over underlined +words to get suggestions. + + + + + + + + + + vertical + 12 + 18 + 18 + 18 + 18 + + + text-squiggly-symbolic + 64 + + + + + + center + No Corrections +Found + + + + + + vertical diff --git a/crates/rnote-ui/src/appwindow/imp.rs b/crates/rnote-ui/src/appwindow/imp.rs index 43e004f067..f24821d049 100644 --- a/crates/rnote-ui/src/appwindow/imp.rs +++ b/crates/rnote-ui/src/appwindow/imp.rs @@ -635,6 +635,11 @@ impl RnAppWindow { .typewriter_page() .emojichooser_menubutton() .set_direction(ArrowType::Right); + obj.overlays() + .penssidebar() + .typewriter_page() + .spellcheck_corrections_menubutton() + .set_direction(ArrowType::Right); obj.overlays() .penssidebar() .eraser_page() @@ -762,6 +767,11 @@ impl RnAppWindow { .typewriter_page() .emojichooser_menubutton() .set_direction(ArrowType::Left); + obj.overlays() + .penssidebar() + .typewriter_page() + .spellcheck_corrections_menubutton() + .set_direction(ArrowType::Left); obj.overlays() .penssidebar() .eraser_page() diff --git a/crates/rnote-ui/src/penssidebar/typewriterpage.rs b/crates/rnote-ui/src/penssidebar/typewriterpage.rs index e1f0656094..301f17c9c6 100644 --- a/crates/rnote-ui/src/penssidebar/typewriterpage.rs +++ b/crates/rnote-ui/src/penssidebar/typewriterpage.rs @@ -1,8 +1,15 @@ // Imports use crate::{RnAppWindow, RnCanvasWrapper}; +use gtk4::ListView; +use gtk4::Popover; use gtk4::{ - glib, glib::clone, pango, prelude::*, subclass::prelude::*, Button, CompositeTemplate, - EmojiChooser, FontDialog, MenuButton, SpinButton, ToggleButton, + glib::{self, clone}, + pango, + prelude::*, + subclass::prelude::*, + Button, CompositeTemplate, ConstantExpression, EmojiChooser, FontDialog, ListItem, MenuButton, + NoSelection, PropertyExpression, SignalListItemFactory, SpinButton, StringList, StringObject, + ToggleButton, Widget, }; use rnote_engine::strokes::textstroke::{FontStyle, TextAlignment, TextAttribute, TextStyle}; use std::cell::RefCell; @@ -25,6 +32,16 @@ mod imp { #[template_child] pub(crate) emojichooser: TemplateChild, #[template_child] + pub(crate) spellcheck_corrections_menubutton: TemplateChild, + #[template_child] + pub(crate) spellcheck_corrections_empty: TemplateChild, + #[template_child] + pub(crate) spellcheck_corrections_unavailable: TemplateChild, + #[template_child] + pub(crate) spellcheck_corrections: TemplateChild, + #[template_child] + pub(crate) spellcheck_corrections_listview: TemplateChild, + #[template_child] pub(crate) text_reset_button: TemplateChild