• 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

57.06
/src/plugins/score-plugin-js/JS/JSProcessModel.cpp
1
// This is an open source non-commercial project. Dear PVS-Studio, please check
2
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
3

4
#include "JSProcessModel.hpp"
5

6
#include <State/Expression.hpp>
7

8
#include <Process/Dataflow/Port.hpp>
9
#include <Process/ExternalFiles.hpp>
10
#include <Process/PresetHelpers.hpp>
11

12
#include <JS/ApplicationPlugin.hpp>
13
#include <JS/Commands/EditScript.hpp>
14
#include <JS/Executor/ExecutionHelpers.hpp>
15
#include <JS/JSProcessMetadata.hpp>
16
#include <JS/Qml/QmlObjects.hpp>
17
#include <Library/LibrarySettings.hpp>
18

19
#include <score/application/GUIApplicationContext.hpp>
20
#include <score/command/Dispatchers/MultiOngoingCommandDispatcher.hpp>
21
#include <score/command/Dispatchers/SingleOngoingCommandDispatcher.hpp>
22
#include <score/document/DocumentInterface.hpp>
23
#include <score/model/Identifier.hpp>
24
#include <score/serialization/VisitorCommon.hpp>
25
#include <score/tools/DeleteAll.hpp>
26
#include <score/tools/File.hpp>
27

28
#include <core/document/Document.hpp>
29

30
#include <QDebug>
31
#include <QDir>
32
#include <QFileInfo>
33
#include <QFileSystemWatcher>
34
#include <QGuiApplication>
35
#include <QPointer>
36
#include <QQmlComponent>
37
#include <QQuickItem>
38
#include <QQuickWindow>
39
#include <QStandardPaths>
40

41
#include <wobjectimpl.h>
42

43
#include <vector>
44
W_OBJECT_IMPL(JS::ProcessModel)
1,946✔
45
namespace JS
46
{
47
static constexpr const char* default_js_program =
48
    R"_(import Score
49
import QtQuick
50
// This is a minimal example script that showcases the available API.
51
// View the complete documentation at
52
// https://ossia.io/score-docs/processes/javascript.html
53
Script {
54
  ValueInlet { id: in1; objectName: "Value In" }
55
  ValueOutlet { id: out1; objectName: "Value Out" }
56
  FloatSlider { id: sl; min: 10; max: 100; objectName: "Control" }
57

58
  // Called on every tick
59
  tick: function(token, state)
60
  {
61
    if (typeof in1.value !== 'undefined')
62
    {
63
      console.log(in1.value);
64
      out1.value = in1.value * mx + sl.value * my;
65
    }
66
  }
67
})_";
68
static constexpr const char* default_js_ui =
69
    R"_()_";
70

71
ProcessModel::ProcessModel(
32✔
72
    const TimeVal& duration, const QString& data, const Id<Process::ProcessModel>& id,
73
    QObject* parent)
74
    : Process::ProcessModel{
32✔
75
        duration, id, Metadata<ObjectKey_k, ProcessModel>::get(), parent}
32✔
76
{
32✔
77
  if(data.isEmpty())
32✔
78
  {
79
    (void)setProgram({default_js_program, default_js_ui});
32✔
80
  }
32✔
81
  else
82
  {
83
    if(!data.endsWith(".qml")) {
×
84
      (void)setProgram({data, {}});
×
85
    }
×
86
    else {
87
      auto path = data;
×
88
      QFile f{path};
×
89
      m_root = path;
×
90
      QString exec_data;
×
91
      if(f.open(QIODevice::ReadOnly))
×
92
        exec_data = f.readAll();
×
93

94
      path.resize(data.size() - 3);
×
95
      path.append("ui.qml");
×
96
      QString ui_data;
×
97
      if(QFile ui_f{path}; ui_f.open(QIODevice::ReadOnly)) {
×
98
        ui_data = ui_f.readAll();
×
99
      }
×
100
      (void)setProgram({exec_data, ui_data});
×
101
    }
×
102
  }
103

104
  metadata().setInstanceName(*this);
32✔
105
}
×
106

107
Process::ProcessFlags ProcessModel::flags() const noexcept
913✔
108
{
109
  auto flags = Metadata<Process::ProcessFlags_k, JS::ProcessModel>::get();
913✔
110
  if(m_ui_component)
913✔
111
    // The UI is a Qt Quick scene in a window container: it can be docked.
112
    flags |= Process::ExternalUIAvailable | Process::ExternalUIEmbeddable;
145✔
113
  return flags;
913✔
114
}
115

116
ProcessModel::~ProcessModel()
74✔
117
{
37✔
118
  if(this->externalUI)
37✔
119
  {
120
    this->externalUI->close();
1✔
121
    this->externalUI = nullptr;
1✔
122
  }
1✔
123
}
74✔
124

125
void ProcessModel::mapExternalFiles(Process::ExternalFileMap& map)
3✔
126
{
127
  Process::ProcessModel::mapExternalFiles(map);
3✔
128

129
  auto& ctx = score::IDocument::documentContext(*this);
3✔
130

131
  // The QML import root. Relocating it means collecting a whole include tree,
132
  // which score does not attempt: report it so the user knows the other
133
  // machine needs it.
134
  if(!m_root.isEmpty())
3✔
135
    map.readOnly(m_root, score::FileKind::Script);
×
136

137
  // A script is stored either inline or as a path to a .qml file; only the
138
  // latter is an external dependency.
139
  QmlSource next = m_program;
3✔
140
  bool changed = false;
3✔
141
  const auto relocate = [&](QString& script) {
9✔
142
    if(!Process::looksLikeExistingFile(script, ctx))
6✔
143
      return;
6✔
144

145
    const QString relocated = map.map(
×
146
        {.path = script,
×
147
         .kind = score::FileKind::Script,
148
         .usage = Process::FileUsage::Input,
149
         .directory = false,
150
         .rewritable = true,
151
         .owner = map.owner});
×
152
    if(relocated.isEmpty())
×
153
      return;
×
154

155
    script = relocated;
×
156
    changed = true;
×
157
  };
6✔
158
  relocate(next.execution);
3✔
159
  relocate(next.ui);
3✔
160

161
  if(changed)
3✔
162
    map.addCommand(new JS::EditScript{*this, next, ctx});
×
163
}
3✔
164

165
QString ProcessModel::rootPath() const noexcept
52✔
166
{
167
  if(!m_root.isEmpty())
52✔
168
  {
169
    return m_root;
×
170
  }
171
  else
172
  {
173
    // Not cached: tests run several applications in one process
174
    const auto& lib = score::AppContext().settings<Library::Settings::Model>();
52✔
175

176
    return lib.getDefaultLibraryPath() + QDir::separator() + "Scripts"
104✔
177
           + QDir::separator() + "include" + QDir::separator() + "Script/Script.qml";
52✔
178
  }
179
}
52✔
180

181
bool ProcessModel::validate(const std::vector<QString>& script) const noexcept
6✔
182
{
183
  if(script.empty())
6✔
184
    return false;
×
185
  if(script[0].isEmpty())
6✔
186
    return false;
×
187

188
  const auto trimmed = script[0].trimmed();
6✔
189
  const QByteArray data = trimmed.toUtf8();
6✔
190

191
  auto path = score::locateFilePath(trimmed, score::IDocument::documentContext(*this));
6✔
192

193
  if(QFileInfo::exists(path))
6✔
194
  {
195
    return (bool)m_cache.getExecution(*this, path.toUtf8(), true);
×
196
  }
197
  else
198
  {
199
    if(!data.startsWith("import"))
6✔
200
      return false;
×
201
    return (bool)m_cache.getExecution(*this, data, false);
6✔
202
  }
203
}
6✔
204

205

206
QString ProcessModel::effect() const noexcept
76✔
207
{
208
  return m_qmlData;
76✔
209
}
210

211
QQuickItem* ProcessModel::createItemForUI(const score::DocumentContext& ctx) const noexcept
23✔
212
{
213
  if(!m_ui_component)
23✔
214
    return nullptr;
×
215
  auto& dummyEngine =  ctx.app
23✔
216
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
23✔
217
                          .m_scriptProcessUIEngine;
23✔
218

219
  auto obj = m_ui_component->beginCreate(dummyEngine.rootContext());
23✔
220

221
  if(!obj)
23✔
222
    return nullptr;
×
223
  auto script = qobject_cast<ScriptUI*>(obj);
23✔
224
  auto self = const_cast<JS::ProcessModel*>(this);
23✔
225
  if(script) {
23✔
226
    script->setProcess(self);
23✔
227
  }
23✔
228
  m_ui_component->completeCreate();
23✔
229

230
  if(!script) {
23✔
231
    delete obj;
×
232
    return nullptr;
×
233
  }
234

235
  if(const auto& on_exec = script->executionEvent(); on_exec.isCallable())
23✔
236
  {
237
    connect(this, &JS::ProcessModel::executionToUi,
×
238
            script, [&dummyEngine, on_exec] (const QVariant& v) {
×
239
      on_exec.call({dummyEngine.toScriptValue(v)});
×
240
    });
×
241
  }
×
242

243
  connect(script, &ScriptUI::executionSend, this, [self](const QJSValue& v) {
23✔
244
    self->uiToExecution(v.toVariant());
×
245
  });
×
246

247
  struct StateUpdater
248
  {
249
    const score::DocumentContext& ctx;
250
    JS::ProcessModel& self;
251
    std::unique_ptr<MultiOngoingCommandDispatcher> disp;
252
    int count = 0;
253

254
    void beginUpdateState(const QString& name)
×
255
    {
256
      if(count > 0) {
×
257
        count++;
×
258
      }
×
259
      else {
260
        disp = std::make_unique<MultiOngoingCommandDispatcher>(ctx.commandStack);
×
261
        count = 1;
×
262
      }
263
    }
×
264

265
    void endUpdateState()
×
266
    {
267
      if(!disp)
×
268
        return;
×
269
      count--;
×
270
      if(count > 0)
×
271
        return;
×
272

273
      disp->commit<JS::UpdateStateMacro>();
×
274
      disp.reset();
×
275
      count = 0;
×
276
    }
×
277

278
    void updateState(const QString& k, const QJSValue& v)
×
279
    {
280
      beginUpdateState("Update");
×
281

282
      disp->submit<JS::UpdateStateElement>(self, k, ossia::qt::value_from_js(v));
×
283

284
      endUpdateState();
×
285
    }
×
286

287
    void cancelUpdateState()
×
288
    {
289
      if(!disp)
×
290
        return;
×
291

292
      disp->rollback();
×
293
      disp.reset();
×
294
      count = 0;
×
295
    }
×
296

297
    void clearState()
×
298
    {
299
      beginUpdateState("Clear");
×
300

301
      disp->submit<JS::ReplaceState>(self, JS::JSState{});
×
302

303
      endUpdateState();
×
304
    }
×
305

306
    void replaceState(const QJSValue& v)
×
307
    {
308
      beginUpdateState("Replace");
×
309

310
      JS::JSState cur;
×
311
      auto var = v.toVariant().toMap();
×
312
      for(auto it = var.constBegin(); it != var.constEnd(); ++it) {
×
313
        if(it.value().isValid()) {
×
314
          cur.insert_or_assign(it.key(), ossia::qt::qt_to_ossia{}(it.value()));
×
315
        }
×
316
      }
×
317
      disp->submit<JS::ReplaceState>(self, std::move(cur));
×
318

319
      endUpdateState();
×
320
    }
×
321
  };
322
  auto updater = std::make_shared<StateUpdater>(StateUpdater{ctx, *self});
23✔
323

324
  connect(script, &ScriptUI::beginUpdateState,
46✔
325
          this, [updater] (const QString& name) {
23✔
326
    updater->beginUpdateState(name);
×
327
  });
×
328
  connect(script, &ScriptUI::updateState,
46✔
329
          this, [updater] (const QString& name, const QJSValue& v) {
23✔
330
    updater->updateState(name, v);
×
331
  });
×
332
  connect(script, &ScriptUI::endUpdateState,
46✔
333
          this, [updater] () {
23✔
334
    updater->endUpdateState();
×
335
  });
×
336
  connect(script, &ScriptUI::cancelUpdateState,
46✔
337
          this, [updater] () {
23✔
338
    updater->cancelUpdateState();
×
339
  });
×
340
  connect(script, &ScriptUI::clearState,
46✔
341
          this, [updater] () {
23✔
342
    updater->clearState();
×
343
  });
×
344
  connect(script, &ScriptUI::replaceState,
46✔
345
          this, [updater] (const QJSValue& v) {
23✔
346
    updater->replaceState(v);
×
347
  });
×
348

349
  if(const auto& on_stateUpdated = script->stateUpdated(); on_stateUpdated.isCallable())
23✔
350
  {
351
    connect(
×
352
        this, &JS::ProcessModel::stateElementChanged, script,
×
353
        [on_stateUpdated, &dummyEngine](const QString& k, const ossia::value& v) {
×
354
      if(v.valid())
×
355
      {
356
        if(auto res = v.apply(ossia::qt::ossia_to_qvariant{}); res.isValid())
×
357
          on_stateUpdated.call({k, dummyEngine.toScriptValue(res)});
×
358
        else
359
          on_stateUpdated.call({k, QJSValue{}});
×
360
      }
×
361
      else
362
        on_stateUpdated.call({k, QJSValue{}});
×
363
    }, Qt::QueuedConnection);
×
364
  }
×
365

366
  if(const auto& on_load = script->loadState(); on_load.isCallable())
23✔
367
  {
368
    QVariantMap vm;
×
369
    for(auto& [k, v]: this->m_state) {
×
370
      if(auto res = v.apply(ossia::qt::ossia_to_qvariant{}); res.isValid())
×
371
        vm[k] = std::move(res);
×
372
    }
373
    on_load.call({dummyEngine.toScriptValue(vm)});
×
374
  }
×
375

376
  return script;
23✔
377
}
23✔
378

379
QWidget* ProcessModel::createWindowForUI(const score::DocumentContext& ctx,
20✔
380
                                         QWidget* parent) const noexcept
381
{
382
  m_ui_object = createItemForUI(ctx);
20✔
383
  if(!m_ui_object)
20✔
384
    return nullptr;
×
385

386
  // A close is deferred: by the time this runs the process may show a new
387
  // UI, which must be left alone. The container pointer is only compared,
388
  // the widget may be half-destroyed.
389
  const auto cleanup_ui = [this](QQuickWindow* win, QWidget* container) {
39✔
390
    if(m_ui_object && m_ui_object->window() == win)
19✔
391
    {
392
      delete m_ui_object;
×
393
      m_ui_object = nullptr;
×
394
    }
×
395

396
    if(externalUI == container)
19✔
397
    {
398
      const_cast<QWidget*&>(externalUI) = nullptr;
×
399
      externalUIVisible(false);
×
400
    }
×
401
  };
19✔
402

403
  // Let the UI take the whole window: needed when the window is docked in
404
  // the main window and follows the size of its pane.
405
  const auto fitToWindow = [](QQuickWindow* win, QQuickItem* item) {
47✔
406
    if(win && item)
27✔
407
      item->setSize(QSizeF(win->width(), win->height()));
27✔
408
  };
27✔
409

410
  // On a new compile the UI object is recreated; put it back in the window
411
  const auto reloadUI = [this, &ctx, fitToWindow](QQuickWindow* win) -> bool {
23✔
412
    // A window closed and waiting for its deletion while a new one holds
413
    // the UI: nothing to do in it
414
    if(m_ui_object && m_ui_object->window() != win)
3✔
415
      return true;
×
416

417
    delete m_ui_object;
3✔
418
    m_ui_object = nullptr;
3✔
419
    if(!m_ui_component)
3✔
420
      return false;
×
421

422
    m_ui_object = createItemForUI(ctx);
3✔
423
    if(!m_ui_object)
3✔
424
      return false;
×
425
    m_ui_object->setParentItem(win->contentItem());
3✔
426
    m_ui_object->setParent(win->contentItem());
3✔
427
    fitToWindow(win, m_ui_object);
3✔
428
    return true;
3✔
429
  };
3✔
430

431
  // The requested size: the ScriptUI root asks for one through its implicit
432
  // width / height (e.g. `implicitWidth: 1280`), defaulting to 640x640.
433
  const QSize requested = [this] {
40✔
434
    int w = 640, h = 640;
20✔
435
    if(m_ui_object->implicitWidth() >= 100.)
20✔
436
      w = static_cast<int>(m_ui_object->implicitWidth());
20✔
437
    if(m_ui_object->implicitHeight() >= 100.)
20✔
438
      h = static_cast<int>(m_ui_object->implicitHeight());
20✔
439
    return QSize{w, h};
20✔
440
  }();
441

442
  auto win = new QQuickWindow{};
20✔
443
  // QWidget gets these from QWidgetPrivate::adjustFlags; a bare QQuickWindow
444
  // does not, and on platforms where Qt draws the chrome itself (wasm) that
445
  // leaves the window with no title bar, close or minimise button.
446
  win->setFlags(
20✔
447
      win->flags() | Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
20✔
448
      | Qt::WindowCloseButtonHint | Qt::WindowMinimizeButtonHint
20✔
449
      | Qt::WindowMaximizeButtonHint);
20✔
450
#if defined(__EMSCRIPTEN__)
451
  // Qt for wasm reports ShowIsFullScreen unconditionally, so QWindow::show()
452
  // turns into showFullScreen() for every top level: the requested size is
453
  // discarded and the full-screen state suppresses the frame. Only Qt::Dialog
454
  // and Qt::Popup opt out (QWasmIntegration::defaultWindowState).
455
  win->setFlags(win->flags() | Qt::Dialog);
456
#endif
457
  win->setWidth(640);
20✔
458
  win->setHeight(640);
20✔
459
  win->setColor(qApp->palette().color(QPalette::Window));
20✔
460

461
  m_ui_object->setParentItem(win->contentItem());
20✔
462
  connect(win, &QQuickWindow::widthChanged, this, [this, win, fitToWindow] {
32✔
463
    fitToWindow(win, m_ui_object);
12✔
464
  });
12✔
465
  connect(win, &QQuickWindow::heightChanged, this, [this, win, fitToWindow] {
32✔
466
    fitToWindow(win, m_ui_object);
12✔
467
  });
12✔
468

469
  auto widg = QWidget::createWindowContainer(win, parent);
20✔
470
  if(!widg) {
20✔
471
    delete m_ui_object;
×
472
    m_ui_object = nullptr;
×
473
    delete win;
×
474
    return nullptr;
×
475
  }
476
  widg->setAttribute(Qt::WA_DeleteOnClose);
20✔
477
  // The container widget does not follow the QQuickWindow's size
478
  widg->resize(requested);
20✔
479

480
#if QT_VERSION >= QT_VERSION_CHECK(6,8,2)
481
  // Bug in older Qt 6 versions:
482
  // QtCore/qmetatype.h:842:23: error: invalid application of 'sizeof' to an incomplete type 'QQuickCloseEvent'
483
  // static_assert(sizeof(T), "Type argument of Q_PROPERTY or Q_DECLARE_METATYPE(T*) must be fully defined");
484
  connect(win, &QQuickWindow::closing, this, [cleanup_ui, win, widg] {
485
    cleanup_ui(win, widg);
486
  });
487
#endif
488
  connect(win, &QQuickWindow::destroyed, this, [cleanup_ui, win, widg] {
39✔
489
    cleanup_ui(win, widg);
19✔
490
  });
19✔
491
  connect(
20✔
492
      this, &JS::ProcessModel::uiScriptOk, win,
20✔
493
      [reloadUI, win, container = QPointer<QWidget>{widg}] {
23✔
494
    if(!reloadUI(win))
3✔
495
    {
496
      // The container is what the main window holds: closing it takes the
497
      // window with it and runs the cleanup from destroyed()
498
      if(container)
×
499
        container->close();
×
500
    }
×
501
  });
3✔
502
  return widg;
20✔
503
}
20✔
504

505
void ProcessModel::setExecutionScript(const QString& f)
50✔
506
{
507
  if(f == m_program.execution)
50✔
508
    return;
9✔
509
  m_program.execution = std::move(f);
41✔
510

511
  executionScriptChanged(m_program.execution);
41✔
512
}
50✔
513

514
void ProcessModel::setUiScript(const QString& f)
50✔
515
{
516
  if(f == m_program.ui)
50✔
517
    return;
39✔
518
  m_program.ui = std::move(f);
11✔
519

520
  uiScriptChanged(m_program.ui);
11✔
521
}
50✔
522

523
void ProcessModel::setState(const JSState &s)
5✔
524
{
525
  if(s == m_state)
5✔
526
    return;
5✔
527

528
  {
529
    const auto prev = std::move(m_state);
×
530
    for(auto& [prev_k, prev_v] : prev) {
×
531
      stateElementChanged(prev_k, prev_v);
×
532
    }
533
  }
×
534

535
  m_state = std::move(s);
×
536
  for(auto& [k, v] : m_state) {
×
537
    stateElementChanged(k, v);
×
538
  }
539

540
  stateChanged();
×
541
}
5✔
542

543
void ProcessModel::updateState(const QString &k, const ossia::value& res)
×
544
{
545
  if(auto it = m_state.find(k); it != m_state.end())
×
546
  {
547
    if(res.valid())
×
548
    {
549
      if(res != it->second)
×
550
      {
551
        // Updating a new element
552
        m_state[k] = res;
×
553
        stateElementChanged(k, res);
×
554
        stateChanged();
×
555
      }
×
556
    }
×
557
    else
558
    {
559
      // Removing an element
560
      m_state.erase(k);
×
561
      stateElementChanged(k, res);
×
562
      stateChanged();
×
563
    }
564
  }
×
565
  else
566
  {
567
    if(res.valid())
×
568
    {
569
      // Adding a new element
570
      m_state[k] = res;
×
571
      stateElementChanged(k, res);
×
572
      stateChanged();
×
573
    }
×
574
    else
575
    {
576
      // Already not there, nothing to do
577
    }
578
  }
579
}
×
580

581
[[nodiscard]] Process::ScriptChangeResult ProcessModel::setProgram(const JS::QmlSource& script)
50✔
582
{
583
  setExecutionScript(script.execution);
50✔
584
  setUiScript(script.ui);
50✔
585

586
  Process::ScriptChangeResult res;
50✔
587
  const auto trimmed = script.execution.trimmed();
50✔
588
  const QByteArray data = trimmed.toUtf8();
50✔
589

590
  auto path = score::locateFilePath(trimmed, score::IDocument::documentContext(*this));
50✔
591

592
  // setQmlData builds the UI component from m_program.ui: it must be the new
593
  // one, otherwise every compile shows the UI of the previous compile.
594
  const auto previous = m_program;
50✔
595
  m_program = script;
50✔
596

597
  if(QFileInfo::exists(path))
50✔
598
    res = setQmlData(path.toUtf8(), true);
×
599
  else
600
    res = setQmlData(data, false);
50✔
601

602
  if(!res.valid)
50✔
603
    m_program = previous;
×
604
  return res;
50✔
605
}
50✔
606

607
Process::ScriptChangeResult ProcessModel::setQmlData(const QByteArray& data, bool isFile)
50✔
608
{
609
  Process::ScriptChangeResult res;
50✔
610
  if(!isFile && !data.contains("import "))
50✔
611
    return res;
×
612

613
  // When loading inline scripts, pre-create all cache files before loading any QML.
614
  // Qt's QQmlTypeLoader caches directory listings on first access; if the UI cache file
615
  // doesn't exist yet when the execution script triggers that scan, it gets flagged
616
  // as "File name case mismatch" when loaded later.
617
  if(!isFile && !this->m_program.ui.isEmpty())
50✔
618
    ensureJSCacheFile(this->m_program.ui.toUtf8(), true);
13✔
619

620
  auto script = m_cache.getExecution(*this, data, isFile);
50✔
621
  if(!script)
50✔
622
    return res;
×
623

624
  m_isFile = isFile;
50✔
625
  m_qmlData = data;
50✔
626

627
  res.inlets = score::clearAndDeleteLater(m_inlets);
50✔
628
  res.outlets = score::clearAndDeleteLater(m_outlets);
50✔
629
  const bool had_ui = m_ui_component;
50✔
630
  m_ui_component = nullptr;
50✔
631
  delete m_ui_object;
50✔
632
  m_ui_object = nullptr;
50✔
633

634
  SCORE_ASSERT(m_inlets.size() == 0);
50✔
635
  SCORE_ASSERT(m_outlets.size() == 0);
50✔
636

637
  // Check inlets / outlets
638
  {
639
    auto cld_inlet = script->findChildren<Inlet*>();
50✔
640
    int i = 0;
50✔
641
    for(auto n : cld_inlet)
143✔
642
    {
643
      auto port = n->make(Id<Process::Port>(i++), this);
93✔
644
      if(const auto& name = n->objectName(); !name.isEmpty())
179✔
645
        port->setName(name);
86✔
646
      if(auto addr = State::parseAddressAccessor(n->address()))
93✔
647
        port->setAddress(std::move(*addr));
×
648
      m_inlets.push_back(port);
93✔
649
    }
650
  }
50✔
651

652
  {
653
    auto cld_outlet = script->findChildren<Outlet*>();
50✔
654
    int i = 0;
50✔
655
    for(auto n : cld_outlet)
93✔
656
    {
657
      auto port = n->make(Id<Process::Port>(i++), this);
43✔
658
      if(const auto& name = n->objectName(); !name.isEmpty())
86✔
659
        port->setName(name);
43✔
660
      if(auto addr = State::parseAddressAccessor(n->address()))
43✔
661
        port->setAddress(std::move(*addr));
×
662
      m_outlets.push_back(port);
43✔
663
    }
664
  }
50✔
665

666
  // Create ui if any
667
  if(!this->m_program.ui.isEmpty()) {
50✔
668
    m_ui_component = m_cache.getUi(*this, this->m_program.ui.toUtf8(), isFile);
13✔
669
  }
13✔
670

671
  if(m_isFile)
50✔
672
  {
673
    const auto name = QFileInfo{data}.baseName();
×
674
    metadata().setName(name);
×
675
    metadata().setLabel(name);
×
676
  }
×
677
  else if(metadata().getName().isEmpty())
50✔
678
  {
679
    metadata().setName(QStringLiteral("Script"));
32✔
680
  }
32✔
681

682
  executionScriptOk();
50✔
683
  res.valid = true;
50✔
684

685
  if(bool(m_ui_component) != had_ui)
50✔
686
    flagsChanged();
4✔
687

688
  if(m_ui_component)
50✔
689
  {
690
    uiScriptOk();
7✔
691
  }
7✔
692
  else if(externalUI)
43✔
693
  {
694
    externalUI->close();
×
695
    externalUI->deleteLater();
×
696
    externalUI = nullptr;
×
697
    externalUIVisible(false);
×
698
  }
×
699

700
  // inlets / outletsChanged : in ScriptEditCommand
701
  return res;
50✔
702
}
50✔
703

704
Script* ProcessModel::currentExecutionObject() const noexcept
×
705
{
706
  if(auto cache = m_cache.tryGet(m_qmlData, m_isFile))
×
707
    return cache->object.get();
×
708
  return nullptr;
×
709
}
×
710

711
bool ProcessModel::isGpu() const noexcept
×
712
{
713
#if defined(SCORE_HAS_GPU_JS)
714
  if(auto script = currentExecutionObject())
715
  {
716
    return
717
        script->findChild<JS::TextureInlet*>() != nullptr
718
           || script->findChild<JS::TextureOutlet*>() != nullptr
719
           // || script->findChild<JS::BufferInlet*>() != nullptr
720
           // || script->findChild<JS::BufferOutlet*>() != nullptr
721
        ;
722
  }
723
#endif
724
  return false;
×
725
}
726

727
ComponentCache::ComponentCache() { }
37✔
728
ComponentCache::~ComponentCache() { }
37✔
729

730
const ComponentCache::Cache* ComponentCache::tryGet(const QByteArray& str, bool isFile) const noexcept
69✔
731
{
732
  QByteArray content;
69✔
733
  QFile f;
69✔
734
  if(isFile)
69✔
735
  {
736
    f.setFileName(str);
×
737
    if(f.open(QIODevice::ReadOnly))
×
738
      content = score::mapAsByteArray(f);
×
739
    else
740
      return nullptr;
×
741
  }
×
742
  else
743
  {
744
    content = str;
69✔
745
  }
746

747
  if(auto it = ossia::find_if(m_map, [&](const auto& k) { return k.key == content; });
116✔
748
     it != m_map.end())
69✔
749
  {
750
    return &*it;
17✔
751
  }
752
  return nullptr;
52✔
753
}
69✔
754

755
Script* ComponentCache::getExecution(
56✔
756
    const ProcessModel& process, const QByteArray& str, bool isFile) noexcept
757
{
758
  if(auto cache = tryGet(str, isFile))
56✔
759
    return cache->object.get();
15✔
760

761
  auto& dummyEngine = score::GUIAppContext()
82✔
762
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
41✔
763
                          .m_scriptProcessUIEngine;
41✔
764
  std::unique_ptr<QQmlComponent> comp;
41✔
765
  if(!isFile)
41✔
766
  {
767
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
41✔
768
    loadJSObjectFromString(process.rootPath(), str, *comp, false);
41✔
769
  }
41✔
770
  else
771
  {
772
    comp = std::make_unique<QQmlComponent>(&dummyEngine, QUrl::fromLocalFile(str));
×
773
  }
774

775
  const auto& errs = comp->errors();
41✔
776
  if(!errs.empty())
41✔
777
  {
778
    const auto& err = errs.first();
×
779
    qDebug() << err.line() << err.toString();
×
780
    auto str = err.toString();
×
781
    str.remove("<Unknown File>:");
×
782
    process.errorMessage(/* err.line(), */str);
×
783
    return nullptr;
×
784
  }
×
785

786
  auto obj = comp->create();
41✔
787
  auto script = qobject_cast<JS::Script*>(obj);
41✔
788
  if(script)
41✔
789
  {
790
    if(m_map.size() > 5)
41✔
791
      m_map.erase(m_map.begin());
×
792

793
    m_map.emplace_back(
82✔
794
        Cache{str, std::move(comp), std::unique_ptr<JS::Script>(script)});
41✔
795
    return script;
41✔
796
  }
797
  else
798
  {
799
    process.errorMessage(/* 0, */"The component must be of type Script");
×
800
    if(obj)
×
801
    {
802
      delete obj;
×
803
    }
×
804
    return nullptr;
×
805
  }
806
}
56✔
807

808
QQmlComponent* ComponentCache::getUi(
13✔
809
    const ProcessModel& process, const QByteArray& str, bool isFile) noexcept
810
{
811
  if(auto cache = tryGet(str, isFile))
13✔
812
    return cache->component.get();
2✔
813

814
  auto& dummyEngine = score::GUIAppContext()
22✔
815
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
11✔
816
                          .m_scriptProcessUIEngine;
11✔
817

818
  std::unique_ptr<QQmlComponent> comp;
11✔
819
  if(!isFile)
11✔
820
  {
821
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
11✔
822
    loadJSObjectFromString(process.rootPath(), str, *comp, true);
11✔
823
  }
11✔
824
  else
825
  {
826
    comp = std::make_unique<QQmlComponent>(&dummyEngine, QUrl::fromLocalFile(str));
×
827
  }
828

829
  const auto& errs = comp->errors();
11✔
830
  if(!errs.empty())
11✔
831
  {
832
    const auto& err = errs.first();
×
833
    qDebug() << err.line() << err.toString();
×
834
    auto str = err.toString();
×
835
    str.remove("<Unknown File>:");
×
836
    process.errorMessage(/* err.line(), */str);
×
837
    return nullptr;
×
838
  }
×
839

840
  auto obj = comp->beginCreate(dummyEngine.rootContext());
11✔
841
  if(!obj) {
11✔
842
    process.errorMessage(/* 0, */"Cannot create UI object");
×
843
    return nullptr;
×
844
  }
845
  auto script = qobject_cast<ScriptUI*>(obj);
11✔
846
  if(script) {
11✔
847
    script->setProcess((Process::ProcessModel*)&process);
5✔
848
  }
5✔
849
  comp->completeCreate();
11✔
850
  if(script)
11✔
851
  {
852
    if(m_map.size() > 5)
5✔
853
      m_map.erase(m_map.begin());
×
854

855
    m_map.emplace_back(
10✔
856
        Cache{str, std::move(comp), {}});
5✔
857
    delete script;
5✔
858
    return m_map.back().component.get();
5✔
859
  }
860
  else
861
  {
862
    process.errorMessage(/* 0, */"The component must be of type Script");
6✔
863
    if(obj)
6✔
864
      delete obj;
6✔
865
    return nullptr;
6✔
866
  }
867
}
13✔
868

869
void ProcessModel::loadPreset(const Process::Preset& preset)
×
870
{
871
  Process::loadScriptProcessPreset<ProcessModel::p_program>(*this, preset);
×
872
}
×
873

874
Process::Preset ProcessModel::savePreset() const noexcept
×
875
{
876
  // FIXME this should save p_program
877
  return Process::saveScriptProcessPreset(*this, this->m_qmlData);
×
878
}
879

880
}
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