From 77b4539506fb3028ff889453a06d065b1ace2865 Mon Sep 17 00:00:00 2001 From: Olaf Wintermann Date: Fri, 29 May 2026 16:17:26 +0200 Subject: [PATCH] detect note title changes --- application/src/note.rs | 52 +++++++++++++++++++++++++++++++++--- application/src/window.rs | 1 + ui-rs/src/ui/event.rs | 56 +++++++++++++++++++++++++++++++++++---- ui-rs/src/ui/ffi.rs | 5 ++++ ui-rs/src/ui/text.rs | 18 +++++++++++++ ui/cocoa/toolkit.m | 25 ++++++++++++++++- ui/common/args.c | 5 ++++ ui/common/wrapper.c | 22 +++++++++++++++ ui/common/wrapper.h | 7 +++++ ui/gtk/text.c | 52 +++++++++++++++++++++++++++++++++--- ui/gtk/text.h | 14 +++++++++- ui/gtk/toolkit.c | 3 +++ ui/motif/toolkit.c | 3 +++ ui/qt/toolkit.cpp | 3 +++ ui/server/toolkit.c | 3 +++ ui/ui/text.h | 13 +++++++++ ui/ui/toolkit.h | 1 + ui/win32/toolkit.c | 3 +++ ui/winui/toolkit.cpp | 3 +++ 19 files changed, 275 insertions(+), 14 deletions(-) diff --git a/application/src/note.rs b/application/src/note.rs index e529895..311eb72 100644 --- a/application/src/note.rs +++ b/application/src/note.rs @@ -2,7 +2,7 @@ use sea_orm::prelude::DateTimeWithTimeZone; use sea_orm::sea_query::prelude::Utc; use sea_orm::{NotSet, Set}; use entity::note::NoteType; -use ui_rs::{ui_actions, UiModel}; +use ui_rs::{action, ui_actions, UiModel}; use ui_rs::ui::*; use crate::backend::BackendHandle; use crate::window::NoteTypeTabView; @@ -15,6 +15,9 @@ pub struct Note { pub kind: NoteType, pub created: DateTimeWithTimeZone, + title_start: i32, + title_end: i32, + #[bind("note_type")] pub note_type: UiInteger, @@ -37,12 +40,29 @@ impl Note { pub fn init_content(&mut self, content: &entity::notecontent::Model) { self.content_id = content.id; self.text.set(content.content.as_str()); + self.update_title(content.content.as_str()); + } + + pub fn update_title(&mut self, s: &str) { + match generate_title(s) { + Some(result) => { + let title = result.0; + self.title_start = result.1 as i32; + self.title_end = result.1 as i32 + title.len() as i32; + + // TODO: notify notebook that the title has changed + }, + None => { + self.title_start = -1; + self.title_end = -1; + } + } } pub fn save(&self, collection_id: i32, backend: &BackendHandle) { let content = self.text.get(); let title = match generate_title(content.as_str()) { - Some(title) => title.to_string(), + Some(title) => title.0.to_string(), None => "Note".to_string() }; @@ -74,13 +94,37 @@ impl Note { } }); } + + #[action] + pub fn note_text_changed(&mut self, event: &ActionEvent) { + if event.set { + return; + } + + // check if any text in the title range has changed + let pos = match &event.event_type { + EventType::TextInsert(t) => t.pos, + EventType::TextDelete(t) => t.begin, + _ => { + return; + } + }; + if self.title_end != -1 && pos > self.title_end + 1 { + return; // this text edit can not change the title + } + + // TODO: we don't need the full text + self.update_title(self.text.get().as_str()); + } } -fn generate_title(s: &str) -> Option<&str> { +fn generate_title(s: &str) -> Option<(&str, usize)> { for line in s.lines() { if !line.trim().is_empty() { - return Some(line); + let start = line.as_ptr() as usize - s.as_ptr() as usize; + return Some((line, start)); } } + None } \ No newline at end of file diff --git a/application/src/window.rs b/application/src/window.rs index dab067f..1b90746 100644 --- a/application/src/window.rs +++ b/application/src/window.rs @@ -187,6 +187,7 @@ pub fn create_window(app: &App, ctx: &AppContext) -> UiObject { pub obj: &'a mut UiObject, @@ -56,6 +58,8 @@ pub enum EventType<'a> { TextValue(&'a mut UiText), DoubleValue(&'a mut UiDouble), RangeValue(&'a mut UiRange), + TextInsert(&'a mut TextInsert), + TextDelete(&'a mut TextDelete), ListSelection(&'a ListSelection), ListElement, Dnd, @@ -72,6 +76,8 @@ enum EventTypeData { TextValue(UiText), DoubleValue(UiDouble), RangeValue(UiRange), + TextInsert(TextInsert), + TextDelete(TextDelete), ListSelection(ListSelection), ListElement, Dnd, @@ -104,20 +110,52 @@ fn get_event_data(e: *const ffi::UiEvent) -> EventTypeData { EventTypeData::RangeValue( UiRange { ptr: d } ) } 8 => { + let d: *mut ffi::UiTextChangeEventData = ptr.cast(); + let text_change_type = unsafe { ui_text_change_event_get_type(d) }; + match text_change_type { + 0 => { + let begin = unsafe { + ui_text_change_event_get_begin(d) + }; + let str = unsafe { + let cstr: *const u8 = ui_text_change_event_get_text(d).cast(); + let length = ui_text_change_event_get_length(d); + let str = str::from_utf8(slice::from_raw_parts(cstr, length as usize)); + str.unwrap_or_else(|_| "") + }; + let insert = TextInsert { + pos: begin, + text: str.to_string() + }; + EventTypeData::TextInsert(insert) + }, + 1 => { + unsafe { + let delete = TextDelete { + begin: ui_text_change_event_get_begin(d), + end: ui_text_change_event_get_end(d) + }; + EventTypeData::TextDelete(delete) + } + }, + _ => EventTypeData::Null + } + }, + 9 => { EventTypeData::ListSelection( ListSelection::from_ptr(ptr.cast()) ) } - 9 => { + 10 => { // list elm EventTypeData::Null } - 10 => { + 11 => { // dnd EventTypeData::Null } - 11 => { + 12 => { EventTypeData::SubList(SubListEvent::from_ptr(ptr.cast())) } - 12 => { + 13 => { // filelist EventTypeData::Null } @@ -135,6 +173,8 @@ fn get_event_type<'a>(data: &'a mut EventTypeData) -> EventType<'a> { EventTypeData::TextValue(t) => EventType::TextValue( t ), EventTypeData::DoubleValue(d) => EventType::DoubleValue( d ), EventTypeData::RangeValue(r) => EventType::RangeValue( r ), + EventTypeData::TextInsert(c) => EventType::TextInsert( c ), + EventTypeData::TextDelete(c) => EventType::TextDelete( c ), EventTypeData::ListSelection(s) => EventType::ListSelection( s ), EventTypeData::ListElement => EventType::ListElement, EventTypeData::Dnd => EventType::Dnd, @@ -260,6 +300,12 @@ extern "C" { fn ui_event_get_int(event: *const UiEvent) -> i32; fn ui_event_get_set(event: *const UiEvent) -> i32; + fn ui_text_change_event_get_type(data: *const UiTextChangeEventData) -> i32; + fn ui_text_change_event_get_begin(data: *const UiTextChangeEventData) -> i32; + fn ui_text_change_event_get_end(data: *const UiTextChangeEventData) -> i32; + fn ui_text_change_event_get_text(data: *const UiTextChangeEventData) -> *const c_char; + fn ui_text_change_event_get_length(data: *const UiTextChangeEventData) -> i32; + fn ui_sublist_event_get_sublist_index(event: *const UiSubListEventData) -> i32; fn ui_sublist_event_get_row_index(event: *const UiSubListEventData) -> i32; } \ No newline at end of file diff --git a/ui-rs/src/ui/ffi.rs b/ui-rs/src/ui/ffi.rs index c2a2858..1908c50 100644 --- a/ui-rs/src/ui/ffi.rs +++ b/ui-rs/src/ui/ffi.rs @@ -51,6 +51,11 @@ pub struct UiSubListEventData { _private: [u8; 0], } +#[repr(C)] +pub struct UiTextChangeEventData { + _private: [u8; 0], +} + #[repr(C)] pub struct UiText { _private: [u8; 0], diff --git a/ui-rs/src/ui/text.rs b/ui-rs/src/ui/text.rs index 1cd3ef3..12eff01 100644 --- a/ui-rs/src/ui/text.rs +++ b/ui-rs/src/ui/text.rs @@ -254,6 +254,14 @@ impl<'a, T> TextAreaBuilder<'a, T> { self } + pub fn action(&mut self, action: &str) -> &mut Self { + let cstr = CString::new(action).unwrap(); + unsafe { + ui_textarea_args_set_action(self.args, cstr.as_ptr()); + } + self + } + pub fn visibility_states(&mut self, states: &[i32]) -> &mut Self { unsafe { ui_textarea_args_set_visibility_states(self.args, states.as_ptr(), states.len() as c_int); @@ -269,6 +277,15 @@ impl<'a, T> TextAreaBuilder<'a, T> { } } +pub struct TextInsert { + pub pos: i32, + pub text: String, +} + +pub struct TextDelete { + pub begin: i32, + pub end: i32 +} /* -------------------------------- TextField -------------------------------- */ @@ -631,6 +648,7 @@ extern "C" { fn ui_textarea_args_set_style_class(args: *mut UiTextAreaArgs, classname: *const c_char); fn ui_textarea_args_set_onchange(args: *mut UiTextAreaArgs, callback: UiCallback); fn ui_textarea_args_set_onchangedata(args: *mut UiTextAreaArgs, data: *mut c_void); + fn ui_textarea_args_set_action(args: *mut UiTextAreaArgs, action: *const c_char); fn ui_textarea_args_set_varname(args: *mut UiTextAreaArgs, varname: *const c_char); fn ui_textarea_args_set_value(args: *mut UiTextAreaArgs, ivalue: *mut UiText); fn ui_textarea_args_set_states(args: *mut UiTextAreaArgs, states: *const c_int, numstates: c_int); diff --git a/ui/cocoa/toolkit.m b/ui/cocoa/toolkit.m index 6eb503e..66e4968 100644 --- a/ui/cocoa/toolkit.m +++ b/ui/cocoa/toolkit.m @@ -50,13 +50,34 @@ static const char *application_name; static int app_argc; static const char **app_argv; -static UiBool exit_on_shutdown; +static UiBool exit_on_shutdown; + +static char *main_thread_error_msg; + +// This function is only used by language bindings, to improve error messages +// for example, when using java bindings, this can provide infos how to fix +// this (-XstartOnFirstThread) +void ui_set_main_thread_error_msg(const char *msg) { + main_thread_error_msg = msg ? strdup(msg) : NULL; +} /* ------------------- App Init / Event Loop functions ------------------- */ static AppDelegate *app_delegate; +static void main_thr_check(const char *func) { + if(![NSThread isMainThread]) { + fprintf(stderr, "Error: %s must run on the main thread.\n", func); + if(main_thread_error_msg) { + fprintf(stderr, "%s\n", main_thread_error_msg); + } + exit(1); + } +} + void ui_init(const char *appname, int argc, char **argv) { + main_thr_check("ui_init"); + application_name = appname ? strdup(appname) : NULL; app_argc = argc; app_argv = (const char**)argv; @@ -124,6 +145,8 @@ void ui_cocoa_onexit(void) { } void ui_main(void) { + main_thr_check("ui_main"); + NSApplicationMain(app_argc, app_argv); //[NSApp finishLaunching]; //[NSApp activateIgnoringOtherApps:YES]; diff --git a/ui/common/args.c b/ui/common/args.c index 602ee16..ac6e05c 100644 --- a/ui/common/args.c +++ b/ui/common/args.c @@ -2227,6 +2227,10 @@ void ui_textarea_args_set_onchangedata(UiTextAreaArgs *args, void *onchangedata) args->onchangedata = onchangedata; } +void ui_textarea_args_set_action(UiTextAreaArgs *args, const char *action) { + args->action = strdup(action); +} + void ui_textarea_args_set_varname(UiTextAreaArgs *args, const char *varname) { args->varname = strdup(varname); } @@ -2251,6 +2255,7 @@ void ui_textarea_args_free(UiTextAreaArgs *args) { free((void*)args->name); free((void*)args->style_class); free((void*)args->varname); + free((void*)args->action); free((void*)args->states); free((void*)args->visibility_states); free(args); diff --git a/ui/common/wrapper.c b/ui/common/wrapper.c index 7f1441d..f918425 100644 --- a/ui/common/wrapper.c +++ b/ui/common/wrapper.c @@ -296,6 +296,28 @@ void ui_list_selection_free(UiListSelection *sel) { free(sel); } +/* -------------------------- UiTextChangedEvent -------------------------- */ + +int ui_text_change_event_get_type(UiTextChangeEventData *event) { + return event->type; +} + +int ui_text_change_event_get_begin(UiTextChangeEventData *event) { + return event->begin; +} + +int ui_text_change_event_get_end(UiTextChangeEventData *event) { + return event->end; +} + +const char* ui_text_change_event_get_text(UiTextChangeEventData *event) { + return event->text; +} + +int ui_text_change_event_get_length(UiTextChangeEventData *event) { + return event->length; +} + /* ---------------------------- UiFileList ---------------------------- */ int ui_filelist_count(UiFileList *flist) { diff --git a/ui/common/wrapper.h b/ui/common/wrapper.h index ddfce62..42d49ac 100644 --- a/ui/common/wrapper.h +++ b/ui/common/wrapper.h @@ -31,6 +31,7 @@ #include "../ui/toolkit.h" #include "../ui/list.h" +#include "../ui/text.h" #ifdef __cplusplus extern "C" { @@ -86,6 +87,12 @@ UIEXPORT int* ui_list_selection_get_rows(UiListSelection *sel); UIEXPORT void ui_list_set_selected_indices(UiList *list, int *indices, int num); UIEXPORT void ui_list_selection_free(UiListSelection *sel); +UIEXPORT int ui_text_change_event_get_type(UiTextChangeEventData *event); +UIEXPORT int ui_text_change_event_get_begin(UiTextChangeEventData *event); +UIEXPORT int ui_text_change_event_get_end(UiTextChangeEventData *event); +UIEXPORT const char* ui_text_change_event_get_text(UiTextChangeEventData *event); +UIEXPORT int ui_text_change_event_get_length(UiTextChangeEventData *event); + UIEXPORT int ui_filelist_count(UiFileList *flist); UIEXPORT char* ui_filelist_get(UiFileList *flist, int index); diff --git a/ui/gtk/text.c b/ui/gtk/text.c index 4fc0eb1..55b8404 100644 --- a/ui/gtk/text.c +++ b/ui/gtk/text.c @@ -97,11 +97,24 @@ static void textarea_set_undomgr(GtkWidget *text_area, UiText *value) { static GtkTextBuffer* create_textbuffer(UiTextArea *textarea) { GtkTextBuffer *buf = gtk_text_buffer_new(NULL); if(textarea) { + /* g_signal_connect( buf, "changed", G_CALLBACK(ui_textbuf_changed), textarea); + */ + + g_signal_connect( + buf, + "insert-text", + G_CALLBACK(ui_textbuf_changed_insert), + textarea); + g_signal_connect( + buf, + "delete-range", + G_CALLBACK(ui_textbuf_changed_delete), + textarea); } else { fprintf(stderr, "Error: create_textbuffer: textarea == NULL\n"); } @@ -450,8 +463,41 @@ void ui_textarea_realize_event(GtkWidget *widget, gpointer data) { } +void ui_textbuf_changed_insert( + GtkTextBuffer *textbuffer, + GtkTextIter *location, + char *text, + int length, + UiTextArea *textarea) +{ + UiTextChangeEventData event; + event.type = UI_TEXT_INSERT; + event.begin = gtk_text_iter_get_offset(location); + event.end = event.begin + length; + event.text = text; + event.length = length; + ui_textbuf_changed(textarea, &event); +} + +void ui_textbuf_changed_delete( + GtkTextBuffer *self, + const GtkTextIter *start, + const GtkTextIter *end, + UiTextArea *textarea) +{ + UiTextChangeEventData event; + event.type = UI_TEXT_DELETE; + event.begin = gtk_text_iter_get_offset(start); + event.end = gtk_text_iter_get_offset(end); + event.text = NULL; + event.length = 0; + ui_textbuf_changed(textarea, &event); +} + + +// void ui_textbuf_changed(GtkTextBuffer *textbuffer, UiTextArea *textarea) -void ui_textbuf_changed(GtkTextBuffer *textbuffer, UiTextArea *textarea) { +void ui_textbuf_changed(UiTextArea *textarea, UiTextChangeEventData *data) { if(!ui_onchange_events_is_enabled()) { return; } @@ -462,8 +508,8 @@ void ui_textbuf_changed(GtkTextBuffer *textbuffer, UiTextArea *textarea) { e.obj = textarea->obj; e.window = e.obj->window; e.document = textarea->ctx->document; - e.eventdata = value; - e.eventdatatype = UI_EVENT_DATA_TEXT_VALUE; + e.eventdata = data; + e.eventdatatype = UI_EVENT_DATA_TEXT_CHANGED; e.intval = 0; e.set = ui_get_setop(); diff --git a/ui/gtk/text.h b/ui/gtk/text.h index 057aef8..6318e05 100644 --- a/ui/gtk/text.h +++ b/ui/gtk/text.h @@ -131,7 +131,19 @@ int ui_textarea_length(UiText *text); void ui_textarea_remove(UiText *text, int begin, int end); void ui_textarea_realize_event(GtkWidget *widget, gpointer data); -void ui_textbuf_changed(GtkTextBuffer *textbuffer, UiTextArea *textarea); +//void ui_textbuf_changed(GtkTextBuffer *textbuffer, UiTextArea *textarea); +void ui_textbuf_changed_insert( + GtkTextBuffer *textbuffer, + GtkTextIter *location, + char *text, + int length, + UiTextArea *textarea); +void ui_textbuf_changed_delete( + GtkTextBuffer *self, + const GtkTextIter *start, + const GtkTextIter *end, + UiTextArea *textarea); +void ui_textbuf_changed(UiTextArea *textarea, UiTextChangeEventData *data); void ui_textbuf_insert( GtkTextBuffer *textbuffer, diff --git a/ui/gtk/toolkit.c b/ui/gtk/toolkit.c index b1bb9d1..db9bbad 100644 --- a/ui/gtk/toolkit.c +++ b/ui/gtk/toolkit.c @@ -62,6 +62,9 @@ static int scale_factor = 1; static UiBool exit_on_shutdown; +// NOOP on most platforms, expect macos +void ui_set_main_thread_error_msg(const char *msg) {} + UIEXPORT void ui_init(const char *appname, int argc, char **argv) { application_name = appname ? strdup(appname) : NULL; uic_init_global_context(); diff --git a/ui/motif/toolkit.c b/ui/motif/toolkit.c index 5a0c9de..b0ec4a4 100644 --- a/ui/motif/toolkit.c +++ b/ui/motif/toolkit.c @@ -89,6 +89,9 @@ void ui_motif_set_fallback_resources(String *fallbackres) { fallback_resources = fallbackres; } +// NOOP on most platforms, expect macos +void ui_set_main_thread_error_msg(const char *msg) {} + void ui_init(const char *appname, int argc, char **argv) { application_name = appname ? strdup(appname) : NULL; uic_init_global_context(); diff --git a/ui/qt/toolkit.cpp b/ui/qt/toolkit.cpp index ed76474..548cfdd 100644 --- a/ui/qt/toolkit.cpp +++ b/ui/qt/toolkit.cpp @@ -48,6 +48,9 @@ static QApplication *application = NULL; static UiBool exit_on_shutdown; +// NOOP on most platforms, expect macos +extern "C" UIEXPORT void ui_set_main_thread_error_msg(const char *msg) {} + void ui_init(const char *appname, int argc, char **argv) { application_name = appname ? strdup(appname) : NULL; diff --git a/ui/server/toolkit.c b/ui/server/toolkit.c index 815e118..1cfd027 100644 --- a/ui/server/toolkit.c +++ b/ui/server/toolkit.c @@ -52,6 +52,9 @@ static UiQueue *event_queue; static CxMap *srv_obj_map; static uint64_t srv_obj_id_counter = 0; +// NOOP on most platforms, expect macos +void ui_set_main_thread_error_msg(const char *msg) {} + void ui_init(const char *appname, int argc, char **argv) { ui_app_name = appname ? strdup(appname) : NULL; diff --git a/ui/ui/text.h b/ui/ui/text.h index 8a6368b..2646da9 100644 --- a/ui/ui/text.h +++ b/ui/ui/text.h @@ -63,6 +63,19 @@ typedef struct UiTextAreaArgs { const int *states; const int *visibility_states; } UiTextAreaArgs; + +typedef enum UiTextChangedEventType { + UI_TEXT_INSERT = 0, + UI_TEXT_DELETE +} UiTextChangedEventType; + +typedef struct UiTextChangeEventData { + UiTextChangedEventType type; + int begin; + int end; + const char *text; + int length; +} UiTextChangeEventData; typedef struct UiTextFieldArgs { UiBool fill; diff --git a/ui/ui/toolkit.h b/ui/ui/toolkit.h index 61ddfff..0184d85 100644 --- a/ui/ui/toolkit.h +++ b/ui/ui/toolkit.h @@ -491,6 +491,7 @@ enum UiEventType { UI_EVENT_DATA_TEXT_VALUE, UI_EVENT_DATA_DOUBLE_VALUE, UI_EVENT_DATA_RANGE_VALUE, + UI_EVENT_DATA_TEXT_CHANGED, UI_EVENT_DATA_LIST_SELECTION, UI_EVENT_DATA_LIST_ELM, UI_EVENT_DATA_DND, diff --git a/ui/win32/toolkit.c b/ui/win32/toolkit.c index 885342d..86c46c2 100644 --- a/ui/win32/toolkit.c +++ b/ui/win32/toolkit.c @@ -49,6 +49,9 @@ static const char *application_name; static HFONT ui_font = NULL; +// NOOP on most platforms, expect macos +UIEXPORT void ui_set_main_thread_error_msg(const char *msg) {} + void ui_init(const char *appname, int argc, char **argv) { application_name = appname ? strdup(appname) : NULL; diff --git a/ui/winui/toolkit.cpp b/ui/winui/toolkit.cpp index 92fea24..360986f 100644 --- a/ui/winui/toolkit.cpp +++ b/ui/winui/toolkit.cpp @@ -149,6 +149,9 @@ void ui_appsdk_bootstrap(void) { } } +// NOOP on most platforms, expect macos +UIEXPORT extern "C" void ui_set_main_thread_error_msg(const char *msg) {} + void ui_init(const char* appname, int argc, char** argv) { application_name = appname ? strdup(appname) : NULL; -- 2.47.3