• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 34039060546

06 Sep 2026 02:24PM UTC coverage: 26.369% (+1.8%) from 24.579%
34039060546

push

github

jcelerier
3rdparty: updat libossia

60871 of 230846 relevant lines covered (26.37%)

63602.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

36.72
/src/plugins/score-lib-process/Process/Script/ScriptEditor.cpp
1
#include "ScriptEditor.hpp"
2

3
#include "MultiScriptEditor.hpp"
4
#include "ScriptTabBar.hpp"
5
#include "ScriptWidget.hpp"
6

7
#include <score/application/GUIApplicationContext.hpp>
8
#include <score/tools/FileWatch.hpp>
9
#include <score/widgets/SetIcons.hpp>
10

11
#include <QCodeEditor>
12
#include <QCoreApplication>
13
#include <QDialogButtonBox>
14
#include <QDir>
15
#include <QFile>
16
#include <QKeyEvent>
17
#include <QMainWindow>
18
#include <QMessageBox>
19
#include <QPlainTextEdit>
20
#include <QProcess>
21
#include <QPushButton>
22
#include <QSettings>
23
#include <QStandardPaths>
24
#include <QTabWidget>
25
#include <QVBoxLayout>
26

27
namespace Process
28
{
29
// Ctrl+Return compiles. The code editor emits livecodeTrigger for it, but
30
// only when the modifiers are exactly Control: the numeric keypad's Enter
31
// carries KeypadModifier. So the key is also caught here, on the editors
32
// themselves, before anything else looks at it. (With the completion popup
33
// open the key goes to the popup first and completes instead.)
34
class CompileKeyFilter final : public QObject
35
{
36
public:
37
  CompileKeyFilter(QObject* parent, std::function<void()> compile)
66✔
38
      : QObject{parent}
66✔
39
      , m_compile{std::move(compile)}
66✔
40
  {
66✔
41
  }
66✔
42

43
  static bool isCompileKey(QKeyEvent* ke) noexcept
24✔
44
  {
45
    const bool enter = ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter;
24✔
46
    const auto mods = ke->modifiers() & ~Qt::KeypadModifier;
24✔
47
    return enter && mods == Qt::ControlModifier;
24✔
48
  }
49

50
  bool eventFilter(QObject* obj, QEvent* ev) override
8,007✔
51
  {
52
    switch(ev->type())
8,007✔
53
    {
54
      case QEvent::ShortcutOverride:
55
      case QEvent::KeyPress: {
56
        auto ke = static_cast<QKeyEvent*>(ev);
24✔
57
        if(!isCompileKey(ke))
24✔
58
          return false;
12✔
59
        ke->accept();
12✔
60
        if(ev->type() == QEvent::KeyPress)
12✔
61
          m_compile();
6✔
62
        return true;
12✔
63
      }
64
      default:
65
        return false;
7,983✔
66
    }
67
  }
8,007✔
68

69
private:
70
  std::function<void()> m_compile;
71
};
72

73
static QObject* addCompileShortcuts(QDialog* dialog, std::function<void()> compile)
66✔
74
{
75
  auto filter = new CompileKeyFilter{dialog, std::move(compile)};
66✔
76
  dialog->installEventFilter(filter);
66✔
77
  for(auto w : dialog->findChildren<QWidget*>())
1,056✔
78
    w->installEventFilter(filter);
990✔
79
  return filter;
66✔
80
}
×
81

82
ScriptDialog::ScriptDialog(
×
83
    const std::string_view language, const score::DocumentContext& ctx, QWidget* parent)
84
    : QDialog{parent}
×
85
    , m_context{ctx}
×
86
{
×
87
  this->resize(800, 800);
×
88
  auto lay = new QVBoxLayout{this};
×
89
  this->setLayout(lay);
×
90

91
  m_textedit = createScriptWidget(language);
×
92

93
  m_error = new QPlainTextEdit{this};
×
94
  m_error->setReadOnly(true);
×
95
  m_error->setMaximumHeight(120);
×
96
  m_error->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
×
97

98
  lay->addWidget(m_textedit);
×
99
  lay->addWidget(m_error);
×
100
  lay->setStretch(0, 3);
×
101
  lay->setStretch(1, 1);
×
102
  auto bbox = new QDialogButtonBox{
×
103
      QDialogButtonBox::Ok | QDialogButtonBox::Reset | QDialogButtonBox::Close, this};
×
104
  if(auto editorPath = QSettings{}.value("Skin/DefaultEditor").toString();
×
105
     !editorPath.isEmpty())
×
106
  {
107
    auto openExternalBtn = new QPushButton{tr("Edit in default editor"), this};
×
108
    connect(openExternalBtn, &QPushButton::clicked, this, [this, editorPath] {
×
109
      openInExternalEditor(editorPath);
×
110
    });
×
111
    bbox->addButton(openExternalBtn, QDialogButtonBox::HelpRole);
×
112
    openExternalBtn->setToolTip(
×
113
        tr("Edit in the default editor set in Score Settings > User Interface"));
×
114
    auto icon = makeIcons(
×
115
        QStringLiteral(":/icons/undock_on.png"),
×
116
        QStringLiteral(":/icons/undock_off.png"),
×
117
        QStringLiteral(":/icons/undock_off.png"));
×
118
    openExternalBtn->setIcon(icon);
×
119
  }
×
120
  bbox->button(QDialogButtonBox::Ok)->setText(tr("Compile"));
×
121
  bbox->button(QDialogButtonBox::Reset)->setText(tr("Clear log"));
×
122
  connect(bbox->button(QDialogButtonBox::Reset), &QPushButton::clicked, this, [this] {
×
123
    m_error->clear();
×
124
  });
×
125
  lay->addWidget(bbox);
×
126

127
  auto ce = qobject_cast<QCodeEditor*>(m_textedit);
×
128
  connect(ce, &QCodeEditor::livecodeTrigger, this, &ScriptDialog::on_accepted);
×
129
  m_compileFilter = addCompileShortcuts(this, [this] { on_accepted(); });
×
130
  connect(bbox, &QDialogButtonBox::accepted, this, &ScriptDialog::on_accepted);
×
131
  connect(bbox, &QDialogButtonBox::rejected, this, &QDialog::reject);
×
132
}
×
133

134
ScriptDialog::~ScriptDialog()
×
135
{
×
136
  stopWatchingFile();
×
137
}
×
138

139
// Escape closes a QDialog, which is fine for an editor in its own window.
140
// Docked in the main window it would tear the editor down in the middle of
141
// a set from a key that also dismisses the completion popup and the search
142
// bar (they handle it themselves before it gets here).
143
static bool swallowEscape(const QDialog& dialog, QKeyEvent* event)
3✔
144
{
145
  if(event->key() == Qt::Key_Escape && !dialog.isWindow())
3✔
146
  {
147
    event->accept();
2✔
148
    return true;
2✔
149
  }
150
  return false;
1✔
151
}
3✔
152

153
void ScriptDialog::keyPressEvent(QKeyEvent* event)
×
154
{
155
  if(swallowEscape(*this, event))
×
156
    return;
×
157
  QDialog::keyPressEvent(event);
×
158
}
×
159

160
void MultiScriptDialog::keyPressEvent(QKeyEvent* event)
3✔
161
{
162
  if(swallowEscape(*this, event))
3✔
163
    return;
2✔
164
  QDialog::keyPressEvent(event);
1✔
165
}
3✔
166

167
void ScriptDialog::hideEvent(QHideEvent* event)
×
168
{
169
#if defined(__EMSCRIPTEN__)
170
  // Return keyboard/text focus to the main window: on wasm each window owns its
171
  // own hidden text-input element and focus follows the active window, so
172
  // without this the main window's text fields stay dead after closing here.
173
  if(auto mw = score::GUIAppContext().mainWindow)
174
  {
175
    mw->activateWindow();
176
    mw->raise();
177
  }
178
#endif
179
  QDialog::hideEvent(event);
×
180
}
×
181

182
QString ScriptDialog::text() const noexcept
×
183
{
184
  return m_textedit->document()->toPlainText();
×
185
}
186

187
void ScriptDialog::setText(const QString& str)
×
188
{
189
  if(str != text())
×
190
  {
191
    m_textedit->setPlainText(str);
×
192
  }
×
193
}
×
194

195
void ScriptDialog::setError(int line, const QString& str)
×
196
{
197
  m_error->setPlainText(str);
×
198
}
×
199

200
void ScriptDialog::openInExternalEditor(const QString& editorPath)
×
201
{
202
#if QT_CONFIG(process)
203
  if(editorPath.isEmpty())
×
204
  {
205
    QMessageBox::warning(
×
206
        this, tr("Error"), tr("no 'Default editor' configured in score settings"));
×
207
    return;
×
208
  }
209

210
  auto& w = score::FileWatch::instance();
×
211

212
  m_watchedFile = QStandardPaths::writableLocation(QStandardPaths::TempLocation)
×
213
                  + "/ossia_script_temp.js";
×
214
  if(m_fileHandle)
×
215
  {
216
    // we have to unwatch the previous file first
217
    w.remove(m_watchedFile, m_fileHandle);
×
218
    m_fileHandle.reset();
×
219
  }
×
220

221
  QFile file(m_watchedFile);
×
222
  if(!file.open(QIODevice::WriteOnly))
×
223
  {
224
    QMessageBox::warning(this, tr("Error"), tr("failed to create temporary file."));
×
225
    m_watchedFile.clear();
×
226
    return;
×
227
  }
228

229
  file.write(this->text().toUtf8());
×
230

231
  m_fileHandle = std::make_shared<std::function<void()>>([this]() {
×
232
    QFile file(m_watchedFile);
×
233
    if(file.open(QIODevice::ReadOnly))
×
234
    {
235
      QMetaObject::invokeMethod(
×
236
          m_textedit, [this, str = QString::fromUtf8(file.readAll())] {
×
237
        if(m_textedit)
×
238
        {
239
          m_textedit->setPlainText(str);
×
240
          on_accepted();
×
241
        }
×
242
      });
×
243
    }
×
244
  });
×
245

246
  if(!QProcess::startDetached(editorPath, QStringList{m_watchedFile}))
×
247
  {
248
    QMessageBox::warning(this, tr("Error"), tr("failed to launch external editor"));
×
249
    m_watchedFile.clear();
×
250
    m_fileHandle.reset();
×
251
  }
×
252
  else
253
  {
254
    w.add(m_watchedFile, m_fileHandle);
×
255
  }
256
#endif
257
}
×
258

259
void ScriptDialog::stopWatchingFile()
×
260
{
261
  if(m_watchedFile.isEmpty())
×
262
    return;
×
263

264
  auto& w = score::FileWatch::instance();
×
265
  w.remove(m_watchedFile, m_fileHandle);
×
266
  m_watchedFile.clear();
×
267
  m_fileHandle.reset();
×
268
}
×
269

270
MultiScriptDialog::MultiScriptDialog(const score::DocumentContext& ctx, QWidget* parent)
66✔
271
    : QDialog{parent}
66✔
272
    , m_context{ctx}
66✔
273
{
66✔
274
  this->resize(800, 800);
66✔
275
  auto lay = new QVBoxLayout{this};
66✔
276
  this->setLayout(lay);
66✔
277

278
  m_tabs = new ScriptTabWidget;
66✔
279
  lay->addWidget(m_tabs);
66✔
280

281
  m_error = new QPlainTextEdit;
66✔
282
  m_error->setReadOnly(true);
66✔
283
  m_error->setMaximumHeight(120);
66✔
284
  m_error->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
66✔
285

286
  lay->addWidget(m_error);
66✔
287

288
  auto bbox = new QDialogButtonBox{
66✔
289
      QDialogButtonBox::Ok | QDialogButtonBox::Reset | QDialogButtonBox::Close, this};
66✔
290

291
  lay->addWidget(bbox);
66✔
292

293
  bbox->button(QDialogButtonBox::Ok)->setText(tr("Compile"));
66✔
294
  bbox->button(QDialogButtonBox::Reset)->setText(tr("Clear log"));
66✔
295
  connect(bbox->button(QDialogButtonBox::Reset), &QPushButton::clicked, this, [this] {
66✔
296
    m_error->clear();
×
297
  });
×
298

299
  m_compileFilter = addCompileShortcuts(this, [this] { on_accepted(); });
72✔
300
  connect(bbox, &QDialogButtonBox::accepted, this, &MultiScriptDialog::on_accepted);
66✔
301
  connect(bbox, &QDialogButtonBox::rejected, this, &QDialog::reject);
66✔
302

303
  if(auto editorPath = QSettings{}.value("Skin/DefaultEditor").toString();
66✔
304
     !editorPath.isEmpty())
132✔
305
  {
306
    auto openExternalBtn = new QPushButton{tr("Edit in default editor"), this};
×
307
    connect(openExternalBtn, &QPushButton::clicked, this, [this, editorPath] {
×
308
      openInExternalEditor(editorPath);
×
309
    });
×
310
    bbox->addButton(openExternalBtn, QDialogButtonBox::HelpRole);
×
311
    openExternalBtn->setToolTip(
×
312
        tr("Edit in the default editor set in Score Settings > User Interface"));
×
313
    auto icon = makeIcons(
×
314
        QStringLiteral(":/icons/undock_on.png"),
×
315
        QStringLiteral(":/icons/undock_off.png"),
×
316
        QStringLiteral(":/icons/undock_off.png"));
×
317
    openExternalBtn->setIcon(icon);
×
318
  }
×
319
}
×
320

321
void MultiScriptDialog::addTab(
132✔
322
    const QString& name, const QString& text, const std::string_view language)
323
{
324
  auto textedit = createScriptWidget(language);
132✔
325
  textedit->setText(text);
132✔
326
  auto ce = qobject_cast<QCodeEditor*>(textedit);
132✔
327
  connect(ce, &QCodeEditor::livecodeTrigger, this, &MultiScriptDialog::on_accepted);
132✔
328

329
  m_tabs->addTab(textedit, name);
132✔
330
  m_editors.push_back({textedit});
132✔
331
  if(m_compileFilter)
132✔
332
    textedit->installEventFilter(m_compileFilter);
132✔
333
}
132✔
334

335
void MultiScriptDialog::hideEvent(QHideEvent* event)
16✔
336
{
337
#if defined(__EMSCRIPTEN__)
338
  if(auto mw = score::GUIAppContext().mainWindow)
339
  {
340
    mw->activateWindow();
341
    mw->raise();
342
  }
343
#endif
344
  QDialog::hideEvent(event);
16✔
345
}
16✔
346

347
std::vector<QString> MultiScriptDialog::text() const noexcept
18✔
348
{
349
  std::vector<QString> vec;
18✔
350
  vec.reserve(m_editors.size());
18✔
351
  for(const auto& tab : m_editors)
54✔
352
    vec.push_back(tab.textedit->document()->toPlainText());
36✔
353
  return vec;
18✔
354
}
18✔
355

356
void MultiScriptDialog::setText(int idx, const QString& str)
24✔
357
{
358
  SCORE_ASSERT(idx >= 0);
24✔
359
  SCORE_ASSERT(std::size_t(idx) < m_editors.size());
24✔
360

361
  auto textEdit = m_editors[idx].textedit;
24✔
362
  if(str != textEdit->document()->toPlainText())
24✔
363
  {
364
    textEdit->setPlainText(str);
×
365
  }
×
366
}
24✔
367

368
void MultiScriptDialog::setError(const QString& str)
6✔
369
{
370
  m_error->setPlainText(str);
6✔
371
}
6✔
372

373
void MultiScriptDialog::clearError()
6✔
374
{
375
  m_error->clear();
6✔
376
}
6✔
377

378
void MultiScriptDialog::openInExternalEditor(const QString& editorPath)
×
379
{
380
#if QT_CONFIG(process)
381
  if(editorPath.isEmpty())
×
382
  {
383
    QMessageBox::warning(
×
384
        this, tr("Error"), tr("no 'Default editor' configured in score settings"));
×
385
    return;
×
386
  }
387

388
  if(!QFile::exists(editorPath))
×
389
  {
390
    QMessageBox::warning(
×
391
        this, tr("Error"), tr("the configured external editor does not exist."));
×
392
    return;
×
393
  }
394

395
  QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
×
396
  QDir dir(tempDir);
×
397
  QStringList openedFiles;
×
398

399
  for(int i = 0; i < m_tabs->count(); ++i)
×
400
  {
401
    QString tabName = m_tabs->tabText(i);
×
402
    QString tempFile = tempDir + "/" + tabName + ".js";
×
403

404
    QFile file(tempFile);
×
405
    if(!file.open(QIODevice::WriteOnly))
×
406
    {
407
      QMessageBox::warning(this, tr("Error"), tr("failed to create temporary files."));
×
408
      return;
×
409
    }
410

411
    QWidget* widget = m_tabs->widget(i);
×
412
    QTextEdit* textedit = qobject_cast<QTextEdit*>(widget);
×
413

414
    if(textedit)
×
415
    {
416
      QString content = textedit->document()->toPlainText();
×
417
      file.write(content.toUtf8());
×
418
    }
×
419

420
    openedFiles.append(tempFile);
×
421
  }
×
422

423
  if(!openedFiles.isEmpty() && !QProcess::startDetached(editorPath, openedFiles))
×
424
  {
425
    QMessageBox::warning(this, tr("Error"), tr("Failed to launch external editor"));
×
426
  }
×
427
#endif
428
}
×
429

430
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc