diff --git a/src/plugins/score-plugin-js/CMakeLists.txt b/src/plugins/score-plugin-js/CMakeLists.txt index 9a72bc0f10..c1c0514759 100644 --- a/src/plugins/score-plugin-js/CMakeLists.txt +++ b/src/plugins/score-plugin-js/CMakeLists.txt @@ -41,6 +41,7 @@ set(HDRS "${CMAKE_CURRENT_SOURCE_DIR}/JS/Qml/QmlProcess.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/JS/Qml/TextureSource.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/JS/Qml/Utils.hpp" + "${CMAKE_CURRENT_SOURCE_DIR}/JS/Qml/ViewContext.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/JS/ApplicationPlugin.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/JS/DocumentPlugin.hpp" @@ -83,6 +84,7 @@ set(SRCS "${CMAKE_CURRENT_SOURCE_DIR}/JS/Qml/TextureSource.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/JS/Qml/ValueTypes.Qt6.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/JS/Qml/Utils.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/JS/Qml/ViewContext.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/JS/ApplicationPlugin.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/JS/DocumentPlugin.cpp" diff --git a/src/plugins/score-plugin-js/JS/ApplicationPlugin.cpp b/src/plugins/score-plugin-js/JS/ApplicationPlugin.cpp index 0766388866..4d25c36a71 100644 --- a/src/plugins/score-plugin-js/JS/ApplicationPlugin.cpp +++ b/src/plugins/score-plugin-js/JS/ApplicationPlugin.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -43,6 +44,7 @@ ApplicationPlugin::ApplicationPlugin(const score::GUIApplicationContext& ctx) m_consoleEngine.globalObject().setProperty( "Library", m_consoleEngine.newQObject(new JsLibrary)); m_consoleEngine.globalObject().setProperty("Device", m_consoleEngine.newQObject(new DeviceContext{m_consoleEngine})); + m_consoleEngine.globalObject().setProperty("View", m_consoleEngine.newQObject(new JsViewContext)); connect(&m_consoleEngine, &QQmlEngine::exit, this, [&] { for(auto& doc : score::GUIAppContext().docManager.documents()) doc->commandStack().markCurrentIndexAsSaved(); @@ -70,6 +72,8 @@ ApplicationPlugin::ApplicationPlugin(const score::GUIApplicationContext& ctx) "System", m_scriptProcessUIEngine.newQObject(new JsSystem)); m_scriptProcessUIEngine.globalObject().setProperty( "Library", m_scriptProcessUIEngine.newQObject(new JsLibrary)); + m_scriptProcessUIEngine.globalObject().setProperty( + "View", m_scriptProcessUIEngine.newQObject(new JsViewContext)); // Command-line option parsing QCommandLineParser parser; diff --git a/src/plugins/score-plugin-js/JS/Qml/ViewContext.cpp b/src/plugins/score-plugin-js/JS/Qml/ViewContext.cpp new file mode 100644 index 0000000000..fe69a01c77 --- /dev/null +++ b/src/plugins/score-plugin-js/JS/Qml/ViewContext.cpp @@ -0,0 +1,189 @@ +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +W_OBJECT_IMPL(JS::JsViewContext) + +namespace JS +{ + +const score::DocumentContext* JsViewContext::ctx() +{ + return score::GUIAppContext().currentDocument(); +} + +Scenario::ScenarioDocumentView* JsViewContext::view() +{ + auto doc = ctx(); + if(!doc) + return nullptr; + // No GUI (e.g. --no-gui): there is no view. + auto dv = doc->document.view(); + if(!dv) + return nullptr; + return qobject_cast(&dv->viewDelegate()); +} + +Scenario::ScenarioDocumentPresenter* JsViewContext::pres() +{ + auto doc = ctx(); + if(!doc) + return nullptr; + // Returns nullptr when there is no presenter (e.g. --no-gui). + return score::IDocument::try_presenterDelegate( + doc->document); +} + +bool JsViewContext::grabScene(QString path) +{ + auto v = view(); + if(!v) + return false; + return !Scenario::renderSceneToSvg(v->scene(), path).isEmpty(); +} + +bool JsViewContext::grabMainWindow(QString path) +{ + auto w = qApp->activeWindow(); + if(!w) + return false; + return w->grab().save(path); +} + +bool JsViewContext::grabScreen(QString path) +{ + auto screen = QGuiApplication::primaryScreen(); + if(!screen) + return false; + return screen->grabWindow(0).save(path); +} + +void JsViewContext::zoom(double zx, double zy) +{ + if(auto v = view()) + v->zoom(zx, zy); +} + +void JsViewContext::scroll(double dx, double dy) +{ + if(auto v = view()) + v->scroll(dx, dy); +} + +void JsViewContext::setZoomRatio(double r) +{ + if(auto p = pres()) + p->setZoomRatio(r); +} + +void JsViewContext::centerOn(QObject* process) +{ + auto p = pres(); + auto v = view(); + if(!p || !v) + return; + auto* model = qobject_cast(process); + if(!model) + return; + const auto& target = model->id(); + + QGraphicsItem* found = nullptr; + + // Nodal (dataflow) mode: the process is drawn as a NodeItem in the scene. + for(auto* it : v->scene().items()) + { + if(auto* node = dynamic_cast(it)) + { + if(node->id() == target) + { + found = node; + break; + } + } + } + + // Temporal mode: walk the displayed interval's slots/layers. + if(!found) + { + if(auto* itv = p->displayedIntervalPresenter()) + { + for(const auto& slot : itv->getSlots()) + { + if(auto* ls = slot.getLayerSlot()) + { + for(const auto& ld : ls->layers) + { + if(ld.model().id() == target && !ld.layers().empty()) + { + found = ld.layers().front().container; + break; + } + } + } + if(found) + break; + } + } + } + + if(found) + v->view().centerOn(found); +} + +void JsViewContext::goToInterval(QObject* interval) +{ + auto p = pres(); + if(!p) + return; + if(auto itv = qobject_cast(interval)) + p->setDisplayedInterval(itv); +} + +void JsViewContext::fit() +{ + if(auto p = pres()) + p->setLargeView(); +} + +void JsViewContext::recenter() +{ + if(auto p = pres()) + p->recenter(); +} + +void JsViewContext::setNodal(bool nodal) +{ + if(auto p = pres()) + p->setNodalMode(nodal); +} + +bool JsViewContext::isNodal() +{ + if(auto p = pres()) + return p->isNodal(); + return false; +} +} diff --git a/src/plugins/score-plugin-js/JS/Qml/ViewContext.hpp b/src/plugins/score-plugin-js/JS/Qml/ViewContext.hpp new file mode 100644 index 0000000000..b3361d7e5a --- /dev/null +++ b/src/plugins/score-plugin-js/JS/Qml/ViewContext.hpp @@ -0,0 +1,66 @@ +#pragma once +#include + +#include +#include + +#include + +namespace score +{ +struct DocumentContext; +} +namespace Scenario +{ +class ScenarioDocumentView; +class ScenarioDocumentPresenter; +} + +namespace JS +{ +//! JS object exposed as `View`: automation of the main scenario view. +//! +//! Every method degrades gracefully when there is no GUI (`--no-gui`): the +//! view/presenter accessors return nullptr and the methods become no-ops. +class SCORE_PLUGIN_JS_EXPORT JsViewContext : public QObject +{ + W_OBJECT(JsViewContext) +public: + // Screenshots + bool grabScene(QString path); + W_SLOT(grabScene) + bool grabMainWindow(QString path); + W_SLOT(grabMainWindow) + bool grabScreen(QString path); + W_SLOT(grabScreen) + + // Zoom / scroll + void zoom(double zx, double zy); + W_SLOT(zoom) + void scroll(double dx, double dy); + W_SLOT(scroll) + void setZoomRatio(double r); + W_SLOT(setZoomRatio) + + // Navigation / focus + void centerOn(QObject* process); + W_SLOT(centerOn) + void goToInterval(QObject* interval); + W_SLOT(goToInterval) + void fit(); + W_SLOT(fit) + void recenter(); + W_SLOT(recenter) + + // Dataflow / temporal mode + void setNodal(bool nodal); + W_SLOT(setNodal) + bool isNodal(); + W_SLOT(isNodal) + +private: + const score::DocumentContext* ctx(); + Scenario::ScenarioDocumentView* view(); + Scenario::ScenarioDocumentPresenter* pres(); +}; +} diff --git a/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/ScenarioDocumentPresenter.cpp b/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/ScenarioDocumentPresenter.cpp index a84b108ac3..f4dac04820 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/ScenarioDocumentPresenter.cpp +++ b/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/ScenarioDocumentPresenter.cpp @@ -290,6 +290,21 @@ void ScenarioDocumentPresenter::recenterNodal() ossia::visit(vis, m_centralDisplay); } +void ScenarioDocumentPresenter::recenter() +{ + recenterNodal(); +} + +void ScenarioDocumentPresenter::setNodalMode(bool nodal) +{ + // Route through the toolbar action so its checked state stays in sync: + // toggling it triggers on_timelineModeSwitch() -> switchMode(). + if(m_timelineAction) + m_timelineAction->setChecked(!nodal); + else + switchMode(nodal); +} + void ScenarioDocumentPresenter::switchMode(bool nodal) { const auto mode diff --git a/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/ScenarioDocumentPresenter.hpp b/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/ScenarioDocumentPresenter.hpp index 360f0cced1..0ea162fb2b 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/ScenarioDocumentPresenter.hpp +++ b/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/ScenarioDocumentPresenter.hpp @@ -106,6 +106,11 @@ class SCORE_PLUGIN_SCENARIO_EXPORT ScenarioDocumentPresenter final void stopTimeBar(); bool isNodal() const noexcept; + // Switch between temporal and dataflow (nodal) mode, keeping the toolbar + // action in sync. Exposed for JS view automation. + void setNodalMode(bool nodal); + // Recenter the view (nodal mode). Exposed for JS view automation. + void recenter(); void setAutoScroll(bool); diff --git a/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/SnapshotAction.cpp b/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/SnapshotAction.cpp index 80b6f2526b..6a1faeb139 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/SnapshotAction.cpp +++ b/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/SnapshotAction.cpp @@ -16,6 +16,36 @@ namespace Scenario { +QByteArray renderSceneToSvg(QGraphicsScene& scene, const QString& path, QRectF rect) +{ +#if __has_include() + // Create a SVG from the scene + QBuffer b; + QSvgGenerator p; + p.setOutputDevice(&b); + QPainter painter; + painter.begin(&p); + painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); + + scene.render(&painter, rect, rect); + painter.end(); + + if(!path.isEmpty()) + { + QFile screenshot(path); + if(screenshot.open(QFile::WriteOnly)) + { + screenshot.write(b.buffer()); + screenshot.close(); + } + } + + return b.buffer(); +#else + return {}; +#endif +} + SnapshotAction::SnapshotAction(QGraphicsScene& scene, QWidget* parent) : QAction{tr("Scenario screenshot"), parent} { @@ -27,35 +57,22 @@ SnapshotAction::SnapshotAction(QGraphicsScene& scene, QWidget* parent) void SnapshotAction::takeScreenshot(QGraphicsScene& scene) { -#if __has_include() - // Create a SVG from the scene - QBuffer b; - QSvgGenerator p; - p.setOutputDevice(&b); - QPainter painter; - painter.begin(&p); - painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); +// Render the scene and save a file for convenience +#if defined(__APPLE__) || defined(__linux__) + auto path = QStringLiteral("/tmp/screenshot.svg"); +#else + auto path = QStringLiteral("screenshot.svg"); +#endif - scene.render(&painter, QRectF(0, 0, 1920, 1080), QRectF(0, 0, 1920, 1080)); - painter.end(); + QByteArray svg = renderSceneToSvg(scene, path); + if(svg.isEmpty()) + return; // Set the clipboard auto d = new QMimeData; - d->setData("image/svg+xml", b.buffer()); + d->setData("image/svg+xml", svg); // TODO investigate : the doc says that setMimeData takes ownership. QApplication::clipboard()->setMimeData(d, QClipboard::Clipboard); QApplication::clipboard()->setMimeData(d, QClipboard::Selection); - -// Also save a file for convenience -#if defined(__APPLE__) || defined(__linux__) - auto path = "/tmp/screenshot.svg"; -#else - auto path = "screenshot.svg"; -#endif - QFile screenshot(path); - screenshot.open(QFile::WriteOnly); - screenshot.write(b.buffer()); - screenshot.close(); -#endif } } diff --git a/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/SnapshotAction.hpp b/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/SnapshotAction.hpp index a5597da6b7..7e0ab6cd9f 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/SnapshotAction.hpp +++ b/src/plugins/score-plugin-scenario/Scenario/Document/ScenarioDocument/SnapshotAction.hpp @@ -1,8 +1,21 @@ #pragma once +#include + #include +#include +#include class QGraphicsScene; namespace Scenario { +// Render a region of a QGraphicsScene to SVG and return the raw SVG bytes. +// If `path` is non-empty, the SVG is also written there. +// Returns an empty QByteArray on failure (e.g. QtSvg unavailable). +// Shared by the F10 SnapshotAction and the JS View.grabScene() API. +SCORE_PLUGIN_SCENARIO_EXPORT +QByteArray renderSceneToSvg( + QGraphicsScene& scene, const QString& path = {}, + QRectF rect = QRectF(0, 0, 1920, 1080)); + struct SnapshotAction : public QAction { public: