// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "../luaengine.h" #include "utils.h" #include #include #include #include #include #include using namespace Utils; using namespace Core; using namespace std::string_view_literals; namespace Lua::Internal { class LuaAspectContainer : public AspectContainer { public: LuaAspectContainer() {} sol::object dynamic_get(const std::string &key) { auto it = m_entries.find(key); if (it == m_entries.cend()) { return sol::lua_nil; } return it->second; } void dynamic_set(const std::string &key, sol::main_object value) { if (!value.is()) throw std::runtime_error("AspectContainer can only contain BaseAspect instances"); registerAspect(value.as(), false); auto it = m_entries.find(key); if (it == m_entries.cend()) { m_entries.insert(it, {std::move(key), std::move(value)}); } else { std::pair &kvp = *it; sol::object &entry = kvp.second; entry = sol::object(std::move(value)); } } size_t size() const { return m_entries.size(); } public: std::unordered_map m_entries; }; std::unique_ptr aspectContainerCreate(const sol::main_table &options) { auto container = std::make_unique(); for (const auto &[k, v] : options) { if (k.is()) { std::string key = k.as(); if (key == "autoApply") { container->setAutoApply(v.as()); } else if (key == "layouter") { if (v.is()) container->setLayouter( [func = v.as()]() -> Layouting::Layout { auto res = safe_call(func); QTC_ASSERT_RESULT(res, return {}); return *res; }); } else if (key == "onApplied") { QObject::connect( container.get(), &AspectContainer::applied, container.get(), [func = v.as()] { void_safe_call(func); }); } else if (key == "settingsGroup") { container->setSettingsGroup(v.as()); } else { if (v.is()) { container->dynamic_set(key, v); } else { qWarning() << "Unknown key:" << key.c_str(); } } } } container->readSettings(); return container; } void baseAspectCreate(BaseAspect *aspect, const std::string &key, const sol::object &value) { if (key == "settingsKey") aspect->setSettingsKey(keyFromString(value.as())); else if (key == "displayName") aspect->setDisplayName(value.as()); else if (key == "labelText") aspect->setLabelText(value.as()); else if (key == "toolTip") aspect->setToolTip(value.as()); else if (key == "onValueChanged") { QObject::connect( aspect, &BaseAspect::changed, aspect, [func = value.as()]() { void_safe_call(func); }); } else if (key == "onVolatileValueChanged") { QObject::connect( aspect, &BaseAspect::volatileValueChanged, aspect, [func = value.as()] { void_safe_call(func); }); } else if (key == "enabler") aspect->setEnabler(value.as()); else if (key == "macroExpander") { if (value.is()) aspect->setMacroExpander(nullptr); else aspect->setMacroExpander(value.as()); } else qWarning() << "Unknown key:" << key.c_str(); } template void typedAspectCreate(T *aspect, const std::string &key, const sol::object &value) { if (key == "defaultValue") aspect->setDefaultValue(value.as()); else if (key == "value") aspect->setValue(value.as()); else baseAspectCreate(aspect, key, value); } template<> void typedAspectCreate(StringAspect *aspect, const std::string &key, const sol::object &value) { if (key == "displayStyle") aspect->setDisplayStyle((StringAspect::DisplayStyle) value.as()); else if (key == "historyId") aspect->setHistoryCompleter(value.as().toLocal8Bit()); else if (key == "valueAcceptor") aspect->setValueAcceptor( [func = value.as()](const QString &oldValue, const QString &newValue) -> std::optional { auto res = safe_call>(func, oldValue, newValue); QTC_ASSERT_RESULT(res, return std::nullopt); return *res; }); else if (key == "showToolTipOnLabel") aspect->setShowToolTipOnLabel(value.as()); else if (key == "displayFilter") aspect->setDisplayFilter([func = value.as()](const QString &value) { auto res = safe_call(func, value); QTC_ASSERT_RESULT(res, return value); return *res; }); else if (key == "placeHolderText") aspect->setPlaceHolderText(value.as()); else if (key == "acceptRichText") aspect->setAcceptRichText(value.as()); else if (key == "autoApplyOnEditingFinished") aspect->setAutoApplyOnEditingFinished(value.as()); else if (key == "elideMode") aspect->setElideMode((Qt::TextElideMode) value.as()); else if (key == "rightSideIconPath") aspect->setRightSideIconPath(value.as()); else if (key == "minimumHeight") aspect->setMinimumHeight(value.as()); else if (key == "completer") aspect->setCompleter(value.as()); else if (key == "addOnRightSideIconClicked") { aspect->addOnRightSideIconClicked(aspect, [func = value.as()]() { void_safe_call(func); }); } else typedAspectCreate(static_cast *>(aspect), key, value); } template<> void typedAspectCreate(FilePathAspect *aspect, const std::string &key, const sol::object &value) { if (key == "defaultPath") aspect->setDefaultPathValue(value.as()); else if (key == "historyId") aspect->setHistoryCompleter(value.as().toLocal8Bit()); else if (key == "promptDialogFilter") aspect->setPromptDialogFilter(value.as()); else if (key == "promptDialogTitle") aspect->setPromptDialogTitle(value.as()); else if (key == "commandVersionArguments") aspect->setCommandVersionArguments(value.as()); else if (key == "allowPathFromDevice") aspect->setAllowPathFromDevice(value.as()); else if (key == "validatePlaceHolder") aspect->setValidatePlaceHolder(value.as()); else if (key == "openTerminalHandler") aspect->setOpenTerminalHandler([func = value.as()]() { auto res = void_safe_call(func); QTC_CHECK_RESULT(res); }); else if (key == "expectedKind") aspect->setExpectedKind((PathChooser::Kind) value.as()); else if (key == "environment") aspect->setEnvironment(value.as()); else if (key == "baseFileName") aspect->setBaseDirectory(value.as()); else if (key == "valueAcceptor") aspect->setValueAcceptor( [func = value.as()](const QString &oldValue, const QString &newValue) -> std::optional { auto res = safe_call>(func, oldValue, newValue); QTC_ASSERT_RESULT(res, return std::nullopt); return *res; }); else if (key == "showToolTipOnLabel") aspect->setShowToolTipOnLabel(value.as()); else if (key == "autoApplyOnEditingFinished") aspect->setAutoApplyOnEditingFinished(value.as()); /*else if (key == "validationFunction") aspect->setValidationFunction( [func = value.as()](const QString &path) { return func.call>(path); }); */ else if (key == "displayFilter") aspect->setDisplayFilter([func = value.as()](const QString &path) { auto res = safe_call(func, path); QTC_ASSERT_RESULT(res, return path); return *res; }); else if (key == "placeHolderText") aspect->setPlaceHolderText(value.as()); else typedAspectCreate(static_cast *>(aspect), key, value); } template<> void typedAspectCreate(BoolAspect *aspect, const std::string &key, const sol::object &value) { if (key == "labelPlacement") { aspect->setLabelPlacement((BoolAspect::LabelPlacement) value.as()); } else { typedAspectCreate(static_cast *>(aspect), key, value); } } template std::unique_ptr createAspectFromTable( const sol::table &options, const std::function &f) { auto aspect = std::make_unique(); for (const auto &[k, v] : options) { if (k.template is()) { f(aspect.get(), k.template as(), v); } } return aspect; } template void addTypedAspectBaseBindings(sol::table &lua) { lua.new_usertype>("TypedAspect", "value", sol::property(&TypedAspect::value, [](TypedAspect *a, const T &v) { a->setValue(v); }), "volatileValue", sol::property(&TypedAspect::volatileValue, [](TypedAspect *a, const T &v) { a->setVolatileValue(v); }), "defaultValue", sol::property(&TypedAspect::defaultValue), sol::base_classes, sol::bases()); } template sol::usertype addTypedAspect(sol::table &lua, const QString &name) { addTypedAspectBaseBindings(lua); return lua.new_usertype( name, "create", [](const sol::main_table &options) { return createAspectFromTable(options, &typedAspectCreate); }, sol::base_classes, sol::bases, BaseAspect>()); } class ObjectPool { public: mutable std::vector> optionsPages; template std::shared_ptr makePage(_Args &&...__args) const { auto page = std::make_shared(std::forward<_Args>(__args)...); optionsPages.push_back(page); return page; } }; void setupSettingsModule() { registerProvider("Settings", [pool = ObjectPool()](sol::state_view lua) -> sol::object { const ScriptPluginSpec *pluginSpec = lua.get("PluginSpec"sv); sol::table async = lua.script("return require('async')", "_process_").get(); sol::function wrap = async["wrap"]; sol::table settings = lua.create_table(); settings.new_usertype( "Aspect", "apply", &BaseAspect::apply, "writeSettings", &BaseAspect::writeSettings, "readSettings", &BaseAspect::readSettings); settings.new_usertype( "AspectContainer", "create", &aspectContainerCreate, "apply", &LuaAspectContainer::apply, sol::meta_function::index, &LuaAspectContainer::dynamic_get, sol::meta_function::new_index, &LuaAspectContainer::dynamic_set, sol::meta_function::length, &LuaAspectContainer::size, sol::base_classes, sol::bases()); addTypedAspect(settings, "BoolAspect"); addTypedAspect(settings, "ColorAspect"); addTypedAspect(settings, "MultiSelectionAspect"); addTypedAspect(settings, "StringAspect"); settings.new_usertype( "SecretAspect", "create", [](const sol::main_table &options) { return createAspectFromTable( options, [](SecretAspect *aspect, const std::string &key, const sol::object &value) { if (key == "settingsKey") aspect->setSettingsKey(keyFromString(value.as())); if (key == "labelText") aspect->setLabelText(value.as()); if (key == "toolTip") aspect->setToolTip(value.as()); else if (key == "displayName") aspect->setDisplayName(value.as()); }); }, "requestValue_cb", [](SecretAspect *aspect, sol::function callback) { aspect->requestValue([callback](const Result &secret) { if (secret) { auto res = void_safe_call(callback, true, secret.value()); QTC_CHECK_RESULT(res); } else { auto res = void_safe_call(callback, false, secret.error()); QTC_CHECK_RESULT(res); } }); }, "setValue", [](SecretAspect *aspect, const QString &value) { aspect->setValue(value); }, sol::base_classes, sol::bases()); settings["SecretAspect"]["requestValue"] = wrap( settings["SecretAspect"]["requestValue_cb"]); settings.new_usertype( "SelectionAspect", "create", [](const sol::main_table &options) { return createAspectFromTable( options, [](SelectionAspect *aspect, const std::string &key, const sol::object &value) { if (key == "options") { sol::table options = value.as(); for (size_t i = 1; i <= options.size(); ++i) { sol::optional optiontable = options[i].get>(); if (optiontable) { sol::table option = *optiontable; sol::optional data = option["data"]; if (data) { aspect->addOption( {option["name"], option["toolTip"].get_or(QString()), QVariant::fromValue(*data)}); } else { aspect->addOption( option["name"], option["toolTip"].get_or(QString())); } } else if ( sol::optional name = options[i].get>()) { aspect->addOption(*name); } else { throw sol::error("Invalid option type"); } } } else if (key == "displayStyle") { aspect->setDisplayStyle((SelectionAspect::DisplayStyle) value.as()); } else typedAspectCreate(aspect, key, value); }); }, "stringValue", sol::property(&SelectionAspect::stringValue, &SelectionAspect::setStringValue), "dataValue", sol::property([](SelectionAspect *aspect) { return qvariant_cast(aspect->itemValue()); }), "addOption", sol::overload( [](SelectionAspect &self, const QString &name) { self.addOption(name); }, [](SelectionAspect &self, const QString &name, const QString &toolTip) { self.addOption(name, toolTip); }, [](SelectionAspect &self, const QString &name, const QString &toolTip, const sol::object &data) { self.addOption({name, toolTip, QVariant::fromValue(data)}); }), sol::base_classes, sol::bases, BaseAspect>()); auto filePathAspectType = addTypedAspect(settings, "FilePathAspect"); filePathAspectType.set( "setValue", sol::overload( [](FilePathAspect &self, const QString &value) { self.setValue(FilePath::fromUserInput(value)); }, [](FilePathAspect &self, const FilePath &value) { self.setValue(value); })); filePathAspectType.set("expandedValue", sol::property(&FilePathAspect::expandedValue)); filePathAspectType.set( "defaultPath", sol::property( [](FilePathAspect &self) { return FilePath::fromUserInput(self.defaultValue()); }, &FilePathAspect::setDefaultPathValue)); addTypedAspect(settings, "IntegerAspect"); addTypedAspect(settings, "DoubleAspect"); addTypedAspect(settings, "StringListAspect"); addTypedAspect(settings, "FilePathListAspect"); addTypedAspect(settings, "IntegersAspect"); addTypedAspect(settings, "StringSelectionAspect"); settings.new_usertype( "ToggleAspect", "create", [](const sol::main_table &options) { return createAspectFromTable( options, [](ToggleAspect *aspect, const std::string &key, const sol::object &value) { if (key == "offIcon") aspect->setOffIcon(toIcon(value.as())->icon()); else if (key == "offTooltip") aspect->setOffTooltip(value.as()); else if (key == "onIcon") aspect->setOnIcon(toIcon(value.as())->icon()); else if (key == "onTooltip") aspect->setOnTooltip(value.as()); else if (key == "onText") aspect->setOnText(value.as()); else if (key == "offText") aspect->setOffText(value.as()); else typedAspectCreate(aspect, key, value); }); }, "action", &ToggleAspect::action, sol::base_classes, sol::bases, BaseAspect>()); static auto triStateFromString = [](const QString &str) -> TriState { const QString l = str.toLower(); if (l == "enabled") return TriState::Enabled; else if (l == "disabled") return TriState::Disabled; else if (l == "default") return TriState::Default; else return TriState::Default; }; static auto triStateToString = [](TriState state) -> QString { if (state == TriState::Enabled) return "enabled"; else if (state == TriState::Disabled) return "disabled"; return "default"; }; settings.new_usertype( "TriStateAspect", "create", [](const sol::main_table &options) { return createAspectFromTable( options, [](TriStateAspect *aspect, const std::string &key, const sol::object &value) { if (key == "defaultValue") aspect->setDefaultValue(triStateFromString(value.as())); else if (key == "value") aspect->setValue(triStateFromString(value.as())); else baseAspectCreate(aspect, key, value); }); }, "value", sol::property( [](TriStateAspect *a) { return triStateToString(a->value()); }, [](TriStateAspect *a, const QString &v) { a->setValue(triStateFromString(v)); }), "volatileValue", sol::property( [](TriStateAspect *a) { return triStateToString(TriState::fromInt(a->volatileValue())); }, [](TriStateAspect *a, const QString &v) { a->setVolatileValue(triStateFromString(v).toInt()); }), "defaultValue", sol::property([](TriStateAspect *a) { return triStateToString(a->defaultValue()); }), sol::base_classes, sol::bases, BaseAspect>()); settings.new_usertype( "TextDisplay", "create", [](const sol::main_table &options) { return createAspectFromTable( options, [](TextDisplay *aspect, const std::string &key, const sol::object &value) { if (key == "text") { aspect->setText(value.as()); } else if (key == "iconType") { const QString type = value.as().toLower(); if (type.isEmpty() || type == "None") aspect->setIconType(InfoLabel::InfoType::None); else if (type == "information") aspect->setIconType(InfoLabel::InfoType::Information); else if (type == "warning") aspect->setIconType(InfoLabel::InfoType::Warning); else if (type == "error") aspect->setIconType(InfoLabel::InfoType::Error); else if (type == "ok") aspect->setIconType(InfoLabel::InfoType::Ok); else if (type == "notok") aspect->setIconType(InfoLabel::InfoType::NotOk); else aspect->setIconType(InfoLabel::InfoType::None); } else { baseAspectCreate(aspect, key, value); } }); }, sol::base_classes, sol::bases()); settings.new_usertype( "AspectList", "create", [](const sol::main_table &options) { return createAspectFromTable( options, [](AspectList *aspect, const std::string &key, const sol::object &value) { if (key == "createItemFunction") { aspect->setCreateItemFunction( [func = value.as()]() -> std::shared_ptr { auto res = safe_call>(func); QTC_ASSERT_RESULT(res, return nullptr); return *res; }); } else if (key == "onItemAdded") { aspect->setItemAddedCallback([func = value.as()]( std::shared_ptr item) { auto res = void_safe_call(func, item); QTC_CHECK_RESULT(res); }); } else if (key == "onItemRemoved") { aspect->setItemRemovedCallback([func = value.as()]( std::shared_ptr item) { auto res = void_safe_call(func, item); QTC_CHECK_RESULT(res); }); } else { baseAspectCreate(aspect, key, value); } }); }, "createAndAddItem", &AspectList::createAndAddItem, "foreach", [](AspectList *a, const sol::function &clbk) { a->forEachItem([clbk](std::shared_ptr item) { auto res = void_safe_call(clbk, item); QTC_CHECK_RESULT(res); }); }, "enumerate", [](AspectList *a, const sol::function &clbk) { a->forEachItem([clbk](std::shared_ptr item, int idx) { auto res = void_safe_call(clbk, item, idx); QTC_CHECK_RESULT(res); }); }, sol::base_classes, sol::bases()); class ExtensionOptionsPage : public Core::IOptionsPage { public: ExtensionOptionsPage(const ScriptPluginSpec *spec, AspectContainer *container) { setId(Id::fromString(QString("Extension.%2").arg(spec->id))); setCategory(Id("ExtensionManager")); setDisplayName(spec->name); if (container->isAutoApply()) throw sol::error("AspectContainer must have autoApply set to false"); setSettingsProvider([container]() { return container; }); } }; class OptionsPage : public Core::IOptionsPage { public: OptionsPage(const ScriptPluginSpec *spec, const sol::table &options) { setCategory(Id::fromString( QString("%1.%2").arg(spec->id).arg(options.get("categoryId"sv)))); const QString catName = options.get("displayCategory"sv); const FilePath catIcon = options.get>("categoryIconPath"sv) .value_or(FilePath::fromUserInput( options.get_or("categoryIconPath"sv, {}))); if (!catName.isEmpty() || !catIcon.isEmpty()) IOptionsPage::registerCategory(category(), catName, catIcon); setId(Id::fromString( QString("%1.%2").arg(spec->id).arg(options.get("id"sv)))); setDisplayName(options.get("displayName"sv)); AspectContainer *container = options.get("aspectContainer"sv); if (container->isAutoApply()) throw sol::error("AspectContainer must have autoApply set to false"); setSettingsProvider([container]() { return container; }); } }; settings.new_usertype( "OptionsPage", "create", [&pool, pluginSpec](const sol::main_table &options) { return pool.makePage(pluginSpec, options); }, "show", [](OptionsPage *page) { Core::ICore::showOptionsDialog(page->id()); }); settings.new_usertype( "ExtensionOptionsPage", "create", [pluginSpec, &pool](AspectContainer *container) { return pool.makePage(pluginSpec, container); }, "show", [](ExtensionOptionsPage *page) { Core::ICore::showOptionsDialog(page->id()); }); // clang-format off settings["StringDisplayStyle"] = lua.create_table_with( "Label", StringAspect::DisplayStyle::LabelDisplay, "LineEdit", StringAspect::DisplayStyle::LineEditDisplay, "TextEdit", StringAspect::DisplayStyle::TextEditDisplay, "PasswordLineEdit", StringAspect::DisplayStyle::PasswordLineEditDisplay ); settings["SelectionDisplayStyle"] = lua.create_table_with( "RadioButtons", SelectionAspect::DisplayStyle::RadioButtons, "ComboBox", SelectionAspect::DisplayStyle::ComboBox ); settings["CheckBoxPlacement"] = lua.create_table_with( "Top", CheckBoxPlacement::Top, "Right", CheckBoxPlacement::Right ); settings["Kind"] = lua.create_table_with( "ExistingDirectory", PathChooser::Kind::ExistingDirectory, "Directory", PathChooser::Kind::Directory, "File", PathChooser::Kind::File, "SaveFile", PathChooser::Kind::SaveFile, "ExistingCommand", PathChooser::Kind::ExistingCommand, "Command", PathChooser::Kind::Command, "Any", PathChooser::Kind::Any ); settings["LabelPlacement"] = lua.create_table_with( "AtCheckBox", BoolAspect::LabelPlacement::AtCheckBox, "Compact", BoolAspect::LabelPlacement::Compact, "InExtraLabel", BoolAspect::LabelPlacement::InExtraLabel ); // clang-format on return settings; }); } } // namespace Lua::Internal