]> uap-core.de Git - note.git/commitdiff
detect note title changes main
authorOlaf Wintermann <olaf.wintermann@gmail.com>
Fri, 29 May 2026 14:17:26 +0000 (16:17 +0200)
committerOlaf Wintermann <olaf.wintermann@gmail.com>
Fri, 29 May 2026 14:17:26 +0000 (16:17 +0200)
19 files changed:
application/src/note.rs
application/src/window.rs
ui-rs/src/ui/event.rs
ui-rs/src/ui/ffi.rs
ui-rs/src/ui/text.rs
ui/cocoa/toolkit.m
ui/common/args.c
ui/common/wrapper.c
ui/common/wrapper.h
ui/gtk/text.c
ui/gtk/text.h
ui/gtk/toolkit.c
ui/motif/toolkit.c
ui/qt/toolkit.cpp
ui/server/toolkit.c
ui/ui/text.h
ui/ui/toolkit.h
ui/win32/toolkit.c
ui/winui/toolkit.cpp

index e5298950ce05e807b88c129bb720712ecc3644ea..311eb7265a9856e5a3cefb3f7cc5569a010f36d6 100644 (file)
@@ -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
index dab067feea030e1e4b475161a20fdc0816860e0c..1b9074626118ff033b637e6dad797fc93bc2c0bb 100644 (file)
@@ -187,6 +187,7 @@ pub fn create_window(app: &App, ctx: &AppContext<MainWindow>) -> UiObject<MainWi
                         obj.textarea(|b|{
                            b.fill(true);
                             b.varname("note_text");
+                            b.action("note_text_changed");
                         });
                     });
                 });
index 7536e3fc1174fc88ff71de821950a6c008cc4439..053b527b4e6ce450ac10071f2f196af15df17bd3 100644 (file)
 #![allow(dead_code)]
 
 use std::ffi::{c_char, c_void, CStr, CString};
+use std::slice;
 use crate::ui::{event, ffi, UiDouble, UiInteger, UiString, UiText, UiRange, ListSelection, UiObject, NoAppData, UiSourceList};
-use crate::ui::ffi::{UiEvent, UiSubListEventData};
+use crate::ui::ffi::{UiEvent, UiSubListEventData, UiTextChangeEventData};
+use crate::ui::text::{TextInsert, TextDelete};
 
 pub struct Event<'a, T> {
     pub obj: &'a mut UiObject<T>,
@@ -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
index c2a28584ed64187019cede9e9be6abb749a13461..1908c509b1d855b57e5ee3d530ff2f28f39d34c4 100644 (file)
@@ -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],
index 1cd3ef344abd513a23e94a473191eafcc9a682ab..12eff0173873d171e65d1f12834c63d0991d3c7c 100644 (file)
@@ -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);
index 6eb503e9684860ccf314d166a859cbe5733c0771..66e496851c684080417a2aeddf4d41c5ff02ad48 100644 (file)
@@ -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];
index 602ee164b7c4f540c0ac267a535737e12dcd5369..ac6e05ca32b39d2e374b1bb8fcd3877036114bab 100644 (file)
@@ -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);
index 7f1441d492a0ede0dd74ab20ebdd87ebb472dacc..f9184251cbd9a20fcf1a1afc6d9561c3ff9e159b 100644 (file)
@@ -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) {
index ddfce62b64eae74107b364987427ca65163cc0e4..42d49ac6f5ede24af29a88ce7631176641879614 100644 (file)
@@ -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);
 
index 4fc0eb152191cb862fed2923a4e665940c61f8b0..55b840450ae6f28491e5a24eb805bf4b0be14458 100644 (file)
@@ -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();
     
index 057aef825759fca6a23c130b8a95a8045d9911ab..6318e05386f672549f8445450757da07766dac2b 100644 (file)
@@ -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,
index b1bb9d10270010143646a3f85ed4731cc58fdf89..db9bbada633cd6a63edb2247338d9b054ad21d63 100644 (file)
@@ -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();
index 5a0c9de26d9d97fcb3e6257efc5521da41793f28..b0ec4a4072368e2cfd743c7550d5b557e2549932 100644 (file)
@@ -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();
index ed764740058575b1d193b6ebd9d1775e895c5518..548cfdda6d24c8eb66217ea2b1c3fb2f1bafd3a4 100644 (file)
@@ -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;
     
index 815e118a1b06dbec86470610aa4adf0d4bae6acf..1cfd027f97e67981273c84974235051044e9ebb6 100644 (file)
@@ -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;
     
index 8a6368be05d65d0d751f89399644c98b74363b84..2646da9d177455ced72560795438829685350c99 100644 (file)
@@ -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;
index 61ddfff7663e70f0226dd6998384688c2d6262ae..0184d85fb49c573d20cd421929ec7e1dac98a6ae 100644 (file)
@@ -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,
index 885342d217ff95b59757c9348fac877dbfa36d93..86c46c2faec7dec1b21753934caf0950f334e663 100644 (file)
@@ -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;
 
index 92fea249a71c0dcfcf24d9af2867476628d77c3a..360986f11554af4e20c4c9297ebdae51ac4d84c6 100644 (file)
@@ -149,6 +149,9 @@ void ui_appsdk_bootstrap(void) {
        }\r
 }\r
 \r
+// NOOP on most platforms, expect macos\r
+UIEXPORT extern "C" void ui_set_main_thread_error_msg(const char *msg) {}\r
+\r
 void ui_init(const char* appname, int argc, char** argv) {\r
        application_name = appname ? strdup(appname) : NULL;\r
 \r