# HG changeset patch # User Olaf Wintermann # Date 1730049877 -3600 # Node ID ee4e4742391e7e959fc46057a3fd4d5853d187fb # Parent 6bd37fe6d9052d9ce7b3c783d5dcf0a4d1b2ac45 add settings window and implement repository editing diff -r 6bd37fe6d905 -r ee4e4742391e application/Makefile --- 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)) diff -r 6bd37fe6d905 -r ee4e4742391e application/application.c --- 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) { diff -r 6bd37fe6d905 -r ee4e4742391e application/application.h --- 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 @@ -156,6 +156,7 @@ */ void application_create_menu(void); +DavApp* get_application(void); DavApp* application_create_app_document(void); diff -r 6bd37fe6d905 -r ee4e4742391e application/config.c --- 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; } diff -r 6bd37fe6d905 -r ee4e4742391e application/config.h --- 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); diff -r 6bd37fe6d905 -r ee4e4742391e application/settings.c --- /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 +#include + + +#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); + } +} + + diff -r 6bd37fe6d905 -r ee4e4742391e application/settings.h --- /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 */ + diff -r 6bd37fe6d905 -r ee4e4742391e application/window.c --- 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 diff -r 6bd37fe6d905 -r ee4e4742391e libidav/config.c --- 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; diff -r 6bd37fe6d905 -r ee4e4742391e libidav/config.h --- 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); diff -r 6bd37fe6d905 -r ee4e4742391e libidav/pwdstore.c --- 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); } diff -r 6bd37fe6d905 -r ee4e4742391e ui/common/context.c --- 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; diff -r 6bd37fe6d905 -r ee4e4742391e ui/common/types.c --- 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); diff -r 6bd37fe6d905 -r ee4e4742391e ui/gtk/button.c --- 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); diff -r 6bd37fe6d905 -r ee4e4742391e ui/gtk/button.h --- 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); diff -r 6bd37fe6d905 -r ee4e4742391e ui/gtk/container.c --- 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; diff -r 6bd37fe6d905 -r ee4e4742391e ui/gtk/container.h --- 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); diff -r 6bd37fe6d905 -r ee4e4742391e ui/gtk/display.c --- 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 + 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("%s", 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; diff -r 6bd37fe6d905 -r ee4e4742391e ui/gtk/entry.c --- 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) { diff -r 6bd37fe6d905 -r ee4e4742391e ui/gtk/headerbar.c --- 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); } diff -r 6bd37fe6d905 -r ee4e4742391e ui/gtk/list.c --- 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]); + } +} diff -r 6bd37fe6d905 -r ee4e4742391e ui/gtk/list.h --- 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 } diff -r 6bd37fe6d905 -r ee4e4742391e ui/gtk/text.c --- 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); diff -r 6bd37fe6d905 -r ee4e4742391e ui/gtk/toolkit.c --- 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) { diff -r 6bd37fe6d905 -r ee4e4742391e ui/gtk/toolkit.h --- 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 diff -r 6bd37fe6d905 -r ee4e4742391e ui/ui/button.h --- 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; diff -r 6bd37fe6d905 -r ee4e4742391e ui/ui/container.h --- 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 { diff -r 6bd37fe6d905 -r ee4e4742391e ui/ui/display.h --- 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; diff -r 6bd37fe6d905 -r ee4e4742391e ui/ui/entry.h --- 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; diff -r 6bd37fe6d905 -r ee4e4742391e ui/ui/text.h --- 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 { diff -r 6bd37fe6d905 -r ee4e4742391e ui/ui/toolkit.h --- 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); diff -r 6bd37fe6d905 -r ee4e4742391e ui/ui/tree.h --- 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, ...);