File indexing completed on 2025-02-02 04:11:27
0001 /* 0002 * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best> 0003 * 0004 * SPDX-License-Identifier: GPL-3.0-or-later 0005 */ 0006 0007 #include "script_console.hpp" 0008 #include "ui_script_console.h" 0009 0010 #include <QEvent> 0011 #include <QRegularExpression> 0012 0013 #include <KCompletion> 0014 0015 #include "app/settings/settings.hpp" 0016 #include "app/scripting/script_engine.hpp" 0017 #include "plugin/plugin.hpp" 0018 #include "widgets/dialogs/plugin_ui_dialog.hpp" 0019 0020 using namespace glaxnimate::gui; 0021 using namespace glaxnimate; 0022 0023 class ScriptConsole::Private 0024 { 0025 public: 0026 Ui::ScriptConsole ui; 0027 0028 std::vector<app::scripting::ScriptContext> script_contexts; 0029 const plugin::Plugin* current_plugin = nullptr; 0030 ScriptConsole* parent; 0031 std::map<QString, QVariant> globals; 0032 QRegularExpression re_completion{R"(^[a-zA-Z_0-9.\s\[\]]+$)"}; 0033 KCompletion completion; 0034 0035 bool ensure_script_contexts() 0036 { 0037 if ( script_contexts.empty() ) 0038 { 0039 create_script_context(); 0040 if ( script_contexts.empty() ) 0041 return false; 0042 } 0043 0044 return true; 0045 } 0046 0047 bool execute_script ( const plugin::Plugin& plugin, const plugin::PluginScript& script, const QVariantList& args ) 0048 { 0049 if ( !ensure_script_contexts() ) 0050 return false; 0051 0052 for ( const auto& ctx : script_contexts ) 0053 { 0054 if ( ctx->engine() == plugin.data().engine ) 0055 { 0056 current_plugin = &plugin; 0057 bool ok = false; 0058 try { 0059 ok = ctx->run_from_module(plugin.data().dir, script.module, script.function, args); 0060 if ( !ok ) 0061 parent->error(plugin.data().name, i18n("Could not run the plugin")); 0062 } catch ( const app::scripting::ScriptError& err ) { 0063 console_error(err); 0064 parent->error(plugin.data().name, i18n("Plugin raised an exception")); 0065 ok = false; 0066 } 0067 current_plugin = nullptr; 0068 return ok; 0069 } 0070 } 0071 0072 parent->error(plugin.data().name, i18n("Could not find an interpreter")); 0073 return false; 0074 } 0075 0076 void set_completions(const QString& prefix) 0077 { 0078 auto match = re_completion.match(prefix); 0079 if ( !match.hasMatch() ) 0080 { 0081 ui.console_input->completionObject()->setItems({}); 0082 return; 0083 } 0084 0085 if ( !ensure_script_contexts() ) 0086 return; 0087 0088 int last_dot = prefix.lastIndexOf('.'); 0089 QString evaluated; 0090 if ( last_dot != -1 ) 0091 evaluated = prefix.left(last_dot); 0092 0093 auto ctx = script_contexts[ui.console_language->currentIndex()].get(); 0094 auto completions = ctx->eval_completions(evaluated); 0095 if ( !evaluated.isEmpty() ) 0096 { 0097 for ( auto& item : completions ) 0098 item = evaluated + "." + item; 0099 } 0100 ui.console_input->completionObject()->setItems(completions); 0101 } 0102 0103 void run_snippet(const QString& text, bool echo) 0104 { 0105 if ( !ensure_script_contexts() ) 0106 return; 0107 0108 auto c = ui.console_output->textCursor(); 0109 0110 if ( echo ) 0111 console_stdout("> " + text); 0112 0113 auto ctx = script_contexts[ui.console_language->currentIndex()].get(); 0114 try { 0115 QString out = ctx->eval_to_string(text); 0116 if ( !out.isEmpty() ) 0117 console_stdout(out); 0118 } catch ( const app::scripting::ScriptError& err ) { 0119 console_error(err); 0120 } 0121 0122 c.clearSelection(); 0123 c.movePosition(QTextCursor::End); 0124 ui.console_output->setTextCursor(c); 0125 } 0126 0127 void console_commit(QString text) 0128 { 0129 if ( text.isEmpty() ) 0130 return; 0131 0132 run_snippet(text.replace("\n", " "), true); 0133 0134 ui.console_input->addToHistory(text); 0135 ui.console_input->clearEditText(); 0136 } 0137 0138 0139 void console_stderr(const QString& line) 0140 { 0141 ui.console_output->setTextColor(Qt::red); 0142 ui.console_output->append(line); 0143 } 0144 0145 void console_stdout(const QString& line) 0146 { 0147 ui.console_output->setTextColor(parent->palette().text().color()); 0148 ui.console_output->append(line); 0149 } 0150 0151 void console_error(const app::scripting::ScriptError& err) 0152 { 0153 console_stderr(err.message()); 0154 } 0155 0156 void create_script_context() 0157 { 0158 for ( const auto& engine : app::scripting::ScriptEngineFactory::instance().engines() ) 0159 { 0160 auto ctx = engine->create_context(); 0161 0162 if ( !ctx ) 0163 continue; 0164 0165 connect(ctx.get(), &app::scripting::ScriptExecutionContext::stdout_line, [this](const QString& s){ console_stdout(s);}); 0166 connect(ctx.get(), &app::scripting::ScriptExecutionContext::stderr_line, [this](const QString& s){ console_stderr(s);}); 0167 0168 try { 0169 ctx->app_module("glaxnimate"); 0170 ctx->app_module("glaxnimate_gui"); 0171 for ( const auto& p : globals ) 0172 ctx->expose(p.first, p.second); 0173 } catch ( const app::scripting::ScriptError& err ) { 0174 console_error(err); 0175 } 0176 0177 script_contexts.push_back(std::move(ctx)); 0178 } 0179 } 0180 0181 PluginUiDialog * create_dialog(const QString& ui_file) 0182 { 0183 if ( !current_plugin ) 0184 return nullptr; 0185 0186 if ( !current_plugin->data().dir.exists(ui_file) ) 0187 { 0188 current_plugin->logger().stream(app::log::Error) << "UI file not found:" << ui_file; 0189 return nullptr; 0190 } 0191 0192 QFile file(current_plugin->data().dir.absoluteFilePath(ui_file)); 0193 if ( !file.open(QIODevice::ReadOnly) ) 0194 { 0195 current_plugin->logger().stream(app::log::Error) << "Could not open UI file:" << ui_file; 0196 return nullptr; 0197 } 0198 0199 return new PluginUiDialog(file, *current_plugin, parent); 0200 } 0201 }; 0202 0203 ScriptConsole::ScriptConsole(QWidget* parent) 0204 : QWidget(parent), d(std::make_unique<Private>()) 0205 { 0206 d->ui.setupUi(this); 0207 d->parent = this; 0208 0209 d->ui.console_input->setHistoryItems(app::settings::get<QStringList>("scripting", "history")); 0210 d->ui.console_input->completionObject()->setCompletionMode(KCompletion::CompletionPopup); 0211 d->ui.console_input->completionObject()->setOrder(KCompletion::Sorted); 0212 connect(d->ui.console_input, &KHistoryComboBox::completion, this, [this](const QString& text){ d->set_completions(text); }); 0213 0214 for ( const auto& engine : app::scripting::ScriptEngineFactory::instance().engines() ) 0215 { 0216 d->ui.console_language->addItem(engine->label()); 0217 if ( engine->slug() == "python" ) 0218 d->ui.console_language->setCurrentIndex(d->ui.console_language->count()-1); 0219 } 0220 0221 connect(d->ui.btn_reload, &QAbstractButton::clicked, this, &ScriptConsole::clear_contexts); 0222 } 0223 0224 ScriptConsole::~ScriptConsole() = default; 0225 0226 void ScriptConsole::changeEvent ( QEvent* e ) 0227 { 0228 QWidget::changeEvent(e); 0229 0230 if ( e->type() == QEvent::LanguageChange) 0231 { 0232 d->ui.retranslateUi(this); 0233 } 0234 } 0235 0236 void ScriptConsole::console_clear() 0237 { 0238 d->ui.console_output->clear(); 0239 } 0240 0241 void ScriptConsole::console_commit(const QString& command) 0242 { 0243 d->console_commit(command); 0244 } 0245 0246 bool ScriptConsole::execute(const plugin::Plugin& plugin, const plugin::PluginScript& script, const QVariantList& args) 0247 { 0248 return d->execute_script(plugin, script, args); 0249 } 0250 0251 QVariant ScriptConsole::get_global(const QString& name) 0252 { 0253 auto it = d->globals.find(name); 0254 if ( it != d->globals.end() ) 0255 return it->second; 0256 return {}; 0257 } 0258 0259 void ScriptConsole::set_global(const QString& name, const QVariant& value) 0260 { 0261 d->globals[name] = value; 0262 } 0263 0264 void ScriptConsole::clear_contexts() 0265 { 0266 d->script_contexts.clear(); 0267 } 0268 0269 void ScriptConsole::clear_output() 0270 { 0271 if ( !d->ui.check_persist->isChecked() ) 0272 console_clear(); 0273 } 0274 0275 PluginUiDialog* ScriptConsole::create_dialog(const QString& ui_file) const 0276 { 0277 return d->create_dialog(ui_file); 0278 } 0279 0280 void ScriptConsole::save_settings() 0281 { 0282 QStringList history = d->ui.console_input->historyItems(); 0283 int max_history = app::settings::get<int>("scripting", "max_history"); 0284 if ( history.size() > max_history ) 0285 history.erase(history.begin(), history.end() - max_history); 0286 app::settings::set("scripting", "history", history); 0287 } 0288 0289 void ScriptConsole::run_snippet(const QString& source) 0290 { 0291 d->run_snippet(source, false); 0292 }