]> uap-core.de Git - note.git/commitdiff
fix sublist_free
authorOlaf Wintermann <olaf.wintermann@gmail.com>
Sun, 30 Nov 2025 17:22:07 +0000 (18:22 +0100)
committerOlaf Wintermann <olaf.wintermann@gmail.com>
Sun, 30 Nov 2025 17:22:07 +0000 (18:22 +0100)
16 files changed:
application/window.c
ui/cocoa/webview.h [new file with mode: 0644]
ui/cocoa/webview.m [new file with mode: 0644]
ui/common/utils.c [new file with mode: 0644]
ui/common/utils.h [new file with mode: 0644]
ui/motif/Fsb.c [new file with mode: 0644]
ui/motif/Fsb.h [new file with mode: 0644]
ui/motif/FsbP.h [new file with mode: 0644]
ui/motif/entry.c [new file with mode: 0644]
ui/motif/entry.h [new file with mode: 0644]
ui/motif/pathbar.c [new file with mode: 0644]
ui/motif/pathbar.h [new file with mode: 0644]
ui/win32/list.c [new file with mode: 0644]
ui/win32/list.h [new file with mode: 0644]
ui/win32/text.c [new file with mode: 0644]
ui/win32/text.h [new file with mode: 0644]

index 0b7bac20fe2b5d7e7c4ff503a073c576dfc9ed2d..cba82f8a83c2ac01963dea988226d03742030561 100644 (file)
@@ -156,9 +156,9 @@ void window_sidebar_getvalue(UiList *list, void *sublist_userdata, void *rowdata
 }
 
 static void sublist_free(UiContext *ctx, UiSubList *sublist) {
-    ui_list_free(sublist->value);
-    free((char*)sublist->header);
-    free(sublist);
+    ui_list_free(ctx, sublist->value);
+    ui_free(ctx, (char*)sublist->header);
+    ui_free(ctx, sublist);
 }
 
 /*
diff --git a/ui/cocoa/webview.h b/ui/cocoa/webview.h
new file mode 100644 (file)
index 0000000..479905b
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 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.
+ */
+
+#import "toolkit.h"
+#import "../ui/webview.h"
+
+#import <WebKit/WebKit.h>
+
+
+enum WebViewDataType {
+    WEBVIEW_CONTENT_URL,
+    WEBVIEW_CONTENT_CONTENT
+};
+    
+struct UiWebViewData {
+    void *webview;
+    char *uri;
+    char *mimetype;
+    char *encoding;
+    char *content;
+    size_t contentlength;
+    enum WebViewDataType type;
+    
+    double zoom;
+    UiBool javascript;
+};
+
+UiWebViewData* ui_webview_data_clone(UiWebViewData *data);
+
+void* ui_webview_get(UiGeneric *g);
+const char* ui_webview_get_type(UiGeneric *g);
+int ui_webview_set(UiGeneric *g, void *data, const char *type);
+void ui_webview_destroy(UiGeneric *g);
diff --git a/ui/cocoa/webview.m b/ui/cocoa/webview.m
new file mode 100644 (file)
index 0000000..088ad5f
--- /dev/null
@@ -0,0 +1,273 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 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.
+ */
+
+#import "webview.h"
+#import "Container.h"
+
+UIWIDGET ui_webview_create(UiObject *obj, UiWebviewArgs *args) {
+    UiVar *var = uic_widget_var(obj->ctx, obj->ctx, args->value, args->varname, UI_VAR_GENERIC);
+    
+    WKWebView *webview = [[WKWebView alloc]init];
+    
+    UiLayout layout = UI_ARGS2LAYOUT(args);
+    ui_container_add(obj, webview, &layout);
+    
+    if(var) {
+        UiGeneric *value = var->value;
+        value->get = ui_webview_get;
+        value->get_type = ui_webview_get_type;
+        value->set = ui_webview_set;
+        value->destroy = ui_webview_destroy;
+        value->obj = (__bridge void*)webview;
+        if(value->value) {
+            ui_webview_set(value, value->value, UI_WEBVIEW_OBJECT_TYPE);
+        } else {
+            UiWebViewData *data = malloc(sizeof(UiWebViewData));
+            memset(data, 0, sizeof(UiWebViewData));
+            data->webview = (__bridge void*)webview;
+            data->javascript = TRUE;
+            data->zoom = 1;
+            value->value = value;
+        }
+    }
+    
+    return (__bridge void*)webview;
+}
+
+UiWebViewData* ui_webview_data_clone(UiWebViewData *data) {
+    UiWebViewData *newdata = malloc(sizeof(UiWebViewData));
+    memset(newdata, 0, sizeof(UiWebViewData));
+    newdata->zoom = 1;
+    newdata->javascript = TRUE;
+    
+    if(data) {
+        newdata->uri = data->uri ? strdup(data->uri) : NULL;
+        newdata->mimetype = data->mimetype ? strdup(data->mimetype) : NULL;
+        newdata->encoding = data->encoding ? strdup(data->encoding) : NULL;
+        newdata->type = data->type;
+        newdata->javascript = data->javascript;
+        newdata->zoom = data->zoom;
+        if(data->content && data->contentlength > 0) {
+            newdata->content = malloc(data->contentlength + 1);
+            memcpy(newdata->content, data->content, data->contentlength);
+            newdata->content[data->contentlength] = 0;
+            newdata->contentlength = data->contentlength;
+        }
+    }
+    
+    return newdata;
+}
+
+void ui_webview_data_free(UiWebViewData *data) {
+    if(!data) {
+        return;
+    }
+    free(data->uri);
+    free(data->mimetype);
+    free(data->encoding);
+    free(data->content);
+    free(data);
+}
+
+void* ui_webview_get(UiGeneric *g) {
+    UiWebViewData *data = g->value;
+    WKWebView *webview = (__bridge WKWebView*)g->obj;
+    
+    if(data->type == WEBVIEW_CONTENT_URL) {
+        (void)ui_webview_get_uri(g); // this updates data->uri
+    }
+    data->zoom = webview.pageZoom;
+    
+    return ui_webview_data_clone(g->value);
+}
+
+const char* ui_webview_get_type(UiGeneric *g) {
+    return UI_WEBVIEW_OBJECT_TYPE;
+}
+
+int ui_webview_set(UiGeneric *g, void *data, const char *type) {
+    if(!data || !type) {
+        return 1;
+    }
+    if(strcmp(type, UI_WEBVIEW_OBJECT_TYPE)) {
+        return 1;
+    }
+    ui_webview_data_free(g->value);
+    g->value = ui_webview_data_clone(data);
+    
+    WKWebView *webview = (__bridge WKWebView*)g->obj;
+    UiWebViewData *webd = data;
+    if(webd->type == WEBVIEW_CONTENT_URL) {
+        const char *uri = webd->uri ? webd->uri : "about:blank";
+        NSURL *url = [NSURL URLWithString:[[NSString alloc] initWithUTF8String:uri]];
+        if(url) {
+            NSURLRequest *req = [NSURLRequest requestWithURL:url];
+            if(req) {
+                [webview loadRequest:req];
+            }
+        }
+    } else {
+        NSString *mimetype = [[NSString alloc]initWithUTF8String: webd->mimetype ? webd->mimetype : "text/plain"];
+        NSString *encoding = [[NSString alloc]initWithUTF8String: webd->encoding ? webd->encoding : "UTF-8"];
+        NSString *urlStr = [[NSString alloc]initWithUTF8String: webd->uri ? webd->uri : "file:///"];
+        NSURL *url = [NSURL URLWithString:urlStr];
+        NSData *content = [NSData dataWithBytes:webd->content length:webd->contentlength];
+        if(!url) {
+            url = [NSURL URLWithString:@"about:blank"];
+        }
+        [webview loadData:content MIMEType:mimetype characterEncodingName:encoding baseURL:url];
+    }
+    
+    webview.pageZoom = webd->zoom;
+    
+    return 1;
+}
+
+void ui_webview_destroy(UiGeneric *g) {
+    ui_webview_data_free(g->value);
+    g->value = NULL;
+}
+
+
+void ui_webview_load_url(UiGeneric *g, const char *url) {
+    WKWebView *webview = (__bridge WKWebView*)g->obj;
+    UiWebViewData *data = g->value;
+    data->type = WEBVIEW_CONTENT_URL;
+    
+    if(!url) {
+        url = "about:blank";
+    }
+    
+    NSURL *nsurl = [NSURL URLWithString:[[NSString alloc] initWithUTF8String:url]];
+    if(nsurl) {
+        NSURLRequest *req = [NSURLRequest requestWithURL:nsurl];
+        if(req) {
+            [webview loadRequest:req];
+        }
+    }
+}
+
+void ui_webview_load_content(
+        UiGeneric *g,
+        const char *uri,
+        const char *content,
+        size_t contentlength,
+        const char *mimetype,
+        const char *encoding)
+{
+    UiWebViewData *data = g->value;
+    WKWebView *webview = (__bridge WKWebView*)g->obj;
+    
+    data->type = WEBVIEW_CONTENT_CONTENT;
+    
+    free(data->uri);
+    data->uri = NULL;
+    free(data->mimetype);
+    free(data->encoding);
+    free(data->content);
+    data->type = WEBVIEW_CONTENT_URL;
+    
+    data->content = malloc(contentlength+1);
+    memcpy(data->content, content, contentlength);
+    data->content[contentlength] = 0;
+    
+    if(!mimetype) {
+        mimetype = "text/plain";
+    }
+    if(!encoding) {
+        encoding = "UTF-8";
+    }
+    
+    data->mimetype = strdup(mimetype);
+    data->encoding = strdup(encoding);
+    
+    NSString *mtype = [[NSString alloc]initWithUTF8String:mimetype];
+    NSString *enc = [[NSString alloc]initWithUTF8String:encoding];
+    NSData *ct = [NSData dataWithBytes:content length:contentlength];
+    NSURL *url;
+    if(uri) {
+        NSString *uriStr = [[NSString alloc]initWithUTF8String:uri];
+        url = [NSURL URLWithString:uriStr];
+    } else {
+        url = [NSURL URLWithString:@"file:///"];
+    }
+    [webview loadData:ct MIMEType:mtype characterEncodingName:enc baseURL:url];
+}
+
+
+void ui_webview_reload(UiGeneric *g) {
+    WKWebView *webview = (__bridge WKWebView*)g->obj;
+    [webview reload];
+}
+
+UiBool ui_webview_can_go_back(UiGeneric *g) {
+    return FALSE;
+}
+
+UiBool ui_webview_can_go_forward(UiGeneric *g) {
+    return FALSE;
+}
+
+void ui_webview_go_back(UiGeneric *g) {
+    
+}
+
+void ui_webview_go_forward(UiGeneric *g) {
+    
+}
+
+const char * ui_webview_get_uri(UiGeneric *g) {
+    UiWebViewData *data = g->value;
+    WKWebView *webview = (__bridge WKWebView*)g->obj;
+    
+    free(data->uri);
+    data->uri = NULL;
+    
+    NSURL *url = webview.URL;
+    if(url) {
+        NSString *s = [url absoluteString];
+        if(s) {
+            data->uri = strdup(s.UTF8String);
+        }
+    }
+    return data->uri;
+}
+
+void ui_webview_enable_javascript(UiGeneric *g, UiBool enable) {
+    // unsupported
+}
+
+void ui_webview_set_zoom(UiGeneric *g, double zoom) {
+    WKWebView *webview = (__bridge WKWebView*)g->obj;
+    webview.pageZoom = zoom;
+}
+
+double ui_webview_get_zoom(UiGeneric *g) {
+    WKWebView *webview = (__bridge WKWebView*)g->obj;
+    return webview.pageZoom;
+}
diff --git a/ui/common/utils.c b/ui/common/utils.c
new file mode 100644 (file)
index 0000000..5e01239
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2017 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 "utils.h"
+#include "properties.h"
+
+#include <cx/string.h>
+
+UiPathElm* ui_default_pathelm_func(const char* full_path, size_t len, size_t* ret_nelm, void* data) {
+    cxstring *pathelms;
+    size_t nelm = cx_strsplit_a(cxDefaultAllocator, cx_strn(full_path, len), CX_STR("/"), 4096, &pathelms);
+
+    if (nelm == 0) {
+        *ret_nelm = 0;
+        return NULL;
+    }
+
+    UiPathElm* elms = (UiPathElm*)calloc(nelm, sizeof(UiPathElm));
+    size_t n = nelm;
+    int j = 0;
+    for (int i = 0; i < nelm; i++) {
+        cxstring c = pathelms[i];
+        if (c.length == 0) {
+            if (i == 0) {
+                c.length = 1;
+            }
+            else {
+                n--;
+                continue;
+            }
+        }
+
+        cxmutstr m = cx_strdup(c);
+        elms[j].name = m.ptr;
+        elms[j].name_len = m.length;
+        
+        size_t elm_path_len = c.ptr + c.length - full_path;
+        cxmutstr elm_path = cx_strdup(cx_strn(full_path, elm_path_len));
+        elms[j].path = elm_path.ptr;
+        elms[j].path_len = elm_path.length;
+
+        j++;
+    }
+    *ret_nelm = n;
+
+    return elms;
+}
+
+void ui_get_window_default_width(int *width, int *height) {
+    const char *width_str = ui_get_property("ui.window.width");
+    const char *height_str = ui_get_property("ui.window.height");
+    if(width_str && height_str) {
+        int w = atoi(width_str);
+        int h = atoi(height_str);
+        if(w > 0 && h > 0) {
+            *width = w;
+            *height = h;
+        }
+    }
+}
diff --git a/ui/common/utils.h b/ui/common/utils.h
new file mode 100644 (file)
index 0000000..592ad4b
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2017 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 UIC_UTILS_H
+#define UIC_UTILS_H
+
+#include "../ui/toolkit.h"
+#include "../ui/text.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+UiPathElm* ui_default_pathelm_func(const char* full_path, size_t len, size_t* ret_nelm, void* data);
+
+/*
+ * overrides width/height with values from
+ * ui.window.width and ui.window.height properties if available
+ */
+void ui_get_window_default_width(int *width, int *height);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UIC_UTILS_H */
+
diff --git a/ui/motif/Fsb.c b/ui/motif/Fsb.c
new file mode 100644 (file)
index 0000000..bcccc7c
--- /dev/null
@@ -0,0 +1,2642 @@
+/*
+ * Copyright 2021 Olaf Wintermann
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a 
+ * copy of this software and associated documentation files (the "Software"), 
+ * to deal in the Software without restriction, including without limitation 
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
+ * and/or sell copies of the Software, and to permit persons to whom the 
+ * Software is furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in 
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+//define FSB_ENABLE_DETAIL
+
+#include "Fsb.h"
+#include "FsbP.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <time.h>
+#include <inttypes.h>
+#include <errno.h>
+
+#include <sys/stat.h>
+#include <limits.h>
+#include <dirent.h>
+#include <fnmatch.h>
+
+#include <Xm/XmAll.h>
+#include <Xm/DropDown.h>
+
+#include "pathbar.h"
+
+#include "../common/utils.h"
+
+#ifdef FSB_ENABLE_DETAIL
+#include <XmL/Grid.h>
+#endif
+
+#define WIDGET_SPACING 5
+#define WINDOW_SPACING 8
+
+#define BUTTON_EXTRA_SPACE 4
+
+#define DATE_FORMAT_SAME_YEAR  "%b %d %H:%M"
+#define DATE_FORMAT_OTHER_YEAR "%b %d  %Y"
+
+#define KB_SUFFIX "KiB"
+#define MB_SUFFIX "MiB"
+#define GB_SUFFIX "GiB"
+#define TB_SUFFIX "TiB"
+
+#define FSB_ERROR_TITLE          "Error"
+#define FSB_ERROR_CHAR           "Character '/' is not allowed in file names"
+#define FSB_ERROR_RENAME         "Cannot rename file: %s"
+#define FSB_ERROR_DELETE         "Cannot delete file: %s"
+#define FSB_ERROR_CREATE_FOLDER  "Cannot create folder: %s"
+#define FSB_ERROR_OPEN_DIR       "Cannot open directory: %s"
+
+#define FSB_DETAIL_HEADINGS "Name|Size|Last Modified"
+
+static void fsb_class_init(void);
+static void fsb_class_part_init (WidgetClass wc);
+static void fsb_init(Widget request, Widget neww, ArgList args, Cardinal *num_args);
+static void fsb_resize(Widget widget);
+static void fsb_realize(Widget widget, XtValueMask *mask, XSetWindowAttributes *attributes);
+static void fsb_destroy(Widget widget);
+static Boolean fsb_set_values(Widget old, Widget request, Widget neww, ArgList args, Cardinal *num_args);
+static Boolean fsb_acceptfocus(Widget widget, Time *time);
+
+static void fsb_insert_child(Widget child);
+
+static void fsb_mapcb(Widget widget, XtPointer u, XtPointer cb);
+
+static int FSBGlobFilter(const char *a, const char *b);
+
+static void FSBUpdateTitle(Widget w);
+
+static void FocusInAP(Widget w, XEvent *event, String *args, Cardinal *nArgs);
+
+static void ErrDialog(XnFileSelectionBox w, const char *title, const char *errmsg);
+
+static void FSBRename(XnFileSelectionBox fsb, const char *path);
+static void FSBDelete(XnFileSelectionBox fsb, const char *path);
+
+static void FSBSelectItem(XnFileSelectionBox fsb, const char *item);
+
+static char* set_selected_path(XnFileSelectionBox data, XmString item);
+
+static void FileContextMenuCB(Widget item, XtPointer index, XtPointer cd);
+
+static Widget CreateContextMenu(XnFileSelectionBox fsb, Widget parent, XtCallbackProc callback);
+
+static void FileListUpdate(Widget fsb, Widget view, FileElm *dirlist, int dircount, FileElm *files, int filecount, const char *filter, int maxnamelen, void *userData);
+static void FileListSelect(Widget fsb, Widget view, const char *item);
+static void FileListCleanup(Widget fsb, Widget view, void *userData);
+static void FileListDestroy(Widget fsb, Widget view, void *userData);
+
+static void FileListActivateCB(Widget w, XnFileSelectionBox data, XmListCallbackStruct *cb);
+static void FileListSelectCB(Widget w, XnFileSelectionBox data, XmListCallbackStruct *cb);
+
+static void FileListWidgetAdd(XnFileSelectionBox fsb, Widget w, int showHidden, const char *filter, FileElm *ls, int count);
+
+#ifdef FSB_ENABLE_DETAIL
+static void FileListDetailUpdate(Widget fsb, Widget view, FileElm *dirlist, int dircount, FileElm *files, int filecount, const char *filter, int maxnamelen, void *userData);
+static void FileListDetailSelect(Widget fsb, Widget view, const char *item);
+static void FileListDetailCleanup(Widget fsb, Widget view, void *userData);
+static void FileListDetailDestroy(Widget fsb, Widget view, void *userData);
+static void FileListDetailAdjustColWidth(Widget grid);
+static void FileListDetailAdd(XnFileSelectionBox fsb, Widget grid, int showHidden, const char *filter, FileElm *ls, int count, int maxWidth);
+#endif
+
+static void FSBNewFolder(Widget w, XnFileSelectionBox data, XtPointer u);
+
+static void FSBHome(Widget w, XnFileSelectionBox data, XtPointer u);
+
+static void FileSelectionCallback(XnFileSelectionBox fsb, XtCallbackList cb, int reason, const char *value);
+
+static void CreateUI(XnFileSelectionBox w);
+static FSBViewWidgets CreateView(XnFileSelectionBox w, FSBViewCreateProc createProc, void *userData, Boolean useDirList);
+static void AddViewMenuItem(XnFileSelectionBox w, const char *name, int viewIndex);
+static void SelectView(XnFileSelectionBox f, int view);
+
+static char* FSBDialogTitle(Widget w);
+
+static FSBViewWidgets CreateListView(Widget fsb, ArgList args, int n, void *userData);
+static FSBViewWidgets CreateDetailView(Widget fsb, ArgList args, int n, void *userData);
+
+static const char* GetHomeDir(void);
+
+static char* ConcatPath(const char *parent, const char *name);
+static char* FileName(char *path);
+static char* ParentPath(const char *path);
+//static int   CheckFileName(const char *name);
+
+static int filedialog_update_dir(XnFileSelectionBox data, const char *path);
+static void filedialog_cleanup_filedata(XnFileSelectionBox data);
+
+
+static XtResource resources[] = {
+    {XmNokCallback, XmCCallback, XmRCallback, sizeof(XtCallbackList), XtOffset(XnFileSelectionBox, fsb.okCallback), XmRCallback, NULL},
+    {XmNcancelCallback, XmCCallback, XmRCallback, sizeof(XtCallbackList), XtOffset(XnFileSelectionBox, fsb.cancelCallback), XmRCallback, NULL},
+    {XnNwidgetSpacing, XmCSpacing, XmRDimension, sizeof(Dimension), XtOffset(XnFileSelectionBox, fsb.widgetSpacing), XmRImmediate, (XtPointer)WIDGET_SPACING},
+    {XnNwindowSpacing, XmCSpacing, XmRDimension, sizeof(Dimension), XtOffset(XnFileSelectionBox, fsb.windowSpacing), XmRImmediate, (XtPointer)WINDOW_SPACING},
+    {XnNfsbType, XnCfsbType, XmRInt, sizeof(int), XtOffset(XnFileSelectionBox, fsb.type), XmRImmediate, (XtPointer)FILEDIALOG_OPEN},
+    {XnNshowHidden, XnCshowHidden, XmRBoolean, sizeof(Boolean), XtOffset(XnFileSelectionBox, fsb.showHidden), XmRImmediate, (XtPointer)False},
+    {XnNshowHiddenButton, XnCshowHiddenButton, XmRBoolean, sizeof(Boolean), XtOffset(XnFileSelectionBox, fsb.showHiddenButton), XmRImmediate, (XtPointer)True},
+    {XnNshowViewMenu, XnCshowViewMenu, XmRBoolean, sizeof(Boolean), XtOffset(XnFileSelectionBox, fsb.showViewMenu), XmRImmediate, (XtPointer)False},
+    {XnNselectedView, XnCselectedView, XmRInt, sizeof(int), XtOffset(XnFileSelectionBox, fsb.selectedview), XmRImmediate, (XtPointer)0},
+    
+    {XnNdirectory, XnCdirectory, XmRString, sizeof(XmString), XtOffset(XnFileSelectionBox, fsb.currentPath), XmRString, NULL},
+    {XnNselectedPath, XnCselectedPath, XmRString, sizeof(XmString), XtOffset(XnFileSelectionBox, fsb.selectedPath), XmRString, NULL},
+    {XnNhomePath, XnChomePath, XmRString, sizeof(XmString), XtOffset(XnFileSelectionBox, fsb.homePath), XmRString, NULL},
+    
+    {XnNfilter,XnCfilter,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.filterStr), XmRString, "*"},
+    {XnNfilterFunc,XnCfilterFunc,XmRFunction,sizeof(FSBFilterFunc),XtOffset(XnFileSelectionBox, fsb.filterFunc), XmRFunction, NULL},
+    
+    {XnNlabelListView,XnClabelListView,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelListView), XmRString, "List"},
+    {XnNlabelDetailView,XnClabelDetailView,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelDetailView), XmRString, "Detail"},
+    {XnNlabelOpenFileTitle,XnClabelOpenFileTitle,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelOpenFileTitle), XmRString, "Open File"},
+    {XnNlabelSaveFileTitle,XnClabelSaveFileTitle,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelSaveFileTitle), XmRString, "Save File"},
+    {XnNlabelDirUp,XnClabelDirUp,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelDirUp), XmRString, "Dir Up"},
+    {XnNlabelHome,XnClabelHome,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelHome), XmRString, "Home"},
+    {XnNlabelNewFolder,XnClabelNewFolder,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelNewFolder), XmRString, "New Folder"},
+    {XnNlabelFilterButton,XnClabelFilterButton,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelFilterButton), XmRString, "Filter"},
+    {XnNlabelShowHiddenFiles,XnClabelShowHiddenFiles,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelShowHiddenFiles), XmRString, "Show hiden files"},
+    {XnNlabelDirectories,XnClabelDirectories,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelDirectories), XmRString, "Directories"},
+    {XnNlabelFiles,XnClabelFiles,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelFiles), XmRString, "Files"},
+    {XnNlabelRename,XnClabelRename,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelRename), XmRString, "Rename"},
+    {XnNlabelDelete,XnClabelDelete,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelDelete), XmRString, "Delete"},
+    {XnNlabelOpen,XnClabelOpen,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelOpen), XmRString, "Open"},
+    {XnNlabelSave,XnClabelSave,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelSave), XmRString, "Save"},
+    {XnNlabelOk,XnClabelOk,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelOk), XmRString, "OK"},
+    {XnNlabelCancel,XnClabelCancel,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelCancel), XmRString, "Cancel"},
+    {XnNlabelHelp,XnClabelHelp,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelHelp), XmRString, "Help"},
+    {XnNlabelFileName,XnClabelFileName,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelFileName), XmRString, "New File Name"},
+    {XnNlabelDirectoryName,XnClabelDirectoryName,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelDirectoryName), XmRString, "Directory name:"},
+    {XnNlabelNewFileName,XnClabelNewFileName,XmRXmString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelNewFileName), XmRString, "New file name:"},
+    {XnNlabelDeleteFile,XnClabelDeleteFile,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.labelDeleteFile), XmRString, "Delete file '%s'?"},
+    {XnNdetailHeadings,XnCdetailHeadings,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.detailHeadings), XmRString,FSB_DETAIL_HEADINGS},
+    {XnNdateFormatSameYear,XnCdateFormatSameYear,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.dateFormatSameYear), XmRString,DATE_FORMAT_SAME_YEAR},
+    {XnNdateFormatOtherYear,XnNdateFormatOtherYear,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.dateFormatOtherYear), XmRString,DATE_FORMAT_OTHER_YEAR},
+    {XnNsuffixBytes,XnCsuffixBytes,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.suffixBytes), XmRString,"bytes"},
+    {XnNsuffixKB,XnCsuffixKB,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.suffixKB), XmRString,KB_SUFFIX},
+    {XnNsuffixMB,XnCsuffixKB,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.suffixMB), XmRString,MB_SUFFIX},
+    {XnNsuffixGB,XnCsuffixKB,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.suffixGB), XmRString,GB_SUFFIX},
+    {XnNsuffixTB,XnCsuffixKB,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.suffixTB), XmRString,TB_SUFFIX},
+    
+    {XnNerrorTitle,XnCerrorTitle,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.errorTitle), XmRString,FSB_ERROR_TITLE},
+    {XnNerrorIllegalChar,XnCerrorIllegalChar,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.errorIllegalChar), XmRString,FSB_ERROR_CHAR},
+    {XnNerrorRename,XnCerrorRename,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.errorRename), XmRString,FSB_ERROR_RENAME},
+    {XnNerrorCreateFolder,XnCerrorCreateFolder,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.errorFolder), XmRString,FSB_ERROR_CREATE_FOLDER},
+    {XnNerrorDelete,XnCerrorDelete,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.errorDelete), XmRString,FSB_ERROR_DELETE},
+    {XnNerrorOpenDir,XnCerrorOpenDir,XmRString,sizeof(XmString),XtOffset(XnFileSelectionBox, fsb.errorOpenDir), XmRString,FSB_ERROR_OPEN_DIR}
+};
+
+static XtActionsRec actionslist[] = {
+  {"focusIn", FocusInAP},
+  {"NULL", NULL}
+};
+
+
+static char defaultTranslations[] = "<FocusIn>:                        focusIn()";
+
+static XtResource constraints[] = {};
+
+FSBClassRec fsbWidgetClassRec = {
+    // Core Class
+    {
+        (WidgetClass)&xmFormClassRec,
+        "XnFSB",                         // class_name
+        sizeof(FSBRec),                  // widget_size
+        fsb_class_init,                  // class_initialize
+        fsb_class_part_init,             // class_part_initialize
+        FALSE,                           // class_inited
+        fsb_init,                        // initialize
+        NULL,                            // initialize_hook
+        fsb_realize,                     // realize
+        actionslist,                     // actions
+        XtNumber(actionslist),           // num_actions
+        resources,                       // resources
+        XtNumber(resources),             // num_resources
+        NULLQUARK,                       // xrm_class
+        True,                            // compress_motion
+        True,                            // compress_exposure
+        True,                            // compress_enterleave
+        False,                           // visible_interest
+        fsb_destroy,                     // destroy
+        fsb_resize,                      // resize
+        XtInheritExpose,                 // expose
+        fsb_set_values,                  // set_values
+        NULL,                            // set_values_hook
+        XtInheritSetValuesAlmost,        // set_values_almost
+        NULL,                            // get_values_hook
+        fsb_acceptfocus,                 // accept_focus
+        XtVersion,                       // version
+        NULL,                            // callback_offsets
+        defaultTranslations,             // tm_table
+        XtInheritQueryGeometry,          // query_geometry
+        XtInheritDisplayAccelerator,     // display_accelerator
+        NULL,                            // extension
+    },
+    // Composite Class
+    {
+        XtInheritGeometryManager, // geometry_manager 
+        XtInheritChangeManaged,   // change_managed
+        fsb_insert_child,         // insert_child 
+        XtInheritDeleteChild,     // delete_child  
+        NULL,                     // extension   
+    },
+    // Constraint Class
+    {
+        constraints,                 // resources
+        XtNumber(constraints),       // num_resources   
+        sizeof(XmFormConstraintRec), // constraint_size  
+        NULL,                        // initialize 
+        NULL,                        // destroy
+        NULL,                        // set_value
+        NULL,                        // extension 
+    },
+    // XmManager Class
+    {
+        XtInheritTranslations, // translations
+        NULL, // syn_resources
+        0,    // num_syn_resources
+        NULL, // syn_constraint_resources
+        0,    // num_syn_constraint_resources
+        XmInheritParentProcess, // parent_process
+        NULL  // extension
+    },
+    // XmBulletinBoard
+    {
+        FALSE,
+        NULL,
+        XmInheritFocusMovedProc,
+        NULL
+    },
+    // XmForm Class
+    {
+        NULL
+    },
+    // FSB Class
+    {
+        0
+    }
+};
+
+WidgetClass xnFsbWidgetClass = (WidgetClass)&fsbWidgetClassRec;
+
+
+static void fsb_class_init(void) {
+
+}
+
+static void fsb_class_part_init (WidgetClass wc) {
+    FSBClassRec *fsbClass = (FSBClassRec*)wc;
+    XmFormClassRec *formClass = (XmFormClassRec*)xmFormWidgetClass;
+    
+    fsbClass->constraint_class.initialize = formClass->constraint_class.initialize;
+    fsbClass->constraint_class.set_values = formClass->constraint_class.set_values;
+}
+
+
+#define STRDUP_RES(a) if(a) a = strdup(a)
+#define XMS_STRDUP_RES(a) if(a) a = XmStringCopy(a)
+
+static void fsb_init(Widget request, Widget neww, ArgList args, Cardinal *num_args) {
+    XnFileSelectionBox fsb = (XnFileSelectionBox)neww;
+    (xmFormClassRec.core_class.initialize)(request, neww, args, num_args);
+    
+    fsb->fsb.disable_set_values = 0;
+    
+    STRDUP_RES(fsb->fsb.homePath);
+    STRDUP_RES(fsb->fsb.selectedPath);
+    STRDUP_RES(fsb->fsb.currentPath);
+    STRDUP_RES(fsb->fsb.filterStr);
+    STRDUP_RES(fsb->fsb.labelListView);
+    STRDUP_RES(fsb->fsb.labelDetailView);
+    STRDUP_RES(fsb->fsb.labelOpenFileTitle);
+    STRDUP_RES(fsb->fsb.labelSaveFileTitle);
+    XMS_STRDUP_RES(fsb->fsb.labelDirUp);
+    XMS_STRDUP_RES(fsb->fsb.labelHome);
+    XMS_STRDUP_RES(fsb->fsb.labelNewFolder);
+    XMS_STRDUP_RES(fsb->fsb.labelFilterButton);
+    XMS_STRDUP_RES(fsb->fsb.labelShowHiddenFiles);
+    XMS_STRDUP_RES(fsb->fsb.labelDirectories);
+    XMS_STRDUP_RES(fsb->fsb.labelFiles);
+    XMS_STRDUP_RES(fsb->fsb.labelRename);
+    XMS_STRDUP_RES(fsb->fsb.labelDelete);
+    XMS_STRDUP_RES(fsb->fsb.labelOpen);
+    XMS_STRDUP_RES(fsb->fsb.labelSave);
+    XMS_STRDUP_RES(fsb->fsb.labelCancel);
+    XMS_STRDUP_RES(fsb->fsb.labelHelp);
+    XMS_STRDUP_RES(fsb->fsb.labelFileName);
+    XMS_STRDUP_RES(fsb->fsb.labelDirectoryName);
+    XMS_STRDUP_RES(fsb->fsb.labelNewFileName);
+    STRDUP_RES(fsb->fsb.labelDeleteFile);
+    STRDUP_RES(fsb->fsb.detailHeadings);
+    STRDUP_RES(fsb->fsb.dateFormatSameYear);
+    STRDUP_RES(fsb->fsb.dateFormatOtherYear);
+    STRDUP_RES(fsb->fsb.suffixBytes);
+    STRDUP_RES(fsb->fsb.suffixKB);
+    STRDUP_RES(fsb->fsb.suffixMB);
+    STRDUP_RES(fsb->fsb.suffixGB);
+    STRDUP_RES(fsb->fsb.suffixTB);
+    STRDUP_RES(fsb->fsb.errorTitle);
+    STRDUP_RES(fsb->fsb.errorIllegalChar);
+    STRDUP_RES(fsb->fsb.errorRename);
+    STRDUP_RES(fsb->fsb.errorFolder);
+    STRDUP_RES(fsb->fsb.errorDelete);
+    STRDUP_RES(fsb->fsb.errorOpenDir);
+    
+    CreateUI((XnFileSelectionBox)fsb);
+    
+    XtAddCallback(neww, XmNmapCallback, fsb_mapcb, NULL);
+}
+
+#define STR_FREE(a) if(a) free(a)
+#define XMSTR_FREE(a) if(a) XmStringFree(a)
+
+static void fsb_destroy(Widget widget) {
+    XnFileSelectionBox w = (XnFileSelectionBox)widget;
+    
+    // destroy all views
+    for(int i=0;i<w->fsb.numviews;i++) {
+        FSBView v = w->fsb.view[i];
+        v.destroy(widget, v.widget, v.userData);
+    }
+    
+    STR_FREE(w->fsb.homePath);
+    
+    // free filelists
+    filedialog_cleanup_filedata(w);
+    STR_FREE(w->fsb.currentPath);
+    STR_FREE(w->fsb.selectedPath);
+    STR_FREE(w->fsb.filterStr);
+    
+    PathBarDestroy(w->fsb.pathBar);
+    
+    // free strings
+    STR_FREE(w->fsb.labelListView);
+    STR_FREE(w->fsb.labelDetailView);
+    STR_FREE(w->fsb.labelOpenFileTitle);
+    STR_FREE(w->fsb.labelSaveFileTitle);
+    
+    XMSTR_FREE(w->fsb.labelDirUp);
+    XMSTR_FREE(w->fsb.labelHome);
+    XMSTR_FREE(w->fsb.labelNewFolder);
+    XMSTR_FREE(w->fsb.labelFilterButton);
+    XMSTR_FREE(w->fsb.labelShowHiddenFiles);
+    XMSTR_FREE(w->fsb.labelDirectories);
+    XMSTR_FREE(w->fsb.labelFiles);
+    XMSTR_FREE(w->fsb.labelRename);
+    XMSTR_FREE(w->fsb.labelDelete);
+    XMSTR_FREE(w->fsb.labelOpen);
+    XMSTR_FREE(w->fsb.labelSave);
+    XMSTR_FREE(w->fsb.labelCancel);
+    XMSTR_FREE(w->fsb.labelHelp);
+    XMSTR_FREE(w->fsb.labelFileName);
+    XMSTR_FREE(w->fsb.labelDirectoryName);
+    XMSTR_FREE(w->fsb.labelNewFileName);
+    STR_FREE(w->fsb.labelDeleteFile);
+    STR_FREE(w->fsb.detailHeadings);
+    
+    STR_FREE(w->fsb.dateFormatSameYear);
+    STR_FREE(w->fsb.dateFormatOtherYear);
+    STR_FREE(w->fsb.suffixBytes);
+    STR_FREE(w->fsb.suffixKB);
+    STR_FREE(w->fsb.suffixMB);
+    STR_FREE(w->fsb.suffixGB);
+    STR_FREE(w->fsb.suffixTB);
+    
+    STR_FREE(w->fsb.errorTitle);
+    STR_FREE(w->fsb.errorIllegalChar);
+    STR_FREE(w->fsb.errorRename);
+    STR_FREE(w->fsb.errorFolder);
+    STR_FREE(w->fsb.errorDelete);
+    STR_FREE(w->fsb.errorOpenDir);
+}
+
+static void fsb_resize(Widget widget) {
+    XnFileSelectionBox w = (XnFileSelectionBox)widget;
+    (xmFormClassRec.core_class.resize)(widget);
+    
+#ifdef FSB_ENABLE_DETAIL
+    if(w->fsb.view[w->fsb.selectedview].update == FileListDetailUpdate) {
+        FileListDetailAdjustColWidth(w->fsb.grid);
+    }
+#endif
+}
+
+static void fsb_realize(Widget widget, XtValueMask *mask, XSetWindowAttributes *attributes) {
+    XnFileSelectionBox w = (XnFileSelectionBox)widget;   
+    (xmFormClassRec.core_class.realize)(widget, mask, attributes);
+    
+    FSBView view = w->fsb.view[w->fsb.selectedview];
+    XmProcessTraversal(view.focus, XmTRAVERSE_CURRENT);
+    
+#ifdef FSB_ENABLE_DETAIL
+    if(w->fsb.view[w->fsb.selectedview].update == FileListDetailUpdate) {
+        FileListDetailAdjustColWidth(w->fsb.grid);
+    }
+#endif
+}
+
+static void FSBUpdateTitle(Widget w) {
+    if(XtParent(w)->core.widget_class == xmDialogShellWidgetClass) {
+        char *title = FSBDialogTitle(w);
+        XtVaSetValues(XtParent(w), XmNtitle, title, NULL);
+    }
+}
+
+static Boolean fsb_set_values(Widget old, Widget request, Widget neww, ArgList args, Cardinal *num_args) {
+    Boolean r = False;
+
+    XnFileSelectionBox o = (XnFileSelectionBox)old;
+    XnFileSelectionBox n = (XnFileSelectionBox)neww;
+    
+    int setOkBtnLabel = 0;
+    int ismanaged = XtIsManaged(neww);
+    Dimension width, height;
+    if(!ismanaged) {
+        width = n->core.width;
+        height = n->core.height;
+        if(n->fsb.pathBar) {
+            n->fsb.pathBar->disableResize = True;
+        }
+    }
+    
+    if(o->fsb.selectedview != n->fsb.selectedview) {
+        int selectedview = n->fsb.selectedview;
+        n->fsb.selectedview = o->fsb.selectedview;
+        SelectView(n, selectedview);
+    }
+    
+    char *updateDir = NULL;
+    int selectItem = 0;
+    if(o->fsb.selectedPath != n->fsb.selectedPath) {
+        STR_FREE(o->fsb.selectedPath);
+        STRDUP_RES(n->fsb.selectedPath);
+        XmTextFieldSetString(n->fsb.name, FileName(n->fsb.selectedPath));
+        // also update current directory
+        updateDir = ParentPath(n->fsb.selectedPath);
+        selectItem = 1;
+    }
+    if(o->fsb.currentPath != n->fsb.currentPath) {
+        STR_FREE(o->fsb.currentPath);
+        updateDir = strdup(n->fsb.currentPath);
+        n->fsb.currentPath = NULL;
+    }
+    
+    if(o->fsb.filterStr != n->fsb.filterStr) {
+        STR_FREE(o->fsb.filterStr);
+        STRDUP_RES(n->fsb.filterStr);
+        XmTextFieldSetString(XmDropDownGetText(n->fsb.filter), n->fsb.filterStr);
+        if(!updateDir) {
+            filedialog_update_dir(n, NULL);
+        }
+    }
+    
+    if(updateDir) {
+        filedialog_update_dir(n, updateDir);
+        PathBarSetPath(n->fsb.pathBar, updateDir);
+        free(updateDir);
+    }
+    
+    if(o->fsb.type != n->fsb.type) {
+        if(n->fsb.type == FILEDIALOG_OPEN) {
+            XtVaSetValues(n->fsb.workarea, XmNbottomWidget, n->fsb.separator, NULL);
+            XtUnmanageChild(n->fsb.name);
+            XtUnmanageChild(n->fsb.nameLabel);
+        } else {
+            XtManageChild(n->fsb.name);
+            XtManageChild(n->fsb.nameLabel);
+            XtVaSetValues(n->fsb.workarea, XmNbottomWidget, n->fsb.nameLabel, NULL);
+        }
+        FSBUpdateTitle(neww);
+        setOkBtnLabel = 1;
+    }
+      
+    // label strings
+    int updateTitle = 0;
+    if(o->fsb.labelListView != n->fsb.labelListView) {
+        STR_FREE(o->fsb.labelListView);
+        STRDUP_RES(n->fsb.labelListView);
+        XmString label = XmStringCreateLocalized(n->fsb.labelListView);
+        XtVaSetValues(n->fsb.viewSelectorList, XmNlabelString, label, NULL);
+        XmStringFree(label);
+    }
+    if(o->fsb.labelDetailView != n->fsb.labelDetailView) {
+        STR_FREE(o->fsb.labelDetailView);
+        STRDUP_RES(n->fsb.labelDetailView);
+        XmString label = XmStringCreateLocalized(n->fsb.labelDetailView);
+        XtVaSetValues(n->fsb.viewSelectorDetail, XmNlabelString, label, NULL);
+        if(n->fsb.detailToggleButton) {
+            XtVaSetValues(n->fsb.detailToggleButton, XmNlabelString, label, NULL);
+        }
+        XmStringFree(label);
+    }
+    if(o->fsb.labelOpenFileTitle != n->fsb.labelOpenFileTitle) {
+        STR_FREE(o->fsb.labelOpenFileTitle);
+        STRDUP_RES(n->fsb.labelOpenFileTitle);
+        updateTitle = 1;
+    }
+    if(o->fsb.labelSaveFileTitle != n->fsb.labelSaveFileTitle) {
+        STR_FREE(o->fsb.labelSaveFileTitle);
+        STRDUP_RES(n->fsb.labelSaveFileTitle);
+        updateTitle = 1;
+    }
+    
+    if(o->fsb.labelDirUp != n->fsb.labelDirUp) {
+        XMSTR_FREE(o->fsb.labelDirUp);
+        XMS_STRDUP_RES(n->fsb.labelDirUp);
+        XtVaSetValues(n->fsb.dirUp, XmNlabelString, n->fsb.labelDirUp, NULL);
+    }
+    if(o->fsb.labelHome != n->fsb.labelHome) {
+        XMSTR_FREE(o->fsb.labelHome);
+        XMS_STRDUP_RES(n->fsb.labelHome);
+        XtVaSetValues(n->fsb.dirUp, XmNlabelString, n->fsb.labelHome, NULL);
+    }
+    if(o->fsb.labelNewFolder != n->fsb.labelNewFolder) {
+        XMSTR_FREE(o->fsb.labelNewFolder);
+        XMS_STRDUP_RES(n->fsb.labelNewFolder);
+        XtVaSetValues(n->fsb.newFolder, XmNlabelString, n->fsb.labelNewFolder, NULL);
+    }
+    if(o->fsb.labelFilterButton != n->fsb.labelFilterButton) {
+        XMSTR_FREE(o->fsb.labelFilterButton);
+        XMS_STRDUP_RES(n->fsb.labelFilterButton);
+        XtVaSetValues(n->fsb.filterButton, XmNlabelString, n->fsb.labelFilterButton, NULL);
+    }
+    if(o->fsb.labelShowHiddenFiles != n->fsb.labelShowHiddenFiles) {
+        XMSTR_FREE(o->fsb.labelShowHiddenFiles);
+        XMS_STRDUP_RES(n->fsb.labelShowHiddenFiles);
+        XtVaSetValues(n->fsb.showHiddenButtonW, XmNlabelString, n->fsb.labelShowHiddenFiles, NULL);
+    }
+    if(o->fsb.labelDirectories != n->fsb.labelDirectories) {
+        XMSTR_FREE(o->fsb.labelDirectories);
+        XMS_STRDUP_RES(n->fsb.labelDirectories);
+        XtVaSetValues(n->fsb.lsDirLabel, XmNlabelString, n->fsb.labelDirectories, NULL);
+    }
+    if(o->fsb.labelFiles != n->fsb.labelFiles) {
+        XMSTR_FREE(o->fsb.labelFiles);
+        XMS_STRDUP_RES(n->fsb.labelFiles);
+        XtVaSetValues(n->fsb.lsFileLabel, XmNlabelString, n->fsb.labelFiles, NULL);
+    }
+    int recreateContextMenu = 0;
+    if(o->fsb.labelRename != n->fsb.labelRename) {
+        XMSTR_FREE(o->fsb.labelRename);
+        XMS_STRDUP_RES(n->fsb.labelRename);
+        recreateContextMenu = 1;
+    }
+    if(o->fsb.labelDelete != n->fsb.labelDelete) {
+        XMSTR_FREE(o->fsb.labelDelete);
+        XMS_STRDUP_RES(n->fsb.labelDelete);
+        recreateContextMenu = 1;
+    }
+
+    if(o->fsb.labelOpen != n->fsb.labelOpen) {
+        XMSTR_FREE(o->fsb.labelOpen);
+        XMS_STRDUP_RES(n->fsb.labelOpen);
+        setOkBtnLabel = 1;
+    }
+    if(o->fsb.labelSave != n->fsb.labelSave) {
+        XMSTR_FREE(o->fsb.labelSave);
+        XMS_STRDUP_RES(n->fsb.labelSave);
+        setOkBtnLabel = 1;
+    }
+    if(o->fsb.labelCancel != n->fsb.labelCancel) {
+        XMSTR_FREE(o->fsb.labelCancel);
+        XMS_STRDUP_RES(n->fsb.labelCancel);
+        XtVaSetValues(n->fsb.cancelBtn, XmNlabelString, n->fsb.labelCancel, NULL);
+    }
+    if(o->fsb.labelHelp != n->fsb.labelHelp) {
+        XMSTR_FREE(o->fsb.labelHelp);
+        XMS_STRDUP_RES(n->fsb.labelHelp);
+        XtVaSetValues(n->fsb.helpBtn, XmNlabelString, n->fsb.labelHelp, NULL);
+    }
+    if(o->fsb.labelFileName != n->fsb.labelFileName) {
+        XMSTR_FREE(o->fsb.labelFileName);
+        XMS_STRDUP_RES(n->fsb.labelFileName);
+        XtVaSetValues(n->fsb.nameLabel, XmNlabelString, n->fsb.labelFileName, NULL);
+    }
+    if(o->fsb.labelDirectoryName != n->fsb.labelDirectoryName) {
+        XMSTR_FREE(o->fsb.labelDirectoryName);
+        XMS_STRDUP_RES(n->fsb.labelDirectoryName);
+    }
+    if(o->fsb.labelNewFileName != n->fsb.labelNewFileName) {
+        XMSTR_FREE(o->fsb.labelNewFileName);
+        XMS_STRDUP_RES(n->fsb.labelNewFileName);
+    }
+    
+    if(o->fsb.labelDeleteFile != n->fsb.labelDeleteFile) {
+        STR_FREE(o->fsb.labelDeleteFile);
+        STRDUP_RES(n->fsb.labelDeleteFile);
+    }
+#ifdef FSB_ENABLE_DETAIL
+    if(o->fsb.detailHeadings != n->fsb.detailHeadings) {
+        STR_FREE(o->fsb.detailHeadings);
+        STRDUP_RES(n->fsb.detailHeadings);
+        XtVaSetValues(n->fsb.grid, XmNsimpleHeadings, n->fsb.detailHeadings, NULL);
+    }
+#endif
+    if(o->fsb.dateFormatSameYear != n->fsb.dateFormatSameYear) {
+        STR_FREE(o->fsb.dateFormatSameYear);
+        STRDUP_RES(n->fsb.dateFormatSameYear);
+    }
+    if(o->fsb.dateFormatOtherYear != n->fsb.dateFormatOtherYear) {
+        STR_FREE(o->fsb.dateFormatOtherYear);
+        STRDUP_RES(n->fsb.dateFormatOtherYear);
+    }
+    if(o->fsb.suffixBytes != n->fsb.suffixBytes) {
+        STR_FREE(o->fsb.suffixBytes);
+        STRDUP_RES(n->fsb.suffixBytes);
+    }
+    if(o->fsb.suffixMB != n->fsb.suffixMB) {
+        STR_FREE(o->fsb.suffixMB);
+        STRDUP_RES(n->fsb.suffixMB);
+    }
+    if(o->fsb.suffixGB != n->fsb.suffixGB) {
+        STR_FREE(o->fsb.suffixGB);
+        STRDUP_RES(n->fsb.suffixGB);
+    }
+    if(o->fsb.suffixTB != n->fsb.suffixTB) {
+        STR_FREE(o->fsb.suffixTB);
+        STRDUP_RES(n->fsb.suffixTB);
+    }
+    if(o->fsb.errorTitle != n->fsb.errorTitle) {
+        STR_FREE(o->fsb.errorTitle);
+        STRDUP_RES(n->fsb.errorTitle);
+    }
+    if(o->fsb.errorIllegalChar != n->fsb.errorIllegalChar) {
+        STR_FREE(o->fsb.errorIllegalChar);
+        STRDUP_RES(n->fsb.errorIllegalChar);
+    }
+    if(o->fsb.errorRename != n->fsb.errorRename) {
+        STR_FREE(o->fsb.errorRename);
+        STRDUP_RES(n->fsb.errorRename);
+    }
+    if(o->fsb.errorFolder != n->fsb.errorFolder) {
+        STR_FREE(o->fsb.errorFolder);
+        STRDUP_RES(n->fsb.errorFolder);
+    }
+    if(o->fsb.errorDelete != n->fsb.errorDelete) {
+        STR_FREE(o->fsb.errorDelete);
+        STRDUP_RES(n->fsb.errorDelete);
+    }
+    if(o->fsb.errorOpenDir != n->fsb.errorOpenDir) {
+        STR_FREE(o->fsb.errorOpenDir);
+        STRDUP_RES(n->fsb.errorOpenDir);
+    }
+    
+    if(updateTitle) {
+        FSBUpdateTitle(neww);
+    }
+    if(recreateContextMenu) {
+        XtDestroyWidget(n->fsb.listContextMenu);
+        XtDestroyWidget(n->fsb.gridContextMenu);
+        n->fsb.listContextMenu = CreateContextMenu(n, n->fsb.filelist, FileContextMenuCB);
+        n->fsb.gridContextMenu = CreateContextMenu(n, n->fsb.grid, FileContextMenuCB);
+    }
+    if(setOkBtnLabel) {
+        XtVaSetValues(n->fsb.okBtn, XmNlabelString, n->fsb.type == FILEDIALOG_OPEN ? n->fsb.labelOpen : n->fsb.labelSave, NULL);
+    }
+    
+    if(!ismanaged && !n->fsb.disable_set_values) {
+        n->fsb.disable_set_values = 1;
+        XtVaSetValues(neww, XmNwidth, width, XmNheight, height, NULL);
+        n->fsb.disable_set_values = 0;
+        
+        if(n->fsb.pathBar)
+            n->fsb.pathBar->disableResize = False;
+    }
+    
+    if(selectItem) {
+        if(ismanaged) {
+            FSBSelectItem(n, FileName(n->fsb.selectedPath));
+        }
+    }
+     
+    Boolean fr = (xmFormClassRec.core_class.set_values)(old, request, neww, args, num_args);
+    return fr ? fr : r;
+}
+
+static void fsb_insert_child(Widget child) {
+    XnFileSelectionBox p = (XnFileSelectionBox)XtParent(child);
+    (xmFormClassRec.composite_class.insert_child)(child);
+    
+    if(!p->fsb.gui_created) {
+        return;
+    }
+    
+    // custom child widget insert
+    XtVaSetValues(child,
+            XmNbottomAttachment, XmATTACH_WIDGET,
+            XmNbottomWidget, p->fsb.bottom_widget,
+            XmNbottomOffset, p->fsb.widgetSpacing,
+            XmNleftAttachment, XmATTACH_FORM,
+            XmNleftOffset, p->fsb.windowSpacing,
+            XmNrightAttachment, XmATTACH_FORM,
+            XmNrightAttachment, XmATTACH_FORM,
+            XmNrightOffset, p->fsb.windowSpacing,
+            NULL);
+    
+    
+    XtVaSetValues(p->fsb.listform,
+            XmNbottomWidget, child,
+            XmNbottomOffset, 0,
+            NULL);
+    
+    p->fsb.workarea = child;
+}
+
+Boolean fsb_acceptfocus(Widget widget, Time *time) {
+    return 0;
+}
+
+static void fsb_mapcb(Widget widget, XtPointer u, XtPointer cb) {
+    XnFileSelectionBox w = (XnFileSelectionBox)widget;
+    pathbar_resize(w->fsb.pathBar->widget, w->fsb.pathBar, NULL);
+    
+    if(w->fsb.type == FILEDIALOG_OPEN) {
+        FSBView view = w->fsb.view[w->fsb.selectedview];
+        XmProcessTraversal(view.focus, XmTRAVERSE_CURRENT);
+    } else {
+        XmProcessTraversal(w->fsb.name, XmTRAVERSE_CURRENT);
+    }
+    
+    
+    if(w->fsb.selectedPath) {
+        FSBSelectItem(w, FileName(w->fsb.selectedPath));
+    }
+}
+
+static void FocusInAP(Widget w, XEvent *event, String *args, Cardinal *nArgs) {
+    
+}
+
+static int apply_filter(XnFileSelectionBox w, const char *pattern, const char *string) {
+    if(!pattern) return 0;
+    
+    FSBFilterFunc func = w->fsb.filterFunc ? w->fsb.filterFunc : FSBGlobFilter;
+    return func(pattern, string);
+}
+
+static int FSBGlobFilter(const char *a, const char *b) {
+    return fnmatch(a, b, 0);
+}
+
+
+static void errCB(Widget w, XtPointer d, XtPointer cbs) {
+    XtDestroyWidget(w);
+}
+
+static void ErrDialog(XnFileSelectionBox w, const char *title, const char *errmsg) {
+    Arg args[16];
+    int n = 0;
+    
+    XmString titleStr = XmStringCreateLocalized((char*)title);
+    XmString msg = XmStringCreateLocalized((char*)errmsg);
+    
+    XtSetArg(args[n], XmNdialogTitle, titleStr); n++;
+    XtSetArg(args[n], XmNselectionLabelString, msg); n++;
+    XtSetArg(args[n], XmNokLabelString, w->fsb.labelOk); n++;
+    XtSetArg(args[n], XmNcancelLabelString, w->fsb.labelCancel); n++;
+    
+    Widget dialog = XmCreatePromptDialog ((Widget)w, "NewFolderPrompt", args, n);
+    
+    Widget help = XmSelectionBoxGetChild(dialog, XmDIALOG_HELP_BUTTON);
+    XtUnmanageChild(help);
+    Widget cancel = XmSelectionBoxGetChild(dialog, XmDIALOG_CANCEL_BUTTON);
+    XtUnmanageChild(cancel);
+    Widget text = XmSelectionBoxGetChild(dialog, XmDIALOG_TEXT);
+    XtUnmanageChild(text);
+    
+    XtAddCallback(dialog, XmNokCallback, errCB, NULL);
+    
+    XtManageChild(dialog);
+    
+    XmStringFree(titleStr);
+    XmStringFree(msg);
+}
+
+static void rename_file_cb(Widget w, const char *path, XmSelectionBoxCallbackStruct *cb) {
+    XnFileSelectionBox fsb = NULL;
+    XtVaGetValues(w, XmNuserData, &fsb, NULL);
+    
+    char *fileName = NULL;
+    XmStringGetLtoR(cb->value, XmSTRING_DEFAULT_CHARSET, &fileName);
+    
+    // make sure the new file name doesn't contain a path separator
+    if(strchr(fileName, '/')) {
+        ErrDialog(fsb, fsb->fsb.errorTitle, fsb->fsb.errorIllegalChar);
+        XtFree(fileName);
+        return;
+    }
+    
+    char *parentPath = ParentPath(path);
+    char *newPath = ConcatPath(parentPath, fileName);
+    
+    if(rename(path, newPath)) {
+        char errmsg[256];
+        snprintf(errmsg, 256, fsb->fsb.errorRename, strerror(errno));
+        ErrDialog(fsb, fsb->fsb.errorTitle, errmsg);
+    } else {
+        filedialog_update_dir(fsb, parentPath);
+    }
+    
+    free(parentPath);
+    free(newPath);
+    XtFree(fileName);
+    XtDestroyWidget(XtParent(w));
+}
+
+static void selectionbox_cancel(Widget w, XtPointer data, XtPointer d) {
+    XtDestroyWidget(XtParent(w));
+}
+
+static void FSBRename(XnFileSelectionBox fsb, const char *path) {
+    Arg args[16];
+    int n = 0;
+    Widget w = (Widget)fsb;
+    
+    char *name = FileName((char*)path);
+    
+    XmString filename = XmStringCreateLocalized(name);
+    XtSetArg(args[n], XmNselectionLabelString,fsb->fsb.labelNewFileName); n++;
+    XtSetArg(args[n], XmNtextString, filename); n++;
+    XtSetArg(args[n], XmNuserData, fsb); n++;
+    XtSetArg(args[n], XmNdialogTitle, fsb->fsb.labelRename); n++;
+    XtSetArg(args[n], XmNokLabelString, fsb->fsb.labelOk); n++;
+    XtSetArg(args[n], XmNcancelLabelString, fsb->fsb.labelCancel); n++;
+    Widget dialog = XmCreatePromptDialog (w, "RenameFilePrompt", args, n);
+    
+    Widget help = XmSelectionBoxGetChild(dialog, XmDIALOG_HELP_BUTTON);
+    XtUnmanageChild(help);
+    
+    XtAddCallback(dialog, XmNokCallback, (XtCallbackProc)rename_file_cb, (char*)path);
+    XtAddCallback(dialog, XmNcancelCallback, (XtCallbackProc)selectionbox_cancel, NULL);
+    
+    XmStringFree(filename);
+    XtManageChild(dialog);
+}
+
+static void delete_file_cb(Widget w, const char *path, XmSelectionBoxCallbackStruct *cb) {
+    XnFileSelectionBox fsb = NULL;
+    XtVaGetValues(w, XmNuserData, &fsb, NULL);
+    
+    if(unlink(path)) {
+        char errmsg[256];
+        snprintf(errmsg, 256, fsb->fsb.errorDelete, strerror(errno));
+        ErrDialog(fsb, fsb->fsb.errorTitle, errmsg);
+    } else {
+        char *parentPath = ParentPath(path);
+        filedialog_update_dir(fsb, parentPath);
+        free(parentPath);
+    }
+    
+    XtDestroyWidget(XtParent(w));
+}
+
+static void FSBDelete(XnFileSelectionBox fsb, const char *path) {
+    Arg args[16];
+    int n = 0;
+    Widget w = (Widget)fsb;
+    
+    char *name = FileName((char*)path);
+    size_t len = strlen(name);
+    size_t msglen = len + strlen(fsb->fsb.labelDeleteFile) + 4;
+    char *msg = malloc(msglen);
+    snprintf(msg, msglen, fsb->fsb.labelDeleteFile, name);
+    
+    XmString prompt = XmStringCreateLocalized(msg);
+    XtSetArg(args[n], XmNselectionLabelString, prompt); n++;
+    XtSetArg(args[n], XmNuserData, fsb); n++;
+    XtSetArg(args[n], XmNdialogTitle, fsb->fsb.labelDelete); n++;
+    XtSetArg(args[n], XmNokLabelString, fsb->fsb.labelOk); n++;
+    XtSetArg(args[n], XmNcancelLabelString, fsb->fsb.labelCancel); n++;
+    Widget dialog = XmCreatePromptDialog (w, "DeleteFilePrompt", args, n);
+    
+    Widget help = XmSelectionBoxGetChild(dialog, XmDIALOG_HELP_BUTTON);
+    XtUnmanageChild(help);
+    Widget text = XmSelectionBoxGetChild(dialog, XmDIALOG_TEXT);
+    XtUnmanageChild(text);
+    
+    XtAddCallback(dialog, XmNokCallback, (XtCallbackProc)delete_file_cb, (char*)path);
+    XtAddCallback(dialog, XmNcancelCallback, (XtCallbackProc)selectionbox_cancel, NULL);
+    
+    free(msg);
+    XmStringFree(prompt);
+    XtManageChild(dialog);
+}
+
+static void FSBSelectItem(XnFileSelectionBox fsb, const char *item) {
+    FSBView view = fsb->fsb.view[fsb->fsb.selectedview];
+    if(view.select) {
+        view.select((Widget)fsb, view.widget, item);
+    }
+}
+
+static char* set_selected_path(XnFileSelectionBox data, XmString item)
+{
+    char *name = NULL;
+    XmStringGetLtoR(item, XmFONTLIST_DEFAULT_TAG, &name);
+    if(!name) {
+        return NULL;
+    }
+    char *path = ConcatPath(data->fsb.currentPath, name);
+    XtFree(name);
+    
+    if(data->fsb.selectedPath) {
+        free(data->fsb.selectedPath);
+    }
+    data->fsb.selectedPath = path;
+    
+    return path;
+}
+
+// item0: rename
+// item1: delete
+static void FileContextMenuCB(Widget item, XtPointer index, XtPointer cd) {
+    intptr_t i = (intptr_t)index;
+    Widget parent = XtParent(item);
+    XnFileSelectionBox fsb = NULL;
+    XtVaGetValues(parent, XmNuserData, &fsb, NULL);
+    
+    const char *path = fsb->fsb.selectedPath;
+    if(path) {
+        if(i == 0) {
+            FSBRename(fsb, path);
+        } else if(i == 1) {
+            FSBDelete(fsb, path);
+        }
+    }
+}
+
+static Widget CreateContextMenu(XnFileSelectionBox fsb, Widget parent, XtCallbackProc callback) {
+    return XmVaCreateSimplePopupMenu(
+            parent, "popup", callback, XmNpopupEnabled, XmPOPUP_AUTOMATIC,
+            XmNuserData, fsb,
+            XmVaPUSHBUTTON, fsb->fsb.labelRename, 'R', NULL, NULL,
+            XmVaPUSHBUTTON, fsb->fsb.labelDelete, 'D', NULL, NULL,
+            NULL);
+}
+
+
+static void FileListUpdate(Widget fsb, Widget view, FileElm *dirs, int dircount, FileElm *files, int filecount, const char *filter, int maxnamelen, void *userData) {
+    XnFileSelectionBox data = userData;
+    FileListWidgetAdd(data, data->fsb.filelist, data->fsb.showHidden, filter, files, filecount);
+}
+
+static void FileListSelect(Widget fsb, Widget view, const char *item) {
+    XnFileSelectionBox w = (XnFileSelectionBox)fsb;
+    
+    int numItems = 0;
+    XmStringTable items = NULL;
+    XtVaGetValues(w->fsb.filelist, XmNitemCount, &numItems, XmNitems, &items, NULL);
+    
+    for(int i=0;i<numItems;i++) {
+        char *str = NULL;
+        XmStringGetLtoR(items[i], XmFONTLIST_DEFAULT_TAG, &str);
+        if(!strcmp(str, item)) {
+            XmListSelectPos(w->fsb.filelist, i+1, False);
+            break;
+        }
+        XtFree(str);
+    }
+}
+
+static void FileListCleanup(Widget fsb, Widget view, void *userData) {
+    XnFileSelectionBox data = userData;
+    XmListDeleteAllItems(data->fsb.filelist);
+}
+
+static void FileListDestroy(Widget fsb, Widget view, void *userData) {
+    // unused
+}
+
+static void FileListActivateCB(Widget w, XnFileSelectionBox data, XmListCallbackStruct *cb)
+{
+    char *path = set_selected_path(data, cb->item);
+    if(path) {
+        data->fsb.end = True;
+        data->fsb.status = FILEDIALOG_OK;
+        data->fsb.selIsDir = False;
+        FileSelectionCallback(data, data->fsb.okCallback, XmCR_OK, data->fsb.selectedPath);
+    }
+}
+
+static void FileListSelectCB(Widget w, XnFileSelectionBox data, XmListCallbackStruct *cb)
+{
+    if(data->fsb.type == FILEDIALOG_SAVE) {
+        char *name = NULL;
+        XmStringGetLtoR(cb->item, XmFONTLIST_DEFAULT_TAG, &name);
+        XmTextFieldSetString(data->fsb.name, name);
+        XtFree(name);
+    } else {
+        char *path = set_selected_path(data, cb->item);
+        if(path) {
+            data->fsb.selIsDir = False;
+        }
+    }
+}
+
+
+static void FileListWidgetAdd(XnFileSelectionBox fsb, Widget w, int showHidden, const char *filter, FileElm *ls, int count)
+{   
+    if(count > 0) {
+        XmStringTable items = calloc(count, sizeof(XmString));
+        int i = 0;
+        
+        for(int j=0;j<count;j++) {
+            FileElm *e = &ls[j];
+            
+            char *name = FileName(e->path);
+            if((!showHidden && name[0] == '.') || apply_filter(fsb, filter, name)) {
+                continue;
+            }
+            
+            items[i] = XmStringCreateLocalized(name);
+            i++;
+        }
+        XmListAddItems(w, items, i, 0);
+        for(i=0;i<count;i++) {
+            XmStringFree(items[i]);
+        }
+        free(items);
+    }
+}
+
+#ifdef FSB_ENABLE_DETAIL
+static void FileListDetailUpdate(Widget fsb, Widget view, FileElm *dirs, int dircount, FileElm *files, int filecount, const char *filter, int maxnamelen, void *userData) {
+    XnFileSelectionBox data = userData;
+    FileListDetailAdd(data, data->fsb.grid, data->fsb.showHidden, filter, files, filecount, maxnamelen);
+}
+#endif
+
+/*
+ * create file size string with kb/mb/gb/tb suffix
+ */
+static char* size_str(XnFileSelectionBox fsb, FileElm *f) {
+    char *str = malloc(16);
+    uint64_t size = f->size;
+    
+    if(f->isDirectory) {
+        str[0] = '\0';
+    } else if(size < 0x400) {
+        snprintf(str, 16, "%d %s", (int)size, fsb->fsb.suffixBytes);
+    } else if(size < 0x100000) {
+        float s = (float)size/0x400;
+        int diff = (s*100 - (int)s*100);
+        if(diff > 90) {
+            diff = 0;
+            s += 0.10f;
+        }
+        if(size < 0x2800 && diff != 0) {
+            // size < 10 KiB
+            snprintf(str, 16, "%.1f %s", s, fsb->fsb.suffixKB);
+        } else {
+            snprintf(str, 16, "%.0f %s", s, fsb->fsb.suffixKB);
+        }
+    } else if(size < 0x40000000) {
+        float s = (float)size/0x100000;
+        int diff = (s*100 - (int)s*100);
+        if(diff > 90) {
+            diff = 0;
+            s += 0.10f;
+        }
+        if(size < 0xa00000 && diff != 0) {
+            // size < 10 MiB
+            snprintf(str, 16, "%.1f %s", s, fsb->fsb.suffixMB);
+        } else {
+            snprintf(str, 16, "%.0f %s", s, fsb->fsb.suffixMB);
+        }
+    } else if(size < 0x1000000000ULL) {
+        float s = (float)size/0x40000000;
+        int diff = (s*100 - (int)s*100);
+        if(diff > 90) {
+            diff = 0;
+            s += 0.10f;
+        }
+        if(size < 0x280000000 && diff != 0) {
+            // size < 10 GiB
+            snprintf(str, 16, "%.1f %s", s, fsb->fsb.suffixGB);
+        } else {
+            snprintf(str, 16, "%.0f %s", s, fsb->fsb.suffixGB);
+        }
+    } else {
+        size /= 1024;
+        float s = (float)size/0x40000000;
+        int diff = (s*100 - (int)s*100);
+        if(diff > 90) {
+            diff = 0;
+            s += 0.10f;
+        }
+        if(size < 0x280000000 && diff != 0) {
+            // size < 10 TiB
+            snprintf(str, 16, "%.1f %s", s, fsb->fsb.suffixTB);
+        } else {
+            snprintf(str, 16, "%.0f %s", s, fsb->fsb.suffixTB);
+        }
+    }
+    return str;
+}
+
+static char* date_str(XnFileSelectionBox fsb, time_t tm) {
+    struct tm t;
+    struct tm n;
+    time_t now = time(NULL);
+    
+    localtime_r(&tm, &t);
+    localtime_r(&now, &n);
+    
+    char *str = malloc(24);
+    if(t.tm_year == n.tm_year) {
+        strftime(str, 24, fsb->fsb.dateFormatSameYear, &t);
+    } else {
+        strftime(str, 24, fsb->fsb.dateFormatOtherYear, &t);
+    }
+    return str;
+}
+
+#ifdef FSB_ENABLE_DETAIL
+static void FileListDetailAdjustColWidth(Widget grid) {
+    XmLGridColumn column0 = XmLGridGetColumn(grid, XmCONTENT, 0);
+    XmLGridColumn column1 = XmLGridGetColumn(grid, XmCONTENT, 1);
+    XmLGridColumn column2 = XmLGridGetColumn(grid, XmCONTENT, 2);
+    
+    Dimension col0Width = XmLGridColumnWidthInPixels(column0);
+    Dimension col1Width = XmLGridColumnWidthInPixels(column1);
+    Dimension col2Width = XmLGridColumnWidthInPixels(column2);
+    
+    Dimension totalWidth = col0Width + col1Width + col2Width;
+    
+    Dimension gridWidth = 0;
+    Dimension gridShadow = 0;
+    XtVaGetValues(grid, XmNwidth, &gridWidth, XmNshadowThickness, &gridShadow, NULL);
+    
+    Dimension widthDiff = gridWidth - totalWidth - gridShadow - gridShadow;
+    
+    if(gridWidth > totalWidth) {
+            XtVaSetValues(grid,
+            XmNcolumnRangeStart, 0,
+            XmNcolumnRangeEnd, 0,
+            XmNcolumnWidth, col0Width + widthDiff - XmLGridVSBWidth(grid) - 2,
+            XmNcolumnSizePolicy, XmCONSTANT,
+            NULL);
+    }
+}
+
+static void FileListDetailAdd(XnFileSelectionBox fsb, Widget grid, int showHidden, const char *filter, FileElm *ls, int count, int maxWidth)
+{
+    XmLGridAddRows(grid, XmCONTENT, 1, count);
+    
+    int row = 0;
+    for(int i=0;i<count;i++) {
+        FileElm *e = &ls[i];
+        
+        char *name = FileName(e->path);
+        if((!showHidden && name[0] == '.') || (!e->isDirectory && apply_filter(fsb, filter, name))) {
+            continue;
+        }
+        
+        // name
+        XmString str = XmStringCreateLocalized(name);
+        XtVaSetValues(grid,
+                XmNcolumn, 0, 
+                XmNrow, row,
+                XmNcellString, str, NULL);
+        XmStringFree(str);
+        // size
+        char *szbuf = size_str(fsb, e);
+        str = XmStringCreateLocalized(szbuf);
+        XtVaSetValues(grid,
+                XmNcolumn, 1, 
+                XmNrow, row,
+                XmNcellString, str, NULL);
+        free(szbuf);
+        XmStringFree(str);
+        // date
+        char *datebuf = date_str(fsb, e->lastModified);
+        str = XmStringCreateLocalized(datebuf);
+        XtVaSetValues(grid,
+                XmNcolumn, 2, 
+                XmNrow, row,
+                XmNcellString, str, NULL);
+        free(datebuf);
+        XmStringFree(str);
+        
+        XtVaSetValues(grid, XmNrow, row, XmNrowUserData, e, NULL);
+        row++;
+    }
+    
+    // remove unused rows
+    if(count > row) {
+        XmLGridDeleteRows(grid, XmCONTENT, row, count-row);
+    }
+    
+    if(maxWidth < 16) {
+        maxWidth = 16;
+    }
+    
+    XtVaSetValues(grid,
+        XmNcolumnRangeStart, 0,
+        XmNcolumnRangeEnd, 0,
+        XmNcolumnWidth, maxWidth,
+        XmNcellAlignment, XmALIGNMENT_LEFT,
+        XmNcolumnSizePolicy, XmVARIABLE,
+        NULL);
+    XtVaSetValues(grid,
+        XmNcolumnRangeStart, 1,
+        XmNcolumnRangeEnd, 1,
+        XmNcolumnWidth, 9,
+        XmNcellAlignment, XmALIGNMENT_LEFT,
+        XmNcolumnSizePolicy, XmVARIABLE,
+        NULL);
+    XtVaSetValues(grid,
+        XmNcolumnRangeStart, 2,
+        XmNcolumnRangeEnd, 2,
+        XmNcolumnWidth, 16,
+        XmNcellAlignment, XmALIGNMENT_RIGHT,
+        XmNcolumnSizePolicy, XmVARIABLE,
+        NULL);
+    
+    FileListDetailAdjustColWidth(grid);
+}
+
+static void FileListDetailSelect(Widget fsb, Widget view, const char *item) {
+    XnFileSelectionBox w = (XnFileSelectionBox)fsb;
+    
+    int numRows = 0;
+    XtVaGetValues(w->fsb.grid, XmNrows, &numRows, NULL);
+    
+    XmLGridColumn col = XmLGridGetColumn(w->fsb.grid, XmCONTENT, 0);
+    for(int i=0;i<numRows;i++) {
+        XmLGridRow row = XmLGridGetRow(w->fsb.grid, XmCONTENT, i);
+        FileElm *elm = NULL;
+        XtVaGetValues(w->fsb.grid, XmNrowPtr, row, XmNcolumnPtr, col, XmNrowUserData, &elm, NULL);
+        if(elm) {
+            if(!strcmp(item, FileName(elm->path))) {
+                XmLGridSelectRow(w->fsb.grid, i, False);
+                XmLGridFocusAndShowRow(w->fsb.grid, i+1);
+                break;
+            }
+        }
+    }
+}
+
+static void FileListDetailCleanup(Widget fsb, Widget view, void *userData) {
+    XnFileSelectionBox data = userData;
+    // cleanup grid
+    Cardinal rows = 0;
+    XtVaGetValues(data->fsb.grid, XmNrows, &rows, NULL);
+    XmLGridDeleteRows(data->fsb.grid, XmCONTENT, 0, rows);
+}
+
+static void FileListDetailDestroy(Widget fsb, Widget view, void *userData) {
+    // unused
+}
+#endif
+
+static void create_folder(Widget w, XnFileSelectionBox data, XmSelectionBoxCallbackStruct *cbs) {
+    char *fileName = NULL;
+    XmStringGetLtoR(cbs->value, XmSTRING_DEFAULT_CHARSET, &fileName);
+    
+    char *newFolder = ConcatPath(data->fsb.currentPath ? data->fsb.currentPath : "", fileName);
+    if(mkdir(newFolder, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH)) {
+        char errmsg[256];
+        snprintf(errmsg, 256, data->fsb.errorFolder, strerror(errno));
+        ErrDialog(data, data->fsb.errorTitle, errmsg);
+    } else {
+        char *p = strdup(data->fsb.currentPath);
+        filedialog_update_dir(data, p);
+        free(p);
+    }
+    free(newFolder);
+    
+    XtDestroyWidget(XtParent(w));
+}
+
+static void new_folder_cancel(Widget w, XnFileSelectionBox data, XtPointer d) {
+    XtDestroyWidget(XtParent(w));
+}
+
+static void FSBNewFolder(Widget w, XnFileSelectionBox data, XtPointer u)
+{
+    Arg args[16];
+    int n = 0;
+    
+    XtSetArg(args[n], XmNdialogTitle, data->fsb.labelNewFolder); n++;
+    XtSetArg (args[n], XmNselectionLabelString, data->fsb.labelDirectoryName); n++;
+    XtSetArg(args[n], XmNokLabelString, data->fsb.labelOk); n++;
+    XtSetArg(args[n], XmNcancelLabelString, data->fsb.labelCancel); n++;
+    Widget dialog = XmCreatePromptDialog (w, "NewFolderPrompt", args, n);
+    
+    Widget help = XmSelectionBoxGetChild(dialog, XmDIALOG_HELP_BUTTON);
+    XtUnmanageChild(help);
+    
+    XtAddCallback(dialog, XmNokCallback, (XtCallbackProc)create_folder, data);
+    XtAddCallback(dialog, XmNcancelCallback, (XtCallbackProc)new_folder_cancel, data);
+    
+    XtManageChild(dialog);
+    
+}
+
+static void FSBHome(Widget w, XnFileSelectionBox data, XtPointer u) {
+    const char *homePath = data->fsb.homePath ? data->fsb.homePath : GetHomeDir();
+    filedialog_update_dir(data, homePath);
+    PathBarSetPath(data->fsb.pathBar, homePath);
+}
+
+
+/*
+ * file_cmp_field
+ * 0: compare path
+ * 1: compare size
+ * 2: compare mtime
+ */
+static int file_cmp_field = 0;
+
+/*
+ * 1 or -1
+ */
+static int file_cmp_order = 1;
+
+static int filecmp(const void *f1, const void *f2)
+{
+    const FileElm *file1 = f1;
+    const FileElm *file2 = f2;
+    if(file1->isDirectory != file2->isDirectory) {
+        return file1->isDirectory < file2->isDirectory;
+    }
+    
+    int cmp_field = file_cmp_field;
+    int cmp_order = file_cmp_order;
+    if(file1->isDirectory) {
+        cmp_field = 0;
+        cmp_order = 1;
+    }
+    
+    int ret = 0;
+    switch(cmp_field) {
+        case 0: {
+            ret = strcmp(FileName(file1->path), FileName(file2->path));
+            break;
+        }
+        case 1: {
+            if(file1->size < file2->size) {
+                ret = -1;
+            } else if(file1->size == file2->size) {
+                ret = 0;
+            } else {
+                ret = 1;
+            }
+            break;
+        }
+        case 2: {
+            if(file1->lastModified < file2->lastModified) {
+                ret = -1;
+            } else if(file1->lastModified == file2->lastModified) {
+                ret = 0;
+            } else {
+                ret = 1;
+            }
+            break;
+        }
+    }
+    
+    return ret * cmp_order;
+}
+
+
+static void free_files(FileElm *ls, int count)
+{
+    for(int i=0;i<count;i++) {
+        if(ls[i].path) {
+            free(ls[i].path);
+        }
+    }
+    free(ls);
+}
+
+static void filedialog_cleanup_filedata(XnFileSelectionBox data)
+{
+    free_files(data->fsb.dirs, data->fsb.dircount);
+    free_files(data->fsb.files, data->fsb.filecount);
+    data->fsb.dirs = NULL;
+    data->fsb.files = NULL;
+    data->fsb.dircount = 0;
+    data->fsb.filecount = 0;
+    data->fsb.maxnamelen = 0;
+}
+
+#define FILE_ARRAY_SIZE 1024
+
+static void file_array_add(FileElm **files, int *alloc, int *count, FileElm elm) {
+    int c = *count;
+    int a = *alloc;
+    if(c >= a) {
+        a *= 2;
+        FileElm *newarray = realloc(*files, sizeof(FileElm) * a);
+        
+        *files = newarray;
+        *alloc = a;
+    }
+    
+    (*files)[c] = elm;
+    c++;
+    *count = c;
+}
+
+static int filedialog_update_dir(XnFileSelectionBox data, const char *path)
+{
+    DIR *dir = NULL;
+    if(path) {
+        // try to check first, if we can open the path
+        dir = opendir(path);
+        if(!dir) {
+            char errmsg[256];
+            snprintf(errmsg, 256, data->fsb.errorOpenDir, strerror(errno));
+            
+            ErrDialog(data, data->fsb.errorTitle, errmsg);
+            return 1;
+        }
+    }
+    
+    FSBView view = data->fsb.view[data->fsb.selectedview];
+    view.cleanup((Widget)data, view.widget, view.userData);
+      
+    if(view.useDirList) {
+        XmListDeleteAllItems(data->fsb.dirlist);
+    }
+    
+    /* read dir and insert items */
+    if(path) {
+        int dircount = 0; 
+        int filecount = 0;
+        size_t maxNameLen = 0;
+        
+        FileElm *dirs = calloc(sizeof(FileElm), FILE_ARRAY_SIZE);
+        FileElm *files = calloc(sizeof(FileElm), FILE_ARRAY_SIZE);
+        int dirs_alloc = FILE_ARRAY_SIZE;
+        int files_alloc = FILE_ARRAY_SIZE;
+        
+        filedialog_cleanup_filedata(data);
+    
+        /* dir reading complete - set the path textfield */  
+        XmTextFieldSetString(data->fsb.path, (char*)path);
+        char *oldPath = data->fsb.currentPath;
+        data->fsb.currentPath = strdup(path);
+        if(oldPath) {
+            free(oldPath);
+        }
+        path = data->fsb.currentPath;
+
+        struct dirent *ent;
+        while((ent = readdir(dir)) != NULL) {
+            if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) {
+                continue;
+            }
+
+            char *entpath = ConcatPath(path, ent->d_name);
+
+            struct stat s;
+            if(stat(entpath, &s)) {
+                free(entpath);
+                continue;
+            }
+
+            FileElm new_entry;
+            new_entry.path = entpath;
+            new_entry.isDirectory = S_ISDIR(s.st_mode);
+            new_entry.size = (uint64_t)s.st_size;
+            new_entry.lastModified = s.st_mtime;
+
+            size_t nameLen = strlen(ent->d_name);
+            if(nameLen > maxNameLen) {
+                maxNameLen = nameLen;
+            }
+
+            if(new_entry.isDirectory) {
+                file_array_add(&dirs, &dirs_alloc, &dircount, new_entry);
+            } else {
+                file_array_add(&files, &files_alloc, &filecount, new_entry);
+            }
+        }
+        closedir(dir);
+        
+        data->fsb.dirs = dirs;
+        data->fsb.files = files;
+        data->fsb.dircount = dircount;
+        data->fsb.filecount = filecount;
+        data->fsb.maxnamelen = maxNameLen;
+        
+        // sort file arrays
+        qsort(dirs, dircount, sizeof(FileElm), filecmp);
+        qsort(files, filecount, sizeof(FileElm), filecmp);
+    }
+    
+    Widget filterTF = XmDropDownGetText(data->fsb.filter);
+    char *filter = XmTextFieldGetString(filterTF);
+    char *filterStr = filter;
+    if(!filter || strlen(filter) == 0) {
+        filterStr = "*";
+    }
+    
+    if(view.useDirList) {
+        FileListWidgetAdd(data, data->fsb.dirlist, data->fsb.showHidden, NULL, data->fsb.dirs, data->fsb.dircount);
+        view.update(
+                (Widget)data,
+                view.widget,
+                NULL,
+                0,
+                data->fsb.files,
+                data->fsb.filecount,
+                filterStr,
+                data->fsb.maxnamelen,
+                view.userData);
+    } else {
+        view.update(
+                (Widget)data,
+                view.widget,
+                data->fsb.dirs,
+                data->fsb.dircount,
+                data->fsb.files,
+                data->fsb.filecount,
+                filterStr,
+                data->fsb.maxnamelen,
+                view.userData);
+    }
+    
+    if(filter) {
+        XtFree(filter);
+    }
+    
+    return 0;
+}
+
+
+static void dirlist_activate(Widget w, XnFileSelectionBox data, XmListCallbackStruct *cb)
+{
+    char *path = set_selected_path(data, cb->item);
+    if(path) {
+        if(!filedialog_update_dir(data, path)) {
+            PathBarSetPath(data->fsb.pathBar, path);
+            data->fsb.selIsDir = TRUE;
+        } 
+    }    
+}
+
+static void dirlist_select(Widget w, XnFileSelectionBox data, XmListCallbackStruct *cb)
+{
+    char *path = set_selected_path(data, cb->item);
+    if(path) {
+        data->fsb.selIsDir = TRUE;
+    }
+}
+
+static void filedialog_enable_detailview(Widget w, XnFileSelectionBox data, XmToggleButtonCallbackStruct *tb) {
+    SelectView(data, tb->set); // 0: list, 1: detail
+}
+
+
+static void filedialog_setshowhidden(
+        Widget w,
+        XnFileSelectionBox data,
+        XmToggleButtonCallbackStruct *tb)
+{
+    data->fsb.showHidden = tb->set;
+    filedialog_update_dir(data, NULL);
+}
+
+static void filedialog_filter(Widget w, XnFileSelectionBox data, XtPointer c)
+{
+    filedialog_update_dir(data, NULL);
+}
+
+static void filedialog_update_filter(Widget w, XnFileSelectionBox data, XtPointer c)
+{
+    filedialog_update_dir(data, NULL);
+    
+}
+
+static void filedialog_goup(Widget w, XnFileSelectionBox data, XtPointer d)
+{
+    char *newPath = ParentPath(data->fsb.currentPath);
+    filedialog_update_dir(data, newPath);
+    PathBarSetPath(data->fsb.pathBar, newPath);
+    free(newPath);
+}
+
+static void filedialog_ok(Widget w, XnFileSelectionBox data, XtPointer d)
+{
+    if(data->fsb.type == FILEDIALOG_SAVE) {
+        char *newName = XmTextFieldGetString(data->fsb.name);
+        if(newName) {
+            if(strchr(newName, '/')) {
+                ErrDialog(data, data->fsb.errorTitle, data->fsb.errorIllegalChar);
+                XtFree(newName);
+                return;
+            }
+            
+            if(strlen(newName) > 0) {
+                char *selPath = ConcatPath(data->fsb.currentPath, newName);
+                if(data->fsb.selectedPath) free(data->fsb.selectedPath);
+                data->fsb.selectedPath = selPath;
+            }
+            XtFree(newName);
+            
+            data->fsb.selIsDir = False;
+        }
+    }
+    
+    if(data->fsb.selectedPath) {
+        if(!data->fsb.selIsDir) {
+            data->fsb.status = FILEDIALOG_OK;
+            data->fsb.end = True;
+            FileSelectionCallback(data, data->fsb.okCallback, XmCR_OK, data->fsb.selectedPath);
+        }
+    }
+}
+
+static void filedialog_cancel(Widget w, XnFileSelectionBox data, XtPointer d)
+{
+    data->fsb.end = 1;
+    data->fsb.status = FILEDIALOG_CANCEL;
+    FileSelectionCallback(data, data->fsb.cancelCallback, XmCR_CANCEL, data->fsb.currentPath);
+}
+
+static void filedialog_help(Widget w, XnFileSelectionBox data, XtPointer d)
+{
+    FileSelectionCallback(data, data->manager.help_callback, XmCR_HELP, data->fsb.currentPath);
+}
+
+static void FileSelectionCallback(XnFileSelectionBox fsb, XtCallbackList cb, int reason, const char *value) {
+    XmFileSelectionBoxCallbackStruct cbs;
+    memset(&cbs, 0, sizeof(XmFileSelectionBoxCallbackStruct));
+    
+    char *dir = fsb->fsb.currentPath;
+    size_t dirlen = dir ? strlen(dir) : 0;
+    if(dir && dirlen > 0) {
+        char *dir2 = NULL;
+        if(dir[dirlen-1] != '/') {
+            // add a trailing / to the dir string 
+            dir2 = malloc(dirlen+2);
+            memcpy(dir2, dir, dirlen);
+            dir2[dirlen] = '/';
+            dir2[dirlen+1] = '\0';
+            dirlen++;
+            dir = dir2;
+        }
+        cbs.dir = XmStringCreateLocalized(dir);
+        cbs.dir_length = dirlen;
+        if(dir2) {
+            free(dir2);
+        }
+    } else {
+        cbs.dir = XmStringCreateLocalized("");
+        cbs.dir_length = 0;
+    }
+    cbs.reason = reason;
+    
+    cbs.value = XmStringCreateLocalized((char*)value);
+    cbs.length = strlen(value);
+    
+    XtCallCallbackList((Widget)fsb, cb, (XtPointer)&cbs);
+    
+    XmStringFree(cbs.dir);
+    XmStringFree(cbs.value);
+}
+
+static void CreateUI(XnFileSelectionBox w) { 
+    Arg args[32];
+    int n = 0;
+    XmString str;
+       
+    int widget_spacing = w->fsb.widgetSpacing;
+    int window_spacing = w->fsb.windowSpacing;
+    
+    Widget form = (Widget)w;
+    int type = w->fsb.type;
+    
+    XtVaSetValues((Widget)w, XmNautoUnmanage, False, NULL);
+     
+    /* upper part of the gui */
+    
+    n = 0;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelDirUp); n++;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNresizable, True); n++;
+    XtSetArg(args[n], XmNarrowDirection, XmARROW_UP); n++;
+    w->fsb.dirUp = XmCreatePushButton(form, "DirUp", args, n);
+    XtManageChild(w->fsb.dirUp);
+    XtAddCallback(w->fsb.dirUp, XmNactivateCallback,
+                 (XtCallbackProc)filedialog_goup, w);
+    
+    // View Option Menu
+    n = 0;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNshadowThickness, 0); n++;
+    Widget viewframe = XmCreateForm(form, "vframe", args, n);
+    XtManageChild(viewframe);
+
+    w->fsb.viewMenu = XmCreatePulldownMenu(viewframe, "menu", NULL, 0);
+    
+    Widget view;
+    if(w->fsb.showViewMenu) {
+        n = 0;
+        XtSetArg(args[n], XmNsubMenuId, w->fsb.viewMenu); n++;
+        XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNmarginHeight, 0); n++;
+        XtSetArg(args[n], XmNmarginWidth, 0); n++;
+        view = XmCreateOptionMenu(viewframe, "option_menu", args, n);
+        XtManageChild(view);
+        w->fsb.viewOption = view;
+        w->fsb.detailToggleButton = NULL;
+    } else {
+        n = 0;
+        str = XmStringCreateLocalized(w->fsb.labelDetailView);
+        XtSetArg(args[n], XmNlabelString, str); n++;
+        XtSetArg(args[n], XmNfillOnSelect, True); n++;
+        XtSetArg(args[n], XmNindicatorOn, False); n++;
+        XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+        if(w->fsb.selectedview == 1) {
+            XtSetArg(args[n], XmNset, 1); n++;
+        }
+        w->fsb.detailToggleButton = XmCreateToggleButton(viewframe, "ToggleDetailView", args, n);
+        XtManageChild(w->fsb.detailToggleButton);
+        view = w->fsb.detailToggleButton;
+        XmStringFree(str);
+        
+        XtAddCallback(
+            w->fsb.detailToggleButton,
+            XmNvalueChangedCallback,
+            (XtCallbackProc)filedialog_enable_detailview,
+            w);
+        
+        w->fsb.viewOption = NULL;
+    }    
+
+    n = 0;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightWidget, view); n++;
+    XtSetArg(args[n], XmNmarginHeight, 0); n++;
+    XtSetArg(args[n], XmNorientation, XmHORIZONTAL); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelNewFolder); n++;
+    w->fsb.newFolder = XmCreatePushButton(viewframe, "NewFolder", args, n);
+    XtManageChild(w->fsb.newFolder);
+    XtAddCallback(
+            w->fsb.newFolder,
+            XmNactivateCallback,
+            (XtCallbackProc)FSBNewFolder,
+            w);
+    
+
+    n = 0;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelHome); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNrightWidget, w->fsb.newFolder); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    w->fsb.home = XmCreatePushButton(viewframe, "Home", args, n);
+    XtManageChild(w->fsb.home);
+    XtAddCallback(
+            w->fsb.home,
+            XmNactivateCallback,
+            (XtCallbackProc)FSBHome,
+            w);
+    
+    // match visual appearance of detailToggleButton with the other buttons
+    if(w->fsb.detailToggleButton) {
+        Dimension highlight, shadow;
+        XtVaGetValues(w->fsb.newFolder, XmNshadowThickness, &shadow, XmNhighlightThickness, &highlight, NULL);
+        XtVaSetValues(w->fsb.detailToggleButton, XmNshadowThickness, shadow, XmNhighlightThickness, highlight, NULL);
+    }
+    
+    // pathbar
+    n = 0;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNleftWidget, w->fsb.dirUp); n++;
+    XtSetArg(args[n], XmNleftOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNrightWidget, viewframe); n++;
+    XtSetArg(args[n], XmNrightOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNshadowType, XmSHADOW_IN); n++;
+    Widget pathBarFrame = XmCreateFrame(form, "pathbar_frame", args, n);
+    XtManageChild(pathBarFrame);
+    w->fsb.pathBar = CreatePathBar(pathBarFrame, args, 0);
+    w->fsb.pathBar->getpathelm = ui_default_pathelm_func;
+    w->fsb.pathBar->updateDir = (updatedir_callback)filedialog_update_dir;
+    w->fsb.pathBar->updateDirData = w;
+    XtManageChild(w->fsb.pathBar->widget);
+    w->fsb.path = XmCreateTextField(form, "textfield", args, 0);
+    
+    XtVaSetValues(w->fsb.dirUp, XmNbottomAttachment, XmATTACH_OPPOSITE_WIDGET, XmNbottomWidget, pathBarFrame, NULL);
+    if(!w->fsb.showViewMenu) {
+        XtVaSetValues(viewframe, XmNbottomAttachment, XmATTACH_OPPOSITE_WIDGET, XmNbottomWidget, pathBarFrame, NULL);
+    }
+    
+    n = 0;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNtopWidget, pathBarFrame); n++;
+    XtSetArg(args[n], XmNtopOffset, 2*widget_spacing); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightOffset, window_spacing); n++;
+    w->fsb.filterForm = XmCreateForm(form, "filterform", args, n);
+    XtManageChild(w->fsb.filterForm);
+    
+    n = 0;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelDirectories); n++;
+    w->fsb.lsDirLabel = XmCreateLabel(w->fsb.filterForm, "labelDirs", args, n);
+    XtManageChild(w->fsb.lsDirLabel);
+    
+    n = 0;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_POSITION); n++;
+    XtSetArg(args[n], XmNleftPosition, 35); n++;
+    XtSetArg(args[n], XmNleftOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelFiles); n++;
+    w->fsb.lsFileLabel = XmCreateLabel(w->fsb.filterForm, "labelFiles", args, n);
+    XtManageChild(w->fsb.lsFileLabel);
+      
+    if(w->fsb.showHiddenButton) {
+        n = 0;
+        XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNlabelString, w->fsb.labelShowHiddenFiles); n++;
+        XtSetArg(args[n], XmNset, w->fsb.showHidden); n++;
+        w->fsb.showHiddenButtonW = XmCreateToggleButton(w->fsb.filterForm, "showHidden", args, n);
+        XtManageChild(w->fsb.showHiddenButtonW);
+        XtAddCallback(w->fsb.showHiddenButtonW, XmNvalueChangedCallback,
+                     (XtCallbackProc)filedialog_setshowhidden, w);
+    }
+    
+    n = 0;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelFilterButton); n++;
+    if(w->fsb.showHiddenButton) {
+        XtSetArg(args[n], XmNrightAttachment, XmATTACH_WIDGET); n++;
+        XtSetArg(args[n], XmNrightWidget, w->fsb.showHiddenButtonW); n++;
+    } else {
+        XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    }
+    w->fsb.filterButton = XmCreatePushButton(w->fsb.filterForm, "filedialog_filter", args, n);
+    XtManageChild(w->fsb.filterButton);
+    XtAddCallback(w->fsb.filterButton, XmNactivateCallback,
+                 (XtCallbackProc)filedialog_filter, w);
+    
+    n = 0;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNleftWidget, w->fsb.lsFileLabel); n++;
+    XtSetArg(args[n], XmNleftOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNrightWidget, w->fsb.filterButton); n++;
+    XtSetArg(args[n], XmNrightOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNshowLabel, False); n++;
+    XtSetArg(args[n], XmNuseTextField, True); n++;
+    XtSetArg(args[n], XmNverify, False); n++;
+    w->fsb.filter = XmCreateDropDown(w->fsb.filterForm, "filedialog_filter_textfield", args, n);
+    XtManageChild(w->fsb.filter);
+    XmTextFieldSetString(XmDropDownGetText(w->fsb.filter), w->fsb.filterStr);
+    XtAddCallback(XmDropDownGetText(w->fsb.filter), XmNactivateCallback,
+                 (XtCallbackProc)filedialog_filter, w);
+    XtAddCallback(w->fsb.filter, XmNupdateTextCallback,
+                 (XtCallbackProc)filedialog_update_filter, w);
+    Widget filterList = XmDropDownGetList(w->fsb.filter);
+    str = XmStringCreateSimple("*");
+    XmListAddItem(filterList, str, 0);
+    XmStringFree(str);
+        
+    /* lower part */
+    n = 0;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNtopOffset, widget_spacing * 2); n++;
+    Widget buttons = XmCreateForm(form, "buttons", args, n);
+    XtManageChild(buttons);
+    
+    n = 0;
+    str = type == FILEDIALOG_OPEN ? w->fsb.labelOpen : w->fsb.labelSave;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNlabelString, str); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_POSITION); n++;
+    XtSetArg(args[n], XmNrightPosition, 14); n++;
+    w->fsb.okBtn = XmCreatePushButton(buttons, "filedialog_open", args, n);
+    XtManageChild(w->fsb.okBtn);
+    XmStringFree(str);
+    XtAddCallback(w->fsb.okBtn, XmNactivateCallback,
+                 (XtCallbackProc)filedialog_ok, w);
+    
+    n = 0;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelHelp); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_POSITION); n++;
+    XtSetArg(args[n], XmNleftPosition, 86); n++;
+    w->fsb.helpBtn = XmCreatePushButton(buttons, "filedialog_help", args, n);
+    XtManageChild(w->fsb.helpBtn);
+    XtAddCallback(w->fsb.helpBtn, XmNactivateCallback,
+                 (XtCallbackProc)filedialog_help, w);
+    
+    n = 0;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_POSITION); n++;
+    XtSetArg(args[n], XmNleftPosition, 43); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_POSITION); n++;
+    XtSetArg(args[n], XmNrightPosition, 57); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelCancel); n++;
+    w->fsb.cancelBtn = XmCreatePushButton(buttons, "filedialog_cancel", args, n);
+    XtManageChild(w->fsb.cancelBtn);
+    XtAddCallback(w->fsb.cancelBtn, XmNactivateCallback,
+                 (XtCallbackProc)filedialog_cancel, w);
+    
+    n = 0;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNbottomWidget, buttons); n++;
+    XtSetArg(args[n], XmNbottomOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftOffset, 1); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightOffset, 1); n++;
+    w->fsb.separator = XmCreateSeparator(form, "ofd_separator", args, n);
+    XtManageChild(w->fsb.separator);
+    
+    Widget bottomWidget = w->fsb.separator;
+    
+    n = 0;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNbottomWidget, w->fsb.separator); n++;
+    XtSetArg(args[n], XmNbottomOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightOffset, window_spacing); n++;
+    w->fsb.name = XmCreateTextField(form, "textfield", args, n);
+    XtAddCallback(w->fsb.name, XmNactivateCallback,
+             (XtCallbackProc)filedialog_ok, w);
+
+    n = 0;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNbottomWidget, w->fsb.name); n++;
+    XtSetArg(args[n], XmNbottomOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNleftOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelFileName); n++;
+    w->fsb.nameLabel = XmCreateLabel(form, "label", args, n);
+        
+    if(type == FILEDIALOG_SAVE) {
+        bottomWidget = w->fsb.nameLabel;
+        XtManageChild(w->fsb.name);
+        XtManageChild(w->fsb.nameLabel);
+    }
+    w->fsb.bottom_widget = bottomWidget;
+    
+    
+    // middle 
+    // form for dir/file lists
+    n = 0;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNtopWidget, w->fsb.filterForm); n++;
+    XtSetArg(args[n], XmNtopOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNbottomWidget, bottomWidget); n++;
+    XtSetArg(args[n], XmNleftOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNrightOffset, window_spacing); n++;
+    XtSetArg(args[n], XmNbottomOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNwidth, 580); n++;
+    XtSetArg(args[n], XmNheight, 400); n++;
+    w->fsb.listform = XmCreateForm(form, "fds_listform", args, n); 
+    
+    // dir/file lists
+    
+    n = 0;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNtopWidget, w->fsb.lsDirLabel); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_POSITION); n++;
+    XtSetArg(args[n], XmNrightPosition, 35); n++;
+    w->fsb.dirlist = XmCreateScrolledList(w->fsb.listform, "dirlist", args, n);
+    Dimension width, height;
+    XtMakeResizeRequest(w->fsb.dirlist, 150, 200, &width, &height);
+    XtManageChild(w->fsb.dirlist);
+    
+    XtAddCallback(
+            w->fsb.dirlist,
+            XmNdefaultActionCallback,
+            (XtCallbackProc)dirlist_activate,
+            w); 
+    XtAddCallback(
+            w->fsb.dirlist,
+            XmNbrowseSelectionCallback,
+            (XtCallbackProc)dirlist_select,
+            w);
+    
+    // FileList
+    XnFileSelectionBoxAddView(
+            (Widget)w,
+            w->fsb.labelListView,
+            CreateListView,
+            FileListUpdate,
+            FileListSelect,
+            FileListCleanup,
+            FileListDestroy,
+            True,
+            w);
+    
+    // Detail FileList
+#ifdef FSB_ENABLE_DETAIL
+    XnFileSelectionBoxAddView(
+            (Widget)w,
+            w->fsb.labelDetailView,
+            CreateDetailView,
+            FileListDetailUpdate,
+            FileListDetailSelect,
+            FileListDetailCleanup,
+            FileListDetailDestroy,
+            True,
+            w);
+#endif
+       
+    /*
+    n = 0;
+    XtSetArg(args[n], XmNleftAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(args[n], XmNleftWidget, w->fsb.dirlist); n++;
+    XtSetArg(args[n], XmNleftOffset, widget_spacing); n++;
+    //XtSetArg(args[n], XmNbottomAttachment, XmATTACH_WIDGET); n++;
+    //XtSetArg(args[n], XmNbottomWidget, w->fsb.filelist); n++;
+    XtSetArg(args[n], XmNbottomOffset, widget_spacing); n++;
+    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNlabelString, w->fsb.labelFiles); n++;
+    w->fsb.lsFileLabel = XmCreateLabel(w->fsb.listform, "label", args, n);
+    XtManageChild(w->fsb.lsFileLabel);
+    */
+    
+    XtManageChild(w->fsb.listform);
+    
+    int selview = w->fsb.selectedview;
+    if(selview < 2) {
+        XtManageChild(w->fsb.view[selview].widget);
+    } else {
+        w->fsb.selectedview = 0;
+    }
+    
+    
+    if(w->fsb.selectedPath) {
+        char *parentPath = ParentPath(w->fsb.selectedPath);
+        filedialog_update_dir(w, parentPath);
+        PathBarSetPath(w->fsb.pathBar, parentPath);
+        free(parentPath);
+        
+        if(w->fsb.type == FILEDIALOG_SAVE) {
+            XmTextFieldSetString(w->fsb.name, FileName(w->fsb.selectedPath));
+        }
+    } else {
+        char cwd[PATH_MAX];
+        const char *currentPath = w->fsb.currentPath;
+        if(!currentPath) {
+            if(getcwd(cwd, PATH_MAX)) {
+                currentPath = cwd;
+            } else {
+                currentPath = GetHomeDir();
+            }
+        }
+        
+        filedialog_update_dir(w, currentPath);
+        PathBarSetPath(w->fsb.pathBar, w->fsb.currentPath);
+    }
+
+
+    w->fsb.selectedview = selview;
+         
+    XtVaSetValues((Widget)w, XmNcancelButton, w->fsb.cancelBtn, NULL);
+    
+    w->fsb.gui_created = 1;
+}
+
+static char* FSBDialogTitle(Widget widget) {
+    XnFileSelectionBox w = (XnFileSelectionBox)widget;
+    if(w->fsb.type == FILEDIALOG_OPEN) {
+        return w->fsb.labelOpenFileTitle;
+    } else {
+        return w->fsb.labelSaveFileTitle;
+    }
+}
+
+static FSBViewWidgets CreateView(XnFileSelectionBox w, FSBViewCreateProc create, void *userData, Boolean useDirList) {
+    Arg args[64];
+    int n = 0;
+    
+    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    if(useDirList) {
+        XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNleftAttachment, XmATTACH_WIDGET); n++;
+        XtSetArg(args[n], XmNleftWidget, w->fsb.dirlist); n++;
+        XtSetArg(args[n], XmNleftOffset, w->fsb.widgetSpacing); n++;
+    } else {
+        XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNtopOffset, w->fsb.widgetSpacing); n++;
+        XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    }
+    
+    return create(w->fsb.listform, args, n, userData);
+}
+
+
+typedef struct FSBViewSelection {
+    XnFileSelectionBox fsb;
+    int index;
+} FSBViewSelection;
+
+static void SelectView(XnFileSelectionBox f, int view) {
+    FSBView current = f->fsb.view[f->fsb.selectedview];
+    FSBView newview = f->fsb.view[view];
+    
+    XtUnmanageChild(current.widget);
+    if(newview.useDirList != current.useDirList) {
+        if(current.useDirList) {
+            XtUnmanageChild(f->fsb.listform);
+        } else {
+            XtManageChild(f->fsb.listform);
+        }
+    }
+    
+    current.cleanup((Widget)f, current.widget, current.userData);
+    XtManageChild(newview.widget);
+    
+    f->fsb.selectedview = view;
+    
+    filedialog_update_dir(f, NULL);
+    XmProcessTraversal(newview.focus, XmTRAVERSE_CURRENT);
+}
+
+static void SelectViewCallback(Widget w, FSBViewSelection *data, XtPointer u) {
+    SelectView(data->fsb, data->index);
+}
+
+static void SelectViewItemDestroy(Widget w, FSBViewSelection *data, XtPointer u) {
+    free(data);
+}
+
+static void AddViewMenuItem(XnFileSelectionBox w, const char *name, int viewIndex) {
+    Arg args[4];
+    int n = 0;
+    
+    XmString label = XmStringCreateLocalized((char*)name);
+    
+    XtSetArg(args[n], XmNlabelString, label); n++;
+    XtSetArg(args[1], XmNpositionIndex, w->fsb.selectedview == w->fsb.numviews ? 0 : w->fsb.numviews+1); n++;
+    Widget item = XmCreatePushButton(w->fsb.viewMenu, "menuitem", args, n);
+    
+    if(viewIndex == 0) {
+        w->fsb.viewSelectorList = item;
+    } else if(viewIndex == 1) {
+        w->fsb.viewSelectorDetail = item;
+    }
+   
+    XtManageChild(item);
+    XmStringFree(label);
+    
+    FSBViewSelection *data = malloc(sizeof(FSBViewSelection));
+    data->fsb = w;
+    data->index = viewIndex;
+    
+    XtAddCallback(
+            item,
+            XmNactivateCallback,
+            (XtCallbackProc)SelectViewCallback,
+            data);
+    XtAddCallback(
+            item,
+            XmNdestroyCallback,
+            (XtCallbackProc)SelectViewItemDestroy,
+            data);
+}
+
+static FSBViewWidgets CreateListView(Widget parent, ArgList args, int n, void *userData) {
+    XnFileSelectionBox fsb = (XnFileSelectionBox)userData;
+    
+    XtSetArg(args[n], XmNshadowThickness, 0); n++;
+    Widget frame = XmCreateFrame(parent, "filelistframe", args, n);
+    
+    fsb->fsb.filelist = XmCreateScrolledList(frame, "filelist", NULL, 0);
+    XtManageChild(fsb->fsb.filelist);
+    
+    XtAddCallback(
+            fsb->fsb.filelist,
+            XmNdefaultActionCallback,
+            (XtCallbackProc)FileListActivateCB,
+            userData); 
+    XtAddCallback(
+            fsb->fsb.filelist,
+            XmNbrowseSelectionCallback,
+            (XtCallbackProc)FileListSelectCB,
+            userData);
+    
+    fsb->fsb.listContextMenu = CreateContextMenu(fsb, fsb->fsb.filelist, FileContextMenuCB);
+    
+    FSBViewWidgets widgets;
+    widgets.view = frame;
+    widgets.focus = fsb->fsb.filelist;
+    return widgets;
+}
+
+#ifdef FSB_ENABLE_DETAIL
+static void set_path_from_row(XnFileSelectionBox data, int row) {
+    FileElm *elm = NULL;
+    XmLGridRow rowPtr = XmLGridGetRow(data->fsb.grid, XmCONTENT, row);
+    XtVaGetValues(data->fsb.grid, XmNrowPtr, rowPtr, XmNrowUserData, &elm, NULL);
+    if(!elm) {
+        fprintf(stderr, "error: no row data\n");
+        return;
+    }
+    
+    char *path = strdup(elm->path);
+    
+    data->fsb.selIsDir = False;
+    if(data->fsb.type == FILEDIALOG_SAVE) {
+        XmTextFieldSetString(data->fsb.name, FileName(path));
+    }
+    
+    if(data->fsb.selectedPath) {
+        free(data->fsb.selectedPath);
+    }
+    data->fsb.selectedPath = path;
+}
+
+static void grid_select(Widget w, XnFileSelectionBox data, XmLGridCallbackStruct *cb) {
+    set_path_from_row(data, cb->row);
+}
+
+static void grid_activate(Widget w, XnFileSelectionBox data, XmLGridCallbackStruct *cb) {
+    set_path_from_row(data, cb->row);
+    data->fsb.end = True;
+    data->fsb.status = FILEDIALOG_OK;
+    
+    FileSelectionCallback(data, data->fsb.okCallback, XmCR_OK, data->fsb.selectedPath);
+}
+static void grid_key_pressed(Widget w, XnFileSelectionBox data, XmLGridCallbackStruct *cb) {
+    char chars[16];
+    KeySym keysym;
+    int nchars;
+    
+    nchars = XLookupString(&cb->event->xkey, chars, 15, &keysym, NULL);
+    
+    if(nchars == 0) return;
+
+    // if data->showHidden is 0, data->files contains more items than the grid
+    // this means SelectedRow might not be the correct index for data->files
+    // we have to count files manually and increase 'row', if the file
+    // is actually displayed in the grid
+    int row = 0;
+    int selectedRow = XmLGridGetSelectedRow(w);
+    
+    int match = -1;
+    
+    for(int i=0;i<data->fsb.filecount;i++) {
+        const char *name = FileName(data->fsb.files[i].path);
+        if(!data->fsb.showHidden && name[0] == '.') continue;
+        
+        size_t namelen = strlen(name);
+        
+        size_t cmplen = namelen < nchars ? namelen : nchars;
+        if(!memcmp(name, chars, cmplen)) {
+            if(row <= selectedRow) {
+                if(match == -1) {
+                    match = row;
+                }
+            } else {
+                match = row;
+                break;
+            }
+        }
+        
+        row++;
+    }
+    
+    if(match > -1) {
+        XmLGridSelectRow(w, match, True);
+        XmLGridFocusAndShowRow(w, match+1);
+    } else {
+        XBell(XtDisplay(w), 0);
+    }
+}
+
+static void grid_header_clicked(Widget w, XnFileSelectionBox data, XmLGridCallbackStruct *cb) { 
+    int new_cmp_field = 0;
+    switch(cb->column) {
+        case 0: {
+            new_cmp_field = 0;            
+            break;
+        }
+        case 1: {
+            new_cmp_field = 1;
+            break;
+        }
+        case 2: {
+            new_cmp_field = 2;
+            break;
+        }
+    }
+    
+    if(new_cmp_field == file_cmp_field) {
+        file_cmp_order = -file_cmp_order; // revert sort order
+    } else {
+        file_cmp_field = new_cmp_field; // change file cmp order to new field
+        file_cmp_order = 1;
+    }
+    
+    int sort_type = file_cmp_order == 1 ? XmSORT_ASCENDING : XmSORT_DESCENDING;
+    XmLGridSetSort(data->fsb.grid, file_cmp_field, sort_type);
+    
+    qsort(data->fsb.files, data->fsb.filecount, sizeof(FileElm), filecmp);
+    
+    // refresh widget
+    filedialog_update_dir(data, NULL);
+} 
+
+static FSBViewWidgets CreateDetailView(Widget parent, ArgList args, int n, void *userData) {
+    XnFileSelectionBox w = userData;
+    
+    XtSetArg(args[n], XmNshadowThickness, 0); n++;
+    Widget gridcontainer = XmCreateFrame(parent, "gridcontainer", args, n);
+    XtManageChild(gridcontainer);
+    
+    n = 0;
+    XtSetArg(args[n], XmNcolumns, 3); n++;
+    XtSetArg(args[n], XmNheadingColumns, 0); n++;
+    XtSetArg(args[n], XmNheadingRows, 1); n++;
+    XtSetArg(args[n], XmNallowColumnResize, 1); n++;
+    XtSetArg(args[n], XmNsimpleHeadings, w->fsb.detailHeadings); n++;
+    XtSetArg(args[n], XmNhorizontalSizePolicy, XmCONSTANT); n++;
+    
+    w->fsb.grid = XmLCreateGrid(gridcontainer, "grid", args, n);
+    XmLGridSetIgnoreModifyVerify(w->fsb.grid, True);
+    XtManageChild(w->fsb.grid);
+    
+    XtVaSetValues(
+            w->fsb.grid,
+            XmNcellDefaults, True,
+            XtVaTypedArg, XmNblankBackground, XmRString, "white", 6,
+            XtVaTypedArg, XmNcellBackground, XmRString, "white", 6,
+            NULL);
+    
+    XtAddCallback(w->fsb.grid, XmNselectCallback, (XtCallbackProc)grid_select, w);
+    XtAddCallback(w->fsb.grid, XmNactivateCallback, (XtCallbackProc)grid_activate, w);
+    XtAddCallback(w->fsb.grid, XmNheaderClickCallback, (XtCallbackProc)grid_header_clicked, w);
+    XtAddCallback(w->fsb.grid, XmNgridKeyPressedCallback, (XtCallbackProc)grid_key_pressed, w);
+    
+    // context menu
+    w->fsb.gridContextMenu = CreateContextMenu(w, w->fsb.grid, FileContextMenuCB);
+    
+    FSBViewWidgets widgets;
+    widgets.view = gridcontainer;
+    widgets.focus = w->fsb.grid;
+    return widgets;
+}
+#endif
+
+
+/* ------------------------------ Path Utils  ------------------------------ */
+
+const char* GetHomeDir(void) {
+    char *home = getenv("HOME");
+    if(!home) {
+        home = getenv("USERPROFILE");
+        if(!home) {
+            home = "/";
+        }
+    }
+    return home;
+}
+
+static char* ConcatPath(const char *parent, const char *name)
+{ 
+    size_t parentlen = strlen(parent);
+    size_t namelen = strlen(name);
+    
+    size_t pathlen = parentlen + namelen + 2;
+    char *path = malloc(pathlen);
+    
+    memcpy(path, parent, parentlen);
+    if(parentlen > 0 && parent[parentlen-1] != '/') {
+        path[parentlen] = '/';
+        parentlen++;
+    }
+    if(name[0] == '/') {
+        name++;
+        namelen--;
+    }
+    memcpy(path+parentlen, name, namelen);
+    path[parentlen+namelen] = '\0';
+    return path;
+}
+
+static char* FileName(char *path) {
+    int si = 0;
+    int osi = 0;
+    int i = 0;
+    int p = 0;
+    char c;
+    while((c = path[i]) != 0) {
+        if(c == '/') {
+            osi = si;
+            si = i;
+            p = 1;
+        }
+        i++;
+    }
+    
+    char *name = path + si + p;
+    if(name[0] == 0) {
+        name = path + osi + p;
+        if(name[0] == 0) {
+            return path;
+        }
+    }
+    
+    return name;
+}
+
+static char* ParentPath(const char *path) {
+    char *name = FileName((char*)path);
+    size_t namelen = strlen(name);
+    size_t pathlen = strlen(path);
+    size_t parentlen = pathlen - namelen;
+    if(parentlen == 0) {
+        parentlen++;
+    }
+    char *parent = malloc(parentlen + 1);
+    memcpy(parent, path, parentlen);
+    parent[parentlen] = '\0';
+    return parent;
+}
+
+// unused at the moment, maybe reactivate if more illegal characters
+// are defined
+/*
+static int CheckFileName(const char *fileName) {
+    size_t len = strlen(fileName);
+    for(int i=0;i<len;i++) {
+        if(fileName[i] == '/') {
+            return 0;
+        }
+    }
+    return 1;
+}
+*/
+
+
+/* ------------------------------ public API ------------------------------ */
+
+Widget XnCreateFileSelectionDialog(
+        Widget parent,
+        String name,
+        ArgList arglist,
+        Cardinal argcount)
+{
+    Widget dialog = XmCreateDialogShell(parent, "FileDialog", NULL, 0);
+    Widget fsb = XnCreateFileSelectionBox(dialog, name, arglist, argcount);
+    char *title = FSBDialogTitle(fsb);
+    XtVaSetValues(dialog, XmNtitle, title, NULL);
+    return fsb;
+}
+
+Widget XnCreateFileSelectionBox(
+        Widget parent,
+        String name,
+        ArgList arglist,
+        Cardinal argcount)
+{
+    Widget fsb = XtCreateWidget(name, xnFsbWidgetClass, parent, arglist, argcount);
+    return fsb;
+}
+
+void XnFileSelectionBoxAddView(
+        Widget fsb,
+        const char *name,
+        FSBViewCreateProc create,
+        FSBViewUpdateProc update,
+        FSBViewSelectProc select,
+        FSBViewCleanupProc cleanup,
+        FSBViewDestroyProc destroy,
+        Boolean useDirList,
+        void *userData)
+{
+    XnFileSelectionBox f = (XnFileSelectionBox)fsb;
+    if(f->fsb.numviews >= FSB_MAX_VIEWS) {
+        fprintf(stderr, "XnFileSelectionBox: too many views\n");
+        return;
+    }
+    
+    FSBView view;
+    view.update = update;
+    view.select = select;
+    view.cleanup = cleanup;
+    view.destroy = destroy;
+    view.useDirList = useDirList;
+    view.userData = userData;
+    
+    FSBViewWidgets widgets = CreateView(f, create, userData, useDirList);
+    view.widget = widgets.view;
+    view.focus = widgets.focus;
+    
+    AddViewMenuItem(f, name, f->fsb.numviews);
+    
+    f->fsb.view[f->fsb.numviews++] = view;
+}
+
+/*
+void XnFileSelectionBoxSetDirList(Widget fsb, const char **dirlist, size_t nelm) {
+    XnFileSelectionBox f = (XnFileSelectionBox)fsb;
+    PathBarSetDirList(f->fsb.pathBar, dirlist, nelm);
+}
+*/
+
+Widget XnFileSelectionBoxWorkArea(Widget fsb) {
+    XnFileSelectionBox f = (XnFileSelectionBox)fsb;
+    return f->fsb.workarea;
+}
+
+Widget XnFileSelectionBoxGetChild(Widget fsb, enum XnFSBChild child) {
+    XnFileSelectionBox w = (XnFileSelectionBox)fsb;
+    switch(child) {
+        case XnFSB_DIR_UP_BUTTON: return w->fsb.dirUp;
+        case XnFSB_HOME_BUTTON: return w->fsb.home;
+        case XnFSB_NEW_FOLDER_BUTTON: return w->fsb.newFolder;
+        case XnFSB_DETAIL_TOGGLE_BUTTON: return w->fsb.detailToggleButton;
+        case XnFSB_VIEW_OPTION_BUTTON: return w->fsb.viewOption;
+        case XnFSB_FILTER_DROPDOWN: return w->fsb.filter;
+        case XnFSB_FILTER_BUTTON: return w->fsb.filterButton;
+        case XnFSB_SHOW_HIDDEN_TOGGLE_BUTTON: return w->fsb.showHiddenButtonW;
+        case XnFSB_DIRECTORIES_LABEL: return w->fsb.lsDirLabel;
+        case XnFSB_FILES_LABEL: return w->fsb.lsFileLabel;
+        case XnFSB_DIRLIST: return w->fsb.dirlist;
+        case XnFSB_FILELIST: return w->fsb.filelist;
+        case XnFSB_GRID: return w->fsb.grid;
+        case XnFSB_OK_BUTTON: return w->fsb.okBtn;
+        case XnFSB_CANCEL_BUTTON: return w->fsb.cancelBtn;
+        case XnFSB_HELP_BUTTON: return w->fsb.helpBtn;
+    }
+    return NULL;
+}
+
+void XnFileSelectionBoxDeleteFilters(Widget fsb) {
+    XnFileSelectionBox w = (XnFileSelectionBox)fsb;
+    Widget filterList = XmDropDownGetList(w->fsb.filter);
+    XmListDeleteAllItems(filterList);
+}
+
+void XnFileSelectionBoxAddFilter(Widget fsb, const char *filter) {
+    XnFileSelectionBox w = (XnFileSelectionBox)fsb;
+    Widget filterList = XmDropDownGetList(w->fsb.filter);
+    
+    XmString str = XmStringCreateSimple((char*)filter);
+    XmListAddItem(filterList, str, 0);
+    XmStringFree(str);
+}
diff --git a/ui/motif/Fsb.h b/ui/motif/Fsb.h
new file mode 100644 (file)
index 0000000..4257137
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2021 Olaf Wintermann
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a 
+ * copy of this software and associated documentation files (the "Software"), 
+ * to deal in the Software without restriction, including without limitation 
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
+ * and/or sell copies of the Software, and to permit persons to whom the 
+ * Software is furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in 
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef FSB_H
+#define FSB_H
+
+#include <X11/Intrinsic.h>
+#include <Xm/PrimitiveP.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern WidgetClass xnFsbWidgetClass;
+
+#define FILEDIALOG_OPEN 1
+#define FILEDIALOG_SAVE 2
+    
+#define FILEDIALOG_OK 1
+#define FILEDIALOG_CANCEL 2
+
+#define XnNwidgetSpacing         "fsbWidgetSpacing"
+#define XnNwindowSpacing         "fsbWindowSpacing"
+
+#define XnNfsbType               "fsbType"
+#define XnCfsbType               "fsbType"
+
+#define XnNshowHidden            "showHidden"
+#define XnCshowHidden            "showHidden"
+#define XnNshowHiddenButton      "showHiddenButton"
+#define XnCshowHiddenButton      "showHiddenButton"
+
+#define XnNshowViewMenu          "showViewMenu"
+#define XnCshowViewMenu          "showViewMenu"
+
+#define XnNselectedView          "fsbSelectedView"
+#define XnCselectedView          "fsbSelectedView"
+
+#define XnNdirectory             "directory"
+#define XnCdirectory             "directory"
+#define XnNselectedPath          "selectedPath"
+#define XnCselectedPath          "selectedPath"
+#define XnNhomePath              "homePath"
+#define XnChomePath              "homePath"
+
+#define XnNfilter                "filter"
+#define XnCfilter                "filter"
+
+#define XnNfilterFunc            "filterFunc"
+#define XnCfilterFunc            "filterFunc"
+
+#define XnNlabelListView         "labelListView"
+#define XnClabelListView         "labelListView"
+#define XnNlabelDetailView       "labelDetailView"
+#define XnClabelDetailView       "labelDetailView"
+#define XnNlabelOpenFileTitle    "labelOpenFileTitle"
+#define XnClabelOpenFileTitle    "labelOpenFileTitle"
+#define XnNlabelSaveFileTitle    "labelSaveFileTitle"
+#define XnClabelSaveFileTitle    "labelSaveFileTitel"
+#define XnNlabelDirUp            "labelDirUp"
+#define XnClabelDirUp            "labelDirUp"
+#define XnNlabelHome             "labelHome"
+#define XnClabelHome             "labelHome"
+#define XnNlabelNewFolder        "labelNewFolder"
+#define XnClabelNewFolder        "labelNewFolder"
+#define XnNlabelFilter           "labelFilter"
+#define XnClabelFilter           "labelFilter"
+#define XnNlabelFilterButton     "labelFilterButton"
+#define XnClabelFilterButton     "labelFilterButton"
+#define XnNlabelShowHiddenFiles  "labelShowHiddenFiles"
+#define XnClabelShowHiddenFiles  "labelShowHiddenFiles"
+#define XnNlabelDirectories      "labelDirectories"
+#define XnClabelDirectories      "labelDirectories"
+#define XnNlabelFiles            "labelFiles"
+#define XnClabelFiles            "labelFiles"
+#define XnNlabelRename           "labelRename"
+#define XnClabelRename           "labelRename"
+#define XnNlabelDelete           "labelDelete"
+#define XnClabelDelete           "labelDelete"
+#define XnNlabelOpen             "labelOpen"
+#define XnClabelOpen             "labelOpen"
+#define XnNlabelSave             "labelSave"
+#define XnClabelSave             "labelSave"
+#define XnNlabelOk               "labelOk"
+#define XnClabelOk               "labelOk"
+#define XnNlabelCancel           "labelCancel"
+#define XnClabelCancel           "labelCancel"
+#define XnNlabelHelp             "labelHelp"
+#define XnClabelHelp             "labelHelp"
+#define XnNlabelFileName         "labelFileName"
+#define XnClabelFileName         "labelFileName"
+#define XnNlabelDirectoryName    "labelDirectoryName"
+#define XnClabelDirectoryName    "labelDirectoryName"
+#define XnNlabelNewFileName      "labelNewFileName"
+#define XnClabelNewFileName      "labelNewFileName"
+#define XnNlabelDeleteFile       "labelDeleteFile"
+#define XnClabelDeleteFile       "labelDeleteFile"
+#define XnNdetailHeadings        "detailHeadings"
+#define XnCdetailHeadings        "detailHeadings"
+#define XnNdateFormatSameYear    "dateFormatSameYear"
+#define XnCdateFormatSameYear    "dateFormatSameYear"
+#define XnNdateFormatOtherYear   "dateFormatOtherYear"
+#define XnCdateFormatOtherYear   "dateFormatOtherYear"
+#define XnNsuffixBytes           "suffixBytes"
+#define XnCsuffixBytes           "suffixBytes"
+#define XnNsuffixKB              "suffixKB"
+#define XnCsuffixKB              "suffixKB"
+#define XnNsuffixMB              "suffixMB"
+#define XnCsuffixMB              "suffixMB"
+#define XnNsuffixGB              "suffixGB"
+#define XnCsuffixGB              "suffixGB"
+#define XnNsuffixTB              "suffixTB"
+#define XnCsuffixTB              "suffixTB"
+#define XnNerrorTitle            "errorTitle"
+#define XnCerrorTitle            "errorTitle"
+#define XnNerrorIllegalChar      "errorIllegalChar"
+#define XnCerrorIllegalChar      "errorIllegalChar"
+#define XnNerrorRename           "errorRename"
+#define XnCerrorRename           "errorRename"
+#define XnNerrorCreateFolder     "errorCreateFolder"
+#define XnCerrorCreateFolder     "errorCreateFolder"
+#define XnNerrorDelete           "errorDelete"
+#define XnCerrorDelete           "errorDelete"
+#define XnNerrorOpenDir          "errorOpenDir"
+#define XnCerrorOpenDir          "errorOpenDir"
+
+/*
+ * int FSBFilterFunc(const char *pattern, const char *string)
+ * 
+ * Checks whether the string matches the pattern
+ * 
+ * Return
+ *   zero if the string matches the pattern
+ *   non-zero if there is no match
+ */
+typedef int(*FSBFilterFunc)(const char*, const char*);
+
+
+typedef struct FileElm FileElm;
+struct FileElm {
+    char *path;
+    int isDirectory;
+    unsigned long long size;
+    time_t lastModified;
+};
+
+typedef struct {
+    Widget view;
+    Widget focus;
+} FSBViewWidgets;
+
+enum XnFSBChild {
+    XnFSB_DIR_UP_BUTTON = 0,
+    XnFSB_HOME_BUTTON,
+    XnFSB_NEW_FOLDER_BUTTON,
+    XnFSB_DETAIL_TOGGLE_BUTTON,
+    XnFSB_VIEW_OPTION_BUTTON,
+    XnFSB_FILTER_DROPDOWN,
+    XnFSB_FILTER_BUTTON,
+    XnFSB_SHOW_HIDDEN_TOGGLE_BUTTON,
+    XnFSB_DIRECTORIES_LABEL,
+    XnFSB_FILES_LABEL,
+    XnFSB_DIRLIST,
+    XnFSB_FILELIST,
+    XnFSB_GRID,
+    XnFSB_OK_BUTTON,
+    XnFSB_CANCEL_BUTTON,
+    XnFSB_HELP_BUTTON
+};
+
+typedef FSBViewWidgets(*FSBViewCreateProc)(Widget parent, ArgList args, int n, void *userData);
+typedef void(*FSBViewUpdateProc)(Widget fsb, Widget view, FileElm *dirs, int dircount, FileElm *files, int filecount, const char *filter, int maxnamelen, void *userData);
+typedef void(*FSBViewSelectProc)(Widget fsb, Widget view, const char *item);
+typedef void(*FSBViewCleanupProc)(Widget fsb, Widget view, void *userData);
+typedef void(*FSBViewDestroyProc)(Widget fsb, Widget view, void *userData);
+
+Widget XnCreateFileSelectionDialog(
+        Widget parent,
+        String name,
+        ArgList arglist,
+        Cardinal argcount);
+
+Widget XnCreateFileSelectionBox(
+        Widget parent,
+        String name,
+        ArgList arglist,
+        Cardinal argcount);
+
+void XnFileSelectionBoxAddView(
+        Widget fsb,
+        const char *name,
+        FSBViewCreateProc create,
+        FSBViewUpdateProc update,
+        FSBViewSelectProc select,
+        FSBViewCleanupProc cleanup,
+        FSBViewDestroyProc destroy,
+        Boolean useDirList,
+        void *userData);
+
+//void XnFileSelectionBoxSetDirList(Widget fsb, const char **dirlist, size_t nelm);
+
+Widget XnFileSelectionBoxWorkArea(Widget fsb);
+
+Widget XnFileSelectionBoxGetChild(Widget fsb, enum XnFSBChild child);
+
+void XnFileSelectionBoxDeleteFilters(Widget fsb);
+
+void XnFileSelectionBoxAddFilter(Widget fsb, const char *filter);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* FSB_H */
diff --git a/ui/motif/FsbP.h b/ui/motif/FsbP.h
new file mode 100644 (file)
index 0000000..6d520ca
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2021 Olaf Wintermann
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a 
+ * copy of this software and associated documentation files (the "Software"), 
+ * to deal in the Software without restriction, including without limitation 
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
+ * and/or sell copies of the Software, and to permit persons to whom the 
+ * Software is furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in 
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef FSBP_H
+#define FSBP_H
+
+#include <X11/CoreP.h>
+#include <Xm/XmP.h>
+#include <Xm/PrimitiveP.h>
+#include <Xm/ManagerP.h>
+#include <Xm/FormP.h>
+
+#include "Fsb.h"
+#include "pathbar.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define FSB_MAX_VIEWS 8
+    
+
+typedef struct FSBView FSBView;
+struct FSBView {
+    Widget widget;
+    Widget focus;
+    FSBViewUpdateProc  update;
+    FSBViewSelectProc  select;
+    FSBViewCleanupProc cleanup;
+    FSBViewDestroyProc destroy;
+    void *userData;
+    Boolean useDirList;
+};
+
+    
+typedef struct FSBClassPart {
+    int unused;
+} FSBClassPart;
+
+typedef struct FSBClassRec {
+    CoreClassPart             core_class;
+    CompositeClassPart        composite_class;
+    ConstraintClassPart       constraint_class;
+    XmManagerClassPart        manager_class;
+    XmBulletinBoardClassPart  bulletin_board_class;
+    XmFormClassPart           form_class;
+    FSBClassPart              fsb_class;
+} FSBClassRec;
+
+typedef struct FSBPart {
+    XtCallbackList okCallback;
+    XtCallbackList cancelCallback;
+    
+    Dimension widgetSpacing;
+    Dimension windowSpacing;
+    
+    Boolean showHiddenButton;
+    
+    Widget path;
+    PathBar *pathBar;
+    Widget filter;
+    Widget filterButton;
+    Widget showHiddenButtonW;
+    
+    FSBFilterFunc filterFunc;
+    
+    char *filterStr;
+    
+    Widget dirUp;
+    Widget home;
+    Widget newFolder;
+    
+    Widget viewSelectorList;
+    Widget viewSelectorDetail;
+    
+    Widget viewMenu;
+    Widget viewOption;
+    Widget detailToggleButton;
+    
+    Widget filterForm;
+    Widget lsDirLabel;
+    Widget lsFileLabel;
+    
+    Widget listContextMenu;
+    Widget gridContextMenu;
+    
+    // icon view
+    
+    // dir/file list view
+    Widget listform;
+    Widget dirlist;
+    
+    FSBView view[FSB_MAX_VIEWS];
+    int numviews;
+    int selectedview;
+    
+    Widget filelist;
+    Widget grid;
+    
+    Widget separator;
+    
+    Widget nameLabel;
+    Widget name;
+    
+    Widget bottom_widget;
+    
+    Widget workarea;
+    
+    Widget okBtn;
+    Widget cancelBtn;
+    Widget helpBtn;
+    
+    FileElm *dirs;
+    FileElm *files;
+    int dircount;
+    int filecount;
+    int maxnamelen;
+    
+    char *homePath;
+    
+    char *currentPath;
+    char *selectedPath;
+    int selIsDir;
+    Boolean showHidden;
+    Boolean showViewMenu;
+      
+    int type;
+    
+    int end;
+    int status;
+    
+    int disable_set_values;
+    int gui_created;
+        
+    char *labelListView;
+    char *labelDetailView;
+    char* labelOpenFileTitle;
+    char* labelSaveFileTitle;
+    XmString labelDirUp;
+    XmString labelHome;
+    XmString labelNewFolder;
+    XmString labelFilterButton;
+    XmString labelShowHiddenFiles;
+    XmString labelDirectories;
+    XmString labelFiles;
+    XmString labelRename;
+    XmString labelDelete;
+    XmString labelOpen;
+    XmString labelSave;
+    XmString labelOk;
+    XmString labelCancel;
+    XmString labelHelp;
+    XmString labelFileName;
+    XmString labelDirectoryName;
+    XmString labelNewFileName;
+    char *labelDeleteFile;
+    
+    char *detailHeadings;
+    
+    char *dateFormatSameYear;
+    char *dateFormatOtherYear;
+    char *suffixBytes;
+    char *suffixKB;
+    char *suffixMB;
+    char *suffixGB;
+    char *suffixTB;
+    
+    char *errorTitle;
+    char *errorIllegalChar;
+    char *errorRename;
+    char *errorFolder;
+    char *errorDelete;
+    char *errorOpenDir;
+} FSBPart;
+
+typedef struct FSBRec {
+   CorePart            core;
+   CompositePart        composite;
+   ConstraintPart       constraint;
+   XmManagerPart        manager;
+   XmBulletinBoardPart  bulletin_board;
+   XmFormPart           form;
+   FSBPart              fsb;
+} FSBRec;
+
+typedef struct FSBRec *XnFileSelectionBox;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* FSBP_H */
+
diff --git a/ui/motif/entry.c b/ui/motif/entry.c
new file mode 100644 (file)
index 0000000..d21a400
--- /dev/null
@@ -0,0 +1,281 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 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 "entry.h"
+
+
+UIWIDGET ui_spinbox_create(UiObject *obj, UiSpinBoxArgs *args) {
+    Arg xargs[16];
+    int n = 0;
+    
+    double min = args->min;
+    double max = args->max != 0 ? args->max : 1000;
+    
+    UiVar *var = NULL;
+    UiVarType vartype = 0;
+    if(args->varname) {
+        var = uic_get_var(obj->ctx, args->varname);
+        if(var) {
+            vartype = var->type;
+        } else {
+            var = uic_widget_var(obj->ctx, obj->ctx, args->rangevalue, args->varname, UI_VAR_RANGE);
+            vartype = UI_VAR_RANGE;
+        }
+    }
+    
+    if(!var) {
+        if(args->intvalue) {
+            var = uic_widget_var(obj->ctx, obj->ctx, args->intvalue, NULL, UI_VAR_INTEGER);
+            vartype = UI_VAR_INTEGER;
+        } else if(args->doublevalue) {
+            var = uic_widget_var(obj->ctx, obj->ctx, args->doublevalue, NULL, UI_VAR_DOUBLE);
+            vartype = UI_VAR_DOUBLE;
+        } else if(args->rangevalue) {
+            var = uic_widget_var(obj->ctx, obj->ctx, args->rangevalue, NULL, UI_VAR_RANGE);
+            vartype = UI_VAR_RANGE;
+        }
+    }
+    
+    if(vartype == UI_VAR_RANGE) {
+        UiRange *r = var->value;
+        min = r->min;
+        max = r->max;
+    }
+    if(args->step == 0) {
+        args->step = 1;
+    }
+    
+    UiContainerPrivate *ctn = ui_obj_container(obj);
+    UiLayout layout = UI_ARGS2LAYOUT(args);
+
+    
+    XtSetArg(xargs[n], XmNminimumValue, 0); n++;
+    XtSetArg(xargs[n], XmNmaximumValue, 100); n++;
+    XtSetArg(xargs[n], XmNincrementValue, 1); n++;
+    XtSetArg(xargs[n], XmNspinBoxChildType, XmNUMERIC); n++;
+    
+    Widget parent = ui_container_prepare(ctn, &layout, xargs, &n);
+    
+    char *name = args->name ? (char*)args->name : "button";
+    Widget spinbox = XmCreateSimpleSpinBox(parent, name, xargs, n);
+    XtManageChild(spinbox);
+    ui_container_add(ctn, spinbox);
+    
+    ui_set_widget_groups(obj->ctx, spinbox, args->groups);
+    
+    WidgetList children;
+    Cardinal num_children;
+    unsigned char type;
+    
+    Widget textfield = NULL;
+    XtVaGetValues(
+            spinbox,
+            XmNchildren, &children,
+            XmNnumChildren, &num_children,
+            NULL);
+
+    for(int i = 0;i<num_children;i++) {
+        XtVaGetValues(children[i], XmNspinBoxChildType, &type, NULL);
+        Widget w = children[i];
+        if(type == XmNUMERIC) {
+            textfield = children[i];
+        }
+    }
+     
+    UiSpinBox *data = malloc(sizeof(UiSpinBox));
+    data->obj = obj;
+    data->textfield = textfield;
+    data->var = var;
+    data->vartype = vartype;
+    data->obs = NULL;
+    data->onchange = args->onchange;
+    data->onchangedata = args->onchangedata;
+    data->value = 0;
+    data->min = min;
+    data->max = max;
+    data->increment = args->step;
+    data->digits = args->digits;
+    
+    UiObserver **obs = NULL;
+    if(var) {
+        double value = 0;
+        switch(vartype) {
+            default: break;
+            case UI_VAR_INTEGER: {
+                UiInteger *i = var->value;
+                i->get = ui_spinbutton_getint;
+                i->set = ui_spinbutton_setint;
+                i->obj = data;
+                value = (double)i->value;
+                obs = &i->observers;
+                break;
+            }
+            case UI_VAR_DOUBLE: {
+                UiDouble *d = var->value;
+                d->get = ui_spinbutton_getdouble;
+                d->set = ui_spinbutton_setdouble;
+                d->obj = data;
+                value = d->value;
+                obs = &d->observers;
+                break;
+            }
+            case UI_VAR_RANGE: {
+                UiRange *r = var->value;
+                r->get = ui_spinbutton_getrangeval;
+                r->set = ui_spinbutton_setrangeval;
+                r->setrange = ui_spinbutton_setrange;
+                r->setextent = ui_spinbutton_setextent;
+                r->obj = data;
+                value = r->value;
+                obs = &r->observers;
+                break;
+            }
+        }
+        ui_spinbox_set_value(data, value);
+    }
+    data->obs = obs;
+    
+    XtAddCallback(
+            spinbox,
+            XmNvalueChangedCallback,
+            (XtCallbackProc)ui_spinbox_value_changed,
+            data);
+    
+    XtAddCallback(
+            spinbox,
+            XmNdestroyCallback,
+            (XtCallbackProc)ui_destroy_data,
+            data);
+    
+    XmTextFieldSetString(textfield, "0");
+    
+    
+    return spinbox;
+}
+
+void ui_spinbox_set_value(UiSpinBox *spinbox, double value) {
+    if(value < spinbox->min) {
+        value = spinbox->min;
+    }
+    if(value > spinbox->max) {
+        value = spinbox->max;
+    }
+    
+    char buf[32];
+    snprintf(buf, 32, "%.*f", spinbox->digits, spinbox->value);
+    XmTextFieldSetString(spinbox->textfield, buf);
+    spinbox->value = value;
+}
+
+void ui_spinbox_value_changed(Widget widget, UiSpinBox *spinbox, XmSpinBoxCallbackStruct *cb) {
+    Boolean update_value = TRUE;
+    double value = spinbox->value;
+    switch(cb->reason) {
+        case XmCR_OK: {
+            update_value = FALSE;
+            break;
+        }
+        case XmCR_SPIN_NEXT: {
+            value += spinbox->increment;
+            break;
+        }
+        case XmCR_SPIN_PRIOR: {
+            value -= spinbox->increment;
+            break;
+        }
+    }
+    
+    if(update_value) {
+        ui_spinbox_set_value(spinbox, value);
+        
+        UiEvent event;
+        event.obj = spinbox->obj;
+        event.window = event.obj->window;
+        event.document = event.obj->ctx->document;
+        event.eventdata = NULL;
+        event.eventdatatype = 0;
+        event.intval = (int64_t)value;
+        event.set = ui_get_setop();
+        
+        if(spinbox->onchange) {
+            spinbox->onchange(&event, spinbox->onchangedata);
+        }
+
+        UiObserver *obs = *spinbox->obs;
+        ui_notify_evt(*spinbox->obs, &event);
+    }
+}
+
+int64_t ui_spinbutton_getint(UiInteger *i) {
+    UiSpinBox *spinbox = i->obj;
+    i->value = (int64_t)spinbox->value;
+    return i->value;
+}
+
+void ui_spinbutton_setint(UiInteger *i, int64_t val) {
+    UiSpinBox *spinbox = i->obj;
+    ui_spinbox_set_value(spinbox, (double)val);
+    i->value = spinbox->value;
+}
+
+double ui_spinbutton_getdouble(UiDouble *d) {
+    UiSpinBox *spinbox = d->obj;
+    d->value = spinbox->value;
+    return d->value;
+}
+
+void ui_spinbutton_setdouble(UiDouble *d, double val) {
+    UiSpinBox *spinbox = d->obj;
+    ui_spinbox_set_value(spinbox, val);
+    d->value = spinbox->value;
+}
+
+double ui_spinbutton_getrangeval(UiRange *r) {
+    UiSpinBox *spinbox = r->obj;
+    r->value = spinbox->value;
+    return r->value;
+}
+
+void ui_spinbutton_setrangeval(UiRange *r, double val) {
+    UiSpinBox *spinbox = r->obj;
+    ui_spinbox_set_value(spinbox, val);
+    r->value = spinbox->value;
+}
+void ui_spinbutton_setrange(UiRange *r, double min, double max) {
+    UiSpinBox *spinbox = r->obj;
+    spinbox->min = min;
+    spinbox->max = max;
+    r->min = min;
+    r->max = max;
+}
+
+void ui_spinbutton_setextent(UiRange *r, double extent) {
+    UiSpinBox *spinbox = r->obj;
+    spinbox->increment = extent;
+    r->extent = extent;
+}
diff --git a/ui/motif/entry.h b/ui/motif/entry.h
new file mode 100644 (file)
index 0000000..ba47f4e
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 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 ENTRY_H
+#define ENTRY_H
+
+#include "../ui/entry.h"
+#include "container.h"
+#include "toolkit.h"
+
+#include <Xm/SSpinB.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct UiSpinBox {
+    UiObject *obj;
+    Widget textfield;
+    UiVar *var;
+    UiVarType vartype;
+    UiObserver **obs;
+    ui_callback onchange;
+    void* onchangedata;
+    double value;
+    double min;
+    double max;
+    double increment;
+    int digits;
+} UiSpinBox;
+
+void ui_spinbox_set_value(UiSpinBox *spinbox, double value);
+    
+void ui_spinbox_value_changed(Widget widget, UiSpinBox *spinbox, XmSpinBoxCallbackStruct *cb);
+
+int64_t ui_spinbutton_getint(UiInteger *i);
+void ui_spinbutton_setint(UiInteger *i, int64_t val);
+
+double ui_spinbutton_getdouble(UiDouble *d);
+void ui_spinbutton_setdouble(UiDouble *d, double val);
+
+double ui_spinbutton_getrangeval(UiRange *r);
+void ui_spinbutton_setrangeval(UiRange *r, double val);
+void ui_spinbutton_setrange(UiRange *r, double min, double max);
+void ui_spinbutton_setextent(UiRange *r, double extent);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ENTRY_H */
+
diff --git a/ui/motif/pathbar.c b/ui/motif/pathbar.c
new file mode 100644 (file)
index 0000000..4e0cde6
--- /dev/null
@@ -0,0 +1,439 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 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 "pathbar.h"
+
+#include <unistd.h>
+#include <cx/string.h>
+
+
+
+void pathbar_resize(Widget w, PathBar *p, XtPointer d)
+{
+    Dimension width, height;
+    XtVaGetValues(w, XmNwidth, &width, XmNheight, &height, NULL);
+    
+    Dimension *segW = (void*)XtCalloc(p->numSegments, sizeof(Dimension));
+    
+    Dimension maxHeight = 0;
+    
+    /* get width/height from all widgets */
+    Dimension pathWidth = 0;
+    for(int i=0;i<p->numSegments;i++) {
+        Dimension segWidth;
+        Dimension segHeight;
+        XtVaGetValues(p->pathSegments[i], XmNwidth, &segWidth, XmNheight, &segHeight, NULL);
+        segW[i] = segWidth;
+        pathWidth += segWidth;
+        if(segHeight > maxHeight) {
+            maxHeight = segHeight;
+        }
+    }
+    Dimension tfHeight;
+    XtVaGetValues(p->textfield, XmNheight, &tfHeight, NULL);
+    if(tfHeight > maxHeight) {
+        maxHeight = tfHeight;
+    }
+    
+    Boolean arrows = False;
+    if(pathWidth + 10 > width) {
+        arrows = True;
+        pathWidth += p->lw + p->rw;
+    }
+    
+    /* calc max visible widgets */
+    int start = 0;
+    if(arrows) {
+        Dimension vis = p->lw+p->rw;
+        for(int i=p->numSegments;i>0;i--) {
+            Dimension segWidth = segW[i-1];
+            if(vis + segWidth + 10 > width) {
+                start = i;
+                arrows = True;
+                break;
+            }
+            vis += segWidth;
+        }
+    } else {
+        p->shift = 0;
+    }
+    
+    int leftShift = 0;
+    if(p->shift < 0) {
+        if(start + p->shift < 0) {
+            leftShift = start;
+            start = 0;
+            p->shift = -leftShift;
+        } else {
+            leftShift = -p->shift; /* negative shift */
+            start += p->shift;
+        }
+    }
+    
+    int x = 0;
+    if(arrows) {
+        XtManageChild(p->left);
+        XtManageChild(p->right);
+        x = p->lw;
+    } else {
+        XtUnmanageChild(p->left);
+        XtUnmanageChild(p->right);
+    }
+    
+    for(int i=0;i<p->numSegments;i++) {
+        if(i >= start && i < p->numSegments - leftShift && !p->input) {
+            XtVaSetValues(p->pathSegments[i], XmNx, x, XmNy, 0, XmNheight, maxHeight, NULL);
+            x += segW[i];
+            XtManageChild(p->pathSegments[i]);
+        } else {
+            XtUnmanageChild(p->pathSegments[i]);
+        }
+    }
+    
+    if(arrows) {
+        XtVaSetValues(p->left, XmNx, 0, XmNy, 0, XmNheight, maxHeight, NULL);
+        XtVaSetValues(p->right, XmNx, x, XmNy, 0, XmNheight, maxHeight, NULL);
+    }
+    
+    free(segW);
+    
+    Dimension rw, rh;
+    XtMakeResizeRequest(w, width, maxHeight, &rw, &rh);
+    
+    XtVaSetValues(p->textfield, XmNwidth, rw, XmNheight, rh, NULL);
+}
+
+static void pathbarActivateTF(PathBar *p)
+{
+    XtUnmanageChild(p->left);
+    XtUnmanageChild(p->right);
+    XNETextSetSelection(p->textfield, 0, XNETextGetLastPosition(p->textfield), 0);
+    XtManageChild(p->textfield);
+    p->input = 1;
+
+    XmProcessTraversal(p->textfield, XmTRAVERSE_CURRENT);
+
+    pathbar_resize(p->widget, p, NULL);
+}
+
+void PathBarActivateTextfield(PathBar *p)
+{
+    p->focus = 1;
+    pathbarActivateTF(p);
+}
+
+void pathbar_input(Widget w, PathBar *p, XtPointer c)
+{
+    XmDrawingAreaCallbackStruct *cbs = (XmDrawingAreaCallbackStruct*)c;
+    XEvent *xevent = cbs->event;
+    
+    if (cbs->reason == XmCR_INPUT) {
+        if (xevent->xany.type == ButtonPress) {
+            p->focus = 0;
+            pathbarActivateTF(p);
+        }
+    }
+}
+
+void pathbar_losingfocus(Widget w, PathBar *p, XtPointer c)
+{
+    if(--p->focus < 0) {
+        p->input = False;
+        XtUnmanageChild(p->textfield);
+    }
+}
+
+static cxmutstr concat_path_s(cxstring base, cxstring path) {
+    if(!path.ptr) {
+        path = CX_STR("");
+    }
+    
+    int add_separator = 0;
+    if(base.length != 0 && base.ptr[base.length-1] == '/') {
+        if(path.ptr[0] == '/') {
+            base.length--;
+        }
+    } else {
+        if(path.length == 0 || path.ptr[0] != '/') {
+            add_separator = 1;
+        }
+    }
+    
+    cxmutstr url;
+    if(add_separator) {
+        url = cx_strcat(3, base, CX_STR("/"), path);
+    } else {
+        url = cx_strcat(2, base, path);
+    }
+    
+    return url;
+}
+
+char* pathbar_concat_path(const char *path1, const char *path2) {
+    return concat_path_s(cx_str(path1), cx_str(path2)).ptr;
+}
+
+void pathbar_pathinput(Widget w, PathBar *p, XtPointer d)
+{
+    char *newpath = XNETextGetString(p->textfield);
+    if(newpath) {
+        if(newpath[0] == '~') {
+            char *p = newpath+1;
+            char *home = getenv("HOME");
+            char *cp = pathbar_concat_path(home, p);
+            XtFree(newpath);
+            newpath = cp;
+        } else if(newpath[0] != '/') {
+            char curdir[2048];
+            curdir[0] = 0;
+            getcwd(curdir, 2048);
+            char *cp = pathbar_concat_path(curdir, newpath);
+            XtFree(newpath);
+            newpath = cp;
+        }
+        
+        /* update path */
+        PathBarSetPath(p, newpath);
+        if(p->updateDir) {
+            p->updateDir(p->updateDirData, newpath, -1);
+        }
+        XtFree(newpath);
+        
+        /* hide textfield and show path as buttons */
+        XtUnmanageChild(p->textfield);
+        pathbar_resize(p->widget, p, NULL);
+        
+        if(p->focus_widget) {
+            XmProcessTraversal(p->focus_widget, XmTRAVERSE_CURRENT);
+        }
+    }
+}
+
+void pathbar_shift_left(Widget w, PathBar *p, XtPointer d)
+{
+    p->shift--;
+    pathbar_resize(p->widget, p, NULL);
+}
+
+void pathbar_shift_right(Widget w, PathBar *p, XtPointer d)
+{
+    if(p->shift < 0) {
+        p->shift++;
+    }
+    pathbar_resize(p->widget, p, NULL);
+}
+
+static void pathTextEH(Widget widget, XtPointer data, XEvent *event, Boolean *dispatch) {
+    PathBar *pb = data;
+    if(event->type == KeyReleaseMask) {
+        if(event->xkey.keycode == 9) {
+            XtUnmanageChild(pb->textfield);
+            pathbar_resize(pb->widget, pb, NULL);
+            *dispatch = False;
+        } else if(event->xkey.keycode == 36) {
+            pathbar_pathinput(pb->textfield, pb, NULL);
+            *dispatch = False;
+        }
+    }
+}
+
+PathBar* CreatePathBar(Widget parent, ArgList args, int n)
+{
+    PathBar *bar = (PathBar*)XtMalloc(sizeof(PathBar));
+    bar->path = NULL;
+    bar->updateDir = NULL;
+    bar->updateDirData = NULL;
+    
+    bar->focus_widget = NULL;
+    
+    bar->getpathelm = NULL;
+    bar->getpathelmdata = NULL;
+    bar->current_pathelms = NULL;
+    
+    bar->shift = 0;
+    
+    XtSetArg(args[n], XmNmarginWidth, 0); n++;
+    XtSetArg(args[n], XmNmarginHeight, 0); n++;
+    bar->widget = XmCreateDrawingArea(parent, "pathbar", args, n);
+    XtAddCallback(
+            bar->widget,
+            XmNresizeCallback,
+            (XtCallbackProc)pathbar_resize,
+            bar);
+    XtAddCallback(
+            bar->widget,
+            XmNinputCallback,
+            (XtCallbackProc)pathbar_input,
+            bar);
+    
+    Arg a[4];
+    XtSetArg(a[0], XmNshadowThickness, 0);
+    XtSetArg(a[1], XmNx, 0);
+    XtSetArg(a[2], XmNy, 0);
+    bar->textfield = XNECreateText(bar->widget, "pbtext", a, 3);
+    bar->input = 0;
+    XtAddCallback(
+            bar->textfield,
+            XmNlosingFocusCallback,
+            (XtCallbackProc)pathbar_losingfocus,
+            bar);
+    XtAddCallback(bar->textfield, XmNactivateCallback,
+                 (XtCallbackProc)pathbar_pathinput, bar);
+    XtAddEventHandler(bar->textfield, KeyPressMask | KeyReleaseMask, FALSE, pathTextEH, bar);
+    
+    XtSetArg(a[0], XmNarrowDirection, XmARROW_LEFT);
+    bar->left = XmCreateArrowButton(bar->widget, "pbbutton", a, 1);
+    XtSetArg(a[0], XmNarrowDirection, XmARROW_RIGHT);
+    bar->right = XmCreateArrowButton(bar->widget, "pbbutton", a, 1);
+    XtAddCallback(
+                bar->left,
+                XmNactivateCallback,
+                (XtCallbackProc)pathbar_shift_left,
+                bar);
+    XtAddCallback(
+                bar->right,
+                XmNactivateCallback,
+                (XtCallbackProc)pathbar_shift_right,
+                bar);
+    
+    Pixel bg;
+    XtVaGetValues(bar->textfield, XmNbackground, &bg, NULL);
+    XtVaSetValues(bar->widget, XmNbackground, bg, NULL);
+    
+    XtManageChild(bar->left);
+    XtManageChild(bar->right);
+    
+    XtVaGetValues(bar->left, XmNwidth, &bar->lw, NULL);
+    XtVaGetValues(bar->right, XmNwidth, &bar->rw, NULL);
+    
+    bar->segmentAlloc = 16;
+    bar->numSegments = 0;
+    bar->pathSegments = (Widget*)XtCalloc(16, sizeof(Widget));
+    
+    bar->selection = 0;
+    
+    return bar;
+}
+
+void PathBarChangeDir(Widget w, PathBar *bar, XtPointer c)
+{
+    XmToggleButtonSetState(bar->pathSegments[bar->selection], False, False);
+    
+    int i;
+    for(i=0;i<bar->numSegments;i++) {  
+        if(bar->pathSegments[i] == w) {
+            bar->selection = i;
+            XmToggleButtonSetState(w, True, False);
+            break;
+        }
+    }
+    
+    UiPathElm elm = bar->current_pathelms[i];
+    cxmutstr path = cx_strdup(cx_strn(elm.path, elm.path_len));
+    if(bar->updateDir) {
+        XNETextSetString(bar->textfield, path.ptr);
+        bar->updateDir(bar->updateDirData, path.ptr, i);
+    }
+    free(path.ptr);
+}
+
+static void ui_pathelm_destroy(UiPathElm *elms, size_t nelm) {
+    for(int i=0;i<nelm;i++) {
+        free(elms[i].name);
+        free(elms[i].path);
+    }
+    free(elms);
+}
+
+void PathBarSetPath(PathBar *bar, const char *path)
+{
+    if(bar->path) {
+        free(bar->path);
+    }
+    bar->path = strdup(path);
+    
+    for(int i=0;i<bar->numSegments;i++) {
+        XtDestroyWidget(bar->pathSegments[i]);
+    }
+    XtUnmanageChild(bar->textfield);
+    XtManageChild(bar->left);
+    XtManageChild(bar->right);
+    bar->input = False;
+    
+    Arg args[4];
+    XmString str;
+    
+    bar->numSegments = 0;
+    
+    ui_pathelm_destroy(bar->current_pathelms, bar->numSegments);
+    size_t nelm = 0;
+    UiPathElm* path_elm = bar->getpathelm(bar->path, strlen(bar->path), &nelm, bar->getpathelmdata);
+    if (!path_elm) {
+        return;
+    }
+    bar->current_pathelms = path_elm;
+    bar->numSegments = nelm;
+    bar->pathSegments = realloc(bar->pathSegments, nelm * sizeof(Widget*));
+    
+    for(int i=0;i<nelm;i++) {
+        UiPathElm elm = path_elm[i];
+        
+        cxmutstr name = cx_strdup(cx_strn(elm.name, elm.name_len));
+        str = XmStringCreateLocalized(elm.name);
+        free(name.ptr);
+        
+        XtSetArg(args[0], XmNlabelString, str);
+        XtSetArg(args[1], XmNfillOnSelect, True);
+        XtSetArg(args[2], XmNindicatorOn, False);
+        Widget button = XmCreateToggleButton(bar->widget, "pbbutton", args, 3);
+        XtAddCallback(
+                button,
+                XmNvalueChangedCallback,
+                (XtCallbackProc)PathBarChangeDir,
+                bar);
+        XmStringFree(str);
+        
+        bar->pathSegments[i] = button;
+    }
+    
+    bar->selection = bar->numSegments-1;
+    XmToggleButtonSetState(bar->pathSegments[bar->selection], True, False);
+    
+    XNETextSetString(bar->textfield, (char*)path);
+    XNETextSetInsertionPosition(bar->textfield, XNETextGetLastPosition(bar->textfield));
+    
+    pathbar_resize(bar->widget, bar, NULL);
+}
+
+void PathBarDestroy(PathBar *pathbar) {
+    if(pathbar->path) {
+        XtFree(pathbar->path);
+    }
+    XtFree((void*)pathbar->pathSegments);
+    XtFree((void*)pathbar);
+}
diff --git a/ui/motif/pathbar.h b/ui/motif/pathbar.h
new file mode 100644 (file)
index 0000000..90ba73b
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 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 PATHBAR_H
+#define PATHBAR_H
+
+#include <Xm/XmAll.h>
+
+#include "../ui/text.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define XNECreateText(parent,name,args,count)   XmCreateTextField(parent,name,args,count)
+#define XNETextSetString(widget,value)          XmTextFieldSetString(widget,value)
+#define XNETextGetString(widget)                XmTextFieldGetString(widget)
+#define XNETextGetLastPosition(widget)          XmTextFieldGetLastPosition(widget)  
+#define XNETextSetInsertionPosition(widget, i)  XmTextFieldSetInsertionPosition(widget, i)  
+#define XNETextSetSelection(w, f, l, t)         XmTextFieldSetSelection(w, f, l, t)
+
+typedef void(*updatedir_callback)(void*,char*,int);
+
+typedef struct PathBar {  
+    Widget widget;
+    Widget textfield;
+    
+    Widget focus_widget;
+    
+    Widget left;
+    Widget right;
+    Dimension lw;
+    Dimension rw;
+    
+    int shift;
+    
+    UiPathElm *current_pathelms;
+    Widget *pathSegments;
+    size_t numSegments;
+    size_t segmentAlloc;
+    
+    char *path;
+    int selection;
+    Boolean input;
+    
+    int focus;
+    
+    Boolean disableResize;
+    
+    updatedir_callback updateDir;
+    void *updateDirData;
+    
+    ui_pathelm_func getpathelm;
+    void *getpathelmdata;
+} PathBar;
+
+PathBar* CreatePathBar(Widget parent, ArgList args, int n);
+void PathBarSetPath(PathBar *bar, const char *path);
+void PathBarDestroy(PathBar *bar);
+
+void pathbar_resize(Widget w, PathBar *p, XtPointer d);
+
+char* pathbar_concat_path(const char *path1, const char *path2);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PATHBAR_H */
+
diff --git a/ui/win32/list.c b/ui/win32/list.c
new file mode 100644 (file)
index 0000000..5626458
--- /dev/null
@@ -0,0 +1,455 @@
+/*\r
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.\r
+ *\r
+ * Copyright 2025 Olaf Wintermann. All rights reserved.\r
+ *\r
+ * Redistribution and use in source and binary forms, with or without\r
+ * modification, are permitted provided that the following conditions are met:\r
+ *\r
+ *   1. Redistributions of source code must retain the above copyright\r
+ *      notice, this list of conditions and the following disclaimer.\r
+ *\r
+ *   2. Redistributions in binary form must reproduce the above copyright\r
+ *      notice, this list of conditions and the following disclaimer in the\r
+ *      documentation and/or other materials provided with the distribution.\r
+ *\r
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"\r
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\r
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\r
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\r
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\r
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\r
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\r
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\r
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\r
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\r
+ * POSSIBILITY OF SUCH DAMAGE.\r
+ */\r
+\r
+#include <stdio.h>\r
+#include <stdlib.h>\r
+\r
+#include <cx/array_list.h>\r
+\r
+#include "list.h"\r
+#include "container.h"\r
+\r
+\r
+\r
+static W32WidgetClass listview_widget_class = {\r
+    .eventproc = ui_listview_eventproc,\r
+    .enable = w32_widget_default_enable,\r
+    .show = w32_widget_default_show,\r
+    .get_preferred_size = ui_listview_get_preferred_size,\r
+    .destroy  = w32_widget_default_destroy\r
+};\r
+\r
+static void* strmodel_getvalue(UiList *list, void *elm, int row, int col, void *userdata, UiBool *freeResult) {\r
+    return col == 0 ? elm : NULL;\r
+}\r
+\r
+static void* getvalue_wrapper(UiList *list, void *elm, int row, int col, void *userdata, UiBool *freeResult) {\r
+    ui_getvaluefunc getvalue = (ui_getvaluefunc)userdata;\r
+    return getvalue(elm, col);\r
+}\r
+\r
+static void* null_getvalue(UiList *list, void *elm, int row, int col, void *userdata, UiBool *freeResult) {\r
+    return NULL;\r
+}\r
+\r
+/*\r
+ * Creates an UiListView widget object and initializes it from the UiListArgs\r
+ */\r
+static UiListView* create_listview_widget(UiObject *obj, W32WidgetClass *widget_class, HWND hwnd, UiListArgs *args, UiBool table) {\r
+    UiListView *listview = w32_widget_create(widget_class, hwnd, sizeof(UiListView));\r
+    listview->widget.hwnd = hwnd;\r
+    listview->obj = obj;\r
+    listview->preferred_width = args->width ? args->width : 300; // 300: default width/height\r
+    listview->preferred_height = args->height ? args->height : 300;\r
+    listview->onactivate = args->onactivate;\r
+    listview->onactivatedata = args->onactivatedata;\r
+    listview->onselection = args->onselection;\r
+    listview->onselectiondata = args->onselectiondata;\r
+    listview->ondragstart = args->ondragstart;\r
+    listview->ondragstartdata = args->ondragstartdata;\r
+    listview->ondragcomplete = args->ondragcomplete;\r
+    listview->ondragcompletedata = args->ondragcompletedata;\r
+    listview->ondrop = args->ondrop;\r
+    listview->ondropdata = args->ondropdata;\r
+    listview->istable = table;\r
+\r
+    // convert ui_getvaluefunc into ui_getvaluefunc2 if necessary\r
+    ui_getvaluefunc2 getvalue = args->getvalue2;\r
+    void *getvaluedata = args->getvalue2data;\r
+    if(!getvalue) {\r
+        if(args->getvalue) {\r
+            getvalue = getvalue_wrapper;\r
+            getvaluedata = (void*)args->getvalue;\r
+        } else {\r
+            getvalue = table ? null_getvalue : strmodel_getvalue;\r
+        }\r
+    }\r
+    listview->getvalue = getvalue;\r
+    listview->getvaluedata = getvaluedata;\r
+\r
+    listview->var = uic_widget_var(obj->ctx, obj->ctx, args->list, args->varname, UI_VAR_LIST);\r
+\r
+    return listview;\r
+}\r
+\r
+static UIWIDGET listview_create(UiObject *obj, UiListArgs *args, UiBool table) {\r
+    HINSTANCE hInstance = GetModuleHandle(NULL);\r
+    UiContainerPrivate *container = ui_obj_container(obj);\r
+    HWND parent = ui_container_get_parent(container);\r
+    UiLayout layout = UI_ARGS2LAYOUT(args);\r
+\r
+    HWND hwnd = CreateWindowEx(\r
+            WS_EX_CLIENTEDGE,\r
+            WC_LISTVIEW,\r
+            "",\r
+            WS_CHILD | WS_VISIBLE | LVS_REPORT,\r
+            0, 0, 100, 100,\r
+            parent,\r
+            (HMENU)1337,\r
+            hInstance,\r
+            NULL);\r
+    ui_win32_set_ui_font(hwnd);\r
+    ListView_SetExtendedListViewStyle(\r
+        hwnd,\r
+        LVS_EX_FULLROWSELECT //| LVS_EX_GRIDLINES\r
+    );\r
+\r
+    UiListView *listview = create_listview_widget(obj, &listview_widget_class, hwnd, args, table);\r
+    ui_container_add(container, (W32Widget*)listview, &layout);\r
+\r
+    // init list model\r
+    // always initialize listview->model\r
+    int numcolumns = 0;\r
+    if (table) {\r
+        if (args->model) {\r
+            listview->model = ui_model_copy(obj->ctx, args->model);\r
+            numcolumns = listview->model->columns;\r
+        } else {\r
+            listview->model = ui_model_new(obj->ctx);\r
+        }\r
+    } else {\r
+        UiModel *model = ui_model_new(obj->ctx);\r
+        ui_model_add_column(obj->ctx, model, UI_STRING, "Test", -1);\r
+        listview->model = model;\r
+        numcolumns = 1;\r
+    }\r
+\r
+    // create columns\r
+    UiModel *model = listview->model;\r
+    for (int i=0;i<numcolumns;i++) {\r
+        LVCOLUMN col;\r
+        UiModelType type = model->types[i];\r
+        char *title = model->titles[i];\r
+        size_t titlelen = title ? strlen(title) : 0;\r
+        int size = model->columnsize[i];\r
+        switch (type) {\r
+            default: {\r
+                col.mask = LVCF_TEXT | LVCF_WIDTH;\r
+                col.pszText = title;\r
+                col.cx = size > 0 ? size : titlelen*10+5;\r
+                break;\r
+            }\r
+            case UI_ICON: {\r
+                break; // TODO\r
+            }\r
+        }\r
+        ListView_InsertColumn(hwnd, i, &col);\r
+    }\r
+\r
+    // bind the listview to the provided UiList\r
+    if (listview->var) {\r
+        UiList *list = listview->var->value;\r
+        list->obj = listview;\r
+        list->update = ui_listview_update;\r
+        list->getselection = ui_listview_getselection;\r
+        list->setselection = ui_listview_setselection;\r
+\r
+        ui_listview_update(list, -1);\r
+    }\r
+\r
+    return (W32Widget*)listview;\r
+}\r
+\r
+static UiListSelection listview_get_selection(UiListView *listview) {\r
+    UiListSelection sel = { 0, NULL };\r
+    HWND hwnd = listview->widget.hwnd;\r
+\r
+    CX_ARRAY_DECLARE(int, indices);\r
+    cx_array_initialize(indices, 8);\r
+\r
+    int index = -1;\r
+    while ((index = ListView_GetNextItem(hwnd, index, LVNI_SELECTED)) != -1) {\r
+        cx_array_simple_add(indices, index);\r
+    }\r
+\r
+    if (indices_size > 0) {\r
+        sel.rows = indices;\r
+        sel.count = indices_size;\r
+    }\r
+\r
+    return sel;\r
+}\r
+\r
+// listview class event proc\r
+int ui_listview_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {\r
+    UiListView *listview = (UiListView*)widget;\r
+    switch (uMsg) {\r
+        case WM_NOTIFY: {\r
+            LPNMHDR hdr = (LPNMHDR)lParam;\r
+            switch (hdr->code) {\r
+                case LVN_ITEMCHANGED: {\r
+                    LPNMLISTVIEW lv = (LPNMLISTVIEW)lParam;\r
+                    int row = lv->iItem;\r
+                    if ((lv->uChanged & LVIF_STATE) && (lv->uNewState & LVIS_SELECTED) && listview->onselection) {\r
+                        UiListSelection sel = listview_get_selection(listview);\r
+\r
+                        UiEvent event;\r
+                        event.obj = listview->obj;\r
+                        event.window = listview->obj->window;\r
+                        event.document = listview->obj->ctx->document;\r
+                        event.eventdata = &sel;\r
+                        event.eventdatatype = UI_EVENT_DATA_LIST_SELECTION;\r
+                        event.intval = row;\r
+                        event.set = ui_get_setop();\r
+                        listview->onselection(&event, listview->onselectiondata);\r
+\r
+                        ui_listselection_free(sel);\r
+                    }\r
+                    break;\r
+                }\r
+                case LVN_ITEMACTIVATE: {\r
+                    LPNMLISTVIEW lv = (LPNMLISTVIEW)lParam;\r
+                    int row = lv->iItem;\r
+                    if (listview->onactivate) {\r
+                        UiEvent event;\r
+                        event.obj = listview->obj;\r
+                        event.window = listview->obj->window;\r
+                        event.document = listview->obj->ctx->document;\r
+                        event.eventdata = NULL;\r
+                        event.eventdatatype = UI_EVENT_DATA_LIST_ELM;\r
+                        event.intval = row;\r
+                        event.set = ui_get_setop();\r
+\r
+                        if (listview->var) {\r
+                            UiList *list = listview->var->value;\r
+                            event.eventdata = list->get(list, row);\r
+                            event.eventdatatype = UI_EVENT_DATA_LIST_ELM;\r
+                        }\r
+\r
+                        listview->onactivate(&event, listview->onactivatedata);\r
+                    }\r
+                    break;\r
+                }\r
+            }\r
+            break;\r
+        }\r
+    }\r
+\r
+    return 0;\r
+}\r
+\r
+W32Size ui_listview_get_preferred_size(W32Widget *widget) {\r
+    UiListView *listview = (UiListView*)widget;\r
+    W32Size size;\r
+    size.width = listview->preferred_width;\r
+    size.height = listview->preferred_height;\r
+    return size;\r
+}\r
+\r
+/*\r
+ * Creates and inserts an LVITEM\r
+ *\r
+ * list: An UiList bound to an UiListView\r
+ * row: row index\r
+ * elm: list element (same as list->get(list, row))\r
+ */\r
+static void insert_item(UiList *list, int row, void *elm) {\r
+    UiListView *listview = (UiListView*)list->obj;\r
+    HWND hwnd = listview->widget.hwnd;\r
+    UiModel *model = listview->model;\r
+\r
+    LVITEM item;\r
+    item.mask = LVIF_TEXT;\r
+    item.iItem = row;\r
+    item.iSubItem = 0;\r
+    int idx = -1;\r
+    for (int col=0;col<model->columns;col++) {\r
+        UiBool freeResult = FALSE;\r
+        // convert the list element to a value, that can be displayed in the list view\r
+        // TODO: handle all model types\r
+        char *str = listview->getvalue(list, elm, row, col, listview->getvaluedata, &freeResult);\r
+        if (col == 0) {\r
+            item.pszText = str;\r
+            idx = ListView_InsertItem(hwnd, &item);\r
+        } else {\r
+            ListView_SetItemText(hwnd, idx, col, str);\r
+        }\r
+\r
+        if (freeResult) {\r
+            free(str);\r
+        }\r
+    }\r
+}\r
+\r
+/*\r
+ * UiList->update function\r
+ *\r
+ * Updates one or all rows\r
+ * row: list index or -1 for updating all rows\r
+ */\r
+void ui_listview_update(UiList *list, int row) {\r
+    UiListView *listview = (UiListView*)list->obj;\r
+    HWND hwnd = listview->widget.hwnd;\r
+    UiModel *model = listview->model;\r
+    if (row < 0) {\r
+        ListView_DeleteAllItems(hwnd);\r
+        void *elm = list->first(list);\r
+        int row = 0;\r
+        while (elm) {\r
+            insert_item(list, row, elm);\r
+            elm = list->next(list);\r
+            row++;\r
+        }\r
+    } else {\r
+        ListView_DeleteItem(hwnd, row);\r
+        void *elm = list->get(list, row);\r
+        insert_item(list, row, elm);\r
+    }\r
+\r
+    // re-adjust all columns\r
+    for (int i=0;i<model->columns;i++) {\r
+        ListView_SetColumnWidth(hwnd, i, LVSCW_AUTOSIZE);\r
+    }\r
+}\r
+\r
+UiListSelection ui_listview_getselection(UiList *list) {\r
+    UiListView *listview = (UiListView*)list->obj;\r
+    return listview_get_selection(listview);\r
+}\r
+\r
+void ui_listview_setselection(UiList *list, UiListSelection selection) {\r
+\r
+}\r
+\r
+// public API\r
+UIWIDGET ui_listview_create(UiObject *obj, UiListArgs *args) {\r
+    return listview_create(obj, args, FALSE);\r
+}\r
+\r
+// public API\r
+UIWIDGET ui_table_create(UiObject *obj, UiListArgs *args) {\r
+    return listview_create(obj, args, TRUE);\r
+}\r
+\r
+\r
+/* ------------------------------------ DropDown ------------------------------------*/\r
+\r
+static W32WidgetClass dropdown_widget_class = {\r
+    .eventproc = ui_dropdown_eventproc,\r
+    .enable = w32_widget_default_enable,\r
+    .show = w32_widget_default_show,\r
+    .get_preferred_size = ui_dropdown_get_preferred_size,\r
+    .destroy  = w32_widget_default_destroy\r
+};\r
+\r
+UIWIDGET ui_combobox_create(UiObject *obj, UiListArgs *args) {\r
+    HINSTANCE hInstance = GetModuleHandle(NULL);\r
+    UiContainerPrivate *container = ui_obj_container(obj);\r
+    HWND parent = ui_container_get_parent(container);\r
+    UiLayout layout = UI_ARGS2LAYOUT(args);\r
+\r
+    HWND hwnd = CreateWindowEx(\r
+            WS_EX_CLIENTEDGE,\r
+            WC_COMBOBOX,\r
+            "",\r
+            WS_CHILD | WS_VISIBLE | WS_VSCROLL | CBS_DROPDOWNLIST,\r
+            0, 0, 100, 100,\r
+            parent,\r
+            (HMENU)1337,\r
+            hInstance,\r
+            NULL);\r
+    ui_win32_set_ui_font(hwnd);\r
+\r
+    UiListView *dropdown = create_listview_widget(obj, &dropdown_widget_class, hwnd, args, FALSE);\r
+    ui_container_add(container, (W32Widget*)dropdown, &layout);\r
+\r
+    // bind the dropdown to the provided UiList\r
+    if (dropdown->var) {\r
+        UiList *list = dropdown->var->value;\r
+        list->obj = dropdown;\r
+        list->update = ui_dropdown_update;\r
+        list->getselection = ui_dropdown_getselection;\r
+        list->setselection = ui_dropdown_setselection;\r
+\r
+        ui_dropdown_update(list, -1);\r
+    }\r
+\r
+\r
+    return (W32Widget*)dropdown;\r
+}\r
+\r
+int ui_dropdown_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {\r
+    return 0;\r
+}\r
+\r
+W32Size ui_dropdown_get_preferred_size(W32Widget *widget) {\r
+    W32Size size;\r
+    size.width = 200;\r
+    size.height = 30;\r
+    return size;\r
+}\r
+\r
+static void dropdown_insert_item(UiList *list, int row, void *elm) {\r
+    UiListView *listview = (UiListView*)list->obj;\r
+    HWND hwnd = listview->widget.hwnd;\r
+\r
+    UiBool freeResult = FALSE;\r
+    char *str = listview->getvalue(list, elm, row, 0, listview->getvaluedata, &freeResult);\r
+    SendMessage(hwnd, CB_ADDSTRING, 0, (LPARAM)str);\r
+\r
+    if (freeResult) {\r
+        free(str);\r
+    }\r
+}\r
+\r
+void ui_dropdown_update(UiList *list, int row) {\r
+    UiListView *listview = (UiListView*)list->obj;\r
+    HWND hwnd = listview->widget.hwnd;\r
+    if (row < 0) {\r
+        SendMessage(hwnd, CB_RESETCONTENT, 0, 0);\r
+\r
+        void *elm = list->first(list);\r
+        int row = 0;\r
+        while (elm) {\r
+            dropdown_insert_item(list, row, elm);\r
+            elm = list->next(list);\r
+            row++;\r
+        }\r
+    } else {\r
+        SendMessage(hwnd, CB_DELETESTRING, row, 0);\r
+        void *elm = list->get(list, row);\r
+        dropdown_insert_item(list, row, elm);\r
+    }\r
+}\r
+\r
+UiListSelection ui_dropdown_getselection(UiList *list) {\r
+    UiListSelection sel = { 0, NULL };\r
+    UiListView *listview = (UiListView*)list->obj;\r
+    int index = (int)SendMessage(listview->widget.hwnd, CB_GETCURSEL, 0, 0);\r
+    if (index >= 0) {\r
+        sel.rows = malloc(sizeof(int));\r
+        sel.rows[0] = index;\r
+        sel.count = 1;\r
+    }\r
+    return sel;\r
+}\r
+\r
+void ui_dropdown_setselection(UiList *list, UiListSelection selection) {\r
+    UiListView *listview = (UiListView*)list->obj;\r
+    SendMessage(listview->widget.hwnd, CB_SETCURSEL, 0, 0);\r
+}\r
diff --git a/ui/win32/list.h b/ui/win32/list.h
new file mode 100644 (file)
index 0000000..7b1d36b
--- /dev/null
@@ -0,0 +1,83 @@
+/*\r
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.\r
+ *\r
+ * Copyright 2025 Olaf Wintermann. All rights reserved.\r
+ *\r
+ * Redistribution and use in source and binary forms, with or without\r
+ * modification, are permitted provided that the following conditions are met:\r
+ *\r
+ *   1. Redistributions of source code must retain the above copyright\r
+ *      notice, this list of conditions and the following disclaimer.\r
+ *\r
+ *   2. Redistributions in binary form must reproduce the above copyright\r
+ *      notice, this list of conditions and the following disclaimer in the\r
+ *      documentation and/or other materials provided with the distribution.\r
+ *\r
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"\r
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\r
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\r
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\r
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\r
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\r
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\r
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\r
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\r
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\r
+ * POSSIBILITY OF SUCH DAMAGE.\r
+ */\r
+\r
+#ifndef LIST_H\r
+#define LIST_H\r
+\r
+#include "toolkit.h"\r
+#include "../ui/tree.h"\r
+#include "win32.h"\r
+#include <commctrl.h>\r
+\r
+#ifdef __cplusplus\r
+extern "C" {\r
+#endif\r
+\r
+typedef struct UiListView {\r
+    W32Widget widget;\r
+    UiObject *obj;\r
+    UiVar *var;\r
+    ui_getvaluefunc2 getvalue;\r
+    void *getvaluedata;\r
+    ui_getstylefunc getstyle;\r
+    void *getstyledata;\r
+    UiModel *model;\r
+    UiBool istable;\r
+    int preferred_width;\r
+    int preferred_height;\r
+    ui_callback onactivate;\r
+    void* onactivatedata;\r
+    ui_callback onselection;\r
+    void* onselectiondata;\r
+    ui_callback ondragstart;\r
+    void* ondragstartdata;\r
+    ui_callback ondragcomplete;\r
+    void* ondragcompletedata;\r
+    ui_callback ondrop;\r
+    void* ondropdata;\r
+} UiListView;\r
+\r
+int ui_listview_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);\r
+W32Size ui_listview_get_preferred_size(W32Widget *widget);\r
+\r
+void ui_listview_update(UiList *list, int row);\r
+UiListSelection ui_listview_getselection(UiList *list);\r
+void ui_listview_setselection(UiList *list, UiListSelection selection);\r
+\r
+int ui_dropdown_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);\r
+W32Size ui_dropdown_get_preferred_size(W32Widget *widget);\r
+\r
+void ui_dropdown_update(UiList *list, int row);\r
+UiListSelection ui_dropdown_getselection(UiList *list);\r
+void ui_dropdown_setselection(UiList *list, UiListSelection selection);\r
+\r
+#ifdef __cplusplus\r
+}\r
+#endif\r
+\r
+#endif //_LIST_H
\ No newline at end of file
diff --git a/ui/win32/text.c b/ui/win32/text.c
new file mode 100644 (file)
index 0000000..cc5f400
--- /dev/null
@@ -0,0 +1,241 @@
+/*\r
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.\r
+ *\r
+ * Copyright 2025 Olaf Wintermann. All rights reserved.\r
+ *\r
+ * Redistribution and use in source and binary forms, with or without\r
+ * modification, are permitted provided that the following conditions are met:\r
+ *\r
+ *   1. Redistributions of source code must retain the above copyright\r
+ *      notice, this list of conditions and the following disclaimer.\r
+ *\r
+ *   2. Redistributions in binary form must reproduce the above copyright\r
+ *      notice, this list of conditions and the following disclaimer in the\r
+ *      documentation and/or other materials provided with the distribution.\r
+ *\r
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"\r
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\r
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\r
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\r
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\r
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\r
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\r
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\r
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\r
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\r
+ * POSSIBILITY OF SUCH DAMAGE.\r
+ */\r
+\r
+#include "text.h"\r
+\r
+static W32WidgetClass textarea_widget_class = {\r
+    .eventproc = ui_textarea_eventproc,\r
+    .enable = w32_widget_default_enable,\r
+    .show = w32_widget_default_show,\r
+    .get_preferred_size = ui_textarea_get_preferred_size,\r
+    .destroy  = w32_widget_default_destroy\r
+};\r
+\r
+UIWIDGET ui_textarea_create(UiObject *obj, UiTextAreaArgs *args) {\r
+    HINSTANCE hInstance = GetModuleHandle(NULL);\r
+    UiContainerPrivate *container = ui_obj_container(obj);\r
+    HWND parent = ui_container_get_parent(container);\r
+    UiLayout layout = UI_ARGS2LAYOUT(args);\r
+\r
+    int width = args->width >= 0 ? args->width : 100;\r
+    int height = args->height >= 0 ? args->height : 100;\r
+\r
+    HWND hwnd = CreateWindowEx(\r
+            0,\r
+            "EDIT",\r
+            "",\r
+            WS_CHILD | WS_VISIBLE | WS_BORDER | ES_LEFT | ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_WANTRETURN | WS_VSCROLL,\r
+            0, 0, width, height,\r
+            parent,\r
+            (HMENU)0,\r
+            hInstance,\r
+            NULL);\r
+    ui_win32_set_ui_font(hwnd);\r
+\r
+    W32Widget *widget = w32_widget_create(&textarea_widget_class, hwnd, sizeof(UiTextArea));\r
+    ui_container_add(container, widget, &layout);\r
+\r
+    UiTextArea *textarea = (UiTextArea*)widget;\r
+    textarea->width = width;\r
+    textarea->widget.var = uic_widget_var(obj->ctx, obj->ctx, args->value, args->varname, UI_VAR_TEXT);\r
+    textarea->widget.callback = args->onchange;\r
+    textarea->widget.callbackdata = args->onchangedata;\r
+\r
+    if (textarea->widget.var) {\r
+        UiText *t = textarea->widget.var->value;\r
+\r
+        if (t->value.ptr) {\r
+            // TODO: set textarea string\r
+        }\r
+        t->obj = widget;\r
+        t->save = ui_textarea_save;\r
+        t->destroy = ui_textarea_destroy;\r
+        t->restore = ui_textarea_restore;\r
+        t->get = ui_textarea_get;\r
+        t->set = ui_textarea_set;\r
+        t->getsubstr = ui_textarea_getsubstr;\r
+        t->insert = ui_textarea_insert;\r
+        t->setposition = ui_textarea_setposition;\r
+        t->position = ui_textarea_position;\r
+        t->setselection = ui_textarea_setselection;\r
+        t->selection = ui_textarea_selection;\r
+        t->length = ui_textarea_length;\r
+        t->remove = ui_textarea_remove;\r
+    }\r
+\r
+    return widget;\r
+}\r
+\r
+W32Size ui_textarea_get_preferred_size(W32Widget *widget) {\r
+    W32Size size;\r
+    UiTextArea *textarea = (UiTextArea*)widget;\r
+    size.width = textarea->width;\r
+    size.height = textarea->height;\r
+    return size;\r
+}\r
+\r
+int ui_textarea_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {\r
+    return 0;\r
+}\r
+\r
+void  ui_textarea_save(UiText *text) {\r
+\r
+}\r
+\r
+void  ui_textarea_destroy(UiText *text) {\r
+\r
+}\r
+\r
+void  ui_textarea_restore(UiText *text) {\r
+\r
+}\r
+\r
+void  ui_textarea_set(UiText *text, const char *str) {\r
+\r
+}\r
+\r
+char* ui_textarea_get(UiText *text) {\r
+    return NULL;\r
+}\r
+\r
+char* ui_textarea_getsubstr(UiText *text, int begin, int end) {\r
+    return NULL;\r
+}\r
+\r
+void  ui_textarea_insert(UiText *text, int pos, char *str) {\r
+\r
+}\r
+\r
+void  ui_textarea_setposition(UiText *text, int pos) {\r
+\r
+}\r
+\r
+int   ui_textarea_position(UiText *text) {\r
+    return 0;\r
+}\r
+\r
+void  ui_textarea_setselection(UiText *text, int begin, int end) {\r
+\r
+}\r
+\r
+void  ui_textarea_selection(UiText *text, int *begin, int *end) {\r
+\r
+}\r
+\r
+int   ui_textarea_length(UiText *text) {\r
+    return 0;\r
+}\r
+\r
+void  ui_textarea_remove(UiText *text, int begin, int end) {\r
+\r
+}\r
+\r
+/* ----------------------------- TextField ----------------------------- */\r
+\r
+static W32WidgetClass textfield_widget_class = {\r
+    .eventproc = ui_textfield_eventproc,\r
+    .enable = w32_widget_default_enable,\r
+    .show = w32_widget_default_show,\r
+    .get_preferred_size = ui_textfield_get_preferred_size,\r
+    .destroy  = w32_widget_default_destroy\r
+};\r
+\r
+UIWIDGET ui_textfield_create(UiObject *obj, UiTextFieldArgs *args) {\r
+    HINSTANCE hInstance = GetModuleHandle(NULL);\r
+    UiContainerPrivate *container = ui_obj_container(obj);\r
+    HWND parent = ui_container_get_parent(container);\r
+    UiLayout layout = UI_ARGS2LAYOUT(args);\r
+\r
+    int width = args->width >= 0 ? args->width : 100;\r
+\r
+    HWND hwnd = CreateWindowEx(\r
+            WS_EX_CLIENTEDGE,\r
+            "EDIT",\r
+            "",\r
+            WS_VISIBLE | WS_CHILD | ES_LEFT | ES_AUTOHSCROLL,\r
+            0, 0, width, 25,\r
+            parent,\r
+            (HMENU)0,\r
+            hInstance,\r
+            NULL);\r
+    ui_win32_set_ui_font(hwnd);\r
+\r
+    W32Widget *widget = w32_widget_create(&textfield_widget_class, hwnd, sizeof(UiTextField));\r
+    ui_container_add(container, widget, &layout);\r
+\r
+    UiTextField *textfield = (UiTextField*)widget;\r
+    textfield->width = width;\r
+    textfield->widget.var = uic_widget_var(obj->ctx, obj->ctx, args->value, args->varname, UI_VAR_STRING);\r
+    textfield->widget.callback = args->onchange;\r
+    textfield->widget.callbackdata = args->onchangedata;\r
+\r
+    if (textfield->widget.var) {\r
+        UiString *s = textfield->widget.var->value;\r
+\r
+        if (s->value.ptr) {\r
+            // TODO: set textfield string\r
+        }\r
+        s->obj = widget;\r
+        s->get = ui_textfield_get;\r
+        s->set = ui_textfield_set;\r
+    }\r
+\r
+    return widget;\r
+}\r
+\r
+W32Size ui_textfield_get_preferred_size(W32Widget *widget) {\r
+    UiTextField *textfield = (UiTextField *)widget;\r
+    return (W32Size){ .width = textfield->width, .height = 32};\r
+}\r
+\r
+int ui_textfield_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {\r
+    return 0;\r
+}\r
+\r
+char* ui_textfield_get(UiString *s) {\r
+    UiTextField *textfield = s->obj;\r
+\r
+    if (s->value.free) {\r
+        s->value.free(s->value.ptr);\r
+    }\r
+\r
+    int len = GetWindowTextLength(textfield->widget.widget.hwnd);\r
+    s->value.ptr = calloc(len+1, 1);\r
+    GetWindowText(textfield->widget.widget.hwnd, s->value.ptr, len+1);\r
+\r
+    return s->value.ptr;\r
+}\r
+\r
+void ui_textfield_set(UiString *s, const char *value) {\r
+    UiTextField *textfield = s->obj;\r
+    if (s->value.free) {\r
+        s->value.free(s->value.ptr);\r
+    }\r
+    s->value.ptr = NULL;\r
+    SetWindowText(textfield->widget.widget.hwnd, value);\r
+}\r
diff --git a/ui/win32/text.h b/ui/win32/text.h
new file mode 100644 (file)
index 0000000..7eed5db
--- /dev/null
@@ -0,0 +1,70 @@
+/*\r
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.\r
+ *\r
+ * Copyright 2025 Olaf Wintermann. All rights reserved.\r
+ *\r
+ * Redistribution and use in source and binary forms, with or without\r
+ * modification, are permitted provided that the following conditions are met:\r
+ *\r
+ *   1. Redistributions of source code must retain the above copyright\r
+ *      notice, this list of conditions and the following disclaimer.\r
+ *\r
+ *   2. Redistributions in binary form must reproduce the above copyright\r
+ *      notice, this list of conditions and the following disclaimer in the\r
+ *      documentation and/or other materials provided with the distribution.\r
+ *\r
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"\r
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\r
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\r
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\r
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\r
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\r
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\r
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\r
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\r
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\r
+ * POSSIBILITY OF SUCH DAMAGE.\r
+ */\r
+\r
+#ifndef TEXT_H\r
+#define TEXT_H\r
+\r
+#include "../ui/text.h"\r
+#include "container.h"\r
+#include "toolkit.h"\r
+\r
+typedef struct UiTextArea {\r
+    UiWidget widget;\r
+    int width;\r
+    int height;\r
+} UiTextArea;\r
+\r
+typedef struct UiTextField {\r
+    UiWidget widget;\r
+    int width;\r
+} UiTextField;\r
+\r
+W32Size ui_textarea_get_preferred_size(W32Widget *widget);\r
+int ui_textarea_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);\r
+\r
+void  ui_textarea_save(UiText *text);\r
+void  ui_textarea_destroy(UiText *text);\r
+void  ui_textarea_restore(UiText *text);\r
+void  ui_textarea_set(UiText *text, const char *str);\r
+char* ui_textarea_get(UiText *text);\r
+char* ui_textarea_getsubstr(UiText *text, int begin, int end);\r
+void  ui_textarea_insert(UiText *text, int pos, char *str);\r
+void  ui_textarea_setposition(UiText *text, int pos);\r
+int   ui_textarea_position(UiText *text);\r
+void  ui_textarea_setselection(UiText *text, int begin, int end);\r
+void  ui_textarea_selection(UiText *text, int *begin, int *end);\r
+int   ui_textarea_length(UiText *text);\r
+void  ui_textarea_remove(UiText *text, int begin, int end);\r
+\r
+W32Size ui_textfield_get_preferred_size(W32Widget *widget);\r
+int ui_textfield_eventproc(W32Widget *widget, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);\r
+\r
+char* ui_textfield_get(UiString *s);\r
+void ui_textfield_set(UiString *s, const char *value);\r
+\r
+#endif /* TEXT_H */
\ No newline at end of file