add settings window and implement repository editing

Sun, 27 Oct 2024 18:24:37 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sun, 27 Oct 2024 18:24:37 +0100
changeset 60
ee4e4742391e
parent 59
6bd37fe6d905
child 61
eb63af2f2bdd

add settings window and implement repository editing

application/Makefile file | annotate | diff | comparison | revisions
application/application.c file | annotate | diff | comparison | revisions
application/application.h file | annotate | diff | comparison | revisions
application/config.c file | annotate | diff | comparison | revisions
application/config.h file | annotate | diff | comparison | revisions
application/settings.c file | annotate | diff | comparison | revisions
application/settings.h file | annotate | diff | comparison | revisions
application/window.c file | annotate | diff | comparison | revisions
libidav/config.c file | annotate | diff | comparison | revisions
libidav/config.h file | annotate | diff | comparison | revisions
libidav/pwdstore.c file | annotate | diff | comparison | revisions
ui/common/context.c file | annotate | diff | comparison | revisions
ui/common/types.c file | annotate | diff | comparison | revisions
ui/gtk/button.c file | annotate | diff | comparison | revisions
ui/gtk/button.h file | annotate | diff | comparison | revisions
ui/gtk/container.c file | annotate | diff | comparison | revisions
ui/gtk/container.h file | annotate | diff | comparison | revisions
ui/gtk/display.c file | annotate | diff | comparison | revisions
ui/gtk/entry.c file | annotate | diff | comparison | revisions
ui/gtk/headerbar.c file | annotate | diff | comparison | revisions
ui/gtk/list.c file | annotate | diff | comparison | revisions
ui/gtk/list.h file | annotate | diff | comparison | revisions
ui/gtk/text.c file | annotate | diff | comparison | revisions
ui/gtk/toolkit.c file | annotate | diff | comparison | revisions
ui/gtk/toolkit.h file | annotate | diff | comparison | revisions
ui/ui/button.h file | annotate | diff | comparison | revisions
ui/ui/container.h file | annotate | diff | comparison | revisions
ui/ui/display.h file | annotate | diff | comparison | revisions
ui/ui/entry.h file | annotate | diff | comparison | revisions
ui/ui/text.h file | annotate | diff | comparison | revisions
ui/ui/toolkit.h file | annotate | diff | comparison | revisions
ui/ui/tree.h file | annotate | diff | comparison | revisions
--- a/application/Makefile	Wed Oct 23 21:46:43 2024 +0200
+++ b/application/Makefile	Sun Oct 27 18:24:37 2024 +0100
@@ -37,6 +37,7 @@
 SRC += davcontroller.c
 SRC += system.c
 SRC += window.c
+SRC += settings.c
 
 OBJ = $(SRC:%.c=../build/application/%.$(OBJ_EXT))
 
--- a/application/application.c	Wed Oct 23 21:46:43 2024 +0200
+++ b/application/application.c	Sun Oct 27 18:24:37 2024 +0100
@@ -36,9 +36,12 @@
 #include "window.h"
 #include "config.h"
 #include "davcontroller.h"
+#include "settings.h"
 
 static DavContext* davctx;
 
+static DavApp *app;
+
 void application_init(void) {
     davctx = dav_context_new();
     dav_context_set_mtsafe(davctx, true);
@@ -46,6 +49,9 @@
     application_create_menu();
 }
 
+DavApp* get_application(void) {
+    return app;
+}
 
 void application_startup(UiEvent* event, void* data) {
     if (load_config(davctx)) {
@@ -56,7 +62,7 @@
     window_init();
 
     // create document for global settings (repolist, ...)
-    DavApp *app = application_create_app_document();
+    app = application_create_app_document();
     UiContext *global = ui_global_context();
     ui_attach_document(global, app);
 
@@ -135,13 +141,14 @@
 
 void application_update_repolist(DavApp *app) {
     DavConfig *config = get_config();
-    DavCfgRepository *repo = config->repositories;
 
     ui_list_clear(app->repos);
 
     for (DavCfgRepository *repo = config->repositories; repo; repo = repo->next) {
         ui_list_append(app->repos, repo);
     }
+    
+    ui_notify(app->repos->observers, NULL);
 }
 
 
@@ -307,7 +314,7 @@
 }
 
 void action_open_settings(UiEvent *event, void *data) {
-    
+    settings_window_open();
 }
 
 void action_open_properties(UiEvent *event, void *data) {
--- a/application/application.h	Wed Oct 23 21:46:43 2024 +0200
+++ b/application/application.h	Sun Oct 27 18:24:37 2024 +0100
@@ -27,7 +27,7 @@
  */
 
 #ifndef IDAV_APPLICATION_H
-#define    IDAV_APPLICATION_H
+#define IDAV_APPLICATION_H
 
 #include <ui/ui.h>
 
@@ -156,6 +156,7 @@
  */
 void application_create_menu(void);
 
+DavApp* get_application(void);
 
 DavApp* application_create_app_document(void);
 
--- a/application/config.c	Wed Oct 23 21:46:43 2024 +0200
+++ b/application/config.c	Sun Oct 27 18:24:37 2024 +0100
@@ -162,6 +162,36 @@
     return dav_config_register_keys(davconfig, ctx, load_key_file);
 }
 
+DavConfig* load_config_file(void) {
+    char *file = util_concat_path(ENV_HOME, ".dav/config.xml");
+    
+    struct stat s;
+    if(stat(file, &s)) {
+        switch(errno) {
+            case ENOENT: {
+                davconfig = dav_config_new(NULL);
+                return NULL;
+            }
+            default: {
+                perror("Cannot load config.xml");
+            }
+        }
+        return NULL;
+    }
+    
+    cxmutstr config_content = config_load_file(file);
+    int config_error;
+    DavConfig *cfg = dav_config_load(config_content, &config_error);
+    free(config_content.ptr);
+    free(file);
+    return cfg;
+}
+
+
+void set_config(DavConfig *cfg) {
+    davconfig = cfg;
+}
+
 DavConfig* get_config(void) {
     return davconfig;
 }
--- a/application/config.h	Wed Oct 23 21:46:43 2024 +0200
+++ b/application/config.h	Sun Oct 27 18:24:37 2024 +0100
@@ -52,6 +52,9 @@
 cxmutstr config_load_file(const char *path);
     
 int load_config(DavContext *ctx);
+DavConfig* load_config_file(void);
+
+void set_config(DavConfig *cfg);
 DavConfig* get_config(void);
 int store_config(void);
 void free_config(void);
@@ -61,6 +64,7 @@
 PwdStore* get_pwdstore(void);
 int pwdstore_save(PwdStore *pwdstore);
 
+
 int get_stored_credentials(char *credid, char **user, char **password);
 int get_location_credentials(DavCfgRepository *repo, const char *path, char **user, char **password);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/application/settings.c	Sun Oct 27 18:24:37 2024 +0100
@@ -0,0 +1,537 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2024 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "settings.h"
+
+#include <cx/list.h>
+#include <libidav/utils.h>
+
+
+#define SETTINGS_STATE_REPOLIST_SELECTED 1
+
+#define SETTINGS_STATE_REPO_ENCRYPTION 20
+
+
+static void repolist_activate(UiEvent *event, void *userdata) {
+    UiListSelection *selection = event->eventdata;
+    SettingsWindow *settings = event->window;
+    settings_edit_repository(settings, selection->rows[0]);
+}
+
+static void repolist_selection(UiEvent *event, void *userdata) {
+    UiListSelection *selection = event->eventdata;
+    SettingsWindow *settings = event->window;
+    if(selection->count > 0) {
+        ui_set_group(event->obj->ctx, SETTINGS_STATE_REPOLIST_SELECTED);
+        settings->selected_repo = selection->rows[0];
+    } else {
+        ui_unset_group(event->obj->ctx, SETTINGS_STATE_REPOLIST_SELECTED);
+    }
+}
+
+static void repolist_edit(UiEvent *event, void *userdata) {
+    SettingsWindow *settings = event->window;
+    if(settings->selected_repo >= 0) {
+        settings_edit_repository(settings, settings->selected_repo);
+    }
+}
+
+static void repolist_remove(UiEvent *event, void *userdata) {
+    // TODO
+}
+
+static void editrepo_go_back(UiEvent *event, void *userdata) {
+    SettingsWindow *settings = event->window;
+    settings_store_repository(settings);
+    ui_set(settings->repo_tabview, 0);
+}
+
+
+void settings_ok(UiEvent *event, void *userdata) {
+    SettingsWindow *settings = event->window;
+    settings->config_saved = TRUE;
+    // save any changed settings
+    settings_store_repository(settings);
+    
+    set_config(settings->config); // TODO: free old config
+    if(store_config()) {
+        ui_dialog(event->obj, .title = "Error", .content = "Cannot store settings", .closebutton_label = "OK");
+    }
+    application_update_repolist(get_application());
+    ui_close(event->obj);
+}
+
+void settings_cancel(UiEvent *event, void *userdata) {
+    SettingsWindow *settings = event->window;
+    if(settings->config_saved) {
+        // function was called as context closefunc by ui_close
+        // in settings_ok
+        // don't free anything
+        return;
+    }
+    dav_config_free(settings->config);
+    if(userdata) {
+        ui_close(event->obj);
+    }
+}
+
+void settings_window_open() {
+    DavConfig *config = load_config_file();
+    if(!config) {
+        return;
+    }
+    
+    UiObject *obj = ui_simple_window("Settings", NULL);
+    ui_context_closefunc(obj->ctx, settings_cancel, NULL);
+    SettingsWindow *wdata = ui_malloc(obj->ctx, sizeof(SettingsWindow));
+    wdata->config = config;
+    obj->window = wdata;
+    settings_init(obj, wdata);
+    
+    ui_tabview(obj) {
+        ui_tab(obj, "General") {
+            ui_grid(obj, .margin = 10) {
+                ui_label(obj, .label = "TODO");
+            }
+        }
+        
+        ui_tab(obj, "Repositories") {
+            
+            ui_tabview(obj, .value = wdata->repo_tabview, .margin = 10, .tabview = UI_TABVIEW_INVISIBLE) {
+                ui_tab(obj, "list") {
+                    ui_grid(obj, .margin = 10, .columnspacing = 10, .rowspacing = 10) {
+                        ui_hbox(obj, .spacing = 10) {
+                            ui_button(obj, .label = "Add");
+                            ui_button(obj, .label = "Edit", .onclick = repolist_edit, .groups = UI_GROUPS(SETTINGS_STATE_REPOLIST_SELECTED));
+                            ui_button(obj, .label = "Remove", .groups = UI_GROUPS(SETTINGS_STATE_REPOLIST_SELECTED));
+                        }
+                        ui_newline(obj);
+                        
+                        UiModel* model = ui_model(obj->ctx, UI_STRING, "Name", UI_STRING, "URL", UI_STRING, "User", UI_STRING, "Encrypted", -1);
+                        model->getvalue = (ui_getvaluefunc) settings_repolist_getvalue;
+                        ui_table(obj,
+                                .model = model,
+                                .list = wdata->repos,
+                                .multiselection = FALSE,
+                                .onactivate = repolist_activate,
+                                .onselection = repolist_selection,
+                                .vexpand = TRUE, .hexpand = TRUE, .colspan = 3);
+                    }
+                }
+                
+                ui_tab(obj, "repo") {
+                    ui_vbox(obj, .margin = 10, .spacing = 10) {
+                        ui_hbox(obj, .fill = UI_OFF, .spacing = 4) {
+                            ui_button(obj, .icon = UI_ICON_GO_BACK, .onclick = editrepo_go_back);
+                            ui_label(obj, .label = "Repository List");
+                        }
+                        
+                        ui_scrolledwindow(obj, .hexpand = TRUE, .vexpand = TRUE, .subcontainer = UI_CONTAINER_NO_SUB) {
+                            ui_grid(obj, .margin = 10, .columnspacing = 10, .rowspacing = 10) {
+                                ui_llabel(obj, .label = "Name");
+                                ui_hbox0(obj) {
+                                    ui_textfield(obj, .value = wdata->repo_name, .width = 15);
+                                }
+                                ui_newline(obj);
+                                ui_llabel(obj, .label = "URL");
+                                ui_textfield(obj, .value = wdata->repo_url, .hexpand = TRUE);
+                                ui_newline(obj);
+                                
+                                ui_llabel(obj, .label = "Credentials", .style = UI_LABEL_STYLE_TITLE, .colspan = 2);
+                                ui_newline(obj);
+                                ui_hbox(obj, .spacing = 4, .colspan = 2) {
+                                    ui_combobox(obj, .list = wdata->repo_credentials);
+                                    ui_hbox0(obj) {
+                                        ui_button(obj, .label = "New Credentials");
+                                    }
+                                }
+                                ui_newline(obj);
+                                ui_expander(obj, .spacing = 10, .colspan = 2, .label = "Unencrypted User/Password", .margin = 10) {
+                                    ui_llabel(obj, .label = "Store the credentials unencrypted in the repository and not in the secret store", .style = UI_LABEL_STYLE_DIM);
+                                    ui_grid(obj, .rowspacing = 10, .columnspacing = 10, .fill = UI_OFF) {
+                                        ui_llabel(obj, .label = "User");
+                                        ui_textfield(obj, .value = wdata->repo_user, .width = 15);
+                                        ui_newline(obj);
+                                        
+                                        ui_llabel(obj, .label = "Password");
+                                        ui_passwordfield(obj, .value = wdata->repo_password, .width = 15);
+                                    }
+                                }
+                                ui_newline(obj);
+                                
+                                ui_llabel(obj, .label = "Encryption", .style = UI_LABEL_STYLE_TITLE, .colspan = 2);
+                                ui_newline(obj);
+                                
+                                ui_checkbox(obj, .label = "Enable client-side encryption", .value = wdata->repo_encryption, .colspan = 2, .enable_group = SETTINGS_STATE_REPO_ENCRYPTION);
+                                ui_newline(obj);
+                                ui_llabel(obj, .label = "Default key");
+                                ui_hbox(obj, .spacing = 4) {
+                                    ui_combobox(obj, .list = wdata->repo_keys, .groups = UI_GROUPS(SETTINGS_STATE_REPO_ENCRYPTION));
+                                    ui_button(obj, .label = "Generate Key", .groups = UI_GROUPS(SETTINGS_STATE_REPO_ENCRYPTION));
+                                }
+                                ui_newline(obj);
+                                
+                                ui_llabel(obj, .label = "TLS", .style = UI_LABEL_STYLE_TITLE, .colspan = 2);
+                                ui_newline(obj);
+                                
+                                ui_llabel(obj, .label = "Cert Path");
+                                ui_hbox0(obj) {
+                                    ui_textfield(obj, .value = wdata->repo_cacert, .width = 15);
+                                }
+                                ui_newline(obj);
+                                
+                                ui_llabel(obj, .label = "TLS Version");
+                                ui_hbox0(obj) {
+                                    ui_combobox(obj, .list = wdata->repo_tls_versions);
+                                }
+                                ui_newline(obj);
+                                ui_checkbox(obj, .label = "Disable TLS verification", .value = wdata->repo_disable_verification, .colspan = 2);
+                            }
+                        }
+                    }
+                }
+            }
+            
+            
+        }
+        
+        ui_tab(obj, "Sync Directories") {
+            ui_grid(obj, .margin = 10) {
+                ui_label(obj, .label = "TODO");
+            }
+        }
+        
+        ui_tab(obj, "Credentials") {
+            ui_grid(obj, .margin = 10) {
+                ui_label(obj, .label = "TODO");
+            }
+        }
+        
+        ui_tab(obj, "Keys") {
+            ui_grid(obj, .margin = 10) {
+                ui_label(obj, .label = "TODO");
+            }
+        }
+        
+        ui_tab(obj, "Properties") {
+            ui_grid(obj, .margin = 10) {
+                ui_label(obj, .label = "TODO");
+            }
+        }
+    }
+    
+    ui_hbox(obj, .fill = UI_OFF, .margin = 10) {
+        ui_button(obj, .label = "Cancel", .onclick = settings_cancel, .onclickdata = "close");
+        ui_label(obj, .fill = UI_ON);
+        ui_button(obj, .label = "Save", .onclick = settings_ok);
+    }
+    
+    
+    ui_show(obj);
+}
+
+void settings_init(UiObject *obj, SettingsWindow *settings) {
+    settings->repos = ui_list_new(obj->ctx, NULL);
+    settings->repo_tabview = ui_int_new(obj->ctx, NULL);
+    
+    settings->repo_name = ui_string_new(obj->ctx, NULL);
+    settings->repo_url = ui_string_new(obj->ctx, NULL);
+    settings->repo_user = ui_string_new(obj->ctx, NULL);
+    settings->repo_password = ui_string_new(obj->ctx, NULL);
+    settings->repo_cacert = ui_string_new(obj->ctx, NULL);
+    settings->repo_credentials = ui_list_new(obj->ctx, NULL);
+    settings->repo_keys = ui_list_new(obj->ctx, NULL);
+    settings->repo_tls_versions = ui_list_new(obj->ctx, NULL);
+    settings->repo_encryption = ui_int_new(obj->ctx, NULL);
+    settings->repo_disable_verification = ui_int_new(obj->ctx, NULL);
+    
+    ui_list_append(settings->repo_tls_versions, "Default");
+    ui_list_append(settings->repo_tls_versions, "TLSv1.3");
+    ui_list_append(settings->repo_tls_versions, "TLSv1.2");
+    ui_list_append(settings->repo_tls_versions, "TLSv1.1");
+    ui_list_append(settings->repo_tls_versions, "TLSv1.0");
+    
+    // load some list values, that can be reused
+    settings_update_repolist(settings);
+    settings_reload_keys(settings);
+    settings_reload_credentials(settings);
+    
+    settings->selected_repo = -1;
+}
+
+#define SETTINGS_SET_STRING(str, setting) if(setting.value.ptr) ui_set(str, setting.value.ptr);
+
+void settings_edit_repository(SettingsWindow *settings, int repo_index) {
+    DavCfgRepository *repo = ui_list_get(settings->repos, repo_index);
+    if(!repo) {
+        fprintf(stderr, "Error: cannot get repository at index %d\n", repo_index);
+        return;
+    }
+    settings->selected_repo = repo_index;
+    
+    // load plain string values
+    SETTINGS_SET_STRING(settings->repo_name, repo->name);
+    SETTINGS_SET_STRING(settings->repo_url, repo->url);
+    SETTINGS_SET_STRING(settings->repo_cacert, repo->cert);
+    SETTINGS_SET_STRING(settings->repo_user, repo->user);
+    // load decrypted password
+    if(repo->password.value.ptr) {
+        char *decoded_pw = util_base64decode(repo->password.value.ptr);
+        ui_set(settings->repo_password, decoded_pw);
+        size_t decoded_pw_len = strlen(decoded_pw);
+        memset(decoded_pw, 0, decoded_pw_len);
+        free(decoded_pw);
+    }
+    
+    // select credentials dropdown value
+    CxList *cred = settings->repo_credentials->data;
+    cred->collection.cmpfunc = (cx_compare_func)strcmp;
+    ssize_t cred_index = repo->stored_user.value.ptr ? cxListFind(cred, repo->stored_user.value.ptr) : 0;
+    if(cred_index > 0) {
+        ui_list_setselection(settings->repo_credentials, cred_index);
+    } else {
+        ui_list_setselection(settings->repo_credentials, 0);
+    }
+    
+    // load encryption value and default key value
+    ui_set(settings->repo_encryption, repo->full_encryption.value);
+    CxList *keys = settings->repo_keys->data;
+    keys->collection.cmpfunc = (cx_compare_func)strcmp;
+    ssize_t key_index = repo->default_key.value.ptr ? cxListFind(keys, repo->default_key.value.ptr) : 0;
+    if(key_index > 0) {
+        ui_list_setselection(settings->repo_keys, key_index);
+    } else {
+        ui_list_setselection(settings->repo_keys, 0);
+    }
+    
+    // select tls version from dropdown menu
+    CxList *tlsVersions = settings->repo_tls_versions->data;
+    tlsVersions->collection.cmpfunc = (cx_compare_func)strcmp;
+    const char *tls_str = dav_tlsversion2str(repo->ssl_version.value);
+    if(!tls_str) tls_str = "";
+    ssize_t tlsv_index = cxListFind(tlsVersions, tls_str);
+    if(tlsv_index > 0) {
+        ui_list_setselection(settings->repo_tls_versions, tlsv_index);
+    } else {
+        ui_list_setselection(settings->repo_tls_versions, 0);
+    }
+    
+    if(!repo->verification.value) {
+        ui_set(settings->repo_disable_verification, TRUE);
+    }
+    
+    
+    // switch to editing tab
+    ui_set(settings->repo_tabview, 1);
+}
+
+/*
+ * sets a config value
+ * if new_value is null, the xml node is removed
+ */
+static void cfg_string_set_value_or_remove(DavConfig *config, CfgString *str, xmlNode *parent, cxstring new_value, const char *nodename) {
+    if(new_value.length == 0) {
+        new_value.ptr = NULL;
+    }
+    dav_cfg_string_set_value(config, str, parent, new_value, nodename);
+    if(!new_value.ptr) {
+        dav_cfg_string_remove(str);
+    }
+}
+
+/*
+ * return the selected list value, if it is not the default value at index 0
+ */
+static cxstring default_list_get_value(UiList *list) {
+    cxstring ret = { NULL, 0 };
+    UiListSelection sel = ui_list_getselection(list);
+    if(sel.count > 0) {
+        int index = sel.rows[0];
+        if(index > 0) {
+            ret = cx_str(ui_list_get(list, index));
+        }        
+        free(sel.rows);
+    }
+    return ret;
+}
+
+void settings_store_repository(SettingsWindow *settings) {
+    DavConfig *config = settings->config;
+    DavCfgRepository *repo = ui_list_get(settings->repos, settings->selected_repo);
+    if(!repo) {
+        fprintf(stderr, "Error: cannot get repository at index %d\n", settings->selected_repo);
+        return;
+    }
+    
+    // always store name/url nodes
+    dav_cfg_string_set_value(config, &repo->name, repo->node, cx_str(ui_get(settings->repo_name)), "name");
+    dav_cfg_string_set_value(config, &repo->url, repo->node, cx_str(ui_get(settings->repo_url)), "url");
+    // store user of remove node, if no user is configured
+    cfg_string_set_value_or_remove(config, &repo->user, repo->node, cx_str(ui_get(settings->repo_user)), "user");
+    // store cert or remove node
+    cfg_string_set_value_or_remove(config, &repo->cert, repo->node, cx_str(ui_get(settings->repo_cacert)), "cert");
+    
+    
+    // store password or remove node, if no password is configured
+    char *pw = ui_get(settings->repo_password);
+    size_t pwlen = strlen(pw);
+    if(pwlen > 0) {
+        char *pwenc = util_base64encode(pw, pwlen);
+        memset(pw, 0, pwlen);
+        dav_cfg_string_set_value(config, &repo->password, repo->node, cx_str(pwenc), "password");
+        free(pwenc);
+    } else {
+        // set password 0 NULL and remove password config node
+        cfg_string_set_value_or_remove(config, &repo->password, repo->node, cx_strn(NULL, 0), "password");
+    }
+    
+    // get the selected credentials and set "stored-user"
+    // remove node if not needed
+    cxstring stored_user = default_list_get_value(settings->repo_credentials);
+    cfg_string_set_value_or_remove(config, &repo->stored_user, repo->node, stored_user, "stored-user");
+    
+    // adjust full-encryption node if necessary
+    int encryption = ui_get(settings->repo_encryption);
+    if(encryption || repo->full_encryption.node) {
+        dav_cfg_bool_set_value(config, &repo->full_encryption, repo->node, encryption, "full-encryption");
+    }
+    
+    // set default-key
+    cxstring key = default_list_get_value(settings->repo_keys);
+    cfg_string_set_value_or_remove(config, &repo->default_key, repo->node, key, "default-key");
+    
+    // if disable_verification is enabled, set repo verification to false
+    // otherwise remove the node, because verification is enabled by default
+    int disable_verification = ui_get(settings->repo_disable_verification);
+    if(disable_verification) {
+        dav_cfg_bool_set_value(config, &repo->verification, repo->node, !disable_verification, "verification");
+    } else {
+        dav_cfg_bool_remove(&repo->verification);
+    }
+    
+    // set tls version if configured
+    cxstring tlsversion_str = default_list_get_value(settings->repo_tls_versions);
+    int tlsversion = dav_str2ssl_version(tlsversion_str.ptr);
+    if(tlsversion >= 0) {
+        dav_cfg_int_set_value(config, &repo->ssl_version, repo->node, tlsversion, "ssl-version");
+    } else {
+        dav_cfg_int_remove(&repo->ssl_version);
+    }
+    
+    settings->selected_repo = -1;
+}
+
+void settings_update_repolist(SettingsWindow *settings) {
+    DavConfig *config = settings->config;
+
+    ui_list_clear(settings->repos);
+
+    for (DavCfgRepository *repo = config->repositories; repo; repo = repo->next) {
+        ui_list_append(settings->repos, repo);
+    }
+}
+
+void* settings_repolist_getvalue(DavCfgRepository *repo, int col) {
+    switch(col) {
+        case 0: {
+            return repo->name.value.ptr;
+        }
+        case 1: {
+            return repo->url.value.ptr;
+        }
+        case 2: {
+            return repo->user.value.ptr;
+        }
+        case 3: {
+            return repo->full_encryption.value ? "yes" : "no";
+        }
+    }
+    return NULL;
+}
+
+void settings_reload_keys(SettingsWindow *settings) {
+    DavConfig *config = settings->config;
+    DavCfgKey *key = config->keys; 
+    ui_list_clear(settings->repo_keys);
+    ui_list_append(settings->repo_keys, "-");
+    while(key) {
+        if(key->name.value.ptr) {
+            ui_list_append(settings->repo_keys, key->name.value.ptr);
+        }
+        key = key->next;
+    }
+    if(settings->repo_keys->update) {
+        ui_list_update(settings->repo_keys);
+    }
+}
+
+const char* dav_tlsversion2str(int value) {
+    if(value == CURL_SSLVERSION_TLSv1) {
+        return "TLSv1";
+    } else if(value == CURL_SSLVERSION_SSLv2) {
+        return "SSLv2";
+    } else if(value == CURL_SSLVERSION_SSLv3) {
+        return "SSLv3";
+    }
+#if LIBCURL_VERSION_MAJOR * 1000 + LIBCURL_VERSION_MINOR >= 7034
+    else if(value == CURL_SSLVERSION_TLSv1_0) {
+        return "TLSv1.0";
+    } else if(value == CURL_SSLVERSION_TLSv1_1) {
+        return "TLSv1.1";
+    } else if(value == CURL_SSLVERSION_TLSv1_2) {
+        return "TLSv1.2";
+    }
+#endif
+#if LIBCURL_VERSION_MAJOR * 1000 + LIBCURL_VERSION_MINOR >= 7052
+    else if(value == CURL_SSLVERSION_TLSv1_3) {
+        return "TLSv1.3";
+    }
+#endif
+    return NULL;
+}
+
+void settings_reload_credentials(SettingsWindow *settings) {
+    PwdStore *pwd = get_pwdstore();
+    if(!pwd) {
+        return;
+    }
+    
+    ui_list_clear(settings->repo_credentials);
+    ui_list_append(settings->repo_credentials, "-");
+    
+    CxIterator i = cxListIterator(pwd->noloc);
+    cx_foreach(PwdIndexEntry*, entry, i) {
+        ui_list_append(settings->repo_credentials, entry->id);
+    }
+    
+    if(settings->repo_credentials->update) {
+        ui_list_update(settings->repo_credentials);
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/application/settings.h	Sun Oct 27 18:24:37 2024 +0100
@@ -0,0 +1,102 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2024 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef IDAV_SETTINGS_H
+#define IDAV_SETTINGS_H
+
+#include "application.h"
+#include "config.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+    
+
+typedef struct SettingsWindow {
+    DavConfig *config;
+    DavBool config_saved;
+    
+    UiList *repos;
+    UiInteger *repo_tabview;
+    
+    UiString *repo_name;
+    UiString *repo_url;
+    UiList *repo_credentials;
+    UiString *repo_user;
+    UiString *repo_password;
+    UiInteger *repo_encryption;
+    UiList *repo_keys;
+    UiList *repo_tls_versions;
+    UiString *repo_cacert;
+    UiInteger *repo_disable_verification;
+    
+    int selected_repo;
+} SettingsWindow;
+    
+void settings_window_open();
+
+void settings_ok(UiEvent *event, void *userdata);
+
+void settings_cancel(UiEvent *event, void *userdata);
+
+void settings_init(UiObject *obj, SettingsWindow *settings);
+
+/*
+ * Open repository edit page for the repository settings->repos[repo_index]
+ * This function will set settings->selected_repo to repo_index
+ */
+void settings_edit_repository(SettingsWindow *settings, int repo_index);
+
+/*
+ * Store settings from the settings window into the DavCfgRepository object.
+ * 
+ * This functions unsets settings->selected_repo and should only be called
+ * when switching back to the repo list or closing the settings window
+ */
+void settings_store_repository(SettingsWindow *settings);
+
+
+void settings_update_repolist(SettingsWindow *settings);
+
+void* settings_repolist_getvalue(DavCfgRepository *repo, int col);
+
+void settings_reload_keys(SettingsWindow *settings);
+
+const char* dav_tlsversion2str(int value);
+
+void settings_reload_credentials(SettingsWindow *settings);
+
+
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* IDAV_SETTINGS_H */
+
--- a/application/window.c	Wed Oct 23 21:46:43 2024 +0200
+++ b/application/window.c	Sun Oct 27 18:24:37 2024 +0100
@@ -73,7 +73,7 @@
     // main content
     UiModel* model = ui_model(obj->ctx, UI_ICON_TEXT, "Name", UI_STRING, "Type", UI_STRING_FREE, "Last Modified", UI_STRING_FREE, "Size", -1);
     model->getvalue = (ui_getvaluefunc) window_resource_table_getvalue;
-    ui_table(obj, .fill = UI_ON, .model = model, .onactivate = action_list_activate, .ondrop = action_dnd_drop, .varname = "reslist");
+    ui_table(obj, .fill = UI_ON, .model = model, .onactivate = action_list_activate, .ondrop = action_dnd_drop, .varname = "reslist", .multiselection = TRUE);
 
     // status bar
 
--- a/libidav/config.c	Wed Oct 23 21:46:43 2024 +0200
+++ b/libidav/config.c	Sun Oct 27 18:24:37 2024 +0100
@@ -79,7 +79,7 @@
 static int load_secretstore(DavConfig *config, xmlNode *node);
 
 
-int dav_cfg_string_set_value(DavConfig *config, CfgString *str, xmlNode *node) {
+int dav_cfg_string_set_node_value(DavConfig *config, CfgString *str, xmlNode *node) {
     str->node = node;
     char *value = util_xml_get_text(node);
     if(value) {
@@ -91,12 +91,111 @@
     }
 }
 
-void dav_cfg_bool_set_value(DavConfig *config, CfgBool *cbool, xmlNode *node) {
+void dav_cfg_bool_set_node_value(DavConfig *config, CfgBool *cbool, xmlNode *node) {
     cbool->node = node;
     char *value = util_xml_get_text(node);
     cbool->value = util_getboolean(value);
 }
 
+static void set_xml_content(xmlNode *node, const char *content) {
+    xmlNode *child = node->children;
+    while(child) {
+        xmlNode *next = child->next;
+        xmlUnlinkNode(child);
+        xmlFreeNode(child);
+        child = next;
+    }
+    
+    if(content) {
+        xmlChar *encoded = xmlEncodeSpecialChars(node->doc, (const xmlChar*)content);
+        if(encoded) {
+            xmlNodeSetContent(node, encoded);
+            xmlFree(encoded);
+        }
+    }
+}
+
+void dav_cfg_string_set_value(DavConfig *config, CfgString *str, xmlNode *parent, cxstring new_value, const char *nodename) {
+    if(str->value.ptr) {
+        cxFree(config->mp->allocator, str->value.ptr);
+    }
+    if(new_value.ptr) {
+        str->value = cx_strdup_a(config->mp->allocator, new_value);
+    } else {
+        str->value = cx_mutstrn(NULL, 0);
+    }
+    
+    if(!str->node) {
+        str->node = xmlNewNode(NULL, (const xmlChar*) nodename);
+        xmlAddChild(parent, str->node);
+    }
+    set_xml_content(str->node, new_value.ptr);
+}
+
+void dav_cfg_bool_set_value(DavConfig *config, CfgBool *cbool, xmlNode *parent, DavBool new_value, const char *nodename) {
+    const char *content = new_value ? "true" : "false";
+    cbool->value = new_value;
+    if(!cbool->node) {
+        cbool->node = xmlNewNode(NULL, (const xmlChar*) nodename);
+        xmlAddChild(parent, cbool->node);
+    }
+    set_xml_content(cbool->node, content);
+}
+
+void dav_cfg_int_set_value(DavConfig *config, CfgInt *cint, xmlNode *parent, int64_t new_value, const char *nodename) {
+    char content[32];
+    snprintf(content, 32, "%" PRId64, new_value);
+    cint->value = new_value;
+    if(!cint->node) {
+        cint->node = xmlNewNode(NULL, (const xmlChar*) nodename);
+        xmlAddChild(parent, cint->node);
+    }
+    set_xml_content(cint->node, content);
+}
+
+void dav_cfg_uint_set_value(DavConfig *config, CfgUInt *cint, xmlNode *parent, uint64_t new_value, const char *nodename) {
+    char content[32];
+    snprintf(content, 32, "%" PRIu64, new_value);
+    cint->value = new_value;
+    if(!cint->node) {
+        cint->node = xmlNewNode(NULL, (const xmlChar*) nodename);
+        xmlAddChild(parent, cint->node);
+    }
+    set_xml_content(cint->node, content);
+}
+
+void dav_cfg_string_remove(CfgString *str) {
+    if(str->node) {
+        xmlUnlinkNode(str->node);
+        xmlFreeNode(str->node);
+        str->node = NULL;
+    }
+}
+
+void dav_cfg_bool_remove(CfgBool *cbool) {
+    if(cbool->node) {
+        xmlUnlinkNode(cbool->node);
+        xmlFreeNode(cbool->node);
+        cbool->node = NULL;
+    }
+}
+
+void dav_cfg_int_remove(CfgInt *cint) {
+    if(cint->node) {
+        xmlUnlinkNode(cint->node);
+        xmlFreeNode(cint->node);
+        cint->node = NULL;
+    }
+}
+
+void dav_cfg_uint_remove(CfgUInt *cint) {
+    if(cint->node) {
+        xmlUnlinkNode(cint->node);
+        xmlFreeNode(cint->node);
+        cint->node = NULL;
+    }
+}
+
 
 DavConfig* dav_config_new(xmlDoc *doc) {
     CxMempool *cfg_mp = cxMempoolCreate(128, NULL);
@@ -212,55 +311,37 @@
     }
     
     if(xstreq(key, "name")) {
-        dav_cfg_string_set_value(config, &repo->name, node);
+        dav_cfg_string_set_node_value(config, &repo->name, node);
     } else if(xstreq(key, "url")) {
-        dav_cfg_string_set_value(config, &repo->url, node);
+        dav_cfg_string_set_node_value(config, &repo->url, node);
     } else if(xstreq(key, "user")) {
-        dav_cfg_string_set_value(config, &repo->user, node);
+        dav_cfg_string_set_node_value(config, &repo->user, node);
     } else if(xstreq(key, "password")) {
-        dav_cfg_string_set_value(config, &repo->password, node);
+        dav_cfg_string_set_node_value(config, &repo->password, node);
     } else if(xstreq(key, "stored-user")) {
-        dav_cfg_string_set_value(config, &repo->stored_user, node);
+        dav_cfg_string_set_node_value(config, &repo->stored_user, node);
     } else if(xstreq(key, "default-key")) {
-        dav_cfg_string_set_value(config, &repo->default_key, node);
+        dav_cfg_string_set_node_value(config, &repo->default_key, node);
     } else if(xstreq(key, "full-encryption")) {
-        dav_cfg_bool_set_value(config, &repo->full_encryption, node);
+        dav_cfg_bool_set_node_value(config, &repo->full_encryption, node);
     } else if(xstreq(key, "content-encryption")) {
-        dav_cfg_bool_set_value(config, &repo->content_encryption, node);
+        dav_cfg_bool_set_node_value(config, &repo->content_encryption, node);
     } else if(xstreq(key, "decrypt-content")) {
-        dav_cfg_bool_set_value(config, &repo->decrypt_content, node);
+        dav_cfg_bool_set_node_value(config, &repo->decrypt_content, node);
     } else if(xstreq(key, "decrypt-name")) {
-        dav_cfg_bool_set_value(config, &repo->decrypt_name, node);
+        dav_cfg_bool_set_node_value(config, &repo->decrypt_name, node);
     } else if(xstreq(key, "cert")) {
-        dav_cfg_string_set_value(config, &repo->cert, node);
+        dav_cfg_string_set_node_value(config, &repo->cert, node);
     } else if(xstreq(key, "verification")) {
-        dav_cfg_bool_set_value(config, &repo->verification, node);
+        dav_cfg_bool_set_node_value(config, &repo->verification, node);
     } else if(xstreq(key, "ssl-version")) {
         repo->ssl_version.node = node;
-        if(xstrEQ(value, "TLSv1")) {
-            repo->ssl_version.value = CURL_SSLVERSION_TLSv1;
-        } else if(xstrEQ(value, "SSLv2")) {
-            repo->ssl_version.value = CURL_SSLVERSION_SSLv2;
-        } else if(xstrEQ(value, "SSLv3")) {
-            repo->ssl_version.value = CURL_SSLVERSION_SSLv3;
-        }
-#if LIBCURL_VERSION_MAJOR * 1000 + LIBCURL_VERSION_MINOR >= 7034
-        else if(xstrEQ(value, "TLSv1.0")) {
-            repo->ssl_version.value = CURL_SSLVERSION_TLSv1_0;
-        } else if(xstrEQ(value, "TLSv1.1")) {
-            repo->ssl_version.value = CURL_SSLVERSION_TLSv1_1;
-        } else if(xstrEQ(value, "TLSv1.2")) {
-            repo->ssl_version.value = CURL_SSLVERSION_TLSv1_2;
-        }
-#endif
-#if LIBCURL_VERSION_MAJOR * 1000 + LIBCURL_VERSION_MINOR >= 7052
-        else if(xstrEQ(value, "TLSv1.3")) {
-            repo->ssl_version.value = CURL_SSLVERSION_TLSv1_3;
-        }
-#endif
-        else {
+        int ssl_version = dav_str2ssl_version((const char*)value);
+        if(ssl_version == -1) {
             print_warning(lineno, "unknown ssl version: %s\n", value);
             repo->ssl_version.value = CURL_SSLVERSION_DEFAULT;
+        } else {
+            repo->ssl_version.value = ssl_version;
         }
     } else if(xstreq(key, "authmethods")) {
         repo->authmethods.node = node;
@@ -295,6 +376,31 @@
     return 0;
 }
 
+int dav_str2ssl_version(const char *value) {
+    if(xstrEQ(value, "TLSv1")) {
+        return CURL_SSLVERSION_TLSv1;
+    } else if(xstrEQ(value, "SSLv2")) {
+        return CURL_SSLVERSION_SSLv2;
+    } else if(xstrEQ(value, "SSLv3")) {
+        return CURL_SSLVERSION_SSLv3;
+    }
+#if LIBCURL_VERSION_MAJOR * 1000 + LIBCURL_VERSION_MINOR >= 7034
+    else if(xstrEQ(value, "TLSv1.0")) {
+        return CURL_SSLVERSION_TLSv1_0;
+    } else if(xstrEQ(value, "TLSv1.1")) {
+        return CURL_SSLVERSION_TLSv1_1;
+    } else if(xstrEQ(value, "TLSv1.2")) {
+        return CURL_SSLVERSION_TLSv1_2;
+    }
+#endif
+#if LIBCURL_VERSION_MAJOR * 1000 + LIBCURL_VERSION_MINOR >= 7052
+    else if(xstrEQ(value, "TLSv1.3")) {
+        return CURL_SSLVERSION_TLSv1_3;
+    }
+#endif
+    return -1;
+}
+
 static int load_repository(
         DavConfig *config,
         DavCfgRepository **list_begin,
@@ -520,9 +626,9 @@
     while(node) {
         if(node->type == XML_ELEMENT_NODE) {
             if(xstreq(node->name, "name")) {
-                dav_cfg_string_set_value(config, &key->name, node);
+                dav_cfg_string_set_node_value(config, &key->name, node);
             } else if(xstreq(node->name, "file")) {
-                dav_cfg_string_set_value(config, &key->file, node);
+                dav_cfg_string_set_node_value(config, &key->file, node);
             } else if(xstreq(node->name, "type")) {
                 const char *value = util_xml_get_text(node);
                 key->type_node = node;
@@ -613,13 +719,13 @@
         if(node->type == XML_ELEMENT_NODE) {
             int reportmissingvalue = 0;
             if(xstreq(node->name, "url")) {
-                reportmissingvalue = dav_cfg_string_set_value(config, &proxy->url, node);
+                reportmissingvalue = dav_cfg_string_set_node_value(config, &proxy->url, node);
             } else if(xstreq(node->name, "user")) {
-                reportmissingvalue = dav_cfg_string_set_value(config, &proxy->user, node);
+                reportmissingvalue = dav_cfg_string_set_node_value(config, &proxy->user, node);
             } else if(xstreq(node->name, "password")) {
-                reportmissingvalue = dav_cfg_string_set_value(config, &proxy->password, node);
+                reportmissingvalue = dav_cfg_string_set_node_value(config, &proxy->password, node);
             } else if(xstreq(node->name, "no")) {
-                reportmissingvalue = dav_cfg_string_set_value(config, &proxy->noproxy, node);
+                reportmissingvalue = dav_cfg_string_set_node_value(config, &proxy->noproxy, node);
             } else {
                 proxy->unknown_elements++;
             }
@@ -724,9 +830,9 @@
     while(node) {
         if(node->type == XML_ELEMENT_NODE) {
             if(xstreq(node->name, "unlock-command")) {
-                dav_cfg_string_set_value(config, &config->secretstore->unlock_cmd, node);
+                dav_cfg_string_set_node_value(config, &config->secretstore->unlock_cmd, node);
             } else if(xstreq(node->name, "lock-command")) {
-                dav_cfg_string_set_value(config, &config->secretstore->lock_cmd, node);
+                dav_cfg_string_set_node_value(config, &config->secretstore->lock_cmd, node);
             }
         }
         node = node->next;
--- a/libidav/config.h	Wed Oct 23 21:46:43 2024 +0200
+++ b/libidav/config.h	Sun Oct 27 18:24:37 2024 +0100
@@ -178,8 +178,20 @@
 void dav_repository_set_auth(DavConfig *config, DavCfgRepository *repo, cxstring user, cxstring password);
 cxmutstr dav_repository_get_decodedpassword(DavCfgRepository *repo);
 
-int dav_cfg_string_set_value(DavConfig *config, CfgString *str, xmlNode *node);
-void dav_cfg_bool_set_value(DavConfig *config, CfgBool *cbool, xmlNode *node);
+int dav_str2ssl_version(const char *str);
+
+int dav_cfg_string_set_node_value(DavConfig *config, CfgString *str, xmlNode *node);
+void dav_cfg_bool_set_node_value(DavConfig *config, CfgBool *cbool, xmlNode *node);
+
+void dav_cfg_string_set_value(DavConfig *config, CfgString *str, xmlNode *parent, cxstring new_value, const char *nodename);
+void dav_cfg_bool_set_value(DavConfig *config, CfgBool *cbool, xmlNode *parent, DavBool new_value, const char *nodename);
+void dav_cfg_int_set_value(DavConfig *config, CfgInt *cint, xmlNode *parent, int64_t new_value, const char *nodename);
+void dav_cfg_uint_set_value(DavConfig *config, CfgUInt *cint, xmlNode *parent, uint64_t new_value, const char *nodename);
+
+void dav_cfg_string_remove(CfgString *str);
+void dav_cfg_bool_remove(CfgBool *cbool);
+void dav_cfg_int_remove(CfgInt *cint);
+void dav_cfg_uint_remove(CfgUInt *cint);
 
 DavCfgRepository* dav_config_get_repository(DavConfig *config, cxstring name);
 DavCfgRepository* dav_config_url2repo(DavConfig *config, const char *url, char **path);
--- a/libidav/pwdstore.c	Wed Oct 23 21:46:43 2024 +0200
+++ b/libidav/pwdstore.c	Sun Oct 27 18:24:37 2024 +0100
@@ -377,12 +377,13 @@
     }
     PwdIndexEntry *newentry = malloc(sizeof(PwdIndexEntry));
     newentry->id = id;
-    if(locations) {
+    if(cxListSize(locations) > 0) {
         newentry->locations = locations;
         cxListAdd(p->locations, newentry);
     } else {
         newentry->locations = NULL;
         cxListAdd(p->noloc, newentry);
+        cxListDestroy(locations);
     }
     cxMapPut(p->index, cx_hash_key_str(id), newentry);
 }
--- a/ui/common/context.c	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/common/context.c	Sun Oct 27 18:24:37 2024 +0100
@@ -209,7 +209,7 @@
 UiVar* uic_create_value_var(UiContext* ctx, void* value) {
     UiVar *var = (UiVar*)ui_malloc(ctx, sizeof(UiVar));
     var->from = NULL;
-    var->from_ctx = NULL;
+    var->from_ctx = ctx;
     var->value = value;
     var->type = UI_VAR_SPECIAL;
     return var;
--- a/ui/common/types.c	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/common/types.c	Sun Oct 27 18:24:37 2024 +0100
@@ -501,6 +501,15 @@
     return (UiListSelection){ 0, NULL };
 }
 
+UIEXPORT void ui_list_setselection(UiList *list, int index) {
+    if (list->setselection && index >= 0) {
+        UiListSelection sel;
+        sel.count = 1;
+        sel.rows = &index;
+        list->setselection(list, sel);
+    }
+}
+
 UIEXPORT void ui_listselection_free(UiListSelection selection) {
     if (selection.rows) {
         free(selection.rows);
--- a/ui/gtk/button.c	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/gtk/button.c	Sun Oct 27 18:24:37 2024 +0100
@@ -151,6 +151,14 @@
     event->callback(&e, event->userdata);    
 }
 
+static void ui_togglebutton_enable_state_callback(GtkToggleButton *widget, UiEventData *event) {
+    if(gtk_toggle_button_get_active(widget)) {
+        ui_set_group(event->obj->ctx, event->value);
+    } else {
+        ui_unset_group(event->obj->ctx, event->value);
+    }
+}
+
 void ui_setup_togglebutton(
         UiObject *obj,
         GtkWidget *togglebutton,
@@ -159,7 +167,8 @@
         const char *varname,
         UiInteger *value,
         ui_callback onchange,
-        void *onchangedata)
+        void *onchangedata,
+        int enable_state)
 {
     if(label) {
         gtk_button_set_label(GTK_BUTTON(togglebutton), label);
@@ -175,7 +184,10 @@
             value,
             (ui_toggled_func)ui_toggled_callback,
             onchange,
-            onchangedata);
+            onchangedata,
+            (ui_toggled_func)ui_togglebutton_enable_state_callback,
+            enable_state
+            );
 }
 
 void ui_bind_togglebutton(
@@ -187,7 +199,9 @@
         UiInteger *value,
         void (*toggled_callback)(void*, void*),
         ui_callback onchange,
-        void *onchangedata)
+        void *onchangedata,
+        void (*enable_state_func)(void*, void*),
+        int enable_state)
 {
     UiObject* current = uic_current_obj(obj);
     UiVar* var = uic_widget_var(obj->ctx, current->ctx, value, varname, UI_VAR_INTEGER);
@@ -235,12 +249,41 @@
                 G_CALLBACK(ui_destroy_userdata),
                 event);
     }
+    
+    if(enable_state > 0) {
+        UiEventData *event = malloc(sizeof(UiEventData));
+        event->obj = obj;
+        event->userdata = NULL;
+        event->callback = NULL;
+        event->value = enable_state;
+        event->customdata = NULL;
+        
+        g_signal_connect(
+                widget,
+                "toggled",
+                G_CALLBACK(enable_state_func),
+                event);
+        g_signal_connect(
+                widget,
+                "destroy",
+                G_CALLBACK(ui_destroy_userdata),
+                event);
+    }
 }
 
 static UIWIDGET togglebutton_create(UiObject *obj, GtkWidget *widget, UiToggleArgs args) {
     UiObject* current = uic_current_obj(obj);
     
-    ui_setup_togglebutton(current, widget, args.label, args.icon, args.varname, args.value, args.onchange, args.onchangedata);
+    ui_setup_togglebutton(
+            current,
+            widget,
+            args.label,
+            args.icon,
+            args.varname,
+            args.value,
+            args.onchange,
+            args.onchangedata,
+            args.enable_group);
     ui_set_name_and_style(widget, args.name, args.style_class);
     ui_set_widget_groups(obj->ctx, widget, args.groups);
     
@@ -278,6 +321,14 @@
     event->callback(&e, event->userdata);    
 }
 
+static void ui_checkbox_enable_state(GtkCheckButton *widget, UiEventData *event) {
+    if(gtk_check_button_get_active(widget)) {
+        ui_set_group(event->obj->ctx, event->value);
+    } else {
+        ui_unset_group(event->obj->ctx, event->value);
+    }
+}
+
 UIWIDGET ui_checkbox_create(UiObject* obj, UiToggleArgs args) {
     UiObject* current = uic_current_obj(obj);
     
@@ -291,7 +342,9 @@
             args.value,
             (ui_toggled_func)ui_checkbox_callback,
             args.onchange,
-            args.onchangedata);
+            args.onchangedata,
+            (ui_toggled_func)ui_checkbox_enable_state,
+            args.enable_group);
     
     ui_set_name_and_style(widget, args.name, args.style_class);
     ui_set_widget_groups(obj->ctx, widget, args.groups);
--- a/ui/gtk/button.h	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/gtk/button.h	Sun Oct 27 18:24:37 2024 +0100
@@ -58,7 +58,8 @@
         const char *varname,
         UiInteger *value,
         ui_callback onchange,
-        void *onchangedata);
+        void *onchangedata,
+        int enable_state);
 
 void ui_bind_togglebutton(
         UiObject *obj,
@@ -69,7 +70,9 @@
         UiInteger *value,
         void (*toggled_callback)(void*, void*),
         ui_callback onchange,
-        void *onchangedata);
+        void *onchangedata,
+        void (*enable_state_func)(void*, void*),
+        int enable_state);
     
 // event wrapper
 void ui_button_clicked(GtkWidget *widget, UiEventData *event);
--- a/ui/gtk/container.c	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/gtk/container.c	Sun Oct 27 18:24:37 2024 +0100
@@ -68,6 +68,44 @@
 #endif
 }
 
+GtkWidget* ui_subcontainer_create(
+        UiSubContainerType type,
+        UiObject *newobj,
+        int spacing,
+        int columnspacing,
+        int rowspacing,
+        int margin)
+{
+    GtkWidget *sub = NULL;
+    GtkWidget *add = NULL;
+    switch(type) {
+        default: {
+            sub = ui_gtk_vbox_new(spacing);
+            add = ui_box_set_margin(sub, margin);
+            newobj->container = ui_box_container(newobj, sub, type);
+            newobj->widget = sub;
+            break;
+        }
+        case UI_CONTAINER_HBOX: {
+            sub = ui_gtk_hbox_new(spacing);
+            add = ui_box_set_margin(sub, margin);
+            newobj->container = ui_box_container(newobj, sub, type);
+            newobj->widget = sub;
+            break;
+        }
+        case UI_CONTAINER_GRID: {
+            sub = ui_create_grid_widget(columnspacing, rowspacing);
+            add = ui_box_set_margin(sub, margin);
+            newobj->container = ui_grid_container(newobj, sub);
+            newobj->widget = sub;
+            break;
+        }
+        case UI_CONTAINER_NO_SUB: {
+            break;
+        }
+    }
+    return add;
+}
 
 
 /* -------------------- Box Container -------------------- */
@@ -201,6 +239,41 @@
 }
 #endif
 
+UiContainer* ui_frame_container(UiObject *obj, GtkWidget *frame) {
+    UiContainer *ct = cxCalloc(
+            obj->ctx->allocator,
+            1,
+            sizeof(UiContainer));
+    ct->widget = frame;
+    ct->add = ui_frame_container_add;
+    return ct;
+}
+
+void ui_frame_container_add(UiContainer *ct, GtkWidget *widget, UiBool fill) {
+    FRAME_SET_CHILD(ct->widget, widget);
+}
+
+UiContainer* ui_expander_container(UiObject *obj, GtkWidget *expander) {
+    UiContainer *ct = cxCalloc(
+            obj->ctx->allocator,
+            1,
+            sizeof(UiContainer));
+    ct->widget = expander;
+    ct->add = ui_expander_container_add;
+    return ct;
+}
+
+void ui_expander_container_add(UiContainer *ct, GtkWidget *widget, UiBool fill) {
+    EXPANDER_SET_CHILD(ct->widget, widget);
+}
+
+void ui_scrolledwindow_container_add(UiContainer *ct, GtkWidget *widget, UiBool fill) {
+    // TODO: check if the widget implements GtkScrollable
+    SCROLLEDWINDOW_SET_CHILD(ct->widget, widget);
+    ui_reset_layout(ct->layout);
+    ct->current = widget;
+}
+
 UiContainer* ui_scrolledwindow_container(UiObject *obj, GtkWidget *scrolledwindow) {
     UiContainer *ct = cxCalloc(
             obj->ctx->allocator,
@@ -211,13 +284,6 @@
     return ct;
 }
 
-void ui_scrolledwindow_container_add(UiContainer *ct, GtkWidget *widget, UiBool fill) {
-    // TODO: check if the widget implements GtkScrollable
-    SCROLLEDWINDOW_SET_CHILD(ct->widget, widget);
-    ui_reset_layout(ct->layout);
-    ct->current = widget;
-}
-
 UiContainer* ui_tabview_container(UiObject *obj, GtkWidget *tabview) {
     UiTabViewContainer *ct = cxCalloc(
             obj->ctx->allocator,
@@ -318,6 +384,45 @@
     return widget;
 }
 
+UIWIDGET ui_frame_create(UiObject *obj, UiFrameArgs args) {
+    UiObject* current = uic_current_obj(obj);
+    UI_APPLY_LAYOUT1(current, args);
+    
+    GtkWidget *frame = gtk_frame_new(args.label);
+    UiObject *newobj = uic_object_new(obj, frame);
+    GtkWidget *sub = ui_subcontainer_create(args.subcontainer, newobj, args.spacing, args.columnspacing, args.rowspacing, args.margin);
+    if(sub) {
+        FRAME_SET_CHILD(frame, sub);
+    } else {
+        newobj->widget = frame;
+        newobj->container = ui_frame_container(obj, frame);
+    }
+    current->container->add(current->container, frame, FALSE);
+    uic_obj_add(obj, newobj);
+    
+    return frame;
+}
+
+UIEXPORT UIWIDGET ui_expander_create(UiObject *obj, UiFrameArgs args) {
+    UiObject* current = uic_current_obj(obj);
+    UI_APPLY_LAYOUT1(current, args);
+    
+    GtkWidget *expander = gtk_expander_new(args.label);
+    gtk_expander_set_expanded(GTK_EXPANDER(expander), args.isexpanded);
+    UiObject *newobj = uic_object_new(obj, expander);
+    GtkWidget *sub = ui_subcontainer_create(args.subcontainer, newobj, args.spacing, args.columnspacing, args.rowspacing, args.margin);
+    if(sub) {
+        EXPANDER_SET_CHILD(expander, sub);
+    } else {
+        newobj->widget = expander;
+        newobj->container = ui_expander_container(obj, expander);
+    }
+    current->container->add(current->container, expander, FALSE);
+    uic_obj_add(obj, newobj);
+    
+    return expander;
+}
+
 
 UIWIDGET ui_scrolledwindow_create(UiObject* obj, UiFrameArgs args) {
     UiObject* current = uic_current_obj(obj);
@@ -325,8 +430,18 @@
     
     GtkWidget *sw = SCROLLEDWINDOW_NEW();
     ui_set_name_and_style(sw, args.name, args.style_class);
+    GtkWidget *widget = ui_box_set_margin(sw, args.margin);
+    current->container->add(current->container, widget, TRUE);
+    
     UiObject *newobj = uic_object_new(obj, sw);
-    newobj->container = ui_scrolledwindow_container(obj, sw);
+    GtkWidget *sub = ui_subcontainer_create(args.subcontainer, newobj, args.spacing, args.columnspacing, args.rowspacing, args.margin);
+    if(sub) {
+        SCROLLEDWINDOW_SET_CHILD(sw, sub);
+    } else {
+        newobj->widget = sw;
+        newobj->container = ui_scrolledwindow_container(obj, sw);
+    }
+    
     uic_obj_add(obj, newobj);
     
     return sw;
--- a/ui/gtk/container.h	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/gtk/container.h	Sun Oct 27 18:24:37 2024 +0100
@@ -134,6 +134,14 @@
 GtkWidget* ui_gtk_vbox_new(int spacing);
 GtkWidget* ui_gtk_hbox_new(int spacing);
 
+GtkWidget* ui_subcontainer_create(
+        UiSubContainerType type,
+        UiObject *newobj,
+        int spacing,
+        int columnspacing,
+        int rowspacing,
+        int margin);
+
 UiContainer* ui_frame_container(UiObject *obj, GtkWidget *frame);
 void ui_frame_container_add(UiContainer *ct, GtkWidget *widget, UiBool fill);
 
@@ -147,6 +155,12 @@
 UiContainer* ui_grid_container(UiObject *obj, GtkWidget *grid);
 void ui_grid_container_add(UiContainer *ct, GtkWidget *widget, UiBool fill);
 
+UiContainer* ui_frame_container(UiObject *obj, GtkWidget *frame);
+void ui_frame_container_add(UiContainer *ct, GtkWidget *widget, UiBool fill);
+
+UiContainer* ui_expander_container(UiObject *obj, GtkWidget *expander);
+void ui_expander_container_add(UiContainer *ct, GtkWidget *widget, UiBool fill);
+
 UiContainer* ui_scrolledwindow_container(UiObject *obj, GtkWidget *scrolledwindow);
 void ui_scrolledwindow_container_add(UiContainer *ct, GtkWidget *widget, UiBool fill);
 
--- a/ui/gtk/display.c	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/gtk/display.c	Sun Oct 27 18:24:37 2024 +0100
@@ -35,6 +35,8 @@
 #include "../common/object.h"
 #include "../ui/display.h"
 
+#include <cx/printf.h>
+
 static void set_alignment(GtkWidget *widget, float xalign, float yalign) {
 #if GTK_MAJOR_VERSION >= 4 || (GTK_MAJOR_VERSION >= 3 && GTK_MINOR_VERSION >= 16)
     gtk_label_set_xalign(GTK_LABEL(widget), xalign);
@@ -47,7 +49,54 @@
 UIWIDGET ui_label_create(UiObject *obj, UiLabelArgs args) {
     UiObject* current = uic_current_obj(obj);
     
+    const char *css_class = NULL;
+    char *markup = NULL;
+    if(args.label) {
+        #if GTK_MAJOR_VERSION < 3
+            switch(args.style) {
+                case UI_LABEL_STYLE_DEFAULT: break;
+                case UI_LABEL_STYLE_TITLE: {
+                    cxmutstr m = cx_asprintf("<b>%s</b>", args.label);
+                    markup = m.ptr;
+                    args.label = NULL;
+                }
+                case UI_LABEL_STYLE_SUBTITLE: {
+                    break;
+                }
+                case UI_LABEL_STYLE_DIM: {
+                    break;
+                }
+            }
+#       else
+            switch(args.style) {
+                case UI_LABEL_STYLE_DEFAULT: break;
+                case UI_LABEL_STYLE_TITLE: {
+                    css_class = "ui_label_title";
+                    break;
+                }
+                case UI_LABEL_STYLE_SUBTITLE: {
+                    css_class = "subtitle";
+                    break;
+                }
+                case UI_LABEL_STYLE_DIM: {
+                    css_class = "dim-label";
+                    break;
+                }
+            }
+#       endif
+    }
+
+    
     GtkWidget *widget = gtk_label_new(args.label);
+    if(markup) {
+        gtk_label_set_markup(GTK_LABEL(widget), markup);
+        free(markup);
+    }
+    
+    if(css_class) {
+        WIDGET_ADD_CSS_CLASS(widget, css_class);
+    }
+    
     switch(args.align) {
         case UI_ALIGN_DEFAULT: break;
         case UI_ALIGN_LEFT: set_alignment(widget, 0, .5); break;
@@ -55,6 +104,7 @@
         case UI_ALIGN_CENTER: break; // TODO
     }
     
+
     UiVar* var = uic_widget_var(obj->ctx, current->ctx, args.value, args.varname, UI_VAR_STRING);
     if(var) {
         UiString* value = (UiString*)var->value;
--- a/ui/gtk/entry.c	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/gtk/entry.c	Sun Oct 27 18:24:37 2024 +0100
@@ -71,6 +71,7 @@
 #endif
     GtkWidget *spin = gtk_spin_button_new_with_range(min, max, args.step);
     ui_set_name_and_style(spin, args.name, args.style_class);
+    ui_set_widget_groups(obj->ctx, spin, args.groups);
     gtk_spin_button_set_digits(GTK_SPIN_BUTTON(spin), args.digits);
     UiObserver **obs = NULL;
     if(var) {
--- a/ui/gtk/headerbar.c	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/gtk/headerbar.c	Sun Oct 27 18:24:37 2024 +0100
@@ -128,7 +128,7 @@
 {
     GtkWidget *button = gtk_toggle_button_new();
     WIDGET_ADD_CSS_CLASS(button, "flat");
-    ui_setup_togglebutton(obj, button, item->args.label, item->args.icon, item->args.varname, NULL, item->args.onchange, item->args.onchangedata);
+    ui_setup_togglebutton(obj, button, item->args.label, item->args.icon, item->args.varname, NULL, item->args.onchange, item->args.onchangedata, 0);
     headerbar_add(headerbar, box, button, pos);
 }
 
--- a/ui/gtk/list.c	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/gtk/list.c	Sun Oct 27 18:24:37 2024 +0100
@@ -162,6 +162,8 @@
     
     // create treeview
     GtkWidget *view = gtk_tree_view_new();
+    ui_set_name_and_style(view, args.name, args.style_class);
+    ui_set_widget_groups(obj->ctx, view, args.groups);
     GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
     GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(NULL, renderer, "text", 0, NULL);
     gtk_tree_view_append_column(GTK_TREE_VIEW(view), column);
@@ -178,7 +180,7 @@
 #endif
     
     UiModel *model = ui_model(obj->ctx, UI_STRING, "", -1);
-    model->getvalue = args.getvalue;
+    model->getvalue = args.getvalue ? args.getvalue : ui_strmodel_getvalue;
     
     UiVar* var = uic_widget_var(obj->ctx, current->ctx, args.list, args.varname, UI_VAR_LIST);
     
@@ -370,7 +372,9 @@
 
     
     GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(view));
-    gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE);
+    if(args.multiselection) {
+        gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE);
+    }
     
     // add widget to the current container
     GtkWidget *scroll_area = SCROLLEDWINDOW_NEW();
@@ -582,11 +586,13 @@
     UiObject* current = uic_current_obj(obj);
     
     UiModel *model = ui_model(obj->ctx, UI_STRING, "", -1);
-    model->getvalue = args.getvalue;
+    model->getvalue = args.getvalue ? args.getvalue : ui_strmodel_getvalue;
     
     UiVar* var = uic_widget_var(obj->ctx, current->ctx, args.list, args.varname, UI_VAR_LIST);
     
     GtkWidget *combobox = ui_create_combobox(obj, model, var, args.onactivate, args.onactivatedata);
+    ui_set_name_and_style(combobox, args.name, args.style_class);
+    ui_set_widget_groups(obj->ctx, combobox, args.groups);
     UI_APPLY_LAYOUT1(current, args);
     current->container->add(current->container, combobox, FALSE);
     current->container->current = combobox;
@@ -595,7 +601,7 @@
 
 GtkWidget* ui_create_combobox(UiObject *obj, UiModel *model, UiVar *var, ui_callback f, void *udata) {
     GtkWidget *combobox = gtk_combo_box_new();
-     
+       
     UiListView *uicbox = malloc(sizeof(UiListView));
     uicbox->obj = obj;
     uicbox->widget = combobox;
@@ -619,7 +625,8 @@
     // bind var
     if(list) {
         list->update = ui_combobox_modelupdate;
-        // TODO: combobox getselection
+        list->getselection = ui_combobox_getselection;
+        list->setselection = ui_combobox_setselection;
         list->obj = uicbox;
     }
     
@@ -668,3 +675,18 @@
     gtk_combo_box_set_model(GTK_COMBO_BOX(view->widget), GTK_TREE_MODEL(store));
 }
 
+UiListSelection ui_combobox_getselection(UiList *list) {
+    UiListView *combobox = list->obj;
+    UiListSelection ret;
+    ret.rows = malloc(sizeof(int*));
+    ret.count = 1;
+    ret.rows[0] = gtk_combo_box_get_active(GTK_COMBO_BOX(combobox->widget));
+    return ret;
+}
+
+void ui_combobox_setselection(UiList *list, UiListSelection selection) {
+    UiListView *combobox = list->obj;
+    if(selection.count > 0) {
+        gtk_combo_box_set_active(GTK_COMBO_BOX(combobox->widget), selection.rows[0]);
+    }
+}
--- a/ui/gtk/list.h	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/gtk/list.h	Sun Oct 27 18:24:37 2024 +0100
@@ -60,6 +60,7 @@
 
 void ui_listview_update(UiList *list, int i);
 UiListSelection ui_listview_getselection(UiList *list);
+void ui_listview_setselection(UiList *list, UiListSelection selection);
 
 void ui_combobox_destroy(GtkWidget *w, UiListView *v);
 void ui_listview_destroy(GtkWidget *w, UiListView *v);
@@ -81,6 +82,8 @@
 GtkWidget* ui_create_combobox(UiObject *obj, UiModel *model, UiVar *var, ui_callback f, void *udata);
 void ui_combobox_change_event(GtkComboBox *widget, UiEventData *e);
 void ui_combobox_modelupdate(UiList *list, int i);
+UiListSelection ui_combobox_getselection(UiList *list);
+void ui_combobox_setselection(UiList *list, UiListSelection selection);
         
 #ifdef	__cplusplus
 }
--- a/ui/gtk/text.c	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/gtk/text.c	Sun Oct 27 18:24:37 2024 +0100
@@ -68,6 +68,7 @@
     
     GtkWidget *text_area = gtk_text_view_new();
     ui_set_name_and_style(text_area, args.name, args.style_class);
+    ui_set_widget_groups(obj->ctx, text_area, args.groups);
     
     gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text_area), GTK_WRAP_WORD_CHAR);
     g_signal_connect(
@@ -529,6 +530,7 @@
 static UIWIDGET create_textfield(UiObject *obj, UiBool frameless, UiBool password, UiTextFieldArgs args) {
     GtkWidget *textfield = gtk_entry_new();
     ui_set_name_and_style(textfield, args.name, args.style_class);
+    ui_set_widget_groups(obj->ctx, textfield, args.groups);
     
     UiObject* current = uic_current_obj(obj);
     UiVar* var = uic_widget_var(obj->ctx, current->ctx, args.value, args.varname, UI_VAR_STRING);
--- a/ui/gtk/toolkit.c	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/gtk/toolkit.c	Sun Oct 27 18:24:37 2024 +0100
@@ -296,7 +296,7 @@
     uic_unbind_var(var);
     
     if(var->type == UI_VAR_SPECIAL) {
-        free(var);
+        ui_free(var->from_ctx, var);
     } else {
         ui_free(var->from_ctx, var);
         // TODO: free or unbound
@@ -351,15 +351,25 @@
 ".ui_test {\n"
 "  background-color: red;\n"
 "}\n"
+".ui_label_title {\n"
+"  font-weight: bold;\n"
+"}\n"
 ;
 
 #elif GTK_MAJOR_VERSION == 3
 static const char *ui_gtk_css = 
-"#path-textfield-box {"
-"  background-color: @theme_base_color;"
-"  border-radius: 5px;"
-"  padding: 0px;"
-"}";
+"#path-textfield-box {\n"
+"  background-color: @theme_base_color;\n"
+"  border-radius: 5px;\n"
+"  padding: 0px;\n"
+"}"
+".ui_test {\n"
+"  background-color: red;\n"
+"}\n"
+".ui_label_title {\n"
+"  font-weight: bold;\n"
+"}\n"
+;
 #endif
 
 void ui_css_init(void) {
--- a/ui/gtk/toolkit.h	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/gtk/toolkit.h	Sun Oct 27 18:24:37 2024 +0100
@@ -66,6 +66,8 @@
 #define SCROLLEDWINDOW_NEW() gtk_scrolled_window_new()
 #define SCROLLEDWINDOW_SET_CHILD(sw, child) gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(sw), child)
 #define SCROLLEDWINDOW_GET_CHILD(sw) gtk_scrolled_window_get_child(GTK_SCROLLED_WINDOW(sw))
+#define FRAME_SET_CHILD(frame, child) gtk_frame_set_child(GTK_FRAME(frame), child)
+#define EXPANDER_SET_CHILD(expander, child) gtk_expander_set_child(GTK_EXPANDER(expander), child)
 #define WIDGET_ADD_CSS_CLASS(w, cssclass) gtk_widget_add_css_class(w, cssclass)
 #define WIDGET_REMOVE_CSS_CLASS(w, cssclass) gtk_widget_remove_css_class(w, cssclass)
 #else
@@ -80,6 +82,8 @@
 #define SCROLLEDWINDOW_NEW() gtk_scrolled_window_new(NULL, NULL)
 #define SCROLLEDWINDOW_SET_CHILD(sw, child) gtk_container_add(GTK_CONTAINER(sw), child)
 #define SCROLLEDWINDOW_GET_CHILD(sw) gtk_bin_get_child(GTK_BIN(sw))
+#define FRAME_SET_CHILD(frame, child) gtk_container_add(GTK_CONTAINER(frame), child)
+#define EXPANDER_SET_CHILD(expander, child) gtk_container_add(GTK_CONTAINER(expander), child)
 #define WIDGET_ADD_CSS_CLASS(w, cssclass) gtk_style_context_add_class(gtk_widget_get_style_context(w), cssclass)
 #define WIDGET_REMOVE_CSS_CLASS(w, cssclass) gtk_style_context_remove_class(gtk_widget_get_style_context(w), cssclass)
 #endif
--- a/ui/ui/button.h	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/ui/button.h	Sun Oct 27 18:24:37 2024 +0100
@@ -71,6 +71,7 @@
     const char* varname;
     ui_callback onchange;
     void* onchangedata;
+    int enable_group;
     
     const int* groups;
 } UiToggleArgs;
--- a/ui/ui/container.h	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/ui/container.h	Sun Oct 27 18:24:37 2024 +0100
@@ -38,7 +38,8 @@
 typedef enum UiSubContainerType {
     UI_CONTAINER_VBOX = 0,
     UI_CONTAINER_HBOX,
-    UI_CONTAINER_GRID
+    UI_CONTAINER_GRID,
+    UI_CONTAINER_NO_SUB
 } UiSubContainerType;
 
 typedef enum UiTabViewType {
--- a/ui/ui/display.h	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/ui/display.h	Sun Oct 27 18:24:37 2024 +0100
@@ -48,6 +48,15 @@
 
 typedef enum UiAlignment UiAlignment;
 
+enum UiLabelStyle {
+    UI_LABEL_STYLE_DEFAULT = 0,
+    UI_LABEL_STYLE_TITLE,
+    UI_LABEL_STYLE_SUBTITLE,
+    UI_LABEL_STYLE_DIM
+};
+
+typedef enum UiLabelStyle UiLabelStyle;
+
 typedef struct UiLabelArgs {
     UiTri fill;
     UiBool hexpand;
@@ -57,6 +66,7 @@
 
     const char* label;
     UiAlignment align;
+    UiLabelStyle style;
     UiString* value;
     const char* varname;
 } UiLabelArgs;
--- a/ui/ui/entry.h	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/ui/entry.h	Sun Oct 27 18:24:37 2024 +0100
@@ -53,6 +53,8 @@
     const char* varname;
     ui_callback onchange;
     void* onchangedata;
+    
+    const int *groups;
 } UiSpinnerArgs;
 
 
--- a/ui/ui/text.h	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/ui/text.h	Sun Oct 27 18:24:37 2024 +0100
@@ -49,6 +49,8 @@
     const char *varname;
     ui_callback onchange;
     void *onchangedata;
+    
+    const int *groups;
 } UiTextAreaArgs;
     
 typedef struct UiTextFieldArgs {
@@ -65,6 +67,8 @@
     const char *varname;
     ui_callback onchange;
     void *onchangedata;
+    
+    const int *groups;
 } UiTextFieldArgs;
 
 typedef struct UiPathElmRet {
--- a/ui/ui/toolkit.h	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/ui/toolkit.h	Sun Oct 27 18:24:37 2024 +0100
@@ -357,6 +357,7 @@
     /* binding functions */
     void (*update)(UiList *list, int i);
     UiListSelection (*getselection)(UiList *list);
+    void (*setselection)(UiList *list, UiListSelection selection);
     /* binding object */
     void *obj;
     
@@ -518,6 +519,7 @@
 UIEXPORT void ui_list_notify(UiList *list);
 
 UIEXPORT UiListSelection ui_list_getselection(UiList *list);
+UIEXPORT void ui_list_setselection(UiList *list, int index);
 
 UIEXPORT UiFileList ui_filelist_copy(UiFileList list);
 UIEXPORT void ui_filelist_free(UiFileList list);
--- a/ui/ui/tree.h	Wed Oct 23 21:46:43 2024 +0200
+++ b/ui/ui/tree.h	Sun Oct 27 18:24:37 2024 +0100
@@ -105,6 +105,8 @@
     UiBool vexpand;
     int colspan;
     int rowspan;
+    const char *name;
+    const char *style_class;
 
     UiList* list;
     const char* varname;
@@ -121,6 +123,8 @@
     ui_callback ondrop;
     void* ondropsdata;
     UiBool multiselection;
+    
+    const int *groups;
 };
 
 UIEXPORT UiModel* ui_model(UiContext *ctx, ...);

mercurial