From 4f47093cb2c6e0ec150df1f8b649d513c9e82626 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 2 Oct 2026 09:22:10 +1300 Subject: [PATCH] Script buttons: a manager to write, try and delete stored scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Projet > Scripts > Gérer les scripts… lists the stored scripts with their icons, and the files that get no button with the reason. For each one it edits the name, icon (a file copied next to the script, a theme icon, or the initials), tooltip, shortcut, when it is enabled, and the script itself; "Tester" saves it and runs it on the current project, one Ctrl+Z to undo; "Supprimer" deletes it with its icon if no other script uses it. It only reads and writes the files in the scripts folder, so a script written here, by hand or by an assistant over the qet MCP server is the same thing, and the folder's watcher turns each into a button. ScriptHeader gains compose(), bodyOf() and idFor(), header-only and tested: what the manager writes reads back as what was typed. Co-Authored-By: Claude Opus 5.5 --- cmake/qet_compilation_vars.cmake | 2 + sources/qetdiagrameditor.cpp | 19 ++ sources/qetdiagrameditor.h | 2 + sources/scripting/scriptheader.h | 57 ++++ sources/scripting/scriptmanagerdialog.cpp | 396 ++++++++++++++++++++++ sources/scripting/scriptmanagerdialog.h | 86 +++++ tests/qttest/tst_scriptheader.cpp | 42 +++ 7 files changed, 604 insertions(+) create mode 100644 sources/scripting/scriptmanagerdialog.cpp create mode 100644 sources/scripting/scriptmanagerdialog.h diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 8dad6c023..e744edd88 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -926,6 +926,8 @@ list(APPEND QET_SRC_FILES ${QET_DIR}/sources/scripting/scriptheader.h ${QET_DIR}/sources/scripting/scriptlibrary.cpp ${QET_DIR}/sources/scripting/scriptlibrary.h + ${QET_DIR}/sources/scripting/scriptmanagerdialog.cpp + ${QET_DIR}/sources/scripting/scriptmanagerdialog.h ) if(QET_SPACEMOUSE_ENABLED) diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index 0e0a77e19..33075da0e 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -19,6 +19,7 @@ #ifdef QET_HAS_SCRIPTING #include "scripting/qetscripting.h" #include "scripting/scriptlibrary.h" +#include "scripting/scriptmanagerdialog.h" #endif #include #include @@ -710,6 +711,23 @@ void QETDiagramEditor::setUpActions() " dans le script pour l'API disponible)")); connect(m_run_script, &QAction::triggered, this, &QETDiagramEditor::slot_runScript); + //Write, try, give an icon to and delete stored scripts + m_manage_scripts = new QAction(tr("Gérer les scripts…"), this); + m_manage_scripts->setStatusTip(tr("Écrire un script et en faire un bouton avec une icône")); + connect(m_manage_scripts, &QAction::triggered, this, [this]() { + if (!m_script_manager) { + m_script_manager = new ScriptManagerDialog( + [this](const QString &path, const QString &name) { + runStoredScript(path, name); + }, this); + } + m_script_manager->show(); + m_script_manager->raise(); + m_script_manager->activateWindow(); + }); + ShortcutManager::instance().registerAction(m_manage_scripts, "diagrameditor.manage_scripts", + tr("Éditeur de schémas"), QKeySequence()); + //Stored scripts are files in a folder, written by hand, by the //script manager or by an assistant: open it to add one. m_open_scripts_folder = new QAction(QET::Icons::FolderOpen, tr("Ouvrir le dossier des scripts"), this); @@ -3867,6 +3885,7 @@ void QETDiagramEditor::rebuildScriptActions() } m_scripts_menu->addSeparator(); + m_scripts_menu->addAction(m_manage_scripts); m_scripts_menu->addAction(m_run_script); m_scripts_menu->addAction(m_open_scripts_folder); diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index 8455c4476..d8a48e18e 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -250,6 +250,7 @@ class QETDiagramEditor : public QETMainWindow #ifdef QET_HAS_SCRIPTING *m_run_script, ///< Action to run a JavaScript macro against the current project *m_open_scripts_folder, ///< Action to open the folder stored scripts are read from + *m_manage_scripts, ///< Action to open the script manager #endif *m_export_project_db, ///Export to file the internal database of the current project *m_tile_window, ///< Show MDI subwindows as tile @@ -321,6 +322,7 @@ class QETDiagramEditor : public QETMainWindow QMenu *m_scripts_menu = nullptr; QList m_script_actions; ///< One per stored script, rebuilt when the folder changes bool m_had_scripts = false; + QPointer m_script_manager; #endif QUndoGroup undo_group; diff --git a/sources/scripting/scriptheader.h b/sources/scripting/scriptheader.h index ab2fd494d..0592a929a 100644 --- a/sources/scripting/scriptheader.h +++ b/sources/scripting/scriptheader.h @@ -113,6 +113,63 @@ struct ScriptHeader } return h; } + + /** + @brief compose + A whole script file: the header parse() reads, then @a body. Empty + fields are left out, so a file saved by the script manager and a + hand-written one look alike. + */ + static QString compose(const QString &name, const QString &icon, + const QString &tooltip, const QString &shortcut, + const QString &context, const QString &body) + { + QString text = QStringLiteral("// ==QETScript==\n"); + auto line = [&text](const QString &key, const QString &value) { + const QString v = value.simplified(); + if (!v.isEmpty()) + text += QStringLiteral("// @%1 %2\n").arg(key.leftJustified(8), v); + }; + line(QStringLiteral("name"), name); + line(QStringLiteral("icon"), icon); + line(QStringLiteral("tooltip"), tooltip); + line(QStringLiteral("shortcut"), shortcut); + if (context != QLatin1String("canvas")) line(QStringLiteral("context"), context); + line(QStringLiteral("api"), QStringLiteral("1")); + text += QStringLiteral("// ==/QETScript==\n") + body; + if (!text.endsWith(QLatin1Char('\n'))) text += QLatin1Char('\n'); + return text; + } + + /** + @brief bodyOf + Everything after the header, or the whole text if there is none. + */ + static QString bodyOf(const QString &text) + { + const int end = text.indexOf(QStringLiteral("// ==/QETScript==")); + if (end < 0) return text; + const int next = text.indexOf(QLatin1Char('\n'), end); + return next < 0 ? QString() : text.mid(next + 1); + } + + /** + @brief idFor + A file name for a new script called @a name: lower-case letters, + digits and dashes, accents dropped, not one of @a taken. + */ + static QString idFor(const QString &name, const QStringList &taken) + { + QString base = name.normalized(QString::NormalizationForm_KD).toLower(); + static const QRegularExpression not_kept(QStringLiteral("[^a-z0-9\\s_-]")); + base.remove(not_kept); + base = base.simplified().replace(QLatin1Char(' '), QLatin1Char('-')).left(48); + if (base.isEmpty()) base = QStringLiteral("script"); + QString id = base; + for (int i = 2; taken.contains(id); ++i) + id = base + QLatin1Char('-') + QString::number(i); + return id; + } }; #endif // SCRIPTHEADER_H diff --git a/sources/scripting/scriptmanagerdialog.cpp b/sources/scripting/scriptmanagerdialog.cpp new file mode 100644 index 000000000..2b0554abd --- /dev/null +++ b/sources/scripting/scriptmanagerdialog.cpp @@ -0,0 +1,396 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "scriptmanagerdialog.h" + +#include "scriptlibrary.h" +#include "../qetmessagebox.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +ScriptManagerDialog::ScriptManagerDialog(Runner runner, QWidget *parent) : + QDialog(parent), + m_runner(std::move(runner)) +{ + setWindowTitle(tr("Gérer les scripts")); + setAttribute(Qt::WA_DeleteOnClose); + + m_list = new QListWidget(this); + m_list->setIconSize(QSize(24, 24)); + m_list->setMinimumWidth(220); + auto *new_button = new QPushButton(tr("&Nouveau"), this); + m_delete = new QPushButton(tr("Supp&rimer"), this); + auto *left_buttons = new QHBoxLayout(); + left_buttons->addWidget(new_button); + left_buttons->addWidget(m_delete); + auto *left = new QVBoxLayout(); + left->addWidget(m_list); + left->addLayout(left_buttons); + + m_name = new QLineEdit(this); + m_name->setPlaceholderText(tr("Le texte du bouton")); + m_icon = new QLineEdit(this); + m_icon->setPlaceholderText(tr("vide : les initiales du nom ; builtin: : une icône du thème")); + m_icon_preview = new QToolButton(this); + m_icon_preview->setIconSize(QSize(24, 24)); + m_icon_preview->setToolTip(tr("Choisir une image (SVG ou PNG)…")); + auto *icon_row = new QHBoxLayout(); + icon_row->addWidget(m_icon); + icon_row->addWidget(m_icon_preview); + m_tooltip = new QLineEdit(this); + m_shortcut = new QKeySequenceEdit(this); + m_context = new QComboBox(this); + m_context->addItem(tr("Toujours"), QStringLiteral("canvas")); + m_context->addItem(tr("Avec une sélection"), QStringLiteral("selection")); + m_context->addItem(tr("Avec un conducteur sélectionné"), QStringLiteral("conductor")); + + auto *form = new QFormLayout(); + form->addRow(tr("Nom :"), m_name); + form->addRow(tr("Icône :"), icon_row); + form->addRow(tr("Info-bulle :"), m_tooltip); + form->addRow(tr("Raccourci :"), m_shortcut); + form->addRow(tr("Actif :"), m_context); + + m_body = new QPlainTextEdit(this); + m_body->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + m_body->setLineWrapMode(QPlainTextEdit::NoWrap); + m_body->setMinimumSize(480, 260); + + m_status = new QLabel(this); + m_status->setWordWrap(true); + m_status->setTextInteractionFlags(Qt::TextSelectableByMouse); + + m_test = new QPushButton(tr("&Tester"), this); + m_test->setToolTip(tr("Enregistre puis exécute le script sur le projet courant" + " (Ctrl+Z annule l'exécution)")); + m_save = new QPushButton(tr("&Enregistrer"), this); + auto *folder_button = new QPushButton(tr("Ouvrir le &dossier"), this); + auto *close_button = new QPushButton(tr("&Fermer"), this); + //Enter in a field saves; in the script it is a new line + m_save->setDefault(true); + auto *buttons = new QHBoxLayout(); + buttons->addWidget(m_test); + buttons->addWidget(m_save); + buttons->addStretch(); + buttons->addWidget(folder_button); + buttons->addWidget(close_button); + + auto *right = new QVBoxLayout(); + right->addLayout(form); + right->addWidget(new QLabel(tr("Script (l'objet qet ; qet.currentFolio() est le folio affiché) :"), this)); + right->addWidget(m_body, 1); + right->addWidget(m_status); + right->addLayout(buttons); + + auto *layout = new QHBoxLayout(this); + layout->addLayout(left); + layout->addLayout(right, 1); + + connect(new_button, &QPushButton::clicked, this, &ScriptManagerDialog::newScript); + connect(m_delete, &QPushButton::clicked, this, &ScriptManagerDialog::deleteScript); + connect(m_save, &QPushButton::clicked, this, &ScriptManagerDialog::save); + connect(m_test, &QPushButton::clicked, this, &ScriptManagerDialog::testScript); + connect(m_icon_preview, &QToolButton::clicked, this, &ScriptManagerDialog::chooseIcon); + connect(close_button, &QPushButton::clicked, this, &QDialog::close); + connect(folder_button, &QPushButton::clicked, this, []() { + QDir().mkpath(ScriptLibrary::folder()); + QDesktopServices::openUrl(QUrl::fromLocalFile(ScriptLibrary::folder())); + }); + + auto modified = [this]() { if (!m_loading) setModified(true); }; + connect(m_name, &QLineEdit::textEdited, this, modified); + connect(m_icon, &QLineEdit::textEdited, this, modified); + connect(m_icon, &QLineEdit::textChanged, this, &ScriptManagerDialog::updateIconPreview); + connect(m_name, &QLineEdit::textChanged, this, &ScriptManagerDialog::updateIconPreview); + connect(m_tooltip, &QLineEdit::textEdited, this, modified); + connect(m_shortcut, &QKeySequenceEdit::keySequenceChanged, this, modified); + connect(m_context, &QComboBox::currentIndexChanged, this, modified); + connect(m_body, &QPlainTextEdit::textChanged, this, modified); + + connect(m_list, &QListWidget::currentItemChanged, this, + [this](QListWidgetItem *current, QListWidgetItem *previous) { + if (m_loading || !current) return; + if (!confirmDiscard()) { + m_loading = true; + m_list->setCurrentItem(previous); + m_loading = false; + return; + } + showScript(current->data(Qt::UserRole).toString()); + }); + + //A file written by hand or by an assistant shows up here too + connect(&ScriptLibrary::instance(), &ScriptLibrary::changed, + this, &ScriptManagerDialog::reload); + + reload(); + if (m_list->count()) { + m_list->setCurrentRow(0); + } else { + newScript(); + } +} + +void ScriptManagerDialog::reload() +{ + m_loading = true; + const QString current = m_path; + m_list->clear(); + const ScriptLibrary &library = ScriptLibrary::instance(); + for (const ScriptLibrary::Script &script : library.scripts()) { + auto *item = new QListWidgetItem(ScriptLibrary::icon(script), script.header.name, m_list); + item->setData(Qt::UserRole, script.path); + item->setToolTip(QFileInfo(script.path).fileName()); + if (script.path == current) m_list->setCurrentItem(item); + } + //Listed so they can be opened and fixed here + for (const QString &error : library.errors()) { + const QString file = error.section(QStringLiteral(": "), 0, 0); + const QString path = QDir(ScriptLibrary::folder()).filePath(file); + auto *item = new QListWidgetItem(file, m_list); + item->setData(Qt::UserRole, path); + item->setForeground(palette().color(QPalette::PlaceholderText)); + item->setToolTip(tr("Pas de bouton : %1").arg(error.section(QStringLiteral(": "), 1))); + if (path == current) m_list->setCurrentItem(item); + } + m_loading = false; + //Changed on disk and not here: show what is on disk now + if (!m_modified && !m_path.isEmpty()) { + if (QFileInfo::exists(m_path)) showScript(m_path); + else clearForm(); + } +} + +void ScriptManagerDialog::showScript(const QString &path) +{ + QFile file(path); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return; + const QString text = QString::fromUtf8(file.readAll()); + const ScriptHeader h = ScriptHeader::parse(text, QFileInfo(path).completeBaseName()); + + m_loading = true; + m_path = path; + m_name->setText(h.name); + m_icon->setText(h.icon); + m_tooltip->setText(h.tooltip); + m_shortcut->setKeySequence(QKeySequence::fromString(h.shortcut)); + m_context->setCurrentIndex(qMax(0, m_context->findData(h.context))); + m_body->setPlainText(ScriptHeader::bodyOf(text)); + m_loading = false; + setModified(false); + m_status->setText(h.isValid() + ? QDir::toNativeSeparators(path) + : tr("Pas de bouton pour ce fichier : %1").arg(h.error)); +} + +void ScriptManagerDialog::clearForm() +{ + m_loading = true; + m_path.clear(); + m_name->clear(); + m_icon->clear(); + m_tooltip->clear(); + m_shortcut->clear(); + m_context->setCurrentIndex(0); + m_body->clear(); + m_loading = false; + setModified(false); + m_status->clear(); +} + +void ScriptManagerDialog::newScript() +{ + if (!confirmDiscard()) return; + m_loading = true; + m_list->clearSelection(); + m_list->setCurrentItem(nullptr); + m_loading = false; + clearForm(); + m_loading = true; + m_name->setText(tr("Nouveau script")); + m_body->setPlainText(tr( + "// qet.currentFolio() est le folio affiché ; un clic s'annule\n" + "// d'un seul Ctrl+Z. Liste des appels : qet.apiSignatures()\n" + "var f = qet.currentFolio();\n" + "qet.addText(f, \"Texte\", 40, 40);\n")); + m_loading = false; + setModified(true); + m_status->setText(tr("Pas encore enregistré")); + m_name->setFocus(); + m_name->selectAll(); +} + +/** + @brief ScriptManagerDialog::save + Write the file; the library's watcher turns it into a button. + @return true if written +*/ +bool ScriptManagerDialog::save() +{ + const QString name = m_name->text().simplified(); + if (name.isEmpty()) { + m_status->setText(tr("Le script doit avoir un nom.")); + m_name->setFocus(); + return false; + } + QDir dir(ScriptLibrary::folder()); + dir.mkpath(QStringLiteral(".")); + QString path = m_path; + if (path.isEmpty()) { + QStringList taken; + for (const QFileInfo &info : dir.entryInfoList({QStringLiteral("*.js")}, QDir::Files)) + taken << info.completeBaseName(); + path = dir.filePath(ScriptHeader::idFor(name, taken) + QStringLiteral(".js")); + } + + const QString text = ScriptHeader::compose(name, m_icon->text(), m_tooltip->text(), + m_shortcut->keySequence().toString(QKeySequence::PortableText), + m_context->currentData().toString(), m_body->toPlainText()); + const ScriptHeader h = ScriptHeader::parse(text, QFileInfo(path).completeBaseName()); + if (!h.isValid()) { + m_status->setText(tr("Non enregistré : %1").arg(h.error)); + return false; + } + + //QSaveFile renames into place, so the watcher never reads half a file + QSaveFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text) + || file.write(text.toUtf8()) < 0 || !file.commit()) { + m_status->setText(tr("Impossible d'écrire %1").arg(QDir::toNativeSeparators(path))); + return false; + } + m_path = path; + setModified(false); + m_status->setText(QDir::toNativeSeparators(path)); + return true; +} + +void ScriptManagerDialog::deleteScript() +{ + if (m_path.isEmpty()) { + clearForm(); + return; + } + const auto answer = QET::QetMessageBox::question( + this, tr("Supprimer le script"), + tr("Supprimer « %1 » et son bouton ?").arg(m_name->text()), + QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel); + if (answer != QMessageBox::Yes) return; + + const QString icon = m_icon->text(); + QFile::remove(m_path); + //Its icon too, unless another script uses it + if (!icon.isEmpty() && !icon.startsWith(QLatin1String("builtin:")) + && !icon.contains(QLatin1Char('/')) && !icon.contains(QLatin1Char('\\'))) { + bool used = false; + for (const ScriptLibrary::Script &s : ScriptLibrary::instance().scripts()) + if (s.path != m_path && s.header.icon == icon) used = true; + if (!used) QFile::remove(QDir(ScriptLibrary::folder()).filePath(icon)); + } + clearForm(); +} + +void ScriptManagerDialog::testScript() +{ + if (m_modified || m_path.isEmpty()) { + if (!save()) return; + } + if (m_runner) m_runner(m_path, m_name->text().simplified()); +} + +/** + @brief ScriptManagerDialog::chooseIcon + Copy an image into the scripts folder, so the script and its icon + travel together, and name it in the header. +*/ +void ScriptManagerDialog::chooseIcon() +{ + const QString source = QFileDialog::getOpenFileName( + this, tr("Icône du script"), QString(), + tr("Images (*.svg *.png)")); + if (source.isEmpty()) return; + QDir dir(ScriptLibrary::folder()); + dir.mkpath(QStringLiteral(".")); + const QString name = QFileInfo(source).fileName(); + const QString target = dir.filePath(name); + if (QFileInfo(source).absoluteFilePath() != QFileInfo(target).absoluteFilePath()) { + QFile::remove(target); + if (!QFile::copy(source, target)) { + m_status->setText(tr("Impossible de copier %1").arg(QDir::toNativeSeparators(source))); + return; + } + } + m_icon->setText(name); + setModified(true); +} + +void ScriptManagerDialog::updateIconPreview() +{ + ScriptLibrary::Script script; + script.header.name = m_name->text(); + script.header.icon = m_icon->text().trimmed(); + script.path = m_path.isEmpty() ? QDir(ScriptLibrary::folder()).filePath(QStringLiteral("new.js")) + : m_path; + m_icon_preview->setIcon(ScriptLibrary::icon(script)); +} + +void ScriptManagerDialog::setModified(bool modified) +{ + m_modified = modified; + m_save->setEnabled(modified); + m_delete->setEnabled(!m_path.isEmpty() || modified); + setWindowTitle(tr("Gérer les scripts") + (modified ? QStringLiteral(" *") : QString())); +} + +bool ScriptManagerDialog::confirmDiscard() +{ + if (!m_modified) return true; + const auto answer = QET::QetMessageBox::question( + this, tr("Gérer les scripts"), + tr("Le script affiché n'est pas enregistré. Enregistrer ?"), + QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, + QMessageBox::Save); + if (answer == QMessageBox::Cancel) return false; + if (answer == QMessageBox::Save) return save(); + setModified(false); + return true; +} + +void ScriptManagerDialog::closeEvent(QCloseEvent *event) +{ + if (confirmDiscard()) event->accept(); + else event->ignore(); +} diff --git a/sources/scripting/scriptmanagerdialog.h b/sources/scripting/scriptmanagerdialog.h new file mode 100644 index 000000000..6203db31e --- /dev/null +++ b/sources/scripting/scriptmanagerdialog.h @@ -0,0 +1,86 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef SCRIPTMANAGERDIALOG_H +#define SCRIPTMANAGERDIALOG_H + +#include +#include + +class QComboBox; +class QKeySequenceEdit; +class QLabel; +class QLineEdit; +class QListWidget; +class QPlainTextEdit; +class QPushButton; +class QToolButton; + +/** + @brief The ScriptManagerDialog class + Projet > Scripts > Gérer les scripts… : write a stored script, give its + button a name, icon, tooltip, shortcut and context, try it, delete it. + + It only ever reads and writes the files in ScriptLibrary::folder(), + the same files a text editor or the qet MCP server would write: the + library's watcher turns them into buttons. So the dialog, a hand-written + file and an assistant can never disagree about what a script is. +*/ +class ScriptManagerDialog : public QDialog +{ + Q_OBJECT + + public: + using Runner = std::function; + ScriptManagerDialog(Runner runner, QWidget *parent = nullptr); + + protected: + void closeEvent(QCloseEvent *event) override; + + private: + void reload(); + void showScript(const QString &path); + void clearForm(); + bool save(); + void newScript(); + void deleteScript(); + void testScript(); + void chooseIcon(); + void updateIconPreview(); + void setModified(bool modified); + bool confirmDiscard(); + + Runner m_runner; + QString m_path; ///< the file shown, empty for a new script not yet saved + bool m_modified = false; + bool m_loading = false; + + QListWidget *m_list; + QLineEdit *m_name; + QLineEdit *m_icon; + QToolButton *m_icon_preview; + QLineEdit *m_tooltip; + QKeySequenceEdit *m_shortcut; + QComboBox *m_context; + QPlainTextEdit *m_body; + QLabel *m_status; + QPushButton *m_save; + QPushButton *m_delete; + QPushButton *m_test; +}; + +#endif // SCRIPTMANAGERDIALOG_H diff --git a/tests/qttest/tst_scriptheader.cpp b/tests/qttest/tst_scriptheader.cpp index f43e98dd4..b072f94e7 100644 --- a/tests/qttest/tst_scriptheader.cpp +++ b/tests/qttest/tst_scriptheader.cpp @@ -83,6 +83,48 @@ private slots: QCOMPARE(h.name, QStringLiteral("Real")); } + // What the script manager writes reads back as what was typed, and + // leaves the script's code as it was. + void composeReadsBack() + { + const QString body = QStringLiteral("var f = qet.currentFolio();\nqet.addText(f, 'x', 0, 0);\n"); + const QString text = ScriptHeader::compose( + QStringLiteral(" Add note "), QStringLiteral("note.svg"), + QStringLiteral("A tip"), QStringLiteral("Ctrl+Alt+N"), + QStringLiteral("conductor"), body); + const ScriptHeader h = ScriptHeader::parse(text, QStringLiteral("add-note")); + QVERIFY2(h.isValid(), qPrintable(h.error)); + QCOMPARE(h.name, QStringLiteral("Add note")); + QCOMPARE(h.icon, QStringLiteral("note.svg")); + QCOMPARE(h.tooltip, QStringLiteral("A tip")); + QCOMPARE(h.shortcut, QStringLiteral("Ctrl+Alt+N")); + QCOMPARE(h.context, QStringLiteral("conductor")); + QCOMPARE(ScriptHeader::bodyOf(text), body); + + // empty fields and the default context are left out + const QString bare = ScriptHeader::compose(QStringLiteral("A"), QString(), QString(), + QString(), QStringLiteral("canvas"), + QStringLiteral("qet.log(1);")); + QCOMPARE(bare, QStringLiteral("// ==QETScript==\n// @name A\n// @api 1\n" + "// ==/QETScript==\nqet.log(1);\n")); + } + + void bodyWithoutHeader() + { + QCOMPARE(ScriptHeader::bodyOf(QStringLiteral("qet.log(1);\n")), + QStringLiteral("qet.log(1);\n")); + } + + void idFor() + { + QCOMPARE(ScriptHeader::idFor(QStringLiteral("Numéroter les fils !"), {}), + QStringLiteral("numeroter-les-fils")); + QCOMPARE(ScriptHeader::idFor(QStringLiteral("Add note"), + {QStringLiteral("add-note"), QStringLiteral("add-note-2")}), + QStringLiteral("add-note-3")); + QCOMPARE(ScriptHeader::idFor(QStringLiteral("!!!"), {}), QStringLiteral("script")); + } + // Two windows register one id; the id stays until both are gone, and a // script that comes back under a new name is listed by that name. void unregisterAction()