diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index ad1b3abe6..87fceb3a2 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 ${QET_DIR}/sources/scripting/assistantinfo.cpp ${QET_DIR}/sources/scripting/assistantinfo.h ) diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index e73b0ffa6..e014bb41a 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" #include "scripting/assistantinfo.h" #endif #include @@ -711,6 +712,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); @@ -3869,6 +3887,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()