/*
 * Copyright (C) 2026 Linux Studio Plugins Project <https://lsp-plug.in/>
 *           (C) 2026 Vladimir Sadovnikov <sadko4u@gmail.com>
 *
 * This file is part of lsp-plugin-fw
 * Created on: 13 апр. 2021 г.
 *
 * lsp-plugin-fw is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * lsp-plugin-fw is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with lsp-plugin-fw. If not, see <https://www.gnu.org/licenses/>.
 */

#include <lsp-plug.in/common/debug.h>
#include <lsp-plug.in/plug-fw/ctl.h>
#include <lsp-plug.in/stdlib/string.h>
#include <lsp-plug.in/plug-fw/core/config.h>
#include <lsp-plug.in/plug-fw/core/presets.h>
#include <lsp-plug.in/plug-fw/meta/func.h>
#include <lsp-plug.in/plug-fw/meta/ports.h>
#include <lsp-plug.in/runtime/system.h>
#include <lsp-plug.in/tk/tk.h>

#include <lsp-plug.in/plug-fw/ui.h>
#include <private/ui/xml/RootNode.h>
#include <private/ui/xml/Handler.h>
#include <private/ctl/PluginWindowTemplate.h>

namespace lsp
{
    namespace ctl
    {
        //-----------------------------------------------------------------
        // Plugin window
        const ctl_class_t PluginWindow::metadata = { "PluginWindow", &Window::metadata };

        const tk::tether_t PluginWindow::top_tether[] =
        {
            { tk::TF_BOTTOM | tk::TF_LEFT,      1.0f,  1.0f },
            { tk::TF_TOP | tk::TF_LEFT,         1.0f, -1.0f },
        };

        const tk::tether_t PluginWindow::bottom_tether[] =
        {
            { tk::TF_TOP | tk::TF_LEFT,         1.0f, -1.0f },
            { tk::TF_BOTTOM | tk::TF_LEFT,      1.0f,  1.0f },
        };

        PluginWindow::PluginWindow(ui::IWrapper *src, tk::Window *widget):
            Window(src, widget),
            sUIScaling(src),
            sFontScaling(src),
            sDocumentation(src)
        {
            pClass                      = &metadata;

            bResizable                  = false;

            pUserPaths                  = NULL;
            pPresetsWindow              = NULL;
            pAboutWindow                = NULL;

            wContent                    = NULL;
            wGreeting                   = NULL;
            wUserPaths                  = NULL;
            wMenu                       = NULL;
            wPresets                    = NULL;
            wRelPaths                   = NULL;
            wInvertVScroll              = NULL;
            wInvertGraphDotVScroll      = NULL;

            for (size_t i=0; i<2; ++i)
                wPresetSwitch[i]            = NULL;
            wPresetCopy                 = NULL;

            pPVersion                   = NULL;
            pPBypass                    = NULL;
            pR3DBackend                 = NULL;
            pAudioBackend               = NULL;
            pLanguage                   = NULL;
            pVisualSchema               = NULL;
            pInvertVScroll              = NULL;
            pInvertGraphDotVScroll      = NULL;

            init_enum_menu(&sFilterPointThickness);

            sWndScale.nMFlags           = 0;
            sWndScale.sSize.nLeft       = 0;
            sWndScale.sSize.nTop        = 0;
            sWndScale.sSize.nWidth      = 0;
            sWndScale.sSize.nHeight     = 0;
            sWndScale.bActive           = false;
            sWndScale.nMouseX           = 0;
            sWndScale.nMouseY           = 0;
        }

        PluginWindow::~PluginWindow()
        {
            do_destroy();
        }

        void PluginWindow::destroy()
        {
            do_destroy();
            Window::destroy();
        }

        template <class T>
        static void drop_list(lltl::parray<T> & list)
        {
            for (lltl::iterator<T> it=list.values(); it; ++it)
            {
                T * const s = it.get();
                if (s != NULL)
                    delete s;
            }
            list.flush();
        }

        void PluginWindow::do_destroy()
        {
            sUIScaling.destroy();
            sFontScaling.destroy();
            sDocumentation.destroy();

            // Cancel greeting timer
            wGreetingTimer.cancel();

            // Drop different bindings
            drop_list(vAudioSel);
            drop_list(vBackendSel);
            drop_list(vLangSel);
            drop_list(vSchemaSel);
            drop_list(vPresetSel);

            pUserPaths      = NULL;

            pPresetsWindow  = NULL;     // Destroyed by wrapper
            pAboutWindow    = NULL;     // Destroyed by wrapper

            wContent        = NULL;
            wGreeting       = NULL;
            wUserPaths      = NULL;
            wMenu           = NULL;
            wPresets        = NULL;
        }

        void PluginWindow::init_enum_menu(enum_menu_t *menu)
        {
            menu->pWnd      = this;
            menu->pMenu     = NULL;
            menu->pPort     = NULL;
        }

        void PluginWindow::bind_trigger(const char *uid, tk::slot_t ev, tk::event_handler_t handler)
        {
            tk::Widget *w = widgets()->find(uid);
            if (w == NULL)
                return;
            w->slots()->bind(ev, handler, this);
        }

        status_t PluginWindow::slot_window_close(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *_this = static_cast<PluginWindow *>(ptr);
            if (_this->pWrapper != NULL)
                _this->pWrapper->quit_main_loop();
            return STATUS_OK;
        }

        status_t PluginWindow::slot_window_show(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow * const self = static_cast<PluginWindow *>(ptr);
            if (self == NULL)
                return STATUS_OK;

            self->locate_window();
            self->set_greeting_timer();
            return STATUS_OK;
        }

        void PluginWindow::set(ui::UIContext *ctx, const char *name, const char *value)
        {
            set_value(&bResizable, "resizable", name, value);
            Window::set(ctx, name, value);
        }

        void PluginWindow::set_preset_button_text(const char *text)
        {
            tk::Button *presetButton = tk::widget_cast<tk::Button>(widgets()->find("trg_presets_menu"));

            if (presetButton != NULL)
            {
                presetButton->text()->set_raw(text);
            }
        }

        status_t PluginWindow::init()
        {
            Window::init();

            // Get window handle
            tk::Window *wnd = tk::widget_cast<tk::Window>(wWidget);
            if (wnd == NULL)
                return STATUS_BAD_STATE;

            const meta::plugin_t *meta   = pWrapper->ui()->metadata();

            // Initialize window
            wnd->set_class(meta->uid, "lsp-plugins");
            wnd->role()->set("audio-plugin");
            wnd->title()->set_raw(meta->name);

            if (!wnd->nested())
                wnd->actions()->deny(ws::WA_RESIZE);

            // Bind event handlers
            wnd->slots()->bind(tk::SLOT_CLOSE, slot_window_close, this);
            wnd->slots()->bind(tk::SLOT_SHOW, slot_window_show, this);
            wnd->slots()->bind(tk::SLOT_RESIZE, slot_window_resize, this);

            return STATUS_OK;
        }

        status_t PluginWindow::post_init()
        {
            // Bind ports
            BIND_PORT(pWrapper, pPVersion, VERSION_PORT);
            BIND_PORT(pWrapper, pPBypass, meta::PORT_NAME_BYPASS);
            BIND_PORT(pWrapper, pR3DBackend, R3D_BACKEND_PORT);
            BIND_PORT(pWrapper, pAudioBackend, AUDIO_BACKEND_PORT);
            BIND_PORT(pWrapper, pLanguage, LANGUAGE_PORT);
            BIND_PORT(pWrapper, pVisualSchema, UI_VISUAL_SCHEMA_PORT);
            BIND_PORT(pWrapper, pInvertVScroll, UI_INVERT_VSCROLL_PORT);
            BIND_PORT(pWrapper, pInvertGraphDotVScroll, UI_GRAPH_DOT_INVERT_VSCROLL_PORT);
            BIND_PORT(pWrapper, sFilterPointThickness.pPort, UI_FILTER_POINT_THICK_PORT);

            // Initialize menu and presets
            LSP_STATUS_ASSERT(create_main_menu());
            LSP_STATUS_ASSERT(create_presets_window());

            // Get presets widgets
            wPresetSwitch[0]        = sWidgets.find("switch_preset_a");
            wPresetSwitch[1]        = sWidgets.find("switch_preset_b");
            wPresetCopy             = sWidgets.find("switch_preset_copy");

            bind_slot("switch_preset_a", tk::SLOT_SUBMIT, slot_switch_ab_preset);
            bind_slot("switch_preset_b", tk::SLOT_SUBMIT, slot_switch_ab_preset);
            bind_slot("switch_preset_copy", tk::SLOT_SUBMIT, slot_copy_ab_preset);
            sync_ab_state();

            // Bind as preset listener to the wrapper
            pWrapper->add_preset_listener(this);
            bind_slot("trg_prev_preset", tk::SLOT_SUBMIT, slot_select_next_preset);
            bind_slot("trg_next_preset", tk::SLOT_SUBMIT, slot_select_next_preset);

            // Bind different event triggers
            // Header menu
            bind_trigger("trg_main_menu", tk::SLOT_SUBMIT, slot_show_main_menu);
            bind_trigger("trg_presets_menu", tk::SLOT_SUBMIT, slot_show_presets_window);
            bind_trigger("trg_presets_menu", tk::SLOT_MOUSE_SCROLL, slot_scroll_over_presets);
            bind_trigger("trg_export_settings", tk::SLOT_SUBMIT, slot_export_settings_to_file);
            bind_trigger("trg_import_settings", tk::SLOT_SUBMIT, slot_import_settings_from_file);
            bind_trigger("trg_reset_settings", tk::SLOT_SUBMIT, slot_reset_settings);
            bind_trigger("trg_about", tk::SLOT_SUBMIT, slot_show_about);

            // Footer
            bind_trigger("trg_plugin_manual", tk::SLOT_SUBMIT, slot_show_plugin_manual);

            // Window scaling
            bind_trigger("trg_window_scale", tk::SLOT_MOUSE_DOWN, slot_scale_mouse_down);
            bind_trigger("trg_window_scale", tk::SLOT_MOUSE_UP, slot_scale_mouse_up);
            bind_trigger("trg_window_scale", tk::SLOT_MOUSE_MOVE, slot_scale_mouse_move);

            // UI scaling
            sUIScaling.bind_ui_scaling_show("trg_ui_scaling", tk::SLOT_SUBMIT);
            sUIScaling.bind_ui_scaling_zoom_in("trg_ui_zoom_in", tk::SLOT_SUBMIT);
            sUIScaling.bind_ui_scaling_zoom_out("trg_ui_zoom_out", tk::SLOT_SUBMIT);

            sUIScaling.bind_bundle_scaling_show("trg_bundle_scaling", tk::SLOT_SUBMIT);
            sUIScaling.bind_bundle_scaling_zoom_in("trg_bundle_zoom_in", tk::SLOT_SUBMIT);
            sUIScaling.bind_bundle_scaling_zoom_out("trg_bundle_zoom_out", tk::SLOT_SUBMIT);

            // Font scaling
            sFontScaling.bind_show("trg_font_scaling", tk::SLOT_SUBMIT);
            sFontScaling.bind_zoom_in("trg_font_zoom_in", tk::SLOT_SUBMIT);
            sFontScaling.bind_zoom_out("trg_font_zoom_out", tk::SLOT_SUBMIT);

            // Update other parameters
            sync_language_selection();
            sync_visual_schemas();

            // Notify other parameters
            if (pInvertVScroll != NULL)
                pInvertVScroll->notify_all(ui::PORT_NONE);
            if (pInvertGraphDotVScroll != NULL)
                pInvertGraphDotVScroll->notify_all(ui::PORT_NONE);
            if (sFilterPointThickness.pPort != NULL)
                sFilterPointThickness.pPort->notify_all(ui::PORT_NONE);

            sUIScaling.sync_parameters();
            sFontScaling.sync_parameters();
            notify_ui_behaviour_flags(ui::PORT_NONE);
            sync_language_selection();
            if (pInvertVScroll != NULL)
                sync_invert_vscroll(pInvertVScroll);
            if (pInvertGraphDotVScroll != NULL)
                sync_invert_vscroll(pInvertVScroll);

            if (sFilterPointThickness.pPort != NULL)
                sync_enum_menu(&sFilterPointThickness, sFilterPointThickness.pPort);

            return STATUS_OK;
        }

        i18n::IDictionary  *PluginWindow::get_default_dict(tk::Widget *src)
        {
            i18n::IDictionary *dict = src->display()->dictionary();
            if (dict == NULL)
                return dict;

            if (dict->lookup("default", &dict) != STATUS_OK)
                dict = NULL;

            return dict;
        }

        status_t PluginWindow::create_main_menu()
        {
            tk::Window *wnd             = tk::widget_cast<tk::Window>(wWidget);
            tk::Display *dpy            = wnd->display();
            const meta::plugin_t *meta  = pWrapper->ui()->metadata();

            // Initialize menu
            wMenu = new tk::Menu(dpy);
            widgets()->add(WUID_MAIN_MENU, wMenu);
            wMenu->init();

            // TODO: Move to a separate method
            wPresets = new tk::Menu(dpy);
            widgets()->add(WUID_PRESETS_MENU, wPresets);
            wPresets->init();

            // Initialize menu items
            {
                // Add 'Plugin manual' menu item
                tk::MenuItem *itm       = new tk::MenuItem(dpy);
                widgets()->add(itm);
                itm->init();
                itm->text()->set("actions.plugin_manual");
                itm->slots()->bind(tk::SLOT_SUBMIT, slot_show_plugin_manual, this);
                wMenu->add(itm);

                // Add 'UI manual' menu item
                itm                     = new tk::MenuItem(dpy);
                widgets()->add(itm);
                itm->init();
                itm->text()->set("actions.ui_manual");
                itm->slots()->bind(tk::SLOT_SUBMIT, slot_show_ui_manual, this);
                wMenu->add(itm);

                // init_presets(wMenu, true);
                init_presets(wPresets, false);

                // Add separator
                itm     = new tk::MenuItem(dpy);
                widgets()->add(itm);
                itm->init();
                itm->type()->set_separator();
                wMenu->add(itm);

                // Create 'Export' submenu and bind to parent
                tk::Menu *submenu = new tk::Menu(dpy);
                widgets()->add(WUID_EXPORT_MENU, submenu);
                submenu->init();

                itm = new tk::MenuItem(dpy);
                widgets()->add(itm);
                itm->init();
                itm->text()->set("actions.export");
                itm->menu()->set(submenu);
                wMenu->add(itm);

                // Create 'Export' menu items
                {
                    tk::MenuItem *child = new tk::MenuItem(dpy);
                    widgets()->add(child);
                    child->init();
                    child->text()->set("actions.export_settings_to_file");
                    child->slots()->bind(tk::SLOT_SUBMIT, slot_export_settings_to_file, this);
                    submenu->add(child);

                    child = new tk::MenuItem(dpy);
                    widgets()->add(child);
                    child->init();
                    child->text()->set("actions.export_settings_to_clipboard");
                    child->slots()->bind(tk::SLOT_SUBMIT, slot_export_settings_to_clipboard, this);
                    submenu->add(child);
                }

                // Create 'Import' menu and bind to parent
                submenu = new tk::Menu(dpy);
                widgets()->add(WUID_IMPORT_MENU, submenu);
                submenu->init();

                itm = new tk::MenuItem(dpy);
                widgets()->add(itm);
                itm->init();
                itm->text()->set("actions.import");
                itm->menu()->set(submenu);
                wMenu->add(itm);

                // Create import menu items
                {
                    tk::MenuItem *child = new tk::MenuItem(dpy);
                    widgets()->add(child);
                    child->init();
                    child->text()->set("actions.import_settings_from_file");
                    child->slots()->bind(tk::SLOT_SUBMIT, slot_import_settings_from_file, this);
                    submenu->add(child);

                    child = new tk::MenuItem(dpy);
                    widgets()->add(child);
                    child->init();
                    child->text()->set("actions.import_settings_from_clipboard");
                    child->slots()->bind(tk::SLOT_SUBMIT, slot_import_settings_from_clipboard, this);
                    submenu->add(child);
                }

                itm     = new tk::MenuItem(dpy);
                widgets()->add(itm);
                itm->init();
                itm->text()->set("actions.reset_settings");
                itm->slots()->bind(tk::SLOT_SUBMIT, slot_reset_settings, this);
                wMenu->add(itm);

                itm     = new tk::MenuItem(dpy);
                widgets()->add(itm);
                itm->init();
                itm->text()->set("actions.user_paths");
                itm->slots()->bind(tk::SLOT_SUBMIT, slot_show_user_paths_dialog, this);
                wMenu->add(itm);

                // Create 'Dump state' menu item if supported
                if (meta->extensions & meta::E_DUMP_STATE)
                {
                    itm     = new tk::MenuItem(dpy);
                    widgets()->add(itm);
                    itm->init();
                    itm->text()->set("actions.debug_dump");
                    itm->slots()->bind(tk::SLOT_SUBMIT, slot_debug_dump, this);
                    wMenu->add(itm);
                }

                // Add separator
                itm     = new tk::MenuItem(dpy);
                widgets()->add(itm);
                itm->init();
                itm->type()->set_separator();
                wMenu->add(itm);

                // Create UI scaling menu
                init_scaling_support(wMenu);

                // Create font scaling menu
                init_font_scaling_support(wMenu);

                // Create schema selection support menu
                init_visual_schema_support(wMenu);
                sync_visual_schemas();

                // Create language selection menu
                init_i18n_support(wMenu);

                // Add support of 3D rendering backend switch
                if (meta->extensions & meta::E_3D_BACKEND)
                    init_r3d_support(wMenu);

                init_audio_backends(wMenu);

                // Create UI behaviour menu
                init_ui_behaviour(wMenu);

                // Init documentation
                LSP_STATUS_ASSERT(sDocumentation.init());
            }

            return STATUS_OK;
        }

        tk::MenuItem *PluginWindow::create_menu_item(tk::Menu *dst)
        {
            tk::MenuItem *item = new tk::MenuItem(dst->display());
            if (item == NULL)
                return NULL;
            lsp_finally {
                if (item != NULL)
                {
                    item->destroy();
                    delete item;
                }
            };

            status_t res = item->init();
            if (res != STATUS_OK)
                return NULL;

            if (widgets()->add(item) != STATUS_OK)
                return NULL;

            dst->add(item);

            return release_ptr(item);
        }

        tk::Menu *PluginWindow::create_menu()
        {
            status_t res;
            tk::Menu *menu = new tk::Menu(wWidget->display());
            if (menu == NULL)
                return NULL;
            lsp_finally {
                if (menu != NULL)
                {
                    menu->destroy();
                    delete menu;
                }
            };

            if ((res = menu->init()) != STATUS_OK)
                return NULL;
            if (widgets()->add(menu) != STATUS_OK)
                return NULL;

            return release_ptr(menu);
        }

        tk::Menu *PluginWindow::create_enum_menu(enum_menu_t *em, tk::Menu *parent, const char *label)
        {
            // Obtain port metadata
            lltl::parray<tk::MenuItem> list;
            const meta::port_t *meta = (em->pPort != NULL) ? em->pPort->metadata() : NULL;
            if ((meta == NULL) || (!meta::is_enum_unit(meta->unit)))
                return NULL;
            const ssize_t selected   = (em->pPort != NULL) ? em->pPort->value() : -1;

            // Create root menu
            tk::Menu *menu = create_menu();
            if (menu == NULL)
                return menu;

            // Add items from the enumeration
            LSPString lc_key;
            ssize_t index = 0;
            for (const meta::port_item_t *item = meta->items; item->text != NULL; ++item, ++index)
            {
                // Create submenu item
                tk::MenuItem *mi            = create_menu_item(menu);
                if (item == NULL)
                    return NULL;

                // Set the text value
                mi->type()->set_radio();
                if (item->lc_key != NULL)
                {
                    if (!lc_key.set_ascii("lists."))
                        return NULL;
                    if (!lc_key.append_ascii(item->lc_key))
                        return NULL;
                    mi->text()->set(&lc_key);
                }
                else
                    mi->text()->set_raw(item->text);
                mi->checked()->set(index == selected);

                // Bind slots
                mi->slots()->bind(tk::SLOT_SUBMIT, slot_submit_enum_menu_item, em);

                // Add to list
                if (!list.add(mi))
                    return NULL;
            }

            // Return result
            list.swap(em->vItems);

            // Add to parent
            if (parent != NULL)
            {
                tk::MenuItem *item          = create_menu_item(parent);
                if (item != NULL)
                {
                    item->text()->set(label);
                    item->menu()->set(menu);
                }
            }

            return menu;
        }

        status_t PluginWindow::init_i18n_support(tk::Menu *menu)
        {
            if (menu == NULL)
                return STATUS_OK;

            tk::Display *dpy            = menu->display();
            i18n::IDictionary *dict     = get_default_dict(menu);
            if (dict == NULL)
                return STATUS_OK;

            // Perform lookup before loading list of languages
            status_t res = dict->lookup("lang.target", &dict);
            if (res != STATUS_OK)
                return res;

            // Create submenu item
            tk::MenuItem *root          = create_menu_item(menu);
            if (root == NULL)
                return STATUS_NO_MEM;
            root->text()->set("actions.select_language");

            // Create submenu
            menu                        = create_menu();
            if (menu == NULL)
                return STATUS_NO_MEM;
            root->menu()->set(menu);

            // Iterate all children and add language keys
            LSPString key, value;
            lang_sel_t *lang;
            size_t added = 0;
            for (size_t i=0, n=dict->size(); i<n; ++i)
            {
                // Fetch placeholder for language selection key
                if ((res = dict->get_value(i, &key, &value)) != STATUS_OK)
                {
                    // Skip nested dictionaries
                    if (res == STATUS_BAD_TYPE)
                        continue;
                    return res;
                }
                lsp_trace("i18n: key=%s, value=%s", key.get_utf8(), value.get_utf8());

                if ((lang = new lang_sel_t()) == NULL)
                    return STATUS_NO_MEM;
                if (!lang->lang.set(&key))
                {
                    delete lang;
                    return STATUS_NO_MEM;
                }
                if (!vLangSel.add(lang))
                {
                    delete lang;
                    return STATUS_NO_MEM;
                }
                lang->ctl   = this;
                lang->item  = NULL;

                // Create menu item
                tk::MenuItem *item = create_menu_item(menu);
                if (item == NULL)
                    return STATUS_NO_MEM;
                item->text()->set_raw(&value);
                item->type()->set_radio();
                lang->item      = item;

                // Create closure and bind
                item->slots()->bind(tk::SLOT_SUBMIT, slot_select_language, lang);

                ++added;
            }

            // Set menu item visible only if there are available languages
            root->visibility()->set(added > 0);
            if (pLanguage != NULL)
            {
                const char *lang = pLanguage->buffer<char>();
                if ((lang != NULL) && (strlen(lang) > 0))
                {
                    if ((dpy->schema()->set_lanugage(lang)) == STATUS_OK)
                    {
                        lsp_trace("System language set to: %s", lang);
                        pLanguage->notify_all(ui::PORT_NONE);
                    }
                }
            }

            return STATUS_OK;
        }

        status_t PluginWindow::init_scaling_support(tk::Menu *menu)
        {
            // Init UI scaling
            LSP_STATUS_ASSERT(sUIScaling.init(true));

            // Create submenu item for UI scaling
            tk::MenuItem *item;
            if ((item = create_menu_item(menu)) == NULL)
                return STATUS_NO_MEM;
            item->text()->set("actions.ui_scaling.select");
            item->menu()->set(sUIScaling.ui_scaling_menu());

            // Create submenu item for bundle scaling
            if ((item = create_menu_item(menu)) == NULL)
                return STATUS_NO_MEM;
            item->text()->set("actions.bundle_scaling.select");
            item->menu()->set(sUIScaling.bundle_scaling_menu());

            return STATUS_OK;
        }

        status_t PluginWindow::init_font_scaling_support(tk::Menu *menu)
        {
            // Init font scaling
            LSP_STATUS_ASSERT(sFontScaling.init());

            // Create submenu item
            tk::MenuItem *item;
            if ((item = create_menu_item(menu)) == NULL)
                return STATUS_NO_MEM;
            item->text()->set("actions.font_scaling.select");
            item->menu()->set(sFontScaling.menu());

            return STATUS_OK;
        }

        status_t PluginWindow::init_visual_schema_support(tk::Menu *menu)
        {
            status_t res;
            resource::ILoader *loader = pWrapper->resources();
            if ((loader == NULL) || (pVisualSchema == NULL))
                return STATUS_OK;

            // Create submenu item
            tk::MenuItem *item          = create_menu_item(menu);
            if (item == NULL)
                return STATUS_NO_MEM;
            item->text()->set("actions.visual_schema.select");

            // Create submenu
            menu                        = create_menu();
            if (menu == NULL)
                return STATUS_NO_MEM;
            item->menu()->set(menu);
            tk::MenuItem *root  = item;

            // For that we need to scan all available schemas in the resource directory
            resource::resource_t *list = NULL;
            ssize_t count = loader->enumerate(LSP_BUILTIN_PREFIX "schema", &list);
            if ((count <= 0) || (list == NULL))
            {
                if (list != NULL)
                    free(list);
                return STATUS_OK;
            }
            lsp_finally { free(list); };

            // Generate the 'Visual Schema' menu items
            schema_sel_t *sel;

            for (ssize_t i=0; i<count; ++i)
            {
                tk::StyleSheet sheet;
                LSPString path;

                if (list[i].type != resource::RES_FILE)
                    continue;

                if (!path.fmt_ascii(LSP_BUILTIN_PREFIX SCHEMA_PATH "/%s", list[i].name))
                    return STATUS_NO_MEM;

                // Try to load schema
                if ((res = pWrapper->load_stylesheet(&sheet, &path)) != STATUS_OK)
                {
                    if (res == STATUS_NO_MEM)
                        return res;
                    continue;
                }

                // Append menu item
                if ((item = create_menu_item(menu)) == NULL)
                    return STATUS_NO_MEM;
                item->type()->set_radio();
                item->text()->set_key(sheet.title());
                item->text()->params()->set_string("file", &path);

                // Append the shema to the menu
                if ((sel = new schema_sel_t()) == NULL)
                    return STATUS_NO_MEM;

                sel->ctl        = this;
                sel->item       = item;
                sel->location.swap(&path);

                if (!vSchemaSel.add(sel))
                {
                    delete sel;
                    return STATUS_NO_MEM;
                }

                item->slots()->bind(tk::SLOT_SUBMIT, slot_visual_schema_select, sel);
            }

            root->visibility()->set(vSchemaSel.size() > 0);

            return STATUS_OK;
        }

        status_t PluginWindow::init_ui_behaviour(tk::Menu *menu)
        {
            // Create submenu item
            tk::MenuItem *item          = create_menu_item(menu);
            if (item == NULL)
                return STATUS_NO_MEM;
            item->text()->set("actions.ui_behavior");

            // Create submenu
            menu                        = create_menu();
            if (menu == NULL)
                return STATUS_NO_MEM;
            item->menu()->set(menu);

            // Thickness of the enum menu item
            wFilterPointThickness = create_enum_menu(&sFilterPointThickness, menu, "actions.ui_behavior.filter_point_thickness");

            // Create menu items
            LSP_STATUS_ASSERT(
                add_ui_flag( menu,
                    UI_ENABLE_KNOB_SCALE_ACTIONS_PORT,
                    "actions.ui_behavior.ediable_knob_scale"));

            LSP_STATUS_ASSERT(
                add_ui_flag( menu,
                    UI_OVERRIDE_HYDROGEN_KITS_PORT,
                    "actions.ui_behavior.override_hydrogen_kits"));

            // Vertical scroll inversion
            if ((wInvertVScroll = create_menu_item(menu)) != NULL)
            {
                wInvertVScroll->type()->set_check();
                wInvertVScroll->text()->set("actions.ui_behavior.vscroll.invert_global");
                wInvertVScroll->slots()->bind(tk::SLOT_SUBMIT, slot_invert_vscroll_changed, this);
            }

            if ((wInvertGraphDotVScroll = create_menu_item(menu)) != NULL)
            {
                wInvertGraphDotVScroll->type()->set_check();
                wInvertGraphDotVScroll->text()->set("actions.ui_behavior.vscroll.invert_graph_dot");
                wInvertGraphDotVScroll->slots()->bind(tk::SLOT_SUBMIT, slot_invert_graph_dot_vscroll_changed, this);
            }

            LSP_STATUS_ASSERT(
                add_ui_flag(menu,
                    UI_ZOOMABLE_SPECTRUM_GRAPH_PORT,
                    "actions.ui_behavior.enable_zoomable_spectrum"));
            
            LSP_STATUS_ASSERT(
                add_ui_flag(menu,
                    UI_FILELIST_NAVIGATION_AUTOLOAD_PORT,
                    "actions.ui_behavior.file_list_navigation_autoload"));
            LSP_STATUS_ASSERT(
                add_ui_flag(menu,
                    UI_FILELIST_NAVIGATION_AUTOPLAY_PORT,
                    "actions.ui_behavior.file_list_navigation_autoplay"));

            LSP_STATUS_ASSERT(
                add_ui_flag(menu,
                    UI_IR_LIST_NAVIGATION_AUTOLOAD_PORT,
                    "actions.ui_behavior.ir_list_navigation_autoload"));
            LSP_STATUS_ASSERT(
                add_ui_flag(menu,
                    UI_IR_LIST_NAVIGATION_AUTOPLAY_PORT,
                    "actions.ui_behavior.ir_list_navigation_autoplay"));

            LSP_STATUS_ASSERT(
                add_ui_flag(menu,
                    UI_TAKE_INST_NAME_FROM_FILE_PORT,
                    "actions.ui_behavior.take_instrument_name_from_file"));

            LSP_STATUS_ASSERT(
                add_ui_flag(menu,
                    UI_SHOW_PIANO_LAYOUT_ON_GRAPH_PORT,
                    "actions.ui_behavior.show_piano_layout_on_graph"));

            LSP_STATUS_ASSERT(
                add_ui_flag(menu,
                    UI_REVEAL_SAMPLE_ON_LISTEN_PORT,
                    "actions.ui_behavior.reveal_sample_on_listen"));

            return STATUS_OK;
        }

        status_t PluginWindow::add_ui_flag(tk::Menu *menu, const char *port_id, const char *key)
        {
            ui::IPort *port = pWrapper->port(port_id);
            if (port == NULL)
                return STATUS_OK;
            port->bind(this);

            tk::MenuItem *item = create_menu_item(menu);
            if (item == NULL)
                return STATUS_NO_MEM;

            item->type()->set_check();
            item->text()->set(key);
            item->slots()->bind(tk::SLOT_SUBMIT, slot_ui_behaviour_flag_changed, this);

            ui_flag_t *flag = vBoolFlags.add();
            if (flag == NULL)
                return STATUS_NO_MEM;

            flag->pPort     = port;
            flag->wItem     = item;

            return STATUS_OK;
        }

        ssize_t PluginWindow::compare_presets(const resource::resource_t *a, const resource::resource_t *b)
        {
            return strcmp(a->name, b->name);
        }

        status_t PluginWindow::init_presets(tk::Menu *menu, bool add_submenu)
        {
            status_t res;
            if (menu == NULL)
                return STATUS_OK;

            // Enumerate presets
            lltl::darray<resource::resource_t> presets;
            const meta::plugin_t *metadata = pWrapper->ui()->metadata();
            if (metadata == NULL)
                return STATUS_OK;

            tk::MenuItem *item;

            if (add_submenu)
            {
                // Create submenu item
                item          = create_menu_item(menu);
                if (item == NULL)
                    return STATUS_NO_MEM;
                item->text()->set("actions.load_preset");

                // Create submenu
                menu                        = create_menu();
                if (menu == NULL)
                    return STATUS_NO_MEM;
                item->menu()->set(menu);
            }

            preset_sel_t *sel;
            io::Path path;
            LSPString tmp;

            if ((item = create_menu_item(menu)) == NULL) return STATUS_NO_MEM;
            item->text()->set("actions.reset_settings");
            item->slots()->bind(tk::SLOT_SUBMIT, slot_reset_settings, this);

            if ((item = create_menu_item(menu)) == NULL) return STATUS_NO_MEM;
            item->text()->set_raw("actions.manage_presets");
            item->slots()->bind(tk::SLOT_SUBMIT, slot_show_presets_window, this);

            if ((item = create_menu_item(menu)) == NULL) return STATUS_NO_MEM;
            item->type()->set_separator();

            if ((item = create_menu_item(menu)) == NULL) return STATUS_NO_MEM;
            item->text()->set("actions.import_settings_from_file");
            item->slots()->bind(tk::SLOT_SUBMIT, slot_import_settings_from_file, this);

            if ((item = create_menu_item(menu)) == NULL) return STATUS_NO_MEM;
            item->text()->set("actions.import_settings_from_clipboard");
            item->slots()->bind(tk::SLOT_SUBMIT, slot_import_settings_from_clipboard, this);

            // if ((item = create_menu_item(menu)) == NULL) return STATUS_NO_MEM;
            // item->text()->set("*Open presets folder*");

            if (core::scan_presets(&presets, pWrapper->resources(), metadata->ui_presets) != STATUS_OK)
                return STATUS_OK;
            if (presets.is_empty())
                return STATUS_OK;
            core::sort_presets(&presets, true);

            if ((item = create_menu_item(menu)) == NULL)
                return STATUS_NO_MEM;
            item->type()->set_separator();

            for (size_t i=0, n=presets.size(); i<n; ++i)
            {
                // Enumerate next backend information
                const resource::resource_t *preset = presets.uget(i);
                if ((res = path.set(preset->name)) != STATUS_OK)
                    return res;

                // Create menu item
                if ((item = create_menu_item(menu)) == NULL)
                    return STATUS_NO_MEM;

                // Get name of the preset/patch without an extension
                if ((res = path.get_last_noext(&tmp)) != STATUS_OK)
                    return res;
                item->text()->set_raw(&tmp);
                if ((res = path.get_ext(&tmp)) != STATUS_OK)
                    return res;

                // Create backend information
                if ((sel = new preset_sel_t()) == NULL)
                    return STATUS_NO_MEM;

                sel->ctl    = this;
                sel->item   = item;
                sel->patch  = tmp.equals_ascii("patch");
                sel->location.fmt_utf8(LSP_BUILTIN_PREFIX "presets/%s/%s", metadata->ui_presets, preset->name);

                if (!vPresetSel.add(sel))
                {
                    delete sel;
                    return STATUS_NO_MEM;
                }

                item->slots()->bind(tk::SLOT_SUBMIT, slot_select_preset, sel);
            }

            return STATUS_OK;
        }

        status_t PluginWindow::init_r3d_support(tk::Menu *menu)
        {
            if (menu == NULL)
                return STATUS_OK;

            ws::IDisplay *dpy           = menu->display()->display();
            if (dpy == NULL)
                return STATUS_OK;

            // Create submenu item
            tk::MenuItem *item          = create_menu_item(menu);
            if (item == NULL)
                return STATUS_NO_MEM;
            item->text()->set("actions.3d_rendering");

            // Get backend port
            const char *backend = (pR3DBackend != NULL) ? pR3DBackend->buffer<char>() : NULL;

            // Create submenu
            menu                        = create_menu();
            if (menu == NULL)
                return STATUS_NO_MEM;
            item->menu()->set(menu);

            backend_sel_t *sel;

            for (size_t id=0; ; ++id)
            {
                // Enumerate next backend information
                const ws::R3DBackendInfo *info = dpy->enum_backend(id);
                if (info == NULL)
                    break;

                // Create menu item
                if ((item = create_menu_item(menu)) == NULL)
                    return STATUS_NO_MEM;

                item->type()->set_radio();
                if (!info->lc_key.is_empty())
                {
                    LSPString tmp;
                    tmp.set_ascii("lists.rendering.");
                    tmp.append(&info->lc_key);
                    item->text()->set_key(&tmp);
                }
                else
                    item->text()->set_raw(&info->display);

                // Create backend information
                if ((sel = new backend_sel_t()) == NULL)
                    return STATUS_NO_MEM;

                sel->ctl    = this;
                sel->item   = item;
                sel->id     = id;
                item->slots()->bind(tk::SLOT_SUBMIT, slot_select_backend, sel);
                item->checked()->set((backend != NULL) && (info->uid.equals_ascii(backend)));

                if (!vBackendSel.add(sel))
                {
                    delete sel;
                    return STATUS_NO_MEM;
                }
            }

            // Select fallback backend if none
            if ((backend == NULL) && (vBackendSel.size() > 0))
            {
                sel = vBackendSel.uget(0);
                if (sel != NULL)
                    slot_select_backend(sel->item, sel, NULL);
            }

            return STATUS_OK;
        }

        status_t PluginWindow::add_audio_backend(tk::Menu *menu, const char *text, bool raw, const char *id)
        {
            LSPString stext, sid;
            if (!stext.set_utf8(text))
                return STATUS_NO_MEM;
            if (!sid.set_utf8(id))
                return STATUS_NO_MEM;

            return add_audio_backend(menu, stext, raw, sid);
        }

        status_t PluginWindow::add_audio_backend(tk::Menu *menu, const LSPString & text, bool raw, const LSPString & id)
        {
            // Create menu item
            tk::MenuItem * const item = create_menu_item(menu);
            if (item == NULL)
                return STATUS_NO_MEM;

            item->type()->set_radio();
            if (raw)
                item->text()->set_raw(&text);
            else
                item->text()->set_key(&text);

            // Create backend information
            audio_sel_t * sel = new audio_sel_t();
            if (sel == NULL)
                return STATUS_NO_MEM;
            lsp_finally {
                if (sel != NULL)
                    delete sel;
            };

            sel->ctl    = this;
            sel->item   = item;
            if (!sel->id.set(&id))
                return STATUS_NO_MEM;

            item->slots()->bind(tk::SLOT_SUBMIT, slot_select_audio, sel);
            if (!vAudioSel.add(sel))
                return STATUS_NO_MEM;

            sel         = NULL;
            return STATUS_OK;
        }

        status_t PluginWindow::init_audio_backends(tk::Menu *menu)
        {
            if (menu == NULL)
                return STATUS_OK;

            ws::IDisplay * const dpy    = menu->display()->display();
            if (dpy == NULL)
                return STATUS_OK;

            // Enumerate backends
            core::AudioBackendInfoList backends;
            status_t res                = pWrapper->enumerate_backends(backends);
            if (res != STATUS_OK)
                return res;
            lsp_finally { core::free_audio_backends(&backends); };

            // Create submenu item
            tk::MenuItem *item          = create_menu_item(menu);
            if (item == NULL)
                return STATUS_NO_MEM;
            item->text()->set("actions.audio_driver");

            // Create submenu
            menu                        = create_menu();
            if (menu == NULL)
                return STATUS_NO_MEM;
            item->menu()->set(menu);

            // Add 'default' menu item
            if ((res = add_audio_backend(menu, "lists.audio_backend.auto", false, "auto")) != STATUS_OK)
                return res;

            // Add separator
            tk::MenuItem * const sep    = new tk::MenuItem(menu->display());
            if (sep == NULL)
                return STATUS_NO_MEM;
            widgets()->add(sep);
            sep->init();
            sep->type()->set_separator();
            menu->add(sep);

            LSPString name;

            for (lltl::iterator<core::AudioBackendInfo> it = backends.values(); it; ++it)
            {
                // Enumerate next backend information
                const core::AudioBackendInfo * const info = it.get();
                if (info == NULL)
                    break;

                bool raw = false;
                if (!info->lc_key.is_empty())
                {
                    if (name.set_ascii("lists.audio_backend."))
                        name.append(&info->lc_key);
                }
                else if (!info->display.is_empty())
                {
                    name.set(&info->display);
                    raw         = true;
                }
                else
                {
                    if (name.set_ascii("lists.audio_backend."))
                        name.append(&info->uid);
                }

                // Add menu item
                if ((res = add_audio_backend(menu, name, raw, info->uid)) != STATUS_OK)
                    return res;
            }

            // Sync audio backend selection
            sync_audio_selection();

            return STATUS_OK;
        }

        void PluginWindow::sync_audio_selection()
        {
            tk::Display *dpy    = wWidget->display();
            if (dpy == NULL)
                return;

            if (pAudioBackend == NULL)
                return;

            const char *uid     = pAudioBackend->buffer<char>();
            if ((uid == NULL) || (strlen(uid) <= 0))
                uid                 = "auto";

            LSPString suid;
            if (!suid.set_utf8(uid))
                return;

            for (lltl::iterator<audio_sel_t> it = vAudioSel.values(); it; ++it)
            {
                audio_sel_t * const sel = it.get();
                if (sel == NULL)
                    continue;

                sel->item->checked()->set(sel->id.equals(&suid));
            }
        }

        bool PluginWindow::has_path_ports()
        {
            for (size_t i = 0, n = pWrapper->ports(); i < n; ++i)
            {
                ui::IPort *p = pWrapper->port(i);
                const meta::port_t *meta = (p != NULL) ? p->metadata() : NULL;
                if ((meta != NULL) && (meta->role == meta::R_PATH))
                    return true;
            }
            return false;
        }

        status_t PluginWindow::slot_select_backend(tk::Widget *sender, void *ptr, void *data)
        {
            backend_sel_t *sel = reinterpret_cast<backend_sel_t *>(ptr);
            if ((sender == NULL) || (sel == NULL) || (sel->ctl == NULL))
                return STATUS_BAD_ARGUMENTS;

            ws::IDisplay *dpy = sender->display()->display();
            if (dpy == NULL)
                return STATUS_BAD_STATE;

            const ws::R3DBackendInfo *info = dpy->enum_backend(sel->id);
            if (info == NULL)
                return STATUS_BAD_ARGUMENTS;

            // Mark backend as selected
            dpy->select_backend_id(sel->id);

            // Synchronize state of radio buttons
            for (size_t i=0, n=sel->ctl->vBackendSel.size(); i<n; ++i)
            {
                backend_sel_t *xsel     = sel->ctl->vBackendSel.uget(i);
                if (xsel->item == NULL)
                    continue;
                xsel->item->checked()->set(xsel->id == sel->id);
            }

            // Need to commit backend identifier to config file?
            const char *value = info->uid.get_ascii();
            if (value == NULL)
                return STATUS_NO_MEM;

            ui::IPort * const port = sel->ctl->pR3DBackend;
            if (port != NULL)
            {
                const char *backend = port->buffer<char>();
                if ((backend == NULL) || (strcmp(backend, value)))
                {
                    port->begin_edit();
                    lsp_finally { port->end_edit(); };
                    port->write(value, strlen(value));
                    port->notify_all(ui::PORT_USER_EDIT);
                }
            }

            return STATUS_OK;
        }

        status_t PluginWindow::slot_select_audio(tk::Widget *sender, void *ptr, void *data)
        {
            const audio_sel_t * const sel = reinterpret_cast<audio_sel_t *>(ptr);
            if ((sender == NULL) || (sel == NULL) || (sel->ctl == NULL))
                return STATUS_BAD_ARGUMENTS;

            // Select backend
            status_t res = sel->ctl->select_audio_backend(sel->id);
            if (res != STATUS_OK)
                return STATUS_OK;

            // Notify about change of audio backend
            sel->ctl->audio_backend_selected(sel->id);

            return STATUS_OK;
        }


        status_t PluginWindow::slot_select_language(tk::Widget *sender, void *ptr, void *data)
        {
            lang_sel_t * const sel = reinterpret_cast<lang_sel_t *>(ptr);
            lsp_trace("sender=%p, sel=%p", sender, sel);
            if ((sender == NULL) || (sel == NULL))
                return STATUS_BAD_ARGUMENTS;

            PluginWindow * const self = sel->ctl;
            if ((self == NULL) || (sel->item == NULL))
                return STATUS_BAD_ARGUMENTS;

            tk::Display * const dpy = sender->display();
            lsp_trace("dpy = %p", dpy);
            if (dpy == NULL)
                return STATUS_BAD_STATE;

            // Select language
            tk::Schema * const schema  = dpy->schema();
            lsp_trace("Select language: \"%s\"", sel->lang.get_native());
            if ((schema->set_lanugage(&sel->lang)) != STATUS_OK)
            {
                lsp_warn("Failed to select language \"%s\"", sel->lang.get_native());
                return STATUS_OK;
            }

            // Update parameter
            const char * const dlang = sel->lang.get_utf8();
            const char * const slang = self->pLanguage->buffer<char>();
            lsp_trace("Current language in settings: \"%s\"", slang);
            if ((slang == NULL) || (strcmp(slang, dlang)))
            {
                self->pLanguage->begin_edit();
                self->pLanguage->write(dlang, strlen(dlang));
                self->pLanguage->notify_all(ui::PORT_USER_EDIT);
                self->pLanguage->end_edit();
            }

            lsp_trace("Language has been selected");

            return STATUS_OK;
        }

        status_t PluginWindow::slot_select_preset(tk::Widget *sender, void *ptr, void *data)
        {
            preset_sel_t *sel = reinterpret_cast<preset_sel_t *>(ptr);
            lsp_trace("sender=%p, sel=%p", sender, sel);
            if ((sender == NULL) || (sel == NULL) || (sel->ctl == NULL) || (sel->item == NULL))
                return STATUS_BAD_ARGUMENTS;

            sel->ctl->set_preset_button_text(sel->item->text()->raw()->get_utf8());

            lsp_trace("Loading preset %s", sel->location.get_native());
            size_t flags = ui::IMPORT_FLAG_PRESET;
            if (sel->patch)
                flags      |= ui::IMPORT_FLAG_PATCH;
            sel->ctl->pWrapper->import_settings(&sel->location, flags);

            return STATUS_OK;
        }

        void PluginWindow::sync_language_selection()
        {
            tk::Display *dpy    = wWidget->display();
            if (dpy == NULL)
                return;

            LSPString lang;
            if ((dpy->schema()->get_language(&lang)) != STATUS_OK)
                return;

            for (size_t i=0, n=vLangSel.size(); i<n; ++i)
            {
                lang_sel_t *xsel    = vLangSel.uget(i);
                if (xsel->item == NULL)
                    continue;
                xsel->item->checked()->set(xsel->lang.equals(&lang));
            }
        }

        void PluginWindow::host_scaling_changed()
        {
            sUIScaling.host_scaling_changed();
        }

        void PluginWindow::sync_invert_vscroll(ui::IPort *port)
        {
            tk::Display *dpy    = wWidget->display();
            if (dpy == NULL)
                return;

            bool invert_global  = (pInvertVScroll != NULL) ? pInvertVScroll->value() >= 0.5f : false;
            bool invert_gdot    = (pInvertGraphDotVScroll != NULL) ? (pInvertGraphDotVScroll->value() >= 0.5f) ^ invert_global : invert_global;

            lsp_trace("mouse vscroll invert: global=%s, graph_dot=%s",
                (invert_global) ? "true" : "false",
                (invert_gdot) ? "true" : "false");

            if ((port == pInvertVScroll) && (wInvertVScroll != NULL))
                wInvertVScroll->checked()->set(invert_global);
            if ((port == pInvertGraphDotVScroll) && (wInvertGraphDotVScroll != NULL))
                wInvertGraphDotVScroll->checked()->set(invert_gdot);

            // Set global configuration
            dpy->schema()->invert_mouse_vscroll()->set(invert_global);

            tk::Style *gdot_style = dpy->schema()->get("GraphDot");
            if (gdot_style != NULL)
                gdot_style->set_bool("mouse.vscroll.invert", invert_gdot);
        }

        void PluginWindow::sync_ui_behaviour_flags(ui::IPort *port)
        {
            for (size_t i=0, n=vBoolFlags.size(); i<n; ++i)
            {
                ui_flag_t *flag = vBoolFlags.uget(i);
                if ((flag == NULL) || (flag->wItem == NULL) || (flag->pPort == NULL))
                    continue;

                if ((port == NULL) || (port == flag->pPort))
                {
                    const bool checked = flag->pPort->value() >= 0.5f;
                    flag->wItem->checked()->set(checked);
                }
            }
        }

        void PluginWindow::notify_ui_behaviour_flags(size_t flags)
        {
            for (size_t i=0, n=vBoolFlags.size(); i<n; ++i)
            {
                ui_flag_t *flag = vBoolFlags.uget(i);
                if ((flag == NULL) || (flag->pPort == NULL))
                    continue;

                flag->pPort->notify_all(flags);
            }
        }

        void PluginWindow::sync_enum_menu(enum_menu_t *menu, ui::IPort *port)
        {
            if ((port == NULL) || (port != menu->pPort))
                return;
            const meta::port_t *meta = menu->pPort->metadata();
            if (meta == NULL)
                return;

            tk::Display *dpy    = wWidget->display();
            if (dpy == NULL)
                return;

            const ssize_t index = menu->pPort->value() - meta->min;
            for (lltl::iterator<tk::MenuItem> it = menu->vItems.values(); it; ++it)
            {
                tk::MenuItem *mi = it.get();
                mi->checked()->set(ssize_t(it.index()) == index);
            }
        }

        void PluginWindow::sync_visual_schemas()
        {
            const char *path = (pVisualSchema != NULL) ? pVisualSchema->buffer<const char>() : NULL;

            // Update the UI scaling
            for (size_t i=0, n=vSchemaSel.size(); i<n; ++i)
            {
                schema_sel_t *xsel = vSchemaSel.uget(i);
                if (xsel->item != NULL)
                {
                    bool checked = (path != NULL) && xsel->location.equals_utf8(path);
                    xsel->item->checked()->set(checked);
                }
            }
        }

        void PluginWindow::begin(ui::UIContext *ctx)
        {
            Window::begin(ctx);
            status_t res;

            // Create context
            ui::UIContext uctx(pWrapper, controllers(), widgets());
            if ((res = init_ui_context(&uctx, pWrapper->package(), pWrapper->metadata())) != STATUS_OK)
                return;

            // Parse the XML document
            ctl::PluginWindowTemplate wnd(pWrapper, this);
            if (wnd.init() != STATUS_OK)
                return;

            ui::xml::RootNode root(&uctx, "window", &wnd);
            ui::xml::Handler handler(pWrapper->resources());
            res = handler.parse_resource(LSP_BUILTIN_PREFIX "ui/window.xml", &root);
            if (res != STATUS_OK)
                lsp_warn("Error parsing resource: %s, error: %d", LSP_BUILTIN_PREFIX "ui/window.xml", int(res));
            wnd.destroy();

            // Get proper widgets and initialize window layout
            wContent        = tk::widget_cast<tk::WidgetContainer>(widgets()->find("plugin_content"));
        }

        void PluginWindow::end(ui::UIContext *ctx)
        {
            // Check widget pointer
            tk::Window *wnd = tk::widget_cast<tk::Window>(wWidget);

            if (wnd != NULL)
            {
                // Update window geometry
                wnd->border_style()->set(bResizable ? ws::BS_SIZEABLE : ws::BS_DIALOG);
                wnd->policy()->set(bResizable ? tk::WP_NORMAL : tk::WP_GREEDY);
                wnd->actions()->set_resizable(bResizable);
                wnd->actions()->set_maximizable(bResizable);
            }


            // Call for parent class method
            Window::end(ctx);
        }

        void PluginWindow::notify(ui::IPort *port, size_t flags)
        {
            Window::notify(port, flags);

            if (port == pLanguage)
                sync_language_selection();
            if (port == pVisualSchema)
            {
                sync_visual_schemas();

                // Notify other parameters
                if (pInvertVScroll != NULL)
                    pInvertVScroll->notify_all(flags);
                if (pInvertGraphDotVScroll != NULL)
                    pInvertGraphDotVScroll->notify_all(flags);
                if (sFilterPointThickness.pPort != NULL)
                    sFilterPointThickness.pPort->notify_all(flags);

                sUIScaling.sync_parameters();
                sFontScaling.sync_parameters();
                notify_ui_behaviour_flags(flags);
                sync_language_selection();
            }
            if (port == pAudioBackend)
                sync_audio_selection();
            if ((port == pInvertVScroll) || (port == pInvertGraphDotVScroll))
                sync_invert_vscroll(port);

            sync_ui_behaviour_flags(port);
            sync_enum_menu(&sFilterPointThickness, port);
        }

        status_t PluginWindow::add(ui::UIContext *ctx, ctl::Widget *child)
        {
            ctl::Overlay *ov = ctl::ctl_cast<ctl::Overlay>(child);
            tk::WidgetContainer *target = (ov != NULL) ? tk::widget_cast<tk::WidgetContainer>(wWidget) : wContent;

            return (target != NULL) ? target->add(child->widget()) : STATUS_BAD_STATE;
        }

        status_t PluginWindow::slot_export_settings_to_file(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *self      = static_cast<PluginWindow *>(ptr);
            if (self == NULL)
                return STATUS_OK;

            if (self->pPresetsWindow != NULL)
                self->pPresetsWindow->show_export_settings_dialog();

            return STATUS_OK;
        }

        status_t PluginWindow::slot_export_settings_to_clipboard(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *self      = static_cast<PluginWindow *>(ptr);
            if (self == NULL)
                return STATUS_OK;

            if (self->pPresetsWindow != NULL)
                self->pPresetsWindow->export_settings_to_clipboard();

            return STATUS_OK;
        }

        status_t PluginWindow::slot_import_settings_from_file(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *self      = static_cast<PluginWindow *>(ptr);
            if (self == NULL)
                return STATUS_OK;

            if (self->pPresetsWindow != NULL)
                self->pPresetsWindow->show_import_settings_dialog();

            return STATUS_OK;
        }

        status_t PluginWindow::slot_import_settings_from_clipboard(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *self      = static_cast<PluginWindow *>(ptr);
            if (self == NULL)
                return STATUS_OK;

            if (self->pPresetsWindow != NULL)
                self->pPresetsWindow->import_settings_from_clipboard();

            return STATUS_OK;
        }

        status_t PluginWindow::slot_reset_settings(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *self      = static_cast<PluginWindow *>(ptr);
            if (self == NULL)
                return STATUS_OK;

            if (self->pPresetsWindow != NULL)
                self->pPresetsWindow->reset_settings();

            return STATUS_OK;
        }

        status_t PluginWindow::slot_show_plugin_manual(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow * const self = static_cast<PluginWindow *>(ptr);
            self->show_plugin_manual();
            return STATUS_OK;
        }

        status_t PluginWindow::slot_show_ui_manual(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow * const self = static_cast<PluginWindow *>(ptr);
            self->show_ui_manual();
            return STATUS_OK;
        }

        status_t PluginWindow::slot_show_about(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow * const self = static_cast<PluginWindow *>(ptr);
            if (self != NULL)
                self->show_about_window();

            return STATUS_OK;
        }

        status_t PluginWindow::slot_show_presets_window(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow * const self = static_cast<PluginWindow *>(ptr);
            if (self != NULL)
                self->show_presets_window();

            return STATUS_OK;
        }

        status_t PluginWindow::slot_scroll_over_presets(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow * const self = static_cast<PluginWindow *>(ptr);
            const ws::event_t * const ev = static_cast<ws::event_t *>(data);
            if ((self != NULL) && (ev != NULL))
                self->scroll_over_presets(ev);

            return STATUS_OK;
        }

        status_t PluginWindow::slot_debug_dump(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *__this = static_cast<PluginWindow *>(ptr);
            if (__this->pWrapper != NULL)
                __this->pWrapper->dump_state_request();

            return STATUS_OK;
        }

        status_t PluginWindow::slot_show_main_menu(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *__this = static_cast<PluginWindow *>(ptr);
            return __this->show_menu(__this->wMenu, sender, data);
        }

        status_t PluginWindow::slot_show_presets_menu(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *__this = static_cast<PluginWindow *>(ptr);
            return __this->show_menu(__this->wPresets, sender, data);
        }

        status_t PluginWindow::slot_select_next_preset(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *self = static_cast<PluginWindow *>(ptr);
            if (self == NULL)
                return STATUS_OK;

            if (self->pPresetsWindow == NULL)
                return STATUS_OK;

            tk::Widget *w = self->widgets()->find("trg_next_preset");
            self->pPresetsWindow->select_next_preset(sender == w, true);
            return STATUS_OK;
        }

        status_t PluginWindow::show_menu(tk::Widget *menu, tk::Widget *actor, void *data)
        {
            tk::Menu *xmenu = tk::widget_ptrcast<tk::Menu>(menu);
            if (xmenu == NULL)
                return STATUS_OK;

            if (actor != NULL)
            {
                ws::rectangle_t xr, wr;

                wWidget->get_rectangle(&wr);
                actor->get_rectangle(&xr);

                if (xr.nTop > (wr.nHeight >> 1))
                    xmenu->set_tether(bottom_tether, sizeof(bottom_tether)/sizeof(tk::tether_t));
                else
                    xmenu->set_tether(top_tether, sizeof(top_tether)/sizeof(tk::tether_t));
                xmenu->show(actor);
            }
            else
                xmenu->show();
            return STATUS_OK;
        }

        status_t PluginWindow::slot_greeting_close(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *__this = static_cast<PluginWindow *>(ptr);
            if (__this->wGreeting != NULL)
                __this->wGreeting->visibility()->set(false);
            return STATUS_OK;
        }

        tk::Label *PluginWindow::create_label(tk::WidgetContainer *dst, const char *key, const char *style_name)
        {
            tk::Label *lbl = new tk::Label(wWidget->display());
            lbl->init();
            widgets()->add(lbl);
            dst->add(lbl);

            lbl->text()->set(key);
            inject_style(lbl, style_name);

            return lbl;
        }

        tk::Label *PluginWindow::create_plabel(tk::WidgetContainer *dst, const char *key, const expr::Parameters *params, const char *style_name)
        {
            tk::Label *lbl = new tk::Label(wWidget->display());
            lbl->init();
            widgets()->add(lbl);
            dst->add(lbl);

            lbl->text()->set(key, params);
            inject_style(lbl, style_name);

            return lbl;
        }

        tk::Hyperlink *PluginWindow::create_hlink(tk::WidgetContainer *dst, const char *url, const char *text, const expr::Parameters *params, const char *style_name)
        {
            tk::Hyperlink *hlink = new tk::Hyperlink(wWidget->display());
            hlink->init();
            widgets()->add(hlink);
            dst->add(hlink);

            hlink->url()->set(url);
            hlink->text()->set(text);
            if (params != NULL)
                hlink->text()->set_params(params);
            inject_style(hlink, style_name);

            return hlink;
        }

        status_t PluginWindow::set_greeting_timer()
        {
            if (pPVersion == NULL)
                return STATUS_OK;

            LSPString pkgver;
            status_t res = fmt_package_version(pkgver);
            if (res != STATUS_OK)
                return res;

            const char *v = pPVersion->buffer<char>();
            if ((v != NULL) && (pkgver.equals_utf8(v)))
                return STATUS_OK;

            // Skip if current version contains "dev"
            if (strstr(pkgver.get_utf8(), "dev") != NULL)
                return STATUS_OK;

            // Set timer to show the greeting window
            wGreetingTimer.set_handler(timer_show_greeting, this);
            wGreetingTimer.bind(pWrapper->display());
            wGreetingTimer.launch(1, 0, 1000);

            return STATUS_OK;
        }

        status_t PluginWindow::fmt_package_version(LSPString &pkgver)
        {
            const meta::package_t *pkg  = pWrapper->package();
            if (pkg == NULL)
                return STATUS_NO_DATA;

            const meta::plugin_t *meta  = pWrapper->ui()->metadata();
            if (meta == NULL)
                return STATUS_NO_DATA;

            pkgver.fmt_ascii("%d.%d.%d",
                int(pkg->version.major),
                int(pkg->version.minor),
                int(pkg->version.micro));
            if (pkg->version.branch)
                pkgver.fmt_append_utf8("-%s", pkg->version.branch);

            return STATUS_OK;
        }

        status_t PluginWindow::timer_show_greeting(ws::timestamp_t sched, ws::timestamp_t time, void *arg)
        {
            if (arg == NULL)
                return STATUS_OK;

            PluginWindow *self = static_cast<PluginWindow *>(arg);
            self->wGreetingTimer.cancel();
            self->show_greeting_window();
            return STATUS_OK;
        }

        status_t PluginWindow::show_presets_window()
        {
            tk::Window *wnd = tk::widget_cast<tk::Window>(wWidget);
            if (wnd == NULL)
                return STATUS_BAD_STATE;

            if (pPresetsWindow == NULL)
                return STATUS_OK;

            tk::Widget *actor = widgets()->find("trg_presets_menu_area");
            if (actor == NULL)
                actor = widgets()->find("trg_presets_menu");
            return pPresetsWindow->toggle_visibility(actor);
        }

        void PluginWindow::scroll_over_presets(const ws::event_t *ev)
        {
            if (pPresetsWindow == NULL)
                return;
            if (ev->nType != ws::UIE_MOUSE_SCROLL)
                return;

            // Check whether next or previous preset should be selected
            bool next = true;
            if (ev->nCode == ws::MCD_UP)
                next = false;
            else if (ev->nCode != ws::MCD_DOWN)
                return;

            // Check that we needt to invert mouse scrolling
            tk::Style * const style = (wWidget != NULL) ? wWidget->style() : NULL;
            if (style == NULL)
                return;

            bool invert = false;
            style->get_bool("mouse.vscroll.invert", &invert);
            if (invert)
                next    = !next;

            pPresetsWindow->select_next_preset(next, true);
        }

        status_t PluginWindow::show_greeting_window()
        {
            status_t res;
            if (pPVersion == NULL)
                return STATUS_BAD_STATE;
            tk::Window *wnd = tk::widget_cast<tk::Window>(wWidget);
            if (wnd == NULL)
                return STATUS_BAD_STATE;

            LSPString pkgver;
            if ((res = fmt_package_version(pkgver)) != STATUS_OK)
                return res;

            // Check that we really need to show notification window
            const char *vstring = pkgver.get_utf8();
        #ifdef LSP_TRACE
            const char *old_pkgver = pPVersion->buffer<char>();
            lsp_trace("Updating last version from %s to %s", old_pkgver, vstring);
        #endif /* LSP_TRACE */

            pPVersion->write(vstring, strlen(vstring));
            pPVersion->notify_all(ui::PORT_NONE);

            lsp_trace("Showing greeting dialog");

            if (wGreeting == NULL)
            {
                ctl::Window *ctl = NULL;
                res = create_dialog_window(&ctl, &wGreeting, LSP_BUILTIN_PREFIX "ui/greeting.xml");
                if (res != STATUS_OK)
                    return res;

                // Bind slots
                tk::Widget *btn = ctl->widgets()->find("submit");
                if (btn != NULL)
                    btn->slots()->bind(tk::SLOT_SUBMIT, slot_greeting_close, this);
                wGreeting->slots()->bind(tk::SLOT_CLOSE, slot_greeting_close, this);

                // Bind shortcuts
                bind_shortcut(wGreeting, ws::WSK_ESCAPE, tk::KM_NONE, slot_greeting_close);
                bind_shortcut(wGreeting, ws::WSK_RETURN, tk::KM_NONE, slot_greeting_close);
                bind_shortcut(wGreeting, ws::WSK_KEYPAD_ENTER, tk::KM_NONE, slot_greeting_close);
            }

            wGreeting->show(wnd);
            return STATUS_OK;
        }

        void PluginWindow::read_path_param(tk::String *value, const char *port_id)
        {
            ui::IPort *p = pWrapper->port(port_id);
            const char *path = NULL;
            if ((p != NULL) && (meta::is_path_port(p->metadata())))
                path = p->buffer<const char>();
            value->set_raw((path != NULL) ? path : "");
        }

        void PluginWindow::read_path_param(LSPString *value, const char *port_id)
        {
            ui::IPort *p = pWrapper->port(port_id);
            const char *path = NULL;
            if ((p != NULL) && (meta::is_path_port(p->metadata())))
                path = p->buffer<const char>();
            value->set_utf8((path != NULL) ? path : "");
        }

        void PluginWindow::read_bool_param(tk::Boolean *value, const char *port_id)
        {
            ui::IPort *p = pWrapper->port(port_id);
            bool ck = false;
            if (p != NULL)
                ck = p->value() >= 0.5f;
            value->set(ck);
        }

        void PluginWindow::commit_path_param(tk::String *value, const char *port_id)
        {
            ui::IPort *p = pWrapper->port(port_id);
            if ((p != NULL) && (meta::is_path_port(p->metadata())))
            {
                LSPString text;
                value->format(&text);
                const char *path = text.get_utf8();
                if (path != NULL)
                    p->write(path, strlen(path));
                else
                    p->write("", 0);
                p->notify_all(ui::PORT_USER_EDIT);
            }
        }

        void PluginWindow::commit_bool_param(tk::Boolean *value, const char *port_id)
        {
            ui::IPort *p = pWrapper->port(port_id);
            if (p != NULL)
            {
                p->set_value((value->get()) ? 1.0f : 0.0f);
                p->notify_all(ui::PORT_USER_EDIT);
            }
        }

        status_t PluginWindow::show_user_paths_window()
        {
            status_t res;
            tk::Widget *btn;
            tk::Edit *edit;
            tk::CheckBox *check;
            tk::Window *wnd = tk::widget_cast<tk::Window>(wWidget);
            if (wnd == NULL)
                return STATUS_BAD_STATE;

            lsp_trace("Showing user paths dialog");

            if ((wUserPaths == NULL) || (pUserPaths == NULL))
            {
                res = create_dialog_window(&pUserPaths, &wUserPaths, LSP_BUILTIN_PREFIX "ui/user_paths.xml");
                if (res != STATUS_OK)
                    return res;

                // Bind slots
                if ((btn = pUserPaths->widgets()->find("submit")) != NULL)
                    btn->slots()->bind(tk::SLOT_SUBMIT, slot_user_paths_submit, this);
                if ((btn = pUserPaths->widgets()->find("cancel")) != NULL)
                    btn->slots()->bind(tk::SLOT_SUBMIT, slot_user_paths_close, this);
                wUserPaths->slots()->bind(tk::SLOT_CLOSE, slot_user_paths_close, this);
            }

            if ((edit = pUserPaths->widgets()->get<tk::Edit>("user_hydrogen_kit_path")) != NULL)
                read_path_param(edit->text(), UI_USER_HYDROGEN_KIT_PATH_PORT);
            if ((edit = pUserPaths->widgets()->get<tk::Edit>("override_hydrogen_kit_path")) != NULL)
                read_path_param(edit->text(), UI_OVERRIDE_HYDROGEN_KIT_PATH_PORT);
            if ((check = pUserPaths->widgets()->get<tk::CheckBox>("override_hydrogen_kits_check")) != NULL)
                read_bool_param(check->checked(), UI_OVERRIDE_HYDROGEN_KITS_PORT);

            wUserPaths->show(wnd);

            return STATUS_OK;
        }

        void PluginWindow::apply_user_paths_settings()
        {
            tk::Edit *edit;
            tk::CheckBox *check;

            if ((edit = pUserPaths->widgets()->get<tk::Edit>("user_hydrogen_kit_path")) != NULL)
                commit_path_param(edit->text(), UI_USER_HYDROGEN_KIT_PATH_PORT);
            if ((edit = pUserPaths->widgets()->get<tk::Edit>("override_hydrogen_kit_path")) != NULL)
                commit_path_param(edit->text(), UI_OVERRIDE_HYDROGEN_KIT_PATH_PORT);
            if ((check = pUserPaths->widgets()->get<tk::CheckBox>("override_hydrogen_kits_check")) != NULL)
                commit_bool_param(check->checked(), UI_OVERRIDE_HYDROGEN_KITS_PORT);
        }

        status_t PluginWindow::show_about_window()
        {
            lsp_trace("Showing about dialog");

            if (pAboutWindow == NULL)
            {
                ctl::AboutWindow *ctl = new AboutWindow(pWrapper);
                if (ctl == NULL)
                    return STATUS_NO_MEM;
                lsp_finally {
                    if (ctl != NULL)
                    {
                        ctl->destroy();
                        delete ctl;
                    }
                };
                LSP_STATUS_ASSERT(ctl->init());
                LSP_STATUS_ASSERT(controllers()->add(ctl));
                pAboutWindow    = release_ptr(ctl);
            }

            pAboutWindow->show(wWidget);
            return STATUS_OK;
        }

        bool PluginWindow::open_manual_file(const char *fmt...)
        {
            va_list vl;
            va_start(vl, fmt);
            lsp_finally{ va_end(vl); };

            // Form the path
            io::Path path;
            LSPString spath;
            ssize_t res = path.vfmt(fmt, vl);
            if (res <= 0)
                return false;

            if (path.is_empty())
                return false;

            lsp_trace("Checking path: %s", path.as_utf8());

            if (!path.exists())
                return false;

            if (!spath.fmt_utf8("file://%s", path.as_utf8()))
                return false;

            return system::follow_url(&spath) == STATUS_OK;
        }

        status_t PluginWindow::show_plugin_manual()
        {
            return sDocumentation.show_plugin_manual();
        }

        status_t PluginWindow::show_ui_manual()
        {
            return sDocumentation.show_ui_manual();
        }

        status_t PluginWindow::create_dialog_window(ctl::Window **ctl, tk::Window **dst, const char *path)
        {
            status_t res;

            // Create window
            tk::Window *w = new tk::Window(wWidget->display());
            if (w == NULL)
                return STATUS_NO_MEM;
            widgets()->add(w);
            w->init();
            w->actions()->set_actions(ws::WA_DIALOG | ws::WA_RESIZE | ws::WA_CLOSE);

            lsp_trace("Registered window ptr=%p, path=%s", static_cast<tk::Widget *>(w), path);

            // Create controller
            ctl::Window *wc = new ctl::Window(pWrapper, w);
            if (wc == NULL)
                return STATUS_NO_MEM;
            controllers()->add(wc);
            wc->init();

            ui::UIContext uctx(pWrapper, wc->controllers(), wc->widgets());
            if ((res = init_ui_context(&uctx, pWrapper->package(), pWrapper->metadata())) != STATUS_OK)
                return res;

            // Parse the XML document
            ui::xml::RootNode root(&uctx, "window", wc);
            ui::xml::Handler handler(pWrapper->resources());
            if ((res = handler.parse_resource(path, &root)) != STATUS_OK)
                return res;

            if (ctl != NULL)
                *ctl    = wc;
            if (dst != NULL)
                *dst    = w;

            return STATUS_OK;
        }

        status_t PluginWindow::create_presets_window()
        {
            status_t res;

            PluginWindow *self = this;

            // Create controller
            ctl::PresetsWindow *wc = new ctl::PresetsWindow(pWrapper, self);
            if (wc == NULL)
                return STATUS_NO_MEM;
            if ((res = controllers()->add(wc)) != STATUS_OK)
            {
                wc->destroy();
                delete wc;
                return res;
            }
            LSP_STATUS_ASSERT(wc->init());

            pPresetsWindow  = wc;

            return STATUS_OK;
        }

        void PluginWindow::sync_preset_name()
        {
            tk::Widget *w = widgets()->find("trg_presets_menu");
            if (w == NULL)
                return;

            tk::String *prop = NULL;
            tk::Button *btn = NULL;
            tk::Label *lbl = NULL;
            // Try to cast to button
            {
                btn = tk::widget_cast<tk::Button>(w);
                if (btn != NULL)
                    prop        = btn->text();
            }
            // Try to cast to label
            {
                lbl = tk::widget_cast<tk::Label>(w);
                if (lbl != NULL)
                    prop        = lbl->text();
            }
            if (prop == NULL)
                return;

            // Set default label if preset is not selected
            const ui::preset_t *preset = pWrapper->active_preset();
            expr::Parameters params;

            // If text clipping is enabled, just update text
            const bool text_clip = (lbl != NULL) ? lbl->text_clip()->get() : btn->text_clip()->get();
            if (text_clip)
            {
                if (preset == NULL)
                {
                    prop->set("actions.presets.select");
                    return;
                }

                // Determine how to format the prest name
                const char *key = "labels.presets.name.normal";
                const bool changed = pWrapper->active_preset_dirty();
                if (preset->flags & ui::PRESET_FLAG_USER)
                    key     = (changed) ? "labels.presets.name.user_mod" : "labels.presets.name.user";
                else
                    key     = (changed) ? "labels.presets.name.factory_mod" : "labels.presets.name.factory";

                params.set_string("name", &preset->name);
                prop->set(key, &params);

                return;
            }

            // Otherwise, fill esimation list
            const ui::preset_t * const *list = pWrapper->all_presets();
            if (lbl != NULL)
                lbl->clear_text_estimations();
            if (btn != NULL)
                btn->clear_text_estimations();

            for (size_t i=0, n=pWrapper->num_presets(); i<n; ++i)
            {
                const ui::preset_t * const item = list[i];

                // Determine how to format the prest name
                const char *key = "labels.presets.name.normal";
                const bool changed = pWrapper->active_preset_dirty();
                if (item->flags & ui::PRESET_FLAG_USER)
                    key     = (changed) ? "labels.presets.name.user_mod" : "labels.presets.name.user";
                else
                    key     = (changed) ? "labels.presets.name.factory_mod" : "labels.presets.name.factory";

                // Fill the text
                params.set_string("name", &item->name);
                if (item != preset)
                {
                    tk::String *est = (btn != NULL) ? btn->add_text_estimation() : lbl->add_text_estimation();
                    if (est != NULL)
                        est->set(key, &params);
                }
                else
                    prop->set(key, &params);
            }

            if (preset == NULL)
                prop->set("actions.presets.select");
        }

        void PluginWindow::preset_activated(const ui::preset_t *preset)
        {
            sync_preset_name();
        }

        void PluginWindow::presets_updated()
        {
            sync_preset_name();
        }

        void PluginWindow::sync_ab_state()
        {
            const size_t active = pWrapper->active_preset_data();
            for (size_t i=0; i<2; ++i)
            {
                tk::Widget *w       = wPresetSwitch[i];
                if (w == NULL)
                    continue;

                const bool is_active = (i == active);

                revoke_style(w, "PluginWindow::ABSwitch::Active");
                revoke_style(w, "PluginWindow::ABSwitch::Inactive");
                inject_style(w, (is_active) ? "PluginWindow::ABSwitch::Active" : "PluginWindow::ABSwitch::Inactive");

                // Set editable
                tk::Button *btn = tk::widget_cast<tk::Button>(w);
                if (btn != NULL)
                    btn->editable()->set(!is_active);
            }

            // Synchronize icon
            tk::String *prop = NULL;
            {
                tk::Label *lbl = tk::widget_cast<tk::Label>(wPresetCopy);
                if (lbl != NULL)
                    prop = lbl->text();
            }
            {
                tk::Button *btn = tk::widget_cast<tk::Button>(wPresetCopy);
                if (btn != NULL)
                    prop = btn->text();
            }
            if (prop != NULL)
                prop->set((active == 0) ? "icons.arrow.right" : "icons.arrow.left");
        }

        status_t PluginWindow::slot_visual_schema_select(tk::Widget *sender, void *ptr, void *data)
        {
            schema_sel_t *sel  = static_cast<schema_sel_t *>(ptr);
            if (sel == NULL)
                return STATUS_OK;

            PluginWindow * const self = sel->ctl;
            if (self == NULL)
                return STATUS_OK;

            // Try to load schema first
            return self->select_ui_schema(sel->location);
        }

        status_t PluginWindow::slot_window_resize(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow * const self = static_cast<PluginWindow *>(ptr);
            if (self == NULL)
                return STATUS_OK;

            const ws::event_t * const ev = static_cast<const ws::event_t *>(data);
            if (ev == NULL)
                return STATUS_OK;

//            lsp_trace("Resize: x=%d, y=%d, w=%d, h=%d", int(r->nLeft), int(r->nTop), int(r->nWidth), int(r->nHeight));

            tk::Window * const wnd = tk::widget_cast<tk::Window>(self->wWidget);
            if ((wnd == NULL) || (wnd->has_parent()))
                return STATUS_OK;

            ws::rectangle_t wp;
            wp.nLeft        = ev->nLeft;
            wp.nTop         = ev->nTop;
            wp.nWidth       = ev->nWidth;
            wp.nHeight      = ev->nHeight;

            ssize_t sw = 0, sh = 0;
            wnd->display()->screen_size(wnd->screen(), &sw, &sh);

            // Fit plugin window into a screen
            size_t changed = 0;
            if (wp.nLeft >= sw)
            {
                wp.nLeft    = sw - wp.nWidth;
                ++changed;
            }
            if (wp.nTop >= sh)
            {
                wp.nTop     = sh - wp.nHeight;
                ++changed;
            }
            if ((wp.nLeft + wp.nWidth) < 0)
            {
                wp.nLeft    = 0;
                ++changed;
            }
            if ((wp.nTop + wp.nHeight) < 0)
            {
                wp.nTop     = 0;
                ++changed;
            }

            if (changed > 0)
                wnd->position()->set(wp.nLeft, wp.nTop);

            return STATUS_OK;
        }

        status_t PluginWindow::locate_window()
        {
            tk::Window *wnd = tk::widget_cast<tk::Window>(wWidget);
            if ((wnd == NULL) || (wnd->has_parent()))
                return STATUS_OK;

            ws::rectangle_t r;

            // Estimate the real window size if it is still not known
            {
                ws::size_limit_t sr;
                wnd->get_padded_screen_rectangle(&r);
                wnd->get_padded_size_limits(&sr);
                if ((sr.nMinWidth >= 0) && (r.nWidth < sr.nMinWidth))
                    r.nWidth = sr.nMinWidth;
                if ((sr.nMinHeight >= 0) && (r.nHeight < sr.nMinHeight))
                    r.nHeight = sr.nMinHeight;
            }

            // Find the matching monitor (if it is present) and align window to the center
            bool aligned = false;
            {
                size_t num_monitors = 0;
                const ws::MonitorInfo * x = wnd->display()->enum_monitors(&num_monitors);
                if (x != NULL)
                {
                    for (size_t i=0; i<num_monitors; ++i)
                    {
                        if (tk::Position::inside(&x->rect, r.nLeft, r.nHeight))
                        {
                            r.nLeft = (x->rect.nWidth  - r.nWidth)  >> 1;
                            r.nTop  = (x->rect.nHeight - r.nHeight) >> 1;
                            aligned = true;
                            break;
                        }
                    }
                }
            }
            if (!aligned)
            {
                ssize_t sw = 0, sh = 0;
                wnd->display()->screen_size(wnd->screen(), &sw, &sh);

                r.nLeft = (sw - r.nWidth)  >> 1;
                r.nTop  = (sh - r.nHeight) >> 1;
            }

            // Set the actual position of the window
            wnd->position()->set(r.nLeft, r.nTop);

            return STATUS_OK;
        }

        status_t PluginWindow::slot_scale_mouse_down(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *_this = static_cast<PluginWindow *>(ptr);
            if (_this == NULL)
                return STATUS_OK;

            ws::event_t *ev = static_cast<ws::event_t *>(data);
            if (ev == NULL)
                return STATUS_OK;

            size_t old = _this->sWndScale.nMFlags;
            _this->sWndScale.nMFlags   |= 1 << ev->nCode;
            if (old == 0)
            {
                _this->sWndScale.bActive    = ev->nCode == ws::MCB_LEFT;

                if (_this->sWndScale.bActive)
                {
                    _this->wWidget->get_padded_screen_rectangle(&_this->sWndScale.sSize);
                    _this->sWndScale.nMouseX    = ev->nLeft;
                    _this->sWndScale.nMouseY    = ev->nTop;
                }
            }

            return STATUS_OK;
        }

        status_t PluginWindow::slot_scale_mouse_move(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *_this = static_cast<PluginWindow *>(ptr);
            if (_this == NULL)
                return STATUS_OK;

            ws::event_t *ev = static_cast<ws::event_t *>(data);
            if (ev == NULL)
                return STATUS_OK;

            if (_this->sWndScale.bActive)
            {
                tk::Window *wnd = tk::widget_cast<tk::Window>(_this->wWidget);
                if (wnd == NULL)
                    return STATUS_OK;

                ws::size_limit_t sr;
                ws::rectangle_t ws;
                ws::rectangle_t xr  = _this->sWndScale.sSize;
                xr.nWidth          += ev->nLeft - _this->sWndScale.nMouseX;
                xr.nHeight         += ev->nTop - _this->sWndScale.nMouseY;

                _this->wWidget->get_padded_rectangle(&ws);
                _this->wWidget->get_padded_size_limits(&sr);
                tk::SizeConstraints::apply(&xr, &sr);

//                lsp_trace("ws.size={%d, %d}, xr.size={%d, %d}",
//                    int(ws.nWidth), int(ws.nHeight),
//                    int(xr.nWidth), int(xr.nHeight));

                if ((ws.nWidth != xr.nWidth) || (ws.nHeight != xr.nHeight))
                {
                    if (_this->pWrapper->accept_window_size(wnd, xr.nWidth, xr.nHeight))
                    {
                        _this->pWrapper->window_resized(wnd, xr.nWidth, xr.nHeight);
                        wnd->resize_window(xr.nWidth, xr.nHeight);
                    }
                }
            }

            return STATUS_OK;
        }

        status_t PluginWindow::slot_scale_mouse_up(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *_this = static_cast<PluginWindow *>(ptr);
            if (_this == NULL)
                return STATUS_OK;

            ws::event_t *ev = static_cast<ws::event_t *>(data);
            if (ev == NULL)
                return STATUS_OK;

            _this->sWndScale.nMFlags   &= ~(size_t(1) << ev->nCode);
            if (_this->sWndScale.nMFlags == 0)
                _this->sWndScale.bActive    = false;

            return STATUS_OK;
        }

        status_t PluginWindow::slot_invert_vscroll_changed(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *_this = static_cast<PluginWindow *>(ptr);
            if (_this == NULL)
                return STATUS_OK;
            if (_this->pInvertVScroll == NULL)
                return STATUS_OK;
            if (_this->wInvertVScroll == NULL)
                return STATUS_OK;

            _this->wInvertVScroll->checked()->toggle();
            bool checked = _this->wInvertVScroll->checked()->get();
            _this->pInvertVScroll->set_value((checked) ? 1.0f : 0.0f);
            _this->pInvertVScroll->notify_all(ui::PORT_USER_EDIT);

            return STATUS_OK;
        }

        status_t PluginWindow::slot_invert_graph_dot_vscroll_changed(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *_this = static_cast<PluginWindow *>(ptr);
            if (_this == NULL)
                return STATUS_OK;
            if (_this->pInvertGraphDotVScroll == NULL)
                return STATUS_OK;
            if (_this->wInvertGraphDotVScroll == NULL)
                return STATUS_OK;

            _this->wInvertGraphDotVScroll->checked()->toggle();
            bool checked = _this->wInvertGraphDotVScroll->checked()->get();
            _this->pInvertGraphDotVScroll->set_value((checked) ? 1.0f : 0.0f);
            _this->pInvertGraphDotVScroll->notify_all(ui::PORT_USER_EDIT);

            return STATUS_OK;
        }

        status_t PluginWindow::slot_ui_behaviour_flag_changed(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *self = static_cast<PluginWindow *>(ptr);
            if (self == NULL)
                return STATUS_OK;

            // Apply changes
            for (size_t i=0, n=self->vBoolFlags.size(); i<n; ++i)
            {
                ui_flag_t *flag = self->vBoolFlags.uget(i);
                if ((flag == NULL) || (flag->wItem != sender) || (flag->pPort == NULL))
                    continue;

                if (flag->wItem == sender)
                {
                    flag->wItem->checked()->toggle();
                    const bool checked = flag->wItem->checked()->get();
                    flag->pPort->set_value((checked) ? 1.0f : 0.0f);
                    flag->pPort->notify_all(ui::PORT_USER_EDIT);
                }
            }

            return STATUS_OK;
        }

        status_t PluginWindow::slot_submit_enum_menu_item(tk::Widget *sender, void *ptr, void *data)
        {
            enum_menu_t *em = static_cast<enum_menu_t *>(ptr);
            if ((em == NULL) || (em->pPort == NULL))
                return STATUS_OK;
            tk::MenuItem *mi = tk::widget_cast<tk::MenuItem>(sender);
            if (mi == NULL)
                return STATUS_OK;
            ssize_t index = em->vItems.index_of(mi);
            if (index < 0)
                return STATUS_OK;
            const meta::port_t *meta = em->pPort->metadata();
            if (meta == NULL)
                return STATUS_OK;

            float value = meta->min + index;
            lsp_trace("id = %s, index = %d, value=%f", meta->id, int(index), value);

            em->pPort->set_value(value);
            em->pPort->notify_all(ui::PORT_USER_EDIT);

            return STATUS_OK;
        }

        status_t PluginWindow::slot_show_user_paths_dialog(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *_this = static_cast<PluginWindow *>(ptr);
            if (_this == NULL)
                return STATUS_OK;

            return _this->show_user_paths_window();
        }

        status_t PluginWindow::slot_user_paths_submit(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *_this = static_cast<PluginWindow *>(ptr);
            if (_this == NULL)
                return STATUS_OK;

            _this->wUserPaths->visibility()->set(false);
            _this->apply_user_paths_settings();

            return STATUS_OK;
        }

        status_t PluginWindow::slot_user_paths_close(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *_this = static_cast<PluginWindow *>(ptr);
            if (_this == NULL)
                return STATUS_OK;

            _this->wUserPaths->visibility()->set(false);

            return STATUS_OK;
        }

        status_t PluginWindow::slot_switch_ab_preset(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *self = static_cast<PluginWindow *>(ptr);
            if (self == NULL)
                return STATUS_OK;

            const size_t active = self->pWrapper->active_preset_data();
            if (sender != self->wPresetSwitch[active])
            {
                status_t res = self->pWrapper->switch_preset_data();
                if (res == STATUS_OK)
                    self->sync_ab_state();
            }

            return STATUS_OK;
        }

        status_t PluginWindow::slot_copy_ab_preset(tk::Widget *sender, void *ptr, void *data)
        {
            PluginWindow *self = static_cast<PluginWindow *>(ptr);
            if (self == NULL)
                return STATUS_OK;

            self->pWrapper->copy_preset_data();

            return STATUS_OK;
        }

        status_t PluginWindow::select_ui_schema(const LSPString & path)
        {
            // Try to load schema first
            status_t res = pWrapper->load_visual_schema(&path);
            if (res != STATUS_OK)
            {
                lsp_warn("Could not load schema '%s': code=%d", path.get_utf8(), int(res));
                return res;
            }

            lsp_trace("Successfully loaded visual schema '%s'", path.get_utf8());

            return STATUS_OK;
        }

        status_t PluginWindow::select_audio_backend(const LSPString & uid)
        {
            return pWrapper->select_backend(&uid);
        }

        void PluginWindow::audio_backend_selected(const LSPString & uid)
        {
            // Get port
            ui::IPort * const port = pAudioBackend;
            if (port == NULL)
                return;

            // Commit new state
            const char * const new_uid = uid.get_utf8();
            if (new_uid == NULL)
                return;

            const char *old_uid = port->buffer<char>();
            if ((old_uid == NULL) || (strcmp(old_uid, new_uid)))
            {
                port->begin_edit();
                lsp_finally { port->end_edit(); };
                port->write(new_uid, strlen(new_uid));
                port->notify_all(ui::PORT_USER_EDIT);
            }
        }

    } /* namespace ctl */
} /* namespace lsp */


