From 9cffb6bb01e7916e01256f33ddd07d52d385bc46 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Wed, 22 Feb 2023 17:46:46 -0500 Subject: [PATCH] Add a node-based task-editor panel. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show and raise the task panel when loading a project; tasks are illustrated as nodes in a graph whose edges are dependencies and task-adaptors. This also: + Adds task styles to the `smtk::task::Manager` and makes the attribute-editor panel respond to active task changes that provide an attribute view style. + Renames project plugins to make room for a "base" plugin. Now the two paraview-dependent plugins are named + smtkPQCoreProjectPlugin – registers GUI functionality but does not create GUI elements. + smtkPQGuiProjectPlugin – add GUI elements to application. We also now register only the project manager and other core classes (that have no Qt/ParaView dependencies) in a new "core" project plugin. + Fixes an issue with `GatherResources` warnings. Warnings were being emitted even when attribute IDs were discovered because finding an ID never reset the warning flag. + Fixes a `FillOutAttributes` initialization issue; unless a task was set to autoconfigure, it would never report being properly configured (even if its serialized configuration included resource ids as needed). + Ensures application state is available when reading resources. We pass the `smtk::common::Managers` instance from the `ReadResource` operation to the internal `Read` operation it creates so that they can access application state. This is required for projects whose task managers need access to at least a `resource::Manager` in order to find external components specified by UUID in their configuration. + Fixes a task deserialization issue. When tasks are written, there is no guarantee they will be ordered by monotonically increasing ID. Take this into account when restoring saved state through a new method that allows callers to specify (rather than be assigned) a swizzle ID. Co-authored-by: John Tourtellott Co-authored-by: Aron Helser Co-authored-by: David Thompson --- CMake/FindGraphviz.cmake | 80 +++ CMake/Options.h.in | 4 + CMakeLists.txt | 5 + data/baseline/smtk/mesh/MeshSelection_5.png | 3 + data/baseline/smtk/mesh/OpenExodusFile_1.png | 3 + doc/release/notes/task-panel.rst | 17 + .../paraview/appcomponents/CMakeLists.txt | 2 + .../appcomponents/plugin-gui/CMakeLists.txt | 9 +- .../DefaultConfiguration.cxx | 14 +- .../pqSMTKAppComponentsAutoStart.cxx | 1 - .../appcomponents/pqSMTKAttributePanel.cxx | 143 +++++ .../appcomponents/pqSMTKAttributePanel.h | 15 + .../pqSMTKOperationHintsBehavior.cxx | 25 +- .../paraview/appcomponents/pqSMTKTaskDock.h | 26 + .../appcomponents/pqSMTKTaskPanel.cxx | 166 ++++++ .../paraview/appcomponents/pqSMTKTaskPanel.h | 60 ++ .../mesh/testing/xml/MeshSelection.xml | 1 + .../extension/paraview/project/CMakeLists.txt | 1 + .../project/plugin-core/CMakeLists.txt | 5 +- .../project/plugin-core/paraview.plugin | 2 +- .../project/plugin-gui/CMakeLists.txt | 8 +- .../project/plugin-gui/paraview.plugin | 4 +- .../pqSMTKDisplayProjectOnLoadBehavior.cxx | 169 ++++++ .../pqSMTKDisplayProjectOnLoadBehavior.h | 73 +++ .../project/pqSMTKProjectAutoStart.cxx | 4 + smtk/extension/qt/CMakeLists.txt | 38 +- smtk/extension/qt/qtViewRegistrar.cxx | 8 +- .../extension/qt/task/PanelConfiguration.json | 15 + .../qt/task/PanelConfiguration.spec.json | 78 +++ smtk/extension/qt/task/TaskEditorState.cxx | 39 ++ smtk/extension/qt/task/TaskEditorState.h | 43 ++ smtk/extension/qt/task/TaskNode.ui | 185 ++++++ smtk/extension/qt/task/qtTaskArc.cxx | 311 ++++++++++ smtk/extension/qt/task/qtTaskArc.h | 103 ++++ smtk/extension/qt/task/qtTaskEditor.cxx | 549 ++++++++++++++++++ smtk/extension/qt/task/qtTaskEditor.h | 77 +++ smtk/extension/qt/task/qtTaskNode.cxx | 357 ++++++++++++ smtk/extension/qt/task/qtTaskNode.h | 113 ++++ smtk/extension/qt/task/qtTaskScene.cxx | 215 +++++++ smtk/extension/qt/task/qtTaskScene.h | 76 +++ smtk/extension/qt/task/qtTaskView.cxx | 72 +++ smtk/extension/qt/task/qtTaskView.h | 61 ++ .../qt/task/qtTaskViewConfiguration.cxx | 167 ++++++ .../qt/task/qtTaskViewConfiguration.h | 99 ++++ smtk/operation/operators/ReadResource.cxx | 3 + smtk/project/CMakeLists.txt | 5 + smtk/project/Manager.cxx | 8 + smtk/project/operators/Read.cxx | 2 +- smtk/project/plugin/CMakeLists.txt | 9 + smtk/project/plugin/paraview.plugin | 4 + .../mesh/testing/xml/OpenExodusFile.xml | 1 + smtk/task/CMakeLists.txt | 4 + smtk/task/FillOutAttributes.cxx | 49 +- smtk/task/Manager.cxx | 9 + smtk/task/Manager.h | 14 + smtk/task/State.h | 10 +- smtk/task/Task.h | 3 + smtk/task/UIState.cxx | 79 +++ smtk/task/UIState.h | 67 +++ smtk/task/UIStateGenerator.cxx | 20 + smtk/task/UIStateGenerator.h | 43 ++ smtk/task/json/Configurator.h | 5 + smtk/task/json/Configurator.txx | 33 ++ smtk/task/json/Helper.cxx | 12 + smtk/task/json/Helper.h | 3 + smtk/task/json/jsonGatherResources.cxx | 6 + smtk/task/json/jsonManager.cxx | 35 +- smtk/task/pybind11/PybindState.h | 2 +- smtk/task/testing/cxx/CMakeLists.txt | 1 + smtk/task/testing/cxx/TestTaskUIState.cxx | 83 +++ 70 files changed, 3909 insertions(+), 47 deletions(-) create mode 100644 CMake/FindGraphviz.cmake create mode 100644 data/baseline/smtk/mesh/MeshSelection_5.png create mode 100644 data/baseline/smtk/mesh/OpenExodusFile_1.png create mode 100644 doc/release/notes/task-panel.rst create mode 100644 smtk/extension/paraview/appcomponents/pqSMTKTaskDock.h create mode 100644 smtk/extension/paraview/appcomponents/pqSMTKTaskPanel.cxx create mode 100644 smtk/extension/paraview/appcomponents/pqSMTKTaskPanel.h create mode 100644 smtk/extension/paraview/project/pqSMTKDisplayProjectOnLoadBehavior.cxx create mode 100644 smtk/extension/paraview/project/pqSMTKDisplayProjectOnLoadBehavior.h create mode 100644 smtk/extension/qt/task/PanelConfiguration.json create mode 100644 smtk/extension/qt/task/PanelConfiguration.spec.json create mode 100644 smtk/extension/qt/task/TaskEditorState.cxx create mode 100644 smtk/extension/qt/task/TaskEditorState.h create mode 100644 smtk/extension/qt/task/TaskNode.ui create mode 100644 smtk/extension/qt/task/qtTaskArc.cxx create mode 100644 smtk/extension/qt/task/qtTaskArc.h create mode 100644 smtk/extension/qt/task/qtTaskEditor.cxx create mode 100644 smtk/extension/qt/task/qtTaskEditor.h create mode 100644 smtk/extension/qt/task/qtTaskNode.cxx create mode 100644 smtk/extension/qt/task/qtTaskNode.h create mode 100644 smtk/extension/qt/task/qtTaskScene.cxx create mode 100644 smtk/extension/qt/task/qtTaskScene.h create mode 100644 smtk/extension/qt/task/qtTaskView.cxx create mode 100644 smtk/extension/qt/task/qtTaskView.h create mode 100644 smtk/extension/qt/task/qtTaskViewConfiguration.cxx create mode 100644 smtk/extension/qt/task/qtTaskViewConfiguration.h create mode 100644 smtk/project/plugin/CMakeLists.txt create mode 100644 smtk/project/plugin/paraview.plugin create mode 100644 smtk/task/UIState.cxx create mode 100644 smtk/task/UIState.h create mode 100644 smtk/task/UIStateGenerator.cxx create mode 100644 smtk/task/UIStateGenerator.h create mode 100644 smtk/task/testing/cxx/TestTaskUIState.cxx diff --git a/CMake/FindGraphviz.cmake b/CMake/FindGraphviz.cmake new file mode 100644 index 0000000000..a5ce98f106 --- /dev/null +++ b/CMake/FindGraphviz.cmake @@ -0,0 +1,80 @@ +find_path(Graphviz_INCLUDE_DIR + NAMES + cgraph.h + PATH_SUFFIXES + graphviz + ) +mark_as_advanced(Graphviz_INCLUDE_DIR) + +find_library(Graphviz_CDT_LIBRARY + NAMES + cdt + ) +mark_as_advanced(Graphviz_CDT_LIBRARY) + +find_library(Graphviz_GVC_LIBRARY + NAMES + gvc + ) +mark_as_advanced(Graphviz_GVC_LIBRARY) + +find_library(Graphviz_CGRAPH_LIBRARY + NAMES + cgraph + ) +mark_as_advanced(Graphviz_CGRAPH_LIBRARY) + +find_library(Graphviz_PATHPLAN_LIBRARY + NAMES + pathplan + ) +mark_as_advanced(Graphviz_PATHPLAN_LIBRARY) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Graphviz + REQUIRED_VARS + Graphviz_INCLUDE_DIR + Graphviz_CDT_LIBRARY + Graphviz_GVC_LIBRARY + Graphviz_CGRAPH_LIBRARY + Graphviz_PATHPLAN_LIBRARY +) + +if(Graphviz_FOUND) + set(Graphviz_INCLUDE_DIRS "${Graphviz_INCLUDE_DIR}") + set(Graphviz_LIBRARIES + "${Graphviz_CDT_LIBRARY}" + "${Graphviz_GVC_LIBRARY}" + "${Graphviz_CGRAPH_LIBRARY}" + "${Graphviz_PATHPLAN_LIBRARY}") + + if (NOT TARGET Graphviz::cdt) + add_library(Graphviz::cdt UNKNOWN IMPORTED) + set_target_properties(Graphviz::cdt PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${Graphviz_INCLUDE_DIR}" + IMPORTED_LOCATION "${Graphviz_CDT_LIBRARY}") + endif () + + if (NOT TARGET Graphviz::pathplan) + add_library(Graphviz::pathplan UNKNOWN IMPORTED) + set_target_properties(Graphviz::pathplan PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${Graphviz_INCLUDE_DIR}" + IMPORTED_LOCATION "${Graphviz_PATHPLAN_LIBRARY}") + endif () + + if (NOT TARGET Graphviz::cgraph) + add_library(Graphviz::cgraph UNKNOWN IMPORTED) + set_target_properties(Graphviz::cgraph PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${Graphviz_INCLUDE_DIR}" + IMPORTED_LOCATION "${Graphviz_CGRAPH_LIBRARY}" + INTERFACE_LINK_LIBRARIES "Graphviz::cdt") + endif () + + if (NOT TARGET Graphviz::gvc) + add_library(Graphviz::gvc UNKNOWN IMPORTED) + set_target_properties(Graphviz::gvc PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${Graphviz_INCLUDE_DIR}" + IMPORTED_LOCATION "${Graphviz_GVC_LIBRARY}" + INTERFACE_LINK_LIBRARIES "Graphviz::cdt;Graphviz::cgraph;Graphviz::pathplan") + endif () +endif() diff --git a/CMake/Options.h.in b/CMake/Options.h.in index c73dd2b41e..ef36d2f339 100644 --- a/CMake/Options.h.in +++ b/CMake/Options.h.in @@ -18,6 +18,9 @@ // Was SMTK built with VTK? If true, the smtkVTKExt library will exist. #cmakedefine SMTK_ENABLE_VTK_SUPPORT +// Was SMTK build with Graphviz support? It true, the task editor will lay out tasks. +#cmakedefine01 SMTK_ENABLE_GRAPHVIZ_SUPPORT + // Was SMTK built with GDAL support? This affects functionality in smtkVTKExt. #cmakedefine01 SMTK_ENABLE_GDAL_SUPPORT @@ -28,6 +31,7 @@ // Was SMTK built with Remus? If true, smtkRemoteSession library will exist. #cmakedefine SMTK_ENABLE_REMUS_SUPPORT + #define SMTK_INSTALL_PREFIX "@CMAKE_INSTALL_PREFIX@" #endif // smtk_Options_h diff --git a/CMakeLists.txt b/CMakeLists.txt index 14598f01b0..912726868c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -141,6 +141,11 @@ option(SMTK_BUILD_PRECOMPILED_HEADERS "Precompile headers to improve build times" OFF) mark_as_advanced(SMTK_BUILD_PRECOMPILED_HEADERS) +option(SMTK_ENABLE_GRAPHVIZ_SUPPORT "Use graphviz for task layout." OFF) +if (SMTK_ENABLE_GRAPHVIZ_SUPPORT) + find_package(Graphviz REQUIRED) +endif() + option(SMTK_ENABLE_GDAL_SUPPORT "Include I/O for GDAL. This requires ParaView/VTK built with GDAL." OFF) diff --git a/data/baseline/smtk/mesh/MeshSelection_5.png b/data/baseline/smtk/mesh/MeshSelection_5.png new file mode 100644 index 0000000000..f5815c72d9 --- /dev/null +++ b/data/baseline/smtk/mesh/MeshSelection_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23464f395ecdcc07d8cfefcda26499fc743f37dbc42c11a71f631e6142ddb4ee +size 3712 diff --git a/data/baseline/smtk/mesh/OpenExodusFile_1.png b/data/baseline/smtk/mesh/OpenExodusFile_1.png new file mode 100644 index 0000000000..2b91567cb1 --- /dev/null +++ b/data/baseline/smtk/mesh/OpenExodusFile_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87c670d4b59e56e647a6344a3285b2be8c32054d680aa81d13aab902ef7b5a64 +size 2634 diff --git a/doc/release/notes/task-panel.rst b/doc/release/notes/task-panel.rst new file mode 100644 index 0000000000..d61dd96a6e --- /dev/null +++ b/doc/release/notes/task-panel.rst @@ -0,0 +1,17 @@ +User interface for task-based workflows +--------------------------------------- + +SMTK now provides a ParaView plugin which adds a :smtk:`task panel ` +similar to ParaView's node editor. Each task is shown as a node in a graph and +may be made active, marked complete (or incomplete), and manually placed by the user. +Each task may have a collection of style keywords associated with it and the +task manager holds settings for these keywords that affect the application state +when matching tasks are made active. + +In particular, SMTK's attribute-editor panel can be directed to display any view +held by attribute resource when a related :smtk:`smtk::task::FillOutAttributes` task +becomes active. + +This functionality is a preview and still under development; you should expect +changes to user-interface classes that may break backwards compatibility while +this development takes place. diff --git a/smtk/extension/paraview/appcomponents/CMakeLists.txt b/smtk/extension/paraview/appcomponents/CMakeLists.txt index 2eb8b23a31..34ab0be8e4 100644 --- a/smtk/extension/paraview/appcomponents/CMakeLists.txt +++ b/smtk/extension/paraview/appcomponents/CMakeLists.txt @@ -33,6 +33,7 @@ set(classes pqSMTKSaveResourceBehavior pqSMTKSelectionFilterBehavior pqSMTKSubtractUI + pqSMTKTaskPanel vtkSMTKEncodeSelection # For testing @@ -47,6 +48,7 @@ set(headers pqSMTKOperationParameterDock.h pqSMTKOperationToolboxDock.h pqSMTKResourceDock.h + pqSMTKTaskDock.h ) set(CMAKE_AUTOMOC 1) diff --git a/smtk/extension/paraview/appcomponents/plugin-gui/CMakeLists.txt b/smtk/extension/paraview/appcomponents/plugin-gui/CMakeLists.txt index 78cbfd75a1..d952daac2b 100644 --- a/smtk/extension/paraview/appcomponents/plugin-gui/CMakeLists.txt +++ b/smtk/extension/paraview/appcomponents/plugin-gui/CMakeLists.txt @@ -25,14 +25,20 @@ paraview_plugin_add_dock_window( DOCK_AREA Left INTERFACES resource_dock_interfaces SOURCES resource_dock_sources) +paraview_plugin_add_dock_window( + CLASS_NAME pqSMTKTaskDock + DOCK_AREA Left + INTERFACES task_dock_interfaces + SOURCES task_dock_sources) set(interfaces - ${auto_start_interfaces} + ${auto_start_interfaces} ${action_group_interfaces} ${toolbar_interfaces} ${proxy_interfaces} ${attribute_dock_interfaces} ${resource_dock_interfaces} + ${task_dock_interfaces} ) list(APPEND sources ${auto_start_sources} @@ -41,6 +47,7 @@ list(APPEND sources ${proxy_sources} ${attribute_dock_sources} ${resource_dock_sources} + ${task_dock_sources} ) smtk_add_plugin(smtkPQGuiComponentsPlugin diff --git a/smtk/extension/paraview/appcomponents/plugin-panel-defaults/DefaultConfiguration.cxx b/smtk/extension/paraview/appcomponents/plugin-panel-defaults/DefaultConfiguration.cxx index 9b5e18312b..c0c0b0ea3d 100644 --- a/smtk/extension/paraview/appcomponents/plugin-panel-defaults/DefaultConfiguration.cxx +++ b/smtk/extension/paraview/appcomponents/plugin-panel-defaults/DefaultConfiguration.cxx @@ -12,6 +12,8 @@ #include "smtk/extension/paraview/appcomponents/pqSMTKOperationToolboxPanel.h" #include "smtk/extension/paraview/appcomponents/pqSMTKResourceBrowser.h" #include "smtk/extension/paraview/appcomponents/pqSMTKResourcePanel.h" +#include "smtk/extension/paraview/appcomponents/pqSMTKTaskPanel.h" +#include "smtk/extension/qt/task/qtTaskEditor.h" #include "smtk/view/Configuration.h" #include "smtk/view/Information.h" #include "smtk/view/json/jsonView.h" @@ -44,11 +46,21 @@ smtk::view::Information DefaultConfiguration::panelConfiguration(const QWidget* else if (const auto* browser = dynamic_cast(panel)) { (void)browser; - // Use the default JSON configuration for the resource-browser panel. + // We provide a default configuration, but you can manipulate the + // panel or construct your own configuration as needed in your application. auto jsonConfig = nlohmann::json::parse(pqSMTKResourceBrowser::getJSONConfiguration())[0]; std::shared_ptr viewConfig = jsonConfig; result.insert_or_assign(viewConfig); } + else if (const auto* tasks = dynamic_cast(panel)) + { + // We provide a default configuration, but you can manipulate the + // panel or construct your own configuration as needed in your application. + (void)tasks; + std::shared_ptr viewConfig = + smtk::extension::qtTaskEditor::defaultConfiguration(); + result.insert_or_assign(viewConfig); + } else { smtkWarningMacro( diff --git a/smtk/extension/paraview/appcomponents/pqSMTKAppComponentsAutoStart.cxx b/smtk/extension/paraview/appcomponents/pqSMTKAppComponentsAutoStart.cxx index d4b8435036..c5e1b6da04 100644 --- a/smtk/extension/paraview/appcomponents/pqSMTKAppComponentsAutoStart.cxx +++ b/smtk/extension/paraview/appcomponents/pqSMTKAppComponentsAutoStart.cxx @@ -177,7 +177,6 @@ void pqSMTKAppComponentsAutoStart::shutdown() pqCore->unRegisterManager("call observers on main thread"); pqCore->unRegisterManager("smtk register importers"); pqCore->unRegisterManager("smtk pipeline selection sync"); - pqCore->unRegisterManager("smtk display attribute on load"); } } diff --git a/smtk/extension/paraview/appcomponents/pqSMTKAttributePanel.cxx b/smtk/extension/paraview/appcomponents/pqSMTKAttributePanel.cxx index 830a4b58ac..2520f8661c 100644 --- a/smtk/extension/paraview/appcomponents/pqSMTKAttributePanel.cxx +++ b/smtk/extension/paraview/appcomponents/pqSMTKAttributePanel.cxx @@ -22,6 +22,9 @@ #include "smtk/io/Logger.h" +#include "smtk/project/Manager.h" +#include "smtk/project/Project.h" + #include "smtk/resource/Manager.h" #include "smtk/resource/Observer.h" #include "smtk/resource/Properties.h" @@ -44,6 +47,7 @@ #include "vtkVector.h" #include +#include #include pqSMTKAttributePanel::pqSMTKAttributePanel(QWidget* parent) @@ -59,6 +63,20 @@ pqSMTKAttributePanel::pqSMTKAttributePanel(QWidget* parent) SIGNAL(postProcessingModeChanged(bool)), this, SLOT(displayActivePipelineSource(bool))); + QObject::connect( + behavior, + SIGNAL(addedManagerOnServer(pqSMTKWrapper*, pqServer*)), + this, + SLOT(observeProjectsOnServer(pqSMTKWrapper*, pqServer*))); + QObject::connect( + behavior, + SIGNAL(removingManagerFromServer(pqSMTKWrapper*, pqServer*)), + this, + SLOT(unobserveProjectsOnServer(pqSMTKWrapper*, pqServer*))); + behavior->visitResourceManagersOnServers([this](pqSMTKWrapper* wrapper, pqServer* server) { + this->observeProjectsOnServer(wrapper, server); + return false; // terminate early + }); auto* pqCore = pqApplicationCore::instance(); if (pqCore) { @@ -387,3 +405,128 @@ void pqSMTKAttributePanel::updateTitle(const smtk::view::ConfigurationPtr& view) this->setWindowTitle(panelName.c_str()); Q_EMIT titleChanged(panelName.c_str()); } + +void pqSMTKAttributePanel::observeProjectsOnServer(pqSMTKWrapper* mgr, pqServer* server) +{ + (void)server; + if (!mgr) + { + return; + } + auto projectManager = mgr->smtkProjectManager(); + if (!projectManager) + { + return; + } + + QPointer self(this); + auto observerKey = projectManager->observers().insert( + [self](const smtk::project::Project& project, smtk::project::EventType event) { + if (self) + { + self->handleProjectEvent(project, event); + } + }, + 0, // assign a neutral priority + true, // immediatelyNotify + "pqSMTKAttributePanel: Display new attribute project in panel."); + m_projectManagerObservers[projectManager] = std::move(observerKey); +} + +void pqSMTKAttributePanel::unobserveProjectsOnServer(pqSMTKWrapper* mgr, pqServer* server) +{ + (void)server; + if (!mgr) + { + return; + } + auto projectManager = mgr->smtkProjectManager(); + if (!projectManager) + { + return; + } + + auto entry = m_projectManagerObservers.find(projectManager); + if (entry != m_projectManagerObservers.end()) + { + projectManager->observers().erase(entry->second); + m_projectManagerObservers.erase(entry); + } +} + +void pqSMTKAttributePanel::handleProjectEvent( + const smtk::project::Project& project, + smtk::project::EventType event) +{ + auto* taskManager = const_cast(&project.taskManager()); + QPointer self(this); + switch (event) + { + case smtk::project::EventType::ADDED: + // observe the active task + // Use QTimer to wait until the event queue is emptied before trying this; + // that gives operations time to complete. Blech. + QTimer::singleShot(0, [this, taskManager, self]() { + if (!self) + { + return; + } + auto& activeTracker = taskManager->active(); + m_activeObserverKey = activeTracker.observers().insert( + [this, self](smtk::task::Task* oldTask, smtk::task::Task* newTask) { + if (!self) + { + return; + } + (void)oldTask; + if (newTask) + { + auto styles = newTask->style(); + for (const auto& style : styles) + { + auto styleConfig = newTask->manager()->getStyle(style); + // Does this style have a tag for us? + if (styleConfig.contains("attribute-panel")) + { + auto panelConfig = styleConfig.at("attribute-panel"); + if (panelConfig.contains("attribute-editor")) + { + auto viewName = panelConfig.at("attribute-editor").get(); + // std::cout << "Got view name " << viewName << std::endl; + if (auto rsrc = m_rsrc.lock()) + { + auto attrRsrc = std::dynamic_pointer_cast(rsrc); + smtk::view::ConfigurationPtr viewConfig = + attrRsrc ? attrRsrc->findView(viewName) : nullptr; + if (viewConfig) + { + self->resetPanel(attrRsrc->manager()); + // replace the contents with UI for this view. + self->displayResource(attrRsrc, viewConfig); + } + } + } + } + } + } + }, + "AttributePanel active task tracking"); + }); + break; + case smtk::project::EventType::REMOVED: + // stop observing active task + QTimer::singleShot(0, [this, taskManager, self]() { + if (!self) + { + return; + } + auto& activeTracker = taskManager->active(); + activeTracker.observers().erase(m_activeObserverKey); + }); + break; + case smtk::project::EventType::MODIFIED: + default: + // Do nothing. + break; + } +} diff --git a/smtk/extension/paraview/appcomponents/pqSMTKAttributePanel.h b/smtk/extension/paraview/appcomponents/pqSMTKAttributePanel.h index 5cba1f7bef..946628ec44 100644 --- a/smtk/extension/paraview/appcomponents/pqSMTKAttributePanel.h +++ b/smtk/extension/paraview/appcomponents/pqSMTKAttributePanel.h @@ -14,6 +14,7 @@ #include "smtk/extension/qt/qtUIManager.h" +#include "smtk/project/Observer.h" // for EventType #include "smtk/resource/Observer.h" #include "smtk/PublicPointerDefs.h" @@ -26,6 +27,7 @@ class pqServer; class pqPipelineSource; +class pqSMTKWrapper; /**\brief A panel that displays a single SMTK resource for editing by the user. * @@ -123,6 +125,16 @@ protected Q_SLOTS: */ virtual void displayActivePipelineSource(bool doDisplay); + /**\brief Track projects, react to the active task. + * + * These methods are used to add observers to each project loaded on each server + * so that changes to the active task of any can affect the attribute displayed + * in this panel. + */ + virtual void observeProjectsOnServer(pqSMTKWrapper* mgr, pqServer* server); + virtual void unobserveProjectsOnServer(pqSMTKWrapper* mgr, pqServer* server); + virtual void handleProjectEvent(const smtk::project::Project&, smtk::project::EventType); + protected: virtual bool displayResourceInternal( const smtk::attribute::ResourcePtr& rsrc, @@ -136,6 +148,9 @@ protected: smtk::operation::ManagerPtr m_opManager; smtk::resource::Observers::Key m_observer; pqPropertyLinks m_propertyLinks; + + std::map m_projectManagerObservers; + smtk::task::Active::Observers::Key m_activeObserverKey; }; #endif // smtk_extension_paraview_appcomponents_pqSMTKAttributePanel_h diff --git a/smtk/extension/paraview/appcomponents/pqSMTKOperationHintsBehavior.cxx b/smtk/extension/paraview/appcomponents/pqSMTKOperationHintsBehavior.cxx index 5bd3918216..6bce2883b1 100644 --- a/smtk/extension/paraview/appcomponents/pqSMTKOperationHintsBehavior.cxx +++ b/smtk/extension/paraview/appcomponents/pqSMTKOperationHintsBehavior.cxx @@ -12,10 +12,12 @@ #include "smtk/extension/paraview/appcomponents/pqSMTKBehavior.h" #include "smtk/extension/paraview/appcomponents/pqSMTKResourceDock.h" #include "smtk/extension/paraview/appcomponents/pqSMTKResourcePanel.h" +#include "smtk/extension/paraview/appcomponents/pqSMTKTaskPanel.h" #include "smtk/extension/paraview/appcomponents/pqSMTKWrapper.h" #include "smtk/extension/qt/qtDescriptivePhraseModel.h" #include "smtk/extension/qt/qtResourceBrowser.h" +#include "smtk/extension/qt/task/qtTaskEditor.h" #include "smtk/view/PhraseModel.h" #include "smtk/view/SelectionObserver.h" @@ -28,6 +30,8 @@ #include "smtk/attribute/ReferenceItem.h" +#include "pqApplicationCore.h" + #include #include @@ -257,6 +261,12 @@ int pqSMTKOperationHintsBehavior::processHints( { project->taskManager().active().switchTo(task.get()); } + if ( + auto* taskPanel = + dynamic_cast(pqApplicationCore::instance()->manager("smtk task panel"))) + { + taskPanel->taskPanel()->displayProject(project); + } }); return 0; @@ -424,25 +434,16 @@ void pqSMTKOperationHintsBehavior::unobserveWrapper(pqSMTKWrapper* wrapper, pqSe { (void)wrapper; auto oit = m_p->m_opObservers.find(server); + // The observers may not alwats exist (e.g., during tests whose first recorded + // action is to disconnect the pre-existing built-in server). If they do exist, + // remove them. if (oit != m_p->m_opObservers.end()) { m_p->m_opObservers.erase(oit); } - else - { - smtkWarningMacro( - smtk::io::Logger::instance(), - "No operation observer existed for server " << server << " being disconnected."); - } auto sit = m_p->m_selnObservers.find(server); if (sit != m_p->m_selnObservers.end()) { m_p->m_selnObservers.erase(sit); } - else - { - smtkWarningMacro( - smtk::io::Logger::instance(), - "No selection observer existed for server " << server << " being disconnected."); - } } diff --git a/smtk/extension/paraview/appcomponents/pqSMTKTaskDock.h b/smtk/extension/paraview/appcomponents/pqSMTKTaskDock.h new file mode 100644 index 0000000000..c2906dca92 --- /dev/null +++ b/smtk/extension/paraview/appcomponents/pqSMTKTaskDock.h @@ -0,0 +1,26 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_extension_paraview_appcomponents_pqSMTKTaskDock_h +#define smtk_extension_paraview_appcomponents_pqSMTKTaskDock_h + +#include "smtk/extension/paraview/appcomponents/pqSMTKDock.h" +#include "smtk/extension/paraview/appcomponents/pqSMTKTaskPanel.h" + +class SMTKPQCOMPONENTSEXT_EXPORT pqSMTKTaskDock : public pqSMTKDock +{ + Q_OBJECT +public: + pqSMTKTaskDock(QWidget* parent = nullptr) + : pqSMTKDock("pqSMTKTaskDock", parent) + { + } + ~pqSMTKTaskDock() override = default; +}; +#endif // smtk_extension_paraview_appcomponents_pqSMTKTaskDock_h diff --git a/smtk/extension/paraview/appcomponents/pqSMTKTaskPanel.cxx b/smtk/extension/paraview/appcomponents/pqSMTKTaskPanel.cxx new file mode 100644 index 0000000000..c1737c452c --- /dev/null +++ b/smtk/extension/paraview/appcomponents/pqSMTKTaskPanel.cxx @@ -0,0 +1,166 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#include "smtk/extension/paraview/appcomponents/pqSMTKTaskPanel.h" + +#include "smtk/extension/qt/task/qtTaskEditor.h" + +#include "smtk/extension/paraview/appcomponents/ApplicationConfiguration.h" +#include "smtk/extension/paraview/appcomponents/pqSMTKBehavior.h" +#include "smtk/extension/paraview/appcomponents/pqSMTKWrapper.h" + +#include "smtk/extension/qt/qtDescriptivePhraseModel.h" + +#include "smtk/io/Logger.h" + +#include "smtk/view/json/jsonView.h" + +#include "pqApplicationCore.h" + +#include + +pqSMTKTaskPanel::pqSMTKTaskPanel(QWidget* parent) + : Superclass(parent) +{ + this->setObjectName("pqSMTKTaskPanel"); + auto* pqCore = pqApplicationCore::instance(); + if (pqCore) + { + pqCore->registerManager("smtk task panel", this); + } + + // Either we get the application's configuration or we use a default + // until the application's configuration plugin is loaded. + bool immediatelyConfigured = false; + smtk::paraview::ApplicationConfiguration::notify( + [this, &immediatelyConfigured](smtk::paraview::ApplicationConfiguration& configurator) { + auto viewInfo = configurator.panelConfiguration(this); + // Extract just the view configuration. + auto viewConfig = viewInfo.get(); + if (viewConfig) + { + this->setView(viewConfig); + immediatelyConfigured = true; + } + }); + if (!immediatelyConfigured) + { + // Parse a json representation of our default config, and use it + // since the application can't immediately configure us. + smtk::view::ConfigurationPtr config = smtk::extension::qtTaskEditor::defaultConfiguration(); + this->setView(config); + } + + auto* smtkBehavior = pqSMTKBehavior::instance(); + // Now listen for future connections. + QObject::connect( + smtkBehavior, + SIGNAL(addedManagerOnServer(pqSMTKWrapper*, pqServer*)), + this, + SLOT(resourceManagerAdded(pqSMTKWrapper*, pqServer*))); + QObject::connect( + smtkBehavior, + SIGNAL(removingManagerFromServer(pqSMTKWrapper*, pqServer*)), + this, + SLOT(resourceManagerRemoved(pqSMTKWrapper*, pqServer*))); +} + +pqSMTKTaskPanel::~pqSMTKTaskPanel() +{ + auto* pqCore = pqApplicationCore::instance(); + if (pqCore) + { + pqCore->unRegisterManager("smtk task panel"); + } + delete m_viewUIMgr; + // m_viewUIMgr deletes m_taskPanel + // deletion of m_taskPanel->widget() is handled when parent widget is deleted. +} + +void pqSMTKTaskPanel::setView(const smtk::view::ConfigurationPtr& view) +{ + m_view = view; + + auto* smtkBehavior = pqSMTKBehavior::instance(); + + smtkBehavior->visitResourceManagersOnServers([this](pqSMTKWrapper* r, pqServer* s) { + this->resourceManagerAdded(r, s); + return false; + }); +} + +void pqSMTKTaskPanel::resourceManagerAdded(pqSMTKWrapper* wrapper, pqServer* server) +{ + if (!wrapper || !server) + { + return; + } + Q_EMIT this->titleChanged("Tasks"); + + smtk::resource::ManagerPtr rsrcMgr = wrapper->smtkResourceManager(); + smtk::view::ManagerPtr viewMgr = wrapper->smtkViewManager(); + if (!rsrcMgr || !viewMgr) + { + return; + } + if (m_viewUIMgr) + { + delete m_viewUIMgr; + m_viewUIMgr = nullptr; + // m_viewUIMgr deletes m_taskPanel, which deletes the container, which deletes child QWidgets. + m_taskPanel = nullptr; + } + + m_viewUIMgr = new smtk::extension::qtUIManager(rsrcMgr, viewMgr); + m_viewUIMgr->setOperationManager(wrapper->smtkOperationManager()); + m_viewUIMgr->setSelection(wrapper->smtkSelection()); + // m_viewUIMgr->setSelectionBit(1); // ToDo: should be set ? + + smtk::view::Information resinfo; + resinfo.insert(m_view); + resinfo.insert(this); + resinfo.insert(m_viewUIMgr); + + // the top-level "Type" in m_view should be qtTaskEditor or compatible. + auto* baseView = m_viewUIMgr->setSMTKView(resinfo); + m_taskPanel = dynamic_cast(baseView); + if (baseView && !m_taskPanel) + { + smtkErrorMacro(smtk::io::Logger::instance(), "Unsupported task panel type."); + return; + } + else if (!m_taskPanel) + { + return; + } + m_taskPanel->widget()->setObjectName("qtTaskEditor"); + std::string title; + m_view->details().attribute("Title", title); + if (title.empty()) + { + title = "Resources"; + } + this->setWindowTitle(title.c_str()); + Q_EMIT titleChanged(title.c_str()); + if (!m_layout) + { + m_layout = new QVBoxLayout; + m_layout->setObjectName("Layout"); + this->setLayout(m_layout); + } + m_layout->addWidget(m_taskPanel->widget()); +} + +void pqSMTKTaskPanel::resourceManagerRemoved(pqSMTKWrapper* mgr, pqServer* server) +{ + if (!mgr || !server) + { + return; + } +} diff --git a/smtk/extension/paraview/appcomponents/pqSMTKTaskPanel.h b/smtk/extension/paraview/appcomponents/pqSMTKTaskPanel.h new file mode 100644 index 0000000000..af86e515e7 --- /dev/null +++ b/smtk/extension/paraview/appcomponents/pqSMTKTaskPanel.h @@ -0,0 +1,60 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_extension_paraview_appcomponents_pqSMTKTaskPanel_h +#define smtk_extension_paraview_appcomponents_pqSMTKTaskPanel_h + +#include "smtk/extension/paraview/appcomponents/smtkPQComponentsExtModule.h" + +#include "smtk/extension/qt/qtUIManager.h" +#include "smtk/extension/qt/task/qtTaskEditor.h" + +#include +#include + +#include "smtk/extension/paraview/appcomponents/pqQtKeywordWrapping.h" + +class pqSMTKWrapper; +class pqServer; +class QVBoxLayout; + +/**\brief A panel that displays SMTK tasks available to the user. + * + */ +class SMTKPQCOMPONENTSEXT_EXPORT pqSMTKTaskPanel : public QWidget +{ + Q_OBJECT + typedef QWidget Superclass; + +public: + pqSMTKTaskPanel(QWidget* parent = nullptr); + ~pqSMTKTaskPanel() override; + + /// Let the panel display a custom view config, from json or xml. + void setView(const smtk::view::ConfigurationPtr& view); + + /// Access the underlying resource browser. + smtk::extension::qtTaskEditor* taskPanel() const { return m_taskPanel; } + +Q_SIGNALS: + void titleChanged(QString title); + +protected Q_SLOTS: + virtual void resourceManagerAdded(pqSMTKWrapper* mgr, pqServer* server); + virtual void resourceManagerRemoved(pqSMTKWrapper* mgr, pqServer* server); + +protected: + smtk::extension::qtTaskEditor* m_taskPanel{ nullptr }; + smtk::view::ConfigurationPtr m_view; + smtk::extension::qtUIManager* m_viewUIMgr{ nullptr }; + /// The central widget's layout. + QPointer m_layout; +}; + +#endif // smtk_extension_paraview_appcomponents_pqSMTKTaskPanel_h diff --git a/smtk/extension/paraview/mesh/testing/xml/MeshSelection.xml b/smtk/extension/paraview/mesh/testing/xml/MeshSelection.xml index 751da2d8f7..e46d5bed19 100644 --- a/smtk/extension/paraview/mesh/testing/xml/MeshSelection.xml +++ b/smtk/extension/paraview/mesh/testing/xml/MeshSelection.xml @@ -8,6 +8,7 @@ + diff --git a/smtk/extension/paraview/project/CMakeLists.txt b/smtk/extension/paraview/project/CMakeLists.txt index 4a5d61c1ad..bba2c37d19 100644 --- a/smtk/extension/paraview/project/CMakeLists.txt +++ b/smtk/extension/paraview/project/CMakeLists.txt @@ -1,4 +1,5 @@ set(classes + pqSMTKDisplayProjectOnLoadBehavior pqSMTKProjectAutoStart pqSMTKProjectBrowser pqSMTKProjectMenu diff --git a/smtk/extension/paraview/project/plugin-core/CMakeLists.txt b/smtk/extension/paraview/project/plugin-core/CMakeLists.txt index 5dce9bb808..41bd735d1a 100644 --- a/smtk/extension/paraview/project/plugin-core/CMakeLists.txt +++ b/smtk/extension/paraview/project/plugin-core/CMakeLists.txt @@ -2,7 +2,7 @@ set(sources) set(interfaces) -smtk_add_plugin(smtkProjectPlugin +smtk_add_plugin(smtkPQCoreProjectPlugin REGISTRAR smtk::extension::paraview::project::Registrar MANAGERS smtk::common::Managers @@ -12,11 +12,12 @@ smtk_add_plugin(smtkProjectPlugin smtk::view::Manager PARAVIEW_PLUGIN_ARGS VERSION "1.0" + REQUIRED_PLUGINS smtkProjectPlugin UI_INTERFACES ${interfaces} SOURCES ${sources} ) -target_link_libraries(smtkProjectPlugin +target_link_libraries(smtkPQCoreProjectPlugin PRIVATE ParaView::pqApplicationComponents smtkCore diff --git a/smtk/extension/paraview/project/plugin-core/paraview.plugin b/smtk/extension/paraview/project/plugin-core/paraview.plugin index 8cc954cd18..32594ebec0 100644 --- a/smtk/extension/paraview/project/plugin-core/paraview.plugin +++ b/smtk/extension/paraview/project/plugin-core/paraview.plugin @@ -1,4 +1,4 @@ NAME - smtkProjectPlugin + smtkPQCoreProjectPlugin DESCRIPTION SMTK project core plugin for ParaView diff --git a/smtk/extension/paraview/project/plugin-gui/CMakeLists.txt b/smtk/extension/paraview/project/plugin-gui/CMakeLists.txt index ad3126aef1..47b8c4e4ee 100644 --- a/smtk/extension/paraview/project/plugin-gui/CMakeLists.txt +++ b/smtk/extension/paraview/project/plugin-gui/CMakeLists.txt @@ -20,14 +20,14 @@ set(interfaces ${dock_interfaces} ) -paraview_add_plugin(smtkPQProjectPlugin +paraview_add_plugin(smtkPQGuiProjectPlugin VERSION "1.0" - REQUIRED_PLUGINS smtkProjectPlugin + REQUIRED_PLUGINS smtkPQCoreProjectPlugin UI_INTERFACES ${interfaces} SOURCES ${sources} ) -target_link_libraries(smtkPQProjectPlugin +target_link_libraries(smtkPQGuiProjectPlugin PRIVATE ParaView::pqApplicationComponents smtkCore @@ -35,4 +35,4 @@ target_link_libraries(smtkPQProjectPlugin smtkPQProjectExt smtkPVServerExt ) -target_compile_definitions(smtkPQProjectPlugin PRIVATE QT_NO_KEYWORDS) +target_compile_definitions(smtkPQGuiProjectPlugin PRIVATE QT_NO_KEYWORDS) diff --git a/smtk/extension/paraview/project/plugin-gui/paraview.plugin b/smtk/extension/paraview/project/plugin-gui/paraview.plugin index 7750c5575f..c7ba40ea0f 100644 --- a/smtk/extension/paraview/project/plugin-gui/paraview.plugin +++ b/smtk/extension/paraview/project/plugin-gui/paraview.plugin @@ -1,4 +1,4 @@ NAME - smtkPQProjectPlugin + smtkPQGuiProjectPlugin DESCRIPTION - SMTK project gui plugin for ParaView + SMTK project GUI plugin for ParaView diff --git a/smtk/extension/paraview/project/pqSMTKDisplayProjectOnLoadBehavior.cxx b/smtk/extension/paraview/project/pqSMTKDisplayProjectOnLoadBehavior.cxx new file mode 100644 index 0000000000..ed58797b55 --- /dev/null +++ b/smtk/extension/paraview/project/pqSMTKDisplayProjectOnLoadBehavior.cxx @@ -0,0 +1,169 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#include "smtk/extension/paraview/project/pqSMTKDisplayProjectOnLoadBehavior.h" + +// SMTK +#include "smtk/extension/paraview/appcomponents/pqSMTKBehavior.h" +#include "smtk/extension/paraview/appcomponents/pqSMTKTaskPanel.h" +#include "smtk/extension/paraview/appcomponents/pqSMTKWrapper.h" + +#include "smtk/extension/paraview/server/vtkSMSMTKWrapperProxy.h" + +#include "smtk/view/Selection.h" + +#include "smtk/project/Manager.h" +#include "smtk/project/Project.h" + +#include "smtk/io/Logger.h" + +// Client side +#include "pqApplicationCore.h" +#include "pqCoreUtilities.h" +#include "pqLiveInsituManager.h" +#include "pqPipelineSource.h" +#include "pqSelectionManager.h" +#include "pqServerManagerModel.h" + +// Qt +#include +#include +#include + +using namespace smtk; + +static pqSMTKDisplayProjectOnLoadBehavior* g_displayOnLoad = nullptr; + +pqSMTKDisplayProjectOnLoadBehavior::pqSMTKDisplayProjectOnLoadBehavior(QObject* parent) + : Superclass(parent) +{ + if (!g_displayOnLoad) + { + g_displayOnLoad = this; + } + + // Track server connects/disconnects + auto* projectBehavior = pqSMTKBehavior::instance(); + QObject::connect( + projectBehavior, + SIGNAL(addedManagerOnServer(vtkSMSMTKWrapperProxy*, pqServer*)), + this, + SLOT(observeProjectsOnServer(vtkSMSMTKWrapperProxy*, pqServer*))); + QObject::connect( + projectBehavior, + SIGNAL(removingManagerFromServer(vtkSMSMTKWrapperProxy*, pqServer*)), + this, + SLOT(unobserveProjectsOnServer(vtkSMSMTKWrapperProxy*, pqServer*))); +} + +pqSMTKDisplayProjectOnLoadBehavior::~pqSMTKDisplayProjectOnLoadBehavior() +{ + if (g_displayOnLoad == this) + { + g_displayOnLoad = nullptr; + } +} + +pqSMTKDisplayProjectOnLoadBehavior* pqSMTKDisplayProjectOnLoadBehavior::instance(QObject* parent) +{ + if (!g_displayOnLoad) + { + g_displayOnLoad = new pqSMTKDisplayProjectOnLoadBehavior(parent); + } + + return g_displayOnLoad; +} + +void pqSMTKDisplayProjectOnLoadBehavior::observeProjectsOnServer( + vtkSMSMTKWrapperProxy* mgr, + pqServer* server) +{ + (void)server; + if (!mgr) + { + vtkGenericWarningMacro("No wrapper."); + return; + } + auto projectManager = mgr->GetProjectManager(); + if (!projectManager) + { + vtkGenericWarningMacro("No project manager to observe."); + return; + } + + auto observerKey = projectManager->observers().insert( + [this](const smtk::project::Project& project, smtk::project::EventType event) { + this->handleProjectEvent(project, event); + }, + 0, // assign a neutral priority + true, // immediatelyNotify + "pqSMTKDisplayProjectOnLoadBehavior: Display new attribute project in panel."); + m_projectManagerObservers[projectManager] = std::move(observerKey); +} + +void pqSMTKDisplayProjectOnLoadBehavior::unobserveProjectsOnServer( + vtkSMSMTKWrapperProxy* mgr, + pqServer* server) +{ + (void)server; + if (!mgr) + { + return; + } + auto projectManager = mgr->GetProjectManager(); + if (!projectManager) + { + return; + } + + auto entry = m_projectManagerObservers.find(projectManager); + if (entry != m_projectManagerObservers.end()) + { + projectManager->observers().erase(entry->second); + m_projectManagerObservers.erase(entry); + } +} + +void pqSMTKDisplayProjectOnLoadBehavior::handleProjectEvent( + const smtk::project::Project& project, + smtk::project::EventType event) +{ + auto* taskManager = const_cast(&project.taskManager()); + switch (event) + { + case smtk::project::EventType::ADDED: + this->focusTaskPanel(taskManager); + break; + case smtk::project::EventType::REMOVED: + this->focusTaskPanel(nullptr); + break; + case smtk::project::EventType::MODIFIED: + default: + // Do nothing. + break; + } +} + +void pqSMTKDisplayProjectOnLoadBehavior::focusTaskPanel(smtk::task::Manager* taskManager) +{ + // Use QTimer to wait until the event queue is emptied before trying this; + // that gives operations time to complete. Blech. + QTimer::singleShot(0, [this, taskManager]() { + auto* core = pqApplicationCore::instance(); + auto* panel = dynamic_cast(core->manager("smtk task panel")); + if (panel) + { + panel->taskPanel()->displayTaskManager(taskManager); + if (auto* parent = dynamic_cast(panel->parent())) + { + parent->raise(); // Make sure the widget is visible and raised. + } + } + }); +} diff --git a/smtk/extension/paraview/project/pqSMTKDisplayProjectOnLoadBehavior.h b/smtk/extension/paraview/project/pqSMTKDisplayProjectOnLoadBehavior.h new file mode 100644 index 0000000000..3b9f1ea264 --- /dev/null +++ b/smtk/extension/paraview/project/pqSMTKDisplayProjectOnLoadBehavior.h @@ -0,0 +1,73 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_extension_paraview_project_pqSMTKDisplayProjectOnLoadBehavior_h +#define smtk_extension_paraview_project_pqSMTKDisplayProjectOnLoadBehavior_h + +#include "smtk/extension/paraview/project/smtkPQProjectExtModule.h" + +#include "smtk/PublicPointerDefs.h" + +#include "smtk/project/Observer.h" // for EventType + +#include "smtk/extension/paraview/appcomponents/pqQtKeywordWrapping.h" + +#include + +#include + +class vtkSMSMTKWrapperProxy; + +class pqServer; + +namespace smtk +{ +namespace task +{ +class Manager; +} +} // namespace smtk + +/**\brief Make the SMTK task panel display a project when one is loaded. + * + * This is accomplished by adding an observer to the application's project manager. + * When a project is loaded, the task panel is made visible and raised to the top + * (if in a tabbed dock). + */ +class SMTKPQPROJECTEXT_EXPORT pqSMTKDisplayProjectOnLoadBehavior : public QObject +{ + Q_OBJECT + using Superclass = QObject; + +public: + pqSMTKDisplayProjectOnLoadBehavior(QObject* parent = nullptr); + ~pqSMTKDisplayProjectOnLoadBehavior() override; + + /// This behavior is a singleton. + static pqSMTKDisplayProjectOnLoadBehavior* instance(QObject* parent = nullptr); + +protected Q_SLOTS: + //@{ + /// Observe server connections/disconnections so we can monitor each server's + /// projects for updates and, on project load/creation, force the task panel + /// to the top. + virtual void observeProjectsOnServer(vtkSMSMTKWrapperProxy* mgr, pqServer* server); + virtual void unobserveProjectsOnServer(vtkSMSMTKWrapperProxy* mgr, pqServer* server); + virtual void handleProjectEvent(const smtk::project::Project&, smtk::project::EventType); + virtual void focusTaskPanel(smtk::task::Manager* taskManager); + //@} + +protected: + std::map m_projectManagerObservers; + +private: + Q_DISABLE_COPY(pqSMTKDisplayProjectOnLoadBehavior); +}; + +#endif diff --git a/smtk/extension/paraview/project/pqSMTKProjectAutoStart.cxx b/smtk/extension/paraview/project/pqSMTKProjectAutoStart.cxx index a5747d2a7e..dc46dfc299 100644 --- a/smtk/extension/paraview/project/pqSMTKProjectAutoStart.cxx +++ b/smtk/extension/paraview/project/pqSMTKProjectAutoStart.cxx @@ -11,6 +11,7 @@ #include "smtk/extension/paraview/appcomponents/pqSMTKResourceDock.h" #include "smtk/extension/paraview/appcomponents/pqSMTKResourcePanel.h" +#include "smtk/extension/paraview/project/pqSMTKDisplayProjectOnLoadBehavior.h" #include "smtk/extension/paraview/project/pqSMTKProjectMenu.h" #include "smtk/project/Project.h" #include "smtk/view/ResourcePhraseModel.h" @@ -67,10 +68,12 @@ pqSMTKProjectAutoStart::~pqSMTKProjectAutoStart() = default; void pqSMTKProjectAutoStart::startup() { auto* projectMenuMgr = pqSMTKProjectMenu::instance(this); + auto* displayProjectOnLoad = pqSMTKDisplayProjectOnLoadBehavior::instance(this); auto* pqCore = pqApplicationCore::instance(); if (pqCore) { + pqCore->registerManager("smtk display project on load", displayProjectOnLoad); pqCore->registerManager("smtk project menu", projectMenuMgr); } @@ -84,6 +87,7 @@ void pqSMTKProjectAutoStart::shutdown() auto* pqCore = pqApplicationCore::instance(); if (pqCore) { + pqCore->unRegisterManager("smtk display attribute on load"); pqCore->unRegisterManager("smtk project menu"); } } diff --git a/smtk/extension/qt/CMakeLists.txt b/smtk/extension/qt/CMakeLists.txt index 60ce53b4b5..5aa10d5f8a 100644 --- a/smtk/extension/qt/CMakeLists.txt +++ b/smtk/extension/qt/CMakeLists.txt @@ -68,6 +68,14 @@ set(QAttrLibSrcs SVGIconEngine.cxx TypeAndColorBadge.cxx VisibilityBadge.cxx + + task/qtTaskArc.cxx + task/qtTaskEditor.cxx + task/qtTaskNode.cxx + task/qtTaskScene.cxx + task/qtTaskView.cxx + task/qtTaskViewConfiguration.cxx + task/TaskEditorState.cxx ) set(QAttrLibUIs @@ -78,6 +86,7 @@ set(QAttrLibUIs qtNewAttributeWidget.ui qtTimeZoneSelectWidget.ui qtViewInfoDialog.ui + task/TaskNode.ui ) @@ -147,6 +156,14 @@ set(QAttrLibMocHeaders MembershipBadge.h SVGIconEngine.h VisibilityBadge.h + + task/qtTaskArc.h + task/qtTaskEditor.h + task/qtTaskNode.h + task/qtTaskScene.h + task/qtTaskView.h + task/qtTaskViewConfiguration.h + task/TaskEditorState.h ) set(QAttrLibHeaders @@ -164,11 +181,18 @@ set(QAttrLibHeaders qtViewRegistrar.h ) -# put contents of this file in a string in a header, ending _json.h +# Put the contents of this file in a string in a header, ending in `_json.h`. smtk_encode_file("${CMAKE_CURRENT_SOURCE_DIR}/ResourcePanelConfiguration.json" TYPE "_json" HEADER_OUTPUT rpJsonHeader) +# Put the contents of this file in a *function* in a header, ending in `_cpp.h`. +smtk_encode_file("${CMAKE_CURRENT_SOURCE_DIR}/task/PanelConfiguration.json" + NAME "taskPanelConfiguration" + TYPE "_cpp" + HEADER_OUTPUT tpXmlHeader +) + #install the headers smtk_public_headers(smtkQtExt ${QAttrLibHeaders}) @@ -184,6 +208,17 @@ add_library(smtkQtExt qtReferenceItemIcons.qrc qtAttributeIcons.qrc ${rpJsonHeader} + ${tpXmlHeader} +) + +if (SMTK_ENABLE_GRAPHVIZ_SUPPORT) + list(APPEND _extra_private_libraries + Graphviz::cgraph + Graphviz::gvc + ) +endif() +target_compile_definitions(smtkQtExt PRIVATE + "SMTK_ENABLE_GRAPHVIZ_SUPPORT=$" ) if (NOT SMTK_ENABLE_OPERATION_THREADS) @@ -203,6 +238,7 @@ target_link_libraries(smtkQtExt Qt5::Widgets LINK_PRIVATE Threads::Threads + ${_extra_private_libraries} ) # add_dependencies(smtkQtExt ${rpJsonTarget}) diff --git a/smtk/extension/qt/qtViewRegistrar.cxx b/smtk/extension/qt/qtViewRegistrar.cxx index 50bf665b9b..49ba14bd4e 100644 --- a/smtk/extension/qt/qtViewRegistrar.cxx +++ b/smtk/extension/qt/qtViewRegistrar.cxx @@ -25,6 +25,7 @@ #include "smtk/extension/qt/qtResourceBrowser.h" #include "smtk/extension/qt/qtSelectorView.h" #include "smtk/extension/qt/qtSimpleExpressionView.h" +#include "smtk/extension/qt/task/qtTaskEditor.h" #include @@ -34,7 +35,7 @@ namespace extension { namespace { -typedef std::tuple< +using ViewWidgetList = std::tuple< qtAnalysisView, qtAssociationView, qtAttributeView, @@ -46,8 +47,8 @@ typedef std::tuple< qtOperationPalette, qtResourceBrowser, qtSelectorView, - qtSimpleExpressionView> - ViewWidgetList; + qtSimpleExpressionView, + qtTaskEditor>; using BadgeList = std::tuple; } // namespace @@ -70,6 +71,7 @@ void qtViewRegistrar::registerTo(const smtk::view::Manager::Ptr& manager) manager->viewWidgetFactory().addAlias("ModelEntity"); manager->viewWidgetFactory().addAlias("ComponentAttribute"); manager->viewWidgetFactory().addAlias("ResourceBrowser"); + manager->viewWidgetFactory().addAlias("TaskEditor"); manager->badgeFactory().registerTypes(); } diff --git a/smtk/extension/qt/task/PanelConfiguration.json b/smtk/extension/qt/task/PanelConfiguration.json new file mode 100644 index 0000000000..29e0bc0f94 --- /dev/null +++ b/smtk/extension/qt/task/PanelConfiguration.json @@ -0,0 +1,15 @@ +[ + { + "Name": "TaskView", + "Type": "TaskEditor", + "Component": { + "Name": "Details", + "Attributes": { + "Title": "Tasks", + "TopLevel": true, + "Widget": "TaskGraph", + "SearchBar": true + } + } + } +] diff --git a/smtk/extension/qt/task/PanelConfiguration.spec.json b/smtk/extension/qt/task/PanelConfiguration.spec.json new file mode 100644 index 0000000000..69eda5f0b4 --- /dev/null +++ b/smtk/extension/qt/task/PanelConfiguration.spec.json @@ -0,0 +1,78 @@ +[ + "// The object below is the full set of panel configuration options accepted.", + "// Several of the colors should not be specified by workflow developers (such as", + "// BacgroundFill) since they will override user-selected themes (e.g. dark mode).", + { + "Name": "TaskView", + "Type": "TaskEditor", + "Component": { + "Name": "Details", + "Attributes": { + "Title": "Tasks", + "TopLevel": true, + "Widget": "TaskGraph", + "SearchBar": true + }, + "Children": [ + { + "Name": "Style", + "Attributes": { + "NodeInfo": "compact", + "DependencyArcs": true, + "AdaptorArcs": true + }, + "Children": [ + { + "Name": "NodeLayout", + "Attributes": { + "Width": 300.0, + "Radius": 4.0, + "HeadlineHeight": 13.0, + "HeadlinePadding": 4.0, + "BorderThickness": 4.0, + "FontSize": 13, + "Layer": 10 + } + }, + { + "Name": "ArcLayout", + "Attributes": { + "Width": 4.0, + "Outline": 1.0, + "Layer": 5, + "ArrowStemLength": 16.0, + "ArrowHeadLength": 12.0, + "ArrowTipAspectRatio": 2.0 + } + }, + { + "Name": "ArcPalette", + "Attributes": { + "Dependency": "#BF5B17", + "Adaptor": "#386CB0" + } + }, + { + "Name": "StatusPalette", + "Attributes": { + "Unavailable": "#ff9898", + "Incomplete": "#ffeca3", + "Completable": "#e0f1ba", + "Complete": "#7ed637", + "Irrelevant": "#e7e7e7" + } + }, + { + "Name": "ViewPalette", + "Attributes": { + "BackgroundGrid": "#cccccc", + "BackgroundFill": "#ffffff", + "ActiveTask": "#aaaaff" + } + } + ] + } + ] + } + } +] diff --git a/smtk/extension/qt/task/TaskEditorState.cxx b/smtk/extension/qt/task/TaskEditorState.cxx new file mode 100644 index 0000000000..beb61931bb --- /dev/null +++ b/smtk/extension/qt/task/TaskEditorState.cxx @@ -0,0 +1,39 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#include "smtk/extension/qt/task/TaskEditorState.h" + +#include "smtk/extension/qt/task/qtTaskEditor.h" +#include "smtk/task/Task.h" + +#include "nlohmann/json.hpp" + +namespace smtk +{ +namespace extension +{ + +TaskEditorState::TaskEditorState(qtTaskEditor* taskEditor) + : m_editor(taskEditor) +{ +} + +nlohmann::json TaskEditorState::globalState() const +{ + return nlohmann::json(); +} + +nlohmann::json TaskEditorState::taskState(const std::shared_ptr& task) const +{ + (void)task; + return m_editor->uiStateForTask(task.get()); +} + +} // namespace extension +} // namespace smtk diff --git a/smtk/extension/qt/task/TaskEditorState.h b/smtk/extension/qt/task/TaskEditorState.h new file mode 100644 index 0000000000..ed270a0694 --- /dev/null +++ b/smtk/extension/qt/task/TaskEditorState.h @@ -0,0 +1,43 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_extension_TaskEditorState_h +#define smtk_extension_TaskEditorState_h + +#include "smtk/extension/qt/Exports.h" + +#include "smtk/task/UIStateGenerator.h" + +namespace smtk +{ +namespace extension +{ + +class qtTaskEditor; + +/** \brief A widget that holds a Qt scene graph. */ +class SMTKQTEXT_EXPORT TaskEditorState : public smtk::task::UIStateGenerator +{ +public: + TaskEditorState(qtTaskEditor* taskEditor); + ~TaskEditorState() = default; + + /// Provide any global state for the task-editor that should be saved across sessions. + nlohmann::json globalState() const override; + /// Provide UI state for the given task so it can be saved across sessions. + nlohmann::json taskState(const std::shared_ptr& task) const override; + +protected: + qtTaskEditor* m_editor{ nullptr }; +}; + +} // namespace extension +} // namespace smtk + +#endif diff --git a/smtk/extension/qt/task/TaskNode.ui b/smtk/extension/qt/task/TaskNode.ui new file mode 100644 index 0000000000..280858b135 --- /dev/null +++ b/smtk/extension/qt/task/TaskNode.ui @@ -0,0 +1,185 @@ + + + TaskNode + + + + 0 + 0 + 171 + 80 + + + + Form + + + + + + + + + + + + + 192 + 191 + 188 + + + + + + + 119 + 118 + 123 + + + + + + + + + 192 + 191 + 188 + + + + + + + 119 + 118 + 123 + + + + + + + + + 146 + 149 + 149 + + + + + + + 146 + 149 + 149 + + + + + + + + + 13 + 75 + true + + + + ClosedHandCursor + + + + + + + + + + + 0 + 0 + + + + + + + + + 119 + 118 + 123 + + + + + + + + + 119 + 118 + 123 + + + + + + + + + 119 + 118 + 123 + + + + + + + + + 13 + 75 + true + + + + Task title + + + QToolButton::MenuButtonPopup + + + Qt::ToolButtonTextBesideIcon + + + + + + + + + + 0 + 0 + + + + Controls + + + true + + + + + + + + diff --git a/smtk/extension/qt/task/qtTaskArc.cxx b/smtk/extension/qt/task/qtTaskArc.cxx new file mode 100644 index 0000000000..532f791a79 --- /dev/null +++ b/smtk/extension/qt/task/qtTaskArc.cxx @@ -0,0 +1,311 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#include "smtk/extension/qt/task/qtTaskArc.h" + +#include "smtk/extension/qt/qtBaseView.h" +#include "smtk/extension/qt/task/qtTaskNode.h" +#include "smtk/extension/qt/task/qtTaskScene.h" +#include "smtk/extension/qt/task/qtTaskViewConfiguration.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +class QAbstractItemModel; +class QItemSelection; +class QTreeView; + +namespace smtk +{ +namespace extension +{ + +qtTaskArc::qtTaskArc( + qtTaskScene* scene, + qtTaskNode* predecessor, + qtTaskNode* successor, + ArcType arcType, + QGraphicsItem* parent) + : Superclass(parent) + , m_scene(scene) + , m_predecessor(predecessor) + , m_successor(successor) + , m_arcType(arcType) +{ + const auto& cfg(*m_scene->configuration()); + QObject::connect( + m_predecessor, &qtTaskNode::nodeMovedImmediate, this, &qtTaskArc::updateArcPoints); + QObject::connect(m_predecessor, &qtTaskNode::nodeResized, this, &qtTaskArc::updateArcPoints); + QObject::connect(m_successor, &qtTaskNode::nodeMovedImmediate, this, &qtTaskArc::updateArcPoints); + QObject::connect(m_successor, &qtTaskNode::nodeResized, this, &qtTaskArc::updateArcPoints); + this->setAcceptedMouseButtons(Qt::NoButton); + + this->updateArcPoints(); + + m_scene->addItem(this); + + // === Task-specific constructor === + this->setZValue(cfg.arcLayer() + 1); // Draw dependencies on top of adaptors +} + +qtTaskArc::qtTaskArc( + qtTaskScene* scene, + qtTaskNode* predecessor, + qtTaskNode* successor, + smtk::task::Adaptor* adaptor, + QGraphicsItem* parent) + : Superclass(parent) + , m_scene(scene) + , m_predecessor(predecessor) + , m_successor(successor) + , m_adaptor(adaptor) + , m_arcType(ArcType::Adaptor) +{ + const auto& cfg(*m_scene->configuration()); + QObject::connect(m_predecessor, &qtTaskNode::nodeMoved, this, &qtTaskArc::updateArcPoints); + QObject::connect(m_predecessor, &qtTaskNode::nodeResized, this, &qtTaskArc::updateArcPoints); + QObject::connect(m_successor, &qtTaskNode::nodeMoved, this, &qtTaskArc::updateArcPoints); + QObject::connect(m_successor, &qtTaskNode::nodeResized, this, &qtTaskArc::updateArcPoints); + this->setAcceptedMouseButtons(Qt::NoButton); + + this->updateArcPoints(); + + m_scene->addItem(this); + + // === Task-specific constructor === + this->setZValue(cfg.arcLayer()); +} + +qtTaskArc::~qtTaskArc() +{ + m_scene->removeItem(this); +} + +QRectF qtTaskArc::boundingRect() const +{ + const auto& cfg(*m_scene->configuration()); + const qreal margin = cfg.arcWidth() + cfg.arcOutline(); + + QRectF pb = this->path().boundingRect(); + return pb.adjusted(-margin, -margin, margin, margin); +} + +qtTaskArc::ArcType qtTaskArc::arcTypeEnum(const std::string& enumerant, bool* match) +{ + if (match) + { + *match = true; + } + std::string arcTypeName(enumerant); + std::transform(arcTypeName.begin(), arcTypeName.end(), arcTypeName.begin(), [](unsigned char c) { + return std::tolower(c); + }); + if (arcTypeName.substr(0, 6) == "smtk::") + { + arcTypeName = arcTypeName.substr(6); + } + if (arcTypeName.substr(0, 11) == "extension::") + { + arcTypeName = arcTypeName.substr(11); + } + if (arcTypeName.substr(0, 11) == "qttaskarc::") + { + arcTypeName = arcTypeName.substr(11); + } + if (arcTypeName.substr(0, 9) == "arctype::") + { + arcTypeName = arcTypeName.substr(9); + } + if (arcTypeName == "dependency") + { + return ArcType::Dependency; + } + if (arcTypeName == "adaptor") + { + return ArcType::Adaptor; + } + if (match) + { + *match = (arcTypeName == "dependency"); + } + return ArcType::Dependency; +} + +std::string qtTaskArc::arcTypeName(ArcType enumerant) +{ + switch (enumerant) + { + case ArcType::Dependency: + return "dependency"; + case ArcType::Adaptor: + return "adaptor"; + } + return "unknown"; +} + +int qtTaskArc::updateArcPoints() +{ + const auto& cfg(*m_scene->configuration()); + this->prepareGeometryChange(); + m_computedPath.clear(); + m_arrowPath.clear(); + + auto predRect = m_predecessor->boundingRect(); + predRect = m_predecessor->mapRectToScene(predRect); + auto succRect = m_successor->boundingRect(); + succRect = m_successor->mapRectToScene(succRect); + + // If the nodes overlap, there is no arc to draw. + if (predRect.intersects(succRect)) + { + this->setPath(m_computedPath); + return 1; + } + + // Determine a line connecting the node centers. + auto pc = predRect.center(); + auto sc = succRect.center(); + auto dl = sc - pc; // delta. Line is L(t) = pc + dl * t, t ∈ [0,1]. + auto dp = predRect.bottomRight() - pc; // Always positive along both axes. + + // Find point, pi, on boundary of predRect and on line bet. pc and sc + qreal tx = dp.x() / std::abs(dl.x()); + qreal ty = dp.y() / std::abs(dl.y()); + QPointF pi; + pi = pc + (tx < ty ? tx : ty) * dl; + // ni is the "normal" to the node at the intersection point pi. + QPointF ni( + tx < ty ? (std::signbit(dl.x()) ? -1. : +1.) : 0., + tx < ty ? 0. : (std::signbit(dl.y()) ? -1. : +1)); + QPointF nearestCorner( + dl.x() < 0. ? (dl.y() < 0. ? predRect.topLeft() : predRect.bottomLeft()) + : (dl.y() < 0. ? predRect.topRight() : predRect.bottomRight())); + QPointF cornerVector(nearestCorner - pi); + // Make the normal transition smoothly near corners: + if ( + (tx >= ty && std::abs(cornerVector.x()) < cfg.arrowStemLength()) || + (tx < ty && std::abs(cornerVector.y()) < cfg.arrowStemLength())) + { + ni = pi - + (nearestCorner + QPointF(std::signbit(dl.x()) ? +16 : -16, std::signbit(dl.y()) ? +16 : -16)); + qreal invMag = 1.0 / std::sqrt(QPointF::dotProduct(ni, ni)); + ni = invMag * ni; + + // Adjust the intersection point to deal with the rounded rectangle corner: + if ( + (tx >= ty && std::abs(cornerVector.x()) < cfg.nodeRadius()) || + (tx < ty && std::abs(cornerVector.y()) < cfg.nodeRadius())) + { + QPointF centerOfCurvature = + nearestCorner + QPointF(std::signbit(dl.x()) ? +4 : -4, std::signbit(dl.y()) ? +4 : -4); + QPointF tmp = pi - centerOfCurvature; + qreal tmpMag = std::sqrt(QPointF::dotProduct(tmp, tmp)); + pi = centerOfCurvature + (4.0 / tmpMag) * tmp; + } + } + + // Now, telescope a couple points out along the normal. + QPointF pi1 = pi + ni * cfg.arrowStemLength(); + QPointF pi2 = pi + ni * cfg.arrowStemLength() * 2; + + // Repeat the above, but with the successor node (so dl is reversed). + dl = -1.0 * dl; + auto ds = succRect.bottomRight() - sc; // Always positive along both axes. + + tx = ds.x() / std::abs(dl.x()); + ty = ds.y() / std::abs(dl.y()); + QPointF si; + si = sc + (tx < ty ? tx : ty) * dl; + // ni is the "normal" to the node at the intersection point si. + ni = QPointF( + tx < ty ? (std::signbit(dl.x()) ? -1. : +1.) : 0., + tx < ty ? 0. : (std::signbit(dl.y()) ? -1. : +1)); + nearestCorner = QPointF( + dl.x() < 0. ? (dl.y() < 0. ? succRect.topLeft() : succRect.bottomLeft()) + : (dl.y() < 0. ? succRect.topRight() : succRect.bottomRight())); + cornerVector = QPointF(nearestCorner - si); + // Make the normal transition smoothly near corners: + if ( + (tx >= ty && std::abs(cornerVector.x()) < cfg.arrowStemLength()) || + (tx < ty && std::abs(cornerVector.y()) < cfg.arrowStemLength())) + { + ni = si - + (nearestCorner + QPointF(std::signbit(dl.x()) ? +16 : -16, std::signbit(dl.y()) ? +16 : -16)); + qreal invMag = 1.0 / std::sqrt(QPointF::dotProduct(ni, ni)); + ni = invMag * ni; + + // Adjust the intersection point to deal with the rounded rectangle corner: + if ( + (tx >= ty && std::abs(cornerVector.x()) < cfg.nodeRadius()) || + (tx < ty && std::abs(cornerVector.y()) < cfg.nodeRadius())) + { + QPointF centerOfCurvature = + nearestCorner + QPointF(std::signbit(dl.x()) ? +4 : -4, std::signbit(dl.y()) ? +4 : -4); + QPointF tmp = si - centerOfCurvature; + qreal tmpMag = std::sqrt(QPointF::dotProduct(tmp, tmp)); + si = centerOfCurvature + (4.0 / tmpMag) * tmp; + } + } + + // Now, telescope a couple points out along the normal. + QPointF si1 = si + ni * cfg.arrowStemLength(); + QPointF si2 = si + ni * cfg.arrowStemLength() * 2; + + // Midpoint between pi and si + QPointF psm = 0.5 * (pi1 + si1); + + // Arrowhead points that replace si as the path's destination + // 12 = 0.75 * ARC_ARROW_STEM = ARC_ARROW_HEAD + // 6 = 0.50 * ARC_ARROW_HEAD + QPointF a1 = si + 12. * ni; + QPointF ti{ ni.y(), -ni.x() }; + QPointF a2 = a1 + 6. * ti; + QPointF a3 = a1 - 6. * ti; + QPointF a4 = 0.75 * a1 + 0.25 * si; + // Finally, we can declare our path: + // (pi pi1) [pi2 psm si2] (si1 a4) + // The points in parentheses are connected with a straight line. + // The points in square brackets use a rational quadratic curve. + m_computedPath.moveTo(pi); + m_computedPath.lineTo(pi1); + m_computedPath.quadTo(pi2, psm); + m_computedPath.quadTo(si2, si1); + m_computedPath.lineTo(a4); + this->setPath(m_computedPath); + + m_arrowPath.moveTo(si); + m_arrowPath.lineTo(a2); + m_arrowPath.quadTo(a4, a3); + m_arrowPath.lineTo(si); + return 1; +} + +void qtTaskArc::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + (void)option; + (void)widget; + const auto& cfg(*m_scene->configuration()); + + QPen pen; + pen.setWidth(cfg.arcWidth()); + pen.setBrush(cfg.colorForArc(m_arcType)); + + // painter->setPen(pen); + painter->strokePath(this->path(), pen); + painter->fillPath(m_arrowPath, pen.brush()); +} + +} // namespace extension +} // namespace smtk diff --git a/smtk/extension/qt/task/qtTaskArc.h b/smtk/extension/qt/task/qtTaskArc.h new file mode 100644 index 0000000000..1fcdb9d062 --- /dev/null +++ b/smtk/extension/qt/task/qtTaskArc.h @@ -0,0 +1,103 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_extension_qtTaskArc_h +#define smtk_extension_qtTaskArc_h + +#include "smtk/extension/qt/Exports.h" + +#include "smtk/common/TypeContainer.h" + +#include "smtk/PublicPointerDefs.h" + +#include +#include +#include + +namespace smtk +{ +namespace task +{ +class Adaptor; +} + +namespace extension +{ + +class qtTaskEditor; +class qtTaskScene; +class qtTaskNode; + +/**\brief A widget that holds a Qt scene graph. + * + */ +class SMTKQTEXT_EXPORT qtTaskArc + : public QObject + , public QGraphicsPathItem +{ + Q_OBJECT + +public: + using Superclass = QGraphicsPathItem; + + /// Arcs between nodes indicate a required ordering of tasks. + enum class ArcType : int + { + Dependency, //!< Tasks are administratively forced to occur in order. + Adaptor //!< Tasks are technically forced into order by information flow. + }; + + qtTaskArc( + qtTaskScene* scene, + qtTaskNode* predecessor, + qtTaskNode* successor, + ArcType type = ArcType::Dependency, + QGraphicsItem* parent = nullptr); + qtTaskArc( + qtTaskScene* scene, + qtTaskNode* predecessor, + qtTaskNode* successor, + smtk::task::Adaptor* adaptor, + QGraphicsItem* parent = nullptr); + ~qtTaskArc() override; + + qtTaskScene* scene() const { return m_scene; } + ArcType arcType() const { return m_arcType; } + qtTaskNode* predecessor() const { return m_predecessor; } + qtTaskNode* successor() const { return m_successor; } + smtk::task::Adaptor* adaptor() const { return m_adaptor; } + + static ArcType arcTypeEnum(const std::string& enumerant, bool* match = nullptr); + static std::string arcTypeName(ArcType enumerant); + +public Q_SLOTS: // NOLINT(readability-redundant-access-specifiers) + + /// Recompute points specifying the shape of the arc based on the current + /// endpoint-node positions and geometry. + int updateArcPoints(); + +protected: + /// Get the bounding box of the node, which includes the border width and the label. + QRectF boundingRect() const override; + /// Draw the arc into the scene. + void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override; + + qtTaskScene* m_scene{ nullptr }; + qtTaskNode* m_predecessor{ nullptr }; + qtTaskNode* m_successor{ nullptr }; + smtk::task::Adaptor* m_adaptor{ nullptr }; // Only set when ArcType == Adaptor. + ArcType m_arcType{ ArcType::Dependency }; + QPainterPath m_computedPath; + QPainterPath m_arrowPath; +}; + +} // namespace extension +} // namespace smtk + +#endif // smtk_extension_qtTaskArc_h diff --git a/smtk/extension/qt/task/qtTaskEditor.cxx b/smtk/extension/qt/task/qtTaskEditor.cxx new file mode 100644 index 0000000000..18b27756ce --- /dev/null +++ b/smtk/extension/qt/task/qtTaskEditor.cxx @@ -0,0 +1,549 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#include "smtk/extension/qt/task/qtTaskEditor.h" + +#include "smtk/common/Managers.h" + +#include "smtk/extension/qt/task/PanelConfiguration_cpp.h" +#include "smtk/extension/qt/task/TaskEditorState.h" +#include "smtk/extension/qt/task/qtTaskArc.h" +#include "smtk/extension/qt/task/qtTaskNode.h" +#include "smtk/extension/qt/task/qtTaskScene.h" +#include "smtk/extension/qt/task/qtTaskView.h" +#include "smtk/extension/qt/task/qtTaskViewConfiguration.h" + +#include "smtk/project/Manager.h" +#include "smtk/project/Project.h" + +#include "smtk/task/Active.h" +#include "smtk/task/Instances.h" +#include "smtk/task/Manager.h" + +#include "smtk/view/Configuration.h" +#include "smtk/view/json/jsonView.h" + +#include "smtk/io/Logger.h" + +#include "nlohmann/json.hpp" + +#include +#include +#include +#include +#include +#include + +// Uncomment to get debug printouts from workflow events. +// #define SMTK_DBG_WORKFLOWS 1 + +namespace smtk +{ +namespace extension +{ + +class qtTaskEditor::Internal +{ +public: + using Task = smtk::task::Task; + + Internal(qtTaskEditor* self, const smtk::view::Information& info) + { + (void)info; + m_self = self; + m_scene = new qtTaskScene(m_self); + m_widget = new qtTaskView(m_scene, m_self); + m_uiState = std::make_shared(m_self); + auto* layout = new QVBoxLayout; + layout->setObjectName("taskEditor"); + m_self->Widget = m_widget; + m_self->Widget->setLayout(layout); + if (info.configuration()) + { + m_scene->setConfiguration(new qtTaskViewConfiguration(*info.configuration())); + } + } + + void displayTaskManager(smtk::task::Manager* taskManager) + { + if (m_taskManager == taskManager) + { + return; + } + this->removeObservers(); + this->clear(); + m_taskManager = taskManager; + this->installObservers(); + + auto generator = std::dynamic_pointer_cast(m_uiState); + m_taskManager->uiState().setGenerator(m_self->typeName(), generator); + + QTimer::singleShot(0, [this]() { + bool modified = this->computeNodeLayout(); + m_widget->ensureVisible(m_scene->sceneRect()); + + // After layout complete, connect nodeMoved signal + for (const auto& entry : m_taskIndex) + { + qtTaskNode* taskNode = entry.second; + QObject::connect( + taskNode, &qtTaskNode::nodeMoved, m_self, &qtTaskEditor::onNodeGeometryChanged); + } + + if (modified) + { + m_self->onNodeGeometryChanged(); + } + }); + } + + // Returns true if UI configuration was modified + bool computeNodeLayout() + { + if (m_taskIndex.empty()) + { + return false; + } + + // Check if taskManager has UI config objects first + bool configured = false; + auto* firstTask = m_taskIndex.begin()->first; + auto* taskManager = firstTask->manager(); + auto& uiState = taskManager->uiState(); + smtk::string::Token classToken = m_self->typeName(); + for (const auto& entry : m_taskIndex) + { + nlohmann::json uiConfig = uiState.getData(classToken, entry.first); + if (uiConfig.contains("position")) + { + auto jPosition = uiConfig["position"]; + double x = jPosition[0].get(); + double y = jPosition[1].get(); + + auto* node = entry.second; + node->setPos(x, y); + + configured = true; + } // if + } // for + + if (configured) + { + return false; // not modified + } + + // If geometry not configured, call the scenes method to layout nodes + std::unordered_set nodes; + std::unordered_set arcs; + for (const auto& entry : m_taskIndex) + { + nodes.insert(entry.second); + } + for (const auto& entry : m_arcIndex) + { + for (const auto& arc : entry.second) + { + arcs.insert(arc); + } + } + return (m_scene->computeLayout(nodes, arcs) == 1); + } + + void removeObservers() + { + // Reset task-manager observers + m_adaptorObserverKey.release(); + m_instanceObserverKey.release(); + m_workflowObserverKey.release(); + m_activeObserverKey.release(); + } + + void installObservers() + { + if (!m_taskManager) + { + return; + } + + QPointer parent(m_self); + m_instanceObserverKey = m_taskManager->taskInstances().observers().insert( + [this, + parent](smtk::common::InstanceEvent event, const std::shared_ptr& task) { + if (!parent) + { + return; + } + switch (event) + { + case smtk::common::InstanceEvent::Managed: + { + // std::cout << "Add task instance " << task << " " << task->title() << "\n"; + auto* tnode = new qtTaskNode(m_scene, task.get()); + m_taskIndex[task.get()] = tnode; + } + break; + case smtk::common::InstanceEvent::Unmanaged: + { + // std::cout << "Remove task instance " << task << " " << task->title() << "\n"; + auto it = m_taskIndex.find(task.get()); + if (it != m_taskIndex.end()) + { + this->removeArcsAttachedTo(it->second); + delete it->second; + m_taskIndex.erase(it); + } + } + break; + } + }, + "qtTaskEditor watching task instances."); + // Observe task-manager's taskInstances() and active() objects. + m_workflowObserverKey = m_taskManager->taskInstances().workflowObservers().insert( + [this, parent]( + const std::set& workflow, + smtk::task::WorkflowEvent workflowEvent, + smtk::task::Task* task) { + (void)workflow; + (void)task; + if (!parent) + { + return; + } + switch (workflowEvent) + { + case smtk::task::WorkflowEvent::Created: +#ifdef SMTK_DBG_WORKFLOWS + std::cout << "Workflow created, task " << task << ":\n"; +#endif + break; + case smtk::task::WorkflowEvent::TaskAdded: +#ifdef SMTK_DBG_WORKFLOWS + std::cout << "Add task " << task << " with " << workflow.size() << " tasks in flow:\n"; +#endif + // new qtTaskNode(m_scene, task); + break; + case smtk::task::WorkflowEvent::TaskRemoved: +#ifdef SMTK_DBG_WORKFLOWS + std::cout << "Task " << task << " removed:\n"; +#endif + break; + case smtk::task::WorkflowEvent::Destroyed: +#ifdef SMTK_DBG_WORKFLOWS + std::cout << "Workflow destroyed, task " << task << "\n"; +#endif + break; + case smtk::task::WorkflowEvent::Resuming: + this->resetArcs(qtTaskArc::ArcType::Dependency); +#ifdef SMTK_DBG_WORKFLOWS + std::cout << "Resuming w " << workflow.size() << " tasks in flow:\n"; +#endif + break; + } +#ifdef SMTK_DBG_WORKFLOWS + for (const auto& wtask : workflow) + { + std::cout << " " << wtask << " " << wtask->title() << "\n"; + } +#endif + }, + "qtTaskEditor watching task workflows."); + + m_adaptorObserverKey = m_taskManager->adaptorInstances().observers().insert( + [this, parent]( + smtk::common::InstanceEvent event, const std::shared_ptr& adaptor) { + if (!parent) + { + return; + } + auto fromIt = m_taskIndex.find(adaptor->from()); + auto toIt = m_taskIndex.find(adaptor->to()); + if (fromIt == m_taskIndex.end() || toIt == m_taskIndex.end()) + { + smtkWarningMacro( + smtk::io::Logger::instance(), + "An adaptor's arc could not be " + << (event == smtk::common::InstanceEvent::Managed ? "added" : "removed") + << " because the nodes don't exist yet."); + return; + } + switch (event) + { + case smtk::common::InstanceEvent::Managed: + m_arcIndex[fromIt->second].insert( + new qtTaskArc(m_scene, fromIt->second, toIt->second, adaptor.get())); + break; + case smtk::common::InstanceEvent::Unmanaged: + { + qtTaskArc* match = nullptr; + auto it = m_arcIndex.find(fromIt->second); + if (it != m_arcIndex.end()) + { + for (const auto& arcItem : it->second) + { + if (arcItem->adaptor() == adaptor.get()) + { + match = arcItem; + m_arcIndex.erase(it); + break; + } + } + } + if (!match) + { + smtkWarningMacro( + smtk::io::Logger::instance(), + "An adaptor's arc could not be removed because the arc don't exist or is " + "improperly indexed!"); + return; + } + delete match; + } + break; + } + }, + "qtTaskEditor watching task adaptors."); + + m_activeObserverKey = m_taskManager->active().observers().insert( + [this, parent](smtk::task::Task* prev, smtk::task::Task* next) { + if (!parent) + { + return; + } + // std::cout << "Switch active task from " << prev << " to " << next << "\n"; + auto prevNode = m_taskIndex.find(prev); + auto nextNode = m_taskIndex.find(next); + if (prevNode != m_taskIndex.end()) + { + prevNode->second->setOutlineStyle(qtTaskNode::OutlineStyle::Normal); + } + if (nextNode != m_taskIndex.end()) + { + nextNode->second->setOutlineStyle(qtTaskNode::OutlineStyle::Active); + } + }, + "qtTaskEditor watching active task."); + } + + void removeArcsAttachedTo(qtTaskNode* node) + { + // Examine node->task()->dependencies() for upstream arcs + auto deps = node->task()->dependencies(); + // std::cout << " Task " << node->task() << " has " << deps.size() << " dependencies.\n"; + for (const auto& predecessor : deps) + { + auto predIt = m_taskIndex.find(predecessor.get()); + if (predIt == m_taskIndex.end()) + { + continue; + } + auto arcIt = m_arcIndex.find(predIt->second); + if (arcIt != m_arcIndex.end()) + { + std::unordered_set toErase; + for (const auto& arcItem : arcIt->second) + { + if (arcItem->successor() == node) + { + toErase.insert(arcItem); + delete arcItem; + } + } + for (const auto& entry : toErase) + { + arcIt->second.erase(entry); + } + } + } + // Examine m_arcIndex[node] for downstream arcs + auto arcIt = m_arcIndex.find(node); + if (arcIt != m_arcIndex.end()) + { + for (const auto& arcItem : arcIt->second) + { + delete arcItem; + } + m_arcIndex.erase(node); + } + } + + void resetArcs(qtTaskArc::ArcType arcsToReset) + { + // Erase all arcs and repopulate. + for (auto& entry : m_arcIndex) + { + std::unordered_set toErase; + for (const auto& arc : entry.second) + { + if (arc->arcType() == arcsToReset) + { + toErase.insert(arc); + delete arc; + } + } + for (const auto& arcToErase : toErase) + { + entry.second.erase(arcToErase); + } + } + + m_taskManager->taskInstances().visit([this](const std::shared_ptr& successor) { + auto succIt = m_taskIndex.find(successor.get()); + if (succIt == m_taskIndex.end()) + { + // std::cout << " Skip tasks " << successor.get() << "\n"; + return smtk::common::Visit::Continue; + } + auto deps = successor->dependencies(); + // std::cout << " Task " << successor << " has " << deps.size() << " dependencies.\n"; + for (const auto& predecessor : deps) + { + // std::cout << " Task " << predecessor << " is a dependency.\n"; + auto predIt = m_taskIndex.find(predecessor.get()); + if (predIt == m_taskIndex.end()) + { + // std::cout << " Skip task " << predecessor.get() << "\n"; + continue; + } + m_arcIndex[predIt->second].insert(new qtTaskArc(m_scene, predIt->second, succIt->second)); + } + return smtk::common::Visit::Continue; + }); + } + + void clear() + { + for (const auto& entry : m_taskIndex) + { + delete entry.second; + } + // TODO: Delete arcs, too. + } + + qtTaskEditor* m_self; + smtk::task::Manager* m_taskManager{ nullptr }; + qtTaskScene* m_scene{ nullptr }; + qtTaskView* m_widget{ nullptr }; + std::shared_ptr m_uiState; + smtk::task::adaptor::Instances::Observers::Key m_adaptorObserverKey; + smtk::task::Instances::WorkflowObservers::Key m_workflowObserverKey; + smtk::task::Instances::Observers::Key m_instanceObserverKey; + smtk::task::Active::Observers::Key m_activeObserverKey; + std::unordered_map m_taskIndex; + std::unordered_map> + m_arcIndex; // Arcs grouped by their predecessor task. +}; + +qtTaskEditor::qtTaskEditor(const smtk::view::Information& info) + : qtBaseView(info) + , m_p(new Internal(this, info)) +{ +} + +qtTaskEditor::~qtTaskEditor() = default; + +qtBaseView* qtTaskEditor::createViewWidget(const smtk::view::Information& info) +{ + qtTaskEditor* editor = new qtTaskEditor(info); + // editor->buildUI(); + return editor; +} + +void qtTaskEditor::displayProject(const std::shared_ptr& project) +{ + if (project) + { + this->displayTaskManager(&project->taskManager()); + // TODO: Observe project's manager and clear this view if a different project is loaded. + } + else + { + // TODO: Unobserve any project manager + this->displayTaskManager(nullptr); + } +} + +void qtTaskEditor::displayTaskManager(smtk::task::Manager* taskManager) +{ + m_p->displayTaskManager(taskManager); +} + +qtTaskScene* qtTaskEditor::taskScene() const +{ + return m_p->m_scene; +} + +qtTaskView* qtTaskEditor::taskWidget() const +{ + return m_p->m_widget; +} + +std::shared_ptr qtTaskEditor::defaultConfiguration() +{ + std::shared_ptr result; + auto jsonConfig = nlohmann::json::parse(taskPanelConfiguration())[0]; + result = jsonConfig; + return result; +} + +nlohmann::json qtTaskEditor::uiStateForTask(const smtk::task::Task* task) const +{ + auto iter = m_p->m_taskIndex.find(const_cast(task)); + if (iter == m_p->m_taskIndex.end()) + { + return nlohmann::json(); + } + + qtTaskNode* taskNode = iter->second; + QPointF pos = taskNode->scenePos(); + nlohmann::json jUI = nlohmann::json::object(); + jUI["position"] = { pos.x(), pos.y() }; + return jUI; +} + +void qtTaskEditor::onNodeGeometryChanged() +{ + auto managers = m_p->m_taskManager->managers(); + if (managers == nullptr) + { + return; + } + + auto* taskNode = dynamic_cast(this->sender()); + if (taskNode != nullptr) + { + // Check if modified + smtk::string::Token classToken = this->typeName(); + nlohmann::json origData = m_p->m_taskManager->uiState().getData(classToken, taskNode->task()); + nlohmann::json newData = this->uiStateForTask(taskNode->task()); + if (origData == newData) + { + return; + } + } + + // Set project's modified flag + auto projectManager = managers->get(); + if (projectManager == nullptr) + { + return; + } + + auto projects = projectManager->projects(); + if (projects.size() != 1) + { + return; + } + + auto project = *projects.begin(); + auto mutableProject = dynamic_pointer_cast(project); + mutableProject->setClean(false); +} + +} // namespace extension +} // namespace smtk diff --git a/smtk/extension/qt/task/qtTaskEditor.h b/smtk/extension/qt/task/qtTaskEditor.h new file mode 100644 index 0000000000..64ddac3aa6 --- /dev/null +++ b/smtk/extension/qt/task/qtTaskEditor.h @@ -0,0 +1,77 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_extension_qtTaskEditor_h +#define smtk_extension_qtTaskEditor_h + +#include "smtk/extension/qt/Exports.h" +#include "smtk/extension/qt/qtBaseView.h" + +#include "smtk/project/Project.h" +#include "smtk/task/Manager.h" +#include "smtk/view/Configuration.h" + +#include "smtk/common/TypeContainer.h" + +#include "smtk/PublicPointerDefs.h" + +#include "nlohmann/json.hpp" + +#include + +class QAbstractItemModel; +class QItemSelection; +class QTreeView; + +namespace smtk +{ +namespace extension +{ + +class qtTaskScene; +class qtTaskView; + +/**\brief A widget that displays SMTK tasks available to users as a graph. + * + */ +class SMTKQTEXT_EXPORT qtTaskEditor : public qtBaseView +{ + Q_OBJECT + typedef smtk::extension::qtBaseView Superclass; + +public: + smtkTypenameMacro(qtTaskEditor); + + static qtBaseView* createViewWidget(const smtk::view::Information& info); + qtTaskEditor(const smtk::view::Information& info); + ~qtTaskEditor() override; + + qtTaskScene* taskScene() const; + qtTaskView* taskWidget() const; + + static std::shared_ptr defaultConfiguration(); + nlohmann::json uiStateForTask(const smtk::task::Task* task) const; + +public Q_SLOTS: + /// Display the \a project's tasks in this widget. + virtual void displayProject(const std::shared_ptr& project); + /// Display a \a taskManager's tasks in this widget (which need not belong to a project). + virtual void displayTaskManager(smtk::task::Manager* taskManager); + +protected Q_SLOTS: + void onNodeGeometryChanged(); + +protected: + class Internal; + Internal* m_p; +}; + +} // namespace extension +} // namespace smtk +#endif // smtk_extension_qtTaskEditor_h diff --git a/smtk/extension/qt/task/qtTaskNode.cxx b/smtk/extension/qt/task/qtTaskNode.cxx new file mode 100644 index 0000000000..56b20a31e6 --- /dev/null +++ b/smtk/extension/qt/task/qtTaskNode.cxx @@ -0,0 +1,357 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#include "smtk/extension/qt/task/qtTaskNode.h" + +#include "smtk/extension/qt/qtBaseView.h" +#include "smtk/extension/qt/task/qtTaskScene.h" +#include "smtk/extension/qt/task/qtTaskViewConfiguration.h" + +#include "smtk/task/Active.h" +#include "smtk/task/Manager.h" +#include "smtk/task/Task.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "task/ui_TaskNode.h" + +class QAbstractItemModel; +class QItemSelection; +class QTreeView; + +namespace +{ + +template +/** + * Intercept all events from a particular QObject and process them using the + * given @c functor. This is usually used with the QObjects::installEventFilter() + * function. + */ +class Interceptor final : public QObject +{ +public: + /** + * Create an Interceptor that process all events of @c parent using @c functor. + */ + Interceptor(QObject* parent, F fn) + : QObject(parent) + , functor(fn) + { + } + ~Interceptor() override = default; + +protected: + /** + * Filters events if this object has been installed as an event filter for the watched object. + */ + bool eventFilter(QObject* object, QEvent* event) override { return this->functor(object, event); } + + F functor; +}; + +/** + * Create a new Interceptor instance. + */ +template +Interceptor* createInterceptor(QObject* parent, F functor) +{ + return new Interceptor(parent, functor); +}; + +} // namespace + +namespace smtk +{ +namespace extension +{ + +class TaskNodeWidget + : public QWidget + , public Ui::TaskNode +{ +public: + TaskNodeWidget(qtTaskNode* node, QWidget* parent = nullptr) + : QWidget(parent) + , m_node(node) + { + this->setupUi(this); + m_nodeMenu = new QMenu(m_headlineButton); + m_activateTask = new QAction("Work on this"); + m_expandTask = new QAction("Show controls"); + m_markCompleted = new QAction("Mark completed"); + m_nodeMenu->addAction(m_activateTask); + m_nodeMenu->addAction(m_expandTask); + m_nodeMenu->addAction(m_markCompleted); + m_markCompleted->setEnabled(false); + m_headlineButton->setMenu(m_nodeMenu); + QObject::connect(m_activateTask, &QAction::triggered, this, &TaskNodeWidget::activateTask); + QObject::connect(m_markCompleted, &QAction::triggered, this, &TaskNodeWidget::markCompleted); + QObject::connect(m_expandTask, &QAction::triggered, this, &TaskNodeWidget::toggleControls); + m_taskObserver = m_node->task()->observers().insert( + [this](smtk::task::Task&, smtk::task::State prev, smtk::task::State next) { + // Sometimes the application invokes this observer after the GUI + // has been shut down. Calling setEnabled on widgets generates a + // log message and attempting to construct a QPixmap throws exceptions; + // so, check that qApp exists before going further. + if (qApp) + { + this->updateTaskState(prev, next); + } + }, + "TaskNodeWidget observer"); + this->updateTaskState(m_node->m_task->state(), m_node->m_task->state()); + } + + void updateTaskState(smtk::task::State prev, smtk::task::State next) + { + (void)prev; + switch (next) + { + case smtk::task::State::Irrelevant: + m_headlineButton->setEnabled(false); + break; + case smtk::task::State::Unavailable: + m_headlineButton->setEnabled(false); + m_activateTask->setEnabled(false); + break; + case smtk::task::State::Incomplete: + m_headlineButton->setEnabled(true); + m_activateTask->setEnabled(true); + m_markCompleted->setEnabled(false); + break; + case smtk::task::State::Completable: + m_headlineButton->setEnabled(true); + m_activateTask->setEnabled(true); + m_markCompleted->setEnabled(true); + break; + case smtk::task::State::Completed: + m_headlineButton->setEnabled(true); + m_activateTask->setEnabled(false); + m_markCompleted->setEnabled(true); + break; + } + m_headlineButton->setToolTip(QString::fromStdString("Status: " + smtk::task::stateName(next))); + m_headlineButton->setIcon(this->renderStatusIcon(next, m_headlineButton->height() / 2)); + } + + void activateTask() + { + auto* taskManager = m_node->m_task->manager(); + if (taskManager) + { + taskManager->active().switchTo(m_node->m_task); + // TODO: Provide feedback if no action taken (e.g., flash red) + } + } + + void markCompleted() + { + m_node->m_task->markCompleted(m_node->m_task->state() == smtk::task::State::Completable); + // TODO: Provide feedback if no action taken (e.g., flash red) + } + + QIcon renderStatusIcon(smtk::task::State state, int radius) + { + if (radius < 10) + { + radius = 10; + } + QPixmap pix(radius, radius); + pix.fill(QColor(0, 0, 0, 0)); + + auto& cfg = *m_node->m_scene->configuration(); + QPainter painter(&pix); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setBrush(QBrush(cfg.colorForState(state))); + painter.drawEllipse(1, 1, radius - 2, radius - 2); + painter.end(); + return QIcon(pix); + } + + void toggleControls() + { + bool shouldShow = !m_controls->isVisible(); + m_controls->setVisible(shouldShow); + m_expandTask->setText(shouldShow ? "Hide controls" : "Show controls"); + } + + QMenu* m_nodeMenu; + QAction* m_activateTask; + QAction* m_expandTask; + QAction* m_markCompleted; + qtTaskNode* m_node; + smtk::task::Task::Observers::Key m_taskObserver; +}; + +qtTaskNode::qtTaskNode(qtTaskScene* scene, smtk::task::Task* task, QGraphicsItem* parent) + : Superclass(parent) + , m_scene(scene) + , m_task(task) + , m_container(new TaskNodeWidget(this)) +{ + qtTaskViewConfiguration& cfg(*m_scene->configuration()); + // === Base constructor === + this->setFlag(GraphicsItemFlag::ItemIsMovable); + this->setFlag(GraphicsItemFlag::ItemSendsGeometryChanges); + this->setCacheMode(CacheMode::DeviceCoordinateCache); + this->setCursor(Qt::ArrowCursor); + this->setObjectName(QString("node") + QString::fromStdString(m_task->title())); + + // Create a container to hold node contents + { + m_container->setObjectName("nodeContainer"); + m_container->setMinimumWidth(cfg.nodeWidth()); + // m_container->setMaximumWidth(cfg.nodeWidth()); + + // install resize event filter + m_container->installEventFilter( + createInterceptor(m_container, [this](QObject* /*object*/, QEvent* event) { + if (event->type() == QEvent::LayoutRequest) + { + this->updateSize(); + } + return false; + })); + + auto* graphicsProxyWidget = new QGraphicsProxyWidget(this); + graphicsProxyWidget->setObjectName("graphicsProxyWidget"); + graphicsProxyWidget->setWidget(m_container); + graphicsProxyWidget->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + graphicsProxyWidget->setPos(QPointF(0, 0)); + + m_container->m_headlineButton->setText(QString::fromStdString(m_task->title())); + m_container->m_controls->hide(); + + // Configure timer to rate-limit nodeMoved signal + m_moveSignalTimer = new QTimer(this); + m_moveSignalTimer->setSingleShot(true); + m_moveSignalTimer->setInterval(100); + QObject::connect(m_moveSignalTimer, &QTimer::timeout, this, &qtTaskNode::nodeMoved); + } + + this->updateSize(); + m_scene->addItem(this); + + // === Task-specific constructor === + this->setZValue(cfg.nodeLayer()); +} + +qtTaskNode::~qtTaskNode() +{ + m_scene->removeItem(this); +} + +void qtTaskNode::setContentStyle(ContentStyle cs) +{ + m_contentStyle = cs; + switch (cs) + { + case ContentStyle::Minimal: + m_container->hide(); + break; + case ContentStyle::Summary: + case ContentStyle::Details: + m_container->show(); + break; + default: + break; + } +} + +void qtTaskNode::setOutlineStyle(OutlineStyle os) +{ + qtTaskViewConfiguration& cfg(*m_scene->configuration()); + m_outlineStyle = os; + this->setZValue(cfg.nodeLayer() - (os == OutlineStyle::Normal ? 0 : 1)); + this->update(this->boundingRect()); +} + +QRectF qtTaskNode::boundingRect() const +{ + qtTaskViewConfiguration& cfg(*m_scene->configuration()); + const auto& border = cfg.nodeBorderThickness(); + const double height = m_container->height(); + // was = m_headlineHeight + (m_container->isVisible() ? m_container->height() : 0.0); + return QRectF(0, 0, m_container->width(), height).adjusted(-border, -border, border, border); +} + +QVariant qtTaskNode::itemChange(GraphicsItemChange change, const QVariant& value) +{ + if (change == GraphicsItemChange::ItemPositionHasChanged) + { + m_moveSignalTimer->start(); + Q_EMIT this->nodeMovedImmediate(); + } + + return QGraphicsItem::itemChange(change, value); +} + +void qtTaskNode::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + (void)option; + (void)widget; + qtTaskViewConfiguration& cfg(*m_scene->configuration()); + QPainterPath path; + // Make sure the whole node is redrawn to avoid artifacts: + const double borderOffset = 0.5 * cfg.nodeBorderThickness(); + const QRectF br = + this->boundingRect().adjusted(borderOffset, borderOffset, -borderOffset, -borderOffset); + path.addRoundedRect(br, cfg.nodeBorderThickness(), cfg.nodeBorderThickness()); + + const QColor baseColor = QApplication::palette().window().color(); + const QColor highlightColor = QApplication::palette().highlight().color(); + const QColor contrastColor = QColor::fromHslF( + baseColor.hueF(), + baseColor.saturationF(), + baseColor.lightnessF() > 0.5 ? baseColor.lightnessF() - 0.5 : baseColor.lightnessF() + 0.5); + // const QColor greenBaseColor = QColor::fromHslF(0.361, 0.666, baseColor.lightnessF() * 0.4 + 0.2); + + QPen pen; + pen.setWidth(cfg.nodeBorderThickness()); + switch (m_outlineStyle) + { + case OutlineStyle::Normal: + pen.setBrush(contrastColor); + break; + case OutlineStyle::Active: + pen.setBrush(highlightColor); + break; + default: + break; + } + + painter->setPen(pen); + painter->fillPath(path, baseColor); + painter->drawPath(path); +} + +int qtTaskNode::updateSize() +{ + this->prepareGeometryChange(); + + m_container->resize(m_container->layout()->sizeHint()); + Q_EMIT this->nodeResized(); + + return 1; +} + +} // namespace extension +} // namespace smtk diff --git a/smtk/extension/qt/task/qtTaskNode.h b/smtk/extension/qt/task/qtTaskNode.h new file mode 100644 index 0000000000..1651a9d4ea --- /dev/null +++ b/smtk/extension/qt/task/qtTaskNode.h @@ -0,0 +1,113 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_extension_qtTaskNode_h +#define smtk_extension_qtTaskNode_h + +#include "smtk/extension/qt/Exports.h" +#include "smtk/extension/qt/qtBaseView.h" + +#include "smtk/common/TypeContainer.h" + +#include "smtk/PublicPointerDefs.h" + +#include +#include + +class QAbstractItemModel; +class QGraphicsTextItem; +class QItemSelection; +class QTimer; +class QTreeView; + +namespace smtk +{ +namespace task +{ +class Task; +} +namespace extension +{ + +class qtTaskEditor; +class qtTaskScene; +class TaskNodeWidget; + +/**\brief A widget that represents a task as a node in a scene. + * + */ +class SMTKQTEXT_EXPORT qtTaskNode + : public QObject + , public QGraphicsItem +{ + Q_OBJECT + Q_INTERFACES(QGraphicsItem) + +public: + using Superclass = QGraphicsItem; + + /// Determine how the node is presented to users. + enum class ContentStyle : int + { + Minimal, //!< Only the node's title-bar is shown. + Summary, //!< The node's title bar and a "mini" viewer should be shown. + Details //!< The node's full state and any contained view should be shown. + }; + + /// Determine how the border of the node's visual representation should be rendered. + enum class OutlineStyle : int + { + Normal, //!< Render an unobtrusive, subdued border around the node. + Active //!< Render a highlighted border around the node. + }; + + qtTaskNode(qtTaskScene* scene, smtk::task::Task* task, QGraphicsItem* parent = nullptr); + ~qtTaskNode() override; + + /// Return the task this node represents. + smtk::task::Task* task() const { return m_task; } + + /// Set/get how much data the node should render inside its boundary. + void setContentStyle(ContentStyle cs); + ContentStyle contentStyle() const { return m_contentStyle; } + + /// Set/get how the node's boundary should be rendered. + void setOutlineStyle(OutlineStyle cs); + OutlineStyle outlineStyle() const { return m_outlineStyle; } + + /// Get the bounding box of the node, which includes the border width and the label. + QRectF boundingRect() const override; + +Q_SIGNALS: + void nodeResized(); + void nodeMovedImmediate(); + void nodeMoved(); // a rate-limited version of nodeMovedImmediate + +protected: + friend class TaskNodeWidget; + QVariant itemChange(GraphicsItemChange change, const QVariant& value) override; + void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override; + + /// Update the node bounds to fit its content. + int updateSize(); + + qtTaskScene* m_scene{ nullptr }; + smtk::task::Task* m_task{ nullptr }; + TaskNodeWidget* m_container{ nullptr }; + ContentStyle m_contentStyle{ ContentStyle::Minimal }; + OutlineStyle m_outlineStyle{ OutlineStyle::Normal }; + + // Use timer to limit frequency of nodeMoved() signals to 10 hz. + QTimer* m_moveSignalTimer{ nullptr }; +}; + +} // namespace extension +} // namespace smtk + +#endif // smtk_extension_qtTaskNode_h diff --git a/smtk/extension/qt/task/qtTaskScene.cxx b/smtk/extension/qt/task/qtTaskScene.cxx new file mode 100644 index 0000000000..5fe7d28ff3 --- /dev/null +++ b/smtk/extension/qt/task/qtTaskScene.cxx @@ -0,0 +1,215 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#include "smtk/extension/qt/task/qtTaskScene.h" +#include "smtk/extension/qt/task/qtTaskArc.h" +#include "smtk/extension/qt/task/qtTaskEditor.h" +#include "smtk/extension/qt/task/qtTaskNode.h" + +#include "smtk/Options.h" +#include "smtk/io/Logger.h" + +#include +#include + +#if SMTK_ENABLE_GRAPHVIZ_SUPPORT +// Older Graphviz releases (before 2.40.0) require `HAVE_CONFIG_H` to define +// `POINTS_PER_INCH`. +#define HAVE_CONFIG_H +#include +#include +#include +#undef HAVE_CONFIG_H +#endif // SMTK_ENABLE_GRAPHVIZ_SUPPORT + +#include + +namespace smtk +{ +namespace extension +{ + +qtTaskScene::qtTaskScene(qtTaskEditor* parent) + : Superclass(parent->widget()) +{ +} + +qtTaskScene::~qtTaskScene() = default; + +bool qtTaskScene::computeLayout( + const std::unordered_set& nodes, + const std::unordered_set& arcs) +{ +#if SMTK_ENABLE_GRAPHVIZ_SUPPORT + // compute dot string + qreal maxHeight = 0.0; + qreal maxY = 0; + std::string dotString; + { + std::stringstream nodeString; + std::stringstream edgeString; + + for (const auto& node : nodes) + { + // Ignore hidden nodes + if (!node->isVisible() || !node->task()) + { + continue; + } + + const QRectF& b = node->boundingRect(); + qreal width = b.width() / POINTS_PER_INCH; // convert from points to inches + qreal height = b.height() / POINTS_PER_INCH; + if (maxHeight < height) + { + maxHeight = height; + } + + // Construct the string declaring a node. + // See https://www.graphviz.org/pdf/libguide.pdf for more detail + nodeString << "n" << node << "[" + << "shape=record," + << "width=" << width << "," + << "height=" << height << "" + << "];\n"; + } + + // Construct the string representing all arcs in the graph + // See https://www.graphviz.org/pdf/libguide.pdf for more detail + for (const auto& arc : arcs) + { + edgeString << "n" << arc->predecessor() << " -> " + << "n" << arc->successor() << ";\n"; + } + + // describe the overall look of the graph. For example : rankdir=LR -> Left To Right layout + // See https://www.graphviz.org/pdf/libguide.pdf for more detail + dotString += "digraph g {\nrankdir=TB;splines = line;graph[pad=\"0\", ranksep=\"0.6\", " + "nodesep=\"0.6\"];\n" + + nodeString.str() + edgeString.str() + "\n}"; + } + + std::vector coords(2 * nodes.size(), 0.0); + // compute layout + { + Agraph_t* G = agmemread(dotString.data()); + GVC_t* gvc = gvContext(); + if (!G || !gvc || gvLayout(gvc, G, "dot")) + { + smtkErrorMacro( + smtk::io::Logger::instance(), "[NodeEditorPlugin] Cannot intialize Graphviz context."); + return 0; + } + + // read layout + int i = -2; + for (const auto& node : nodes) + { + if (!node->isVisible() || !node->task()) + { + continue; + } + + i += 2; + + std::ostringstream nodeName; + nodeName << "n" << node; + + Agnode_t* n = agnode(G, const_cast(nodeName.str().c_str()), 0); + if (n != nullptr) + { + const auto& coord = ND_coord(n); + const auto& w = ND_width(n); + const auto& h = ND_height(n); + + auto& x = coords[i]; + auto& y = coords[i + 1]; + x = (coord.x - w * POINTS_PER_INCH / 2.0); // convert w/h in inches to points + y = (-coord.y - h * POINTS_PER_INCH / 2.0); + + maxY = std::max(maxY, y); + } + } + + // free memory + int status = gvFreeLayout(gvc, G); + status += agclose(G); + status += gvFreeContext(gvc); + if (status) + { + smtkWarningMacro( + smtk::io::Logger::instance(), "[NodeEditorPlugin] Error when freeing Graphviz resources."); + } + } + + // set positions + { + int i = -2; + for (const auto& node : nodes) + { + if (!node->isVisible() || !node->task()) + { + continue; + } + i += 2; + + node->setPos(qtTaskScene::snapToGrid(coords[i], coords[i + 1])); + } + } + + return 1; +#else // NodeEditor_ENABLE_GRAPHVIZ + (void)nodes; + (void)arcs; + return false; +#endif // NodeEditor_ENABLE_GRAPHVIZ +} + +QPointF qtTaskScene::snapToGrid(const qreal& x, const qreal& y, const qreal& resolution) +{ + // const auto gridSize = pqNodeEditorUtils::CONSTS::GRID_SIZE * resolution; + const auto gridSize = 25 * resolution; + return QPointF(x - std::fmod(x, gridSize), y - std::fmod(y, gridSize)); +} + +void qtTaskScene::drawBackground(QPainter* painter, const QRectF& rect) +{ + // painter->setPen(pqNodeEditorUtils::CONSTS::COLOR_GRID); + painter->setPen(QApplication::palette().mid().color()); + + // get rectangle bounds + const qreal recL = rect.left(); + const qreal recR = rect.right(); + const qreal recT = rect.top(); + const qreal recB = rect.bottom(); + + // determine whether to use low or high resoltion grid + const qreal gridResolution = (recB - recT) > 2000 ? 4 : 1; + // const qreal gridSize = gridResolution * pqNodeEditorUtils::CONSTS::GRID_SIZE; + const qreal gridSize = gridResolution * 25; + + // find top left corner of active rectangle and snap to grid + // const QPointF& snappedTopLeft = pqNodeEditorScene::snapToGrid(recL, recT, gridResolution); + const QPointF& snappedTopLeft = qtTaskScene::snapToGrid(recL, recT, gridResolution); + + // iterate over the x range of the rectangle to draw vertical lines + for (qreal x = snappedTopLeft.x(); x < recR; x += gridSize) + { + painter->drawLine(x, recT, x, recB); + } + + // iterate over the y range of the rectangle to draw horizontal lines + for (qreal y = snappedTopLeft.y(); y < recB; y += gridSize) + { + painter->drawLine(recL, y, recR, y); + } +} + +} // namespace extension +} // namespace smtk diff --git a/smtk/extension/qt/task/qtTaskScene.h b/smtk/extension/qt/task/qtTaskScene.h new file mode 100644 index 0000000000..4748615ca8 --- /dev/null +++ b/smtk/extension/qt/task/qtTaskScene.h @@ -0,0 +1,76 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_extension_qtTaskScene_h +#define smtk_extension_qtTaskScene_h + +#include "smtk/extension/qt/Exports.h" +#include "smtk/extension/qt/qtBaseView.h" + +#include "smtk/string/Token.h" + +#include "smtk/common/TypeContainer.h" + +#include "smtk/PublicPointerDefs.h" + +#include + +class QAbstractItemModel; +class QItemSelection; +class QTreeView; + +namespace smtk +{ +namespace extension +{ + +class qtTaskArc; +class qtTaskEditor; +class qtTaskNode; +class qtTaskViewConfiguration; + +/**\brief A QGraphicsScene that holds workflow-related QGraphicsItems. + * + */ +class SMTKQTEXT_EXPORT qtTaskScene : public QGraphicsScene +{ + Q_OBJECT + +public: + using Superclass = QGraphicsScene; + + qtTaskScene(qtTaskEditor*); + ~qtTaskScene() override; + + /// Set/get the view configuration object passed to us from the parent widget. + void setConfiguration(qtTaskViewConfiguration* config) { m_config = config; } + qtTaskViewConfiguration* configuration() const { return m_config; } + +public Q_SLOTS: + /// Compute a layout of the \a nodes and \a arcs passed into this method. + /// + /// This uses graphviz to perform the layout and returns true on success. + bool computeLayout( + const std::unordered_set& nodes, + const std::unordered_set& arcs); + +protected: + /// Snaps the given \a x and \a y coordinate to the next available top left grid point. + /// Optionally, the grid can be scaled with the \a resolution parameter. + static QPointF snapToGrid(const qreal& x, const qreal& y, const qreal& resolution = 1.0); + + /// Draw a cross-hatched grid for the background. + void drawBackground(QPainter* painter, const QRectF& rect) override; + + qtTaskViewConfiguration* m_config{ nullptr }; +}; + +} // namespace extension +} // namespace smtk +#endif // smtk_extension_qtTaskScene_h diff --git a/smtk/extension/qt/task/qtTaskView.cxx b/smtk/extension/qt/task/qtTaskView.cxx new file mode 100644 index 0000000000..c9c392b336 --- /dev/null +++ b/smtk/extension/qt/task/qtTaskView.cxx @@ -0,0 +1,72 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#include "smtk/extension/qt/task/qtTaskView.h" + +#include "smtk/extension/qt/task/qtTaskEditor.h" +#include "smtk/extension/qt/task/qtTaskScene.h" + +#include +#include +#include + +namespace smtk +{ +namespace extension +{ + +class qtTaskView::Internal +{ +}; + +qtTaskView::qtTaskView(qtTaskScene* scene, qtTaskEditor* widget) + : Superclass(scene, widget->widget()) + , m_p(new Internal) +{ + this->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); + this->setViewportUpdateMode(QGraphicsView::FullViewportUpdate); + + this->setDragMode(QGraphicsView::ScrollHandDrag); + constexpr QRectF MAX_SCENE_SIZE{ -1e4, -1e4, 3e4, 3e4 }; + this->setSceneRect(MAX_SCENE_SIZE); +} + +qtTaskView::~qtTaskView() +{ + delete m_p; + m_p = nullptr; +} + +void qtTaskView::wheelEvent(QWheelEvent* event) +{ + constexpr double ZOOM_INCREMENT_RATIO = 0.0125; + + const ViewportAnchor anchor = this->transformationAnchor(); + this->setTransformationAnchor(QGraphicsView::AnchorUnderMouse); + const int angle = event->angleDelta().y(); + static ulong lastTimestamp = 0; + double dt = + (event->timestamp() > lastTimestamp && lastTimestamp != 0 ? event->timestamp() - lastTimestamp + : 50); + lastTimestamp = event->timestamp(); + double rate = std::abs(angle / dt); + const double factor = 1.0 + rate * ((angle > 0) ? ZOOM_INCREMENT_RATIO : -ZOOM_INCREMENT_RATIO); + + this->scale(factor, factor); + this->setTransformationAnchor(anchor); +} + +void qtTaskView::keyReleaseEvent(QKeyEvent* event) +{ + (void)event; + // do nothing yet. +} + +} // namespace extension +} // namespace smtk diff --git a/smtk/extension/qt/task/qtTaskView.h b/smtk/extension/qt/task/qtTaskView.h new file mode 100644 index 0000000000..d307c92503 --- /dev/null +++ b/smtk/extension/qt/task/qtTaskView.h @@ -0,0 +1,61 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_extension_qtTaskView_h +#define smtk_extension_qtTaskView_h + +#include "smtk/extension/qt/Exports.h" +#include "smtk/extension/qt/qtBaseView.h" + +#include "smtk/common/TypeContainer.h" + +#include "smtk/PublicPointerDefs.h" + +#include + +class QAbstractItemModel; +class QItemSelection; +class QTreeView; + +namespace smtk +{ +namespace extension +{ + +class qtTaskEditor; +class qtTaskScene; + +/**\brief A widget that holds a Qt scene graph. + * + */ +class SMTKQTEXT_EXPORT qtTaskView : public QGraphicsView +{ + Q_OBJECT + +public: + using Superclass = QGraphicsView; + + qtTaskView(qtTaskScene* scene, qtTaskEditor* widget = nullptr); + ~qtTaskView() override; + + // qtTaskScene* taskScene() const; + // qtTaskEditor* taskEditor() const; + +protected: + void wheelEvent(QWheelEvent* event) override; + void keyReleaseEvent(QKeyEvent* event) override; + + class Internal; + Internal* m_p; +}; + +} // namespace extension +} // namespace smtk + +#endif // smtk_extension_qtTaskView_h diff --git a/smtk/extension/qt/task/qtTaskViewConfiguration.cxx b/smtk/extension/qt/task/qtTaskViewConfiguration.cxx new file mode 100644 index 0000000000..5c4cba99b8 --- /dev/null +++ b/smtk/extension/qt/task/qtTaskViewConfiguration.cxx @@ -0,0 +1,167 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#include "smtk/extension/qt/task/qtTaskViewConfiguration.h" + +#include "smtk/io/Logger.h" +#include "smtk/string/Token.h" +#include "smtk/task/State.h" +#include "smtk/view/Configuration.h" + +#include +#include +#include + +namespace smtk +{ +namespace extension +{ + +qtTaskViewConfiguration::qtTaskViewConfiguration(const smtk::view::Configuration& viewConfig) +{ + using namespace smtk::string::literals; + // Create default colors from QApplication's palette. + auto palette = qApp->palette(static_cast(nullptr)); + m_backgroundFillColor = palette.base().color(); + m_backgroundGridColor = palette.midlight().color(); + m_activeTaskColor = palette.highlight().color(); + m_colorForState = { + QColor("#e7e7e7"), // irrelevant + QColor("#ff9898"), // unavailable + QColor("#ffeca3"), // incomplete + QColor("#e0f1ba"), // completable + QColor("#7ed637") // completed + }; + m_colorForArc = { + QColor("#BF5B17"), // dependency + QColor("#386CB0"), // adaptor + }; + + // Look for overrides from the workflow designer. + int styleIdx = viewConfig.details().findChild("Style"); + if (styleIdx < 0) + { + return; + } + const auto& styleComp = viewConfig.details().child(styleIdx); + + int statusColorIdx = styleComp.findChild("StatusPalette"); + if (statusColorIdx >= 0) + { + using smtk::task::State; + const auto& statusColor = styleComp.child(statusColorIdx); + for (const auto& entry : statusColor.attributes()) + { + bool validName; + State state = smtk::task::stateEnum(entry.first, &validName); + // Skip invalid attributes: + if (!validName) + { + continue; + } + m_colorForState[static_cast(state)] = QColor(QString::fromStdString(entry.second)); + } + } + + int arcColorIdx = styleComp.findChild("ArcPalette"); + if (arcColorIdx >= 0) + { + const auto& arcColor = styleComp.child(arcColorIdx); + for (const auto& entry : arcColor.attributes()) + { + bool validName; + qtTaskArc::ArcType arcType = qtTaskArc::arcTypeEnum(entry.first, &validName); + // Skip invalid attributes: + if (!validName) + { + continue; + } + m_colorForArc[static_cast(arcType)] = QColor(QString::fromStdString(entry.second)); + } + } + + int viewPaletteIdx = styleComp.findChild("ViewPalette"); + if (viewPaletteIdx >= 0) + { + const auto& viewPalette = styleComp.child(viewPaletteIdx); + for (const auto& entry : viewPalette.attributes()) + { + smtk::string::Token attName(entry.first); + auto val = QString::fromStdString(entry.second); + switch (attName.id()) + { + // clang-format off + case "ActiveTask"_hash: m_activeTaskColor = QColor(val); break; + case "BackgroundGrid"_hash: m_backgroundGridColor = QColor(val); break; + case "BackgroundFill"_hash: m_backgroundFillColor = QColor(val); break; + default: + smtkWarningMacro(smtk::io::Logger::instance(), + "Unrecognized attribute \"" << entry.first << "\" in ViewPalette."); + break; + // clang-format on + } + } + } + + int nodeLayoutIdx = styleComp.findChild("NodeLayout"); + if (nodeLayoutIdx >= 0) + { + const auto& nodeLayout = styleComp.child(nodeLayoutIdx); + for (const auto& entry : nodeLayout.attributes()) + { + smtk::string::Token attName(entry.first); + switch (attName.id()) + { + // clang-format off + case "Width"_hash: m_nodeWidth = QString::fromStdString(entry.second).toDouble(); break; + case "Radius"_hash: m_nodeRadius = QString::fromStdString(entry.second).toDouble(); break; + case "HeadlineHeight"_hash: m_nodeHeadlineHeight = QString::fromStdString(entry.second).toDouble(); break; + case "HeadlinePadding"_hash: m_nodeHeadlinePadding = QString::fromStdString(entry.second).toDouble(); break; + case "BorderThickness"_hash: m_nodeBorderThickness = QString::fromStdString(entry.second).toDouble(); break; + case "FontSize"_hash: m_nodeFontSize = QString::fromStdString(entry.second).toInt(); break; + case "Layer"_hash: m_nodeLayer = QString::fromStdString(entry.second).toInt(); break; + default: + smtkWarningMacro(smtk::io::Logger::instance(), + "Unrecognized attribute \"" << entry.first << "\" in NodeLayout."); + break; + // clang-format on + } + } + } + + int arcLayoutIdx = styleComp.findChild("ArcLayout"); + if (arcLayoutIdx >= 0) + { + const auto& arcLayout = styleComp.child(arcLayoutIdx); + for (const auto& entry : arcLayout.attributes()) + { + smtk::string::Token attName(entry.first); + switch (attName.id()) + { + // clang-format off + case "Width"_hash: m_arcWidth = QString::fromStdString(entry.second).toDouble(); break; + case "Outline"_hash: m_arcOutline = QString::fromStdString(entry.second).toDouble(); break; + case "ArrowStemLength"_hash: m_arrowStemLength = QString::fromStdString(entry.second).toDouble(); break; + case "ArrowHeadLength"_hash: m_arrowHeadLength = QString::fromStdString(entry.second).toDouble(); break; + case "ArrowTipAspectRatio"_hash: m_arrowTipAspectRatio = QString::fromStdString(entry.second).toDouble(); break; + case "Layer"_hash: m_arcLayer = QString::fromStdString(entry.second).toInt(); break; + default: + smtkWarningMacro(smtk::io::Logger::instance(), + "Unrecognized attribute \"" << entry.first << "\" in ArcLayout."); + break; + // clang-format on + } + } + } + + // TODO: Look for overrides from the user (via QSettings). +} + +} // namespace extension +} // namespace smtk diff --git a/smtk/extension/qt/task/qtTaskViewConfiguration.h b/smtk/extension/qt/task/qtTaskViewConfiguration.h new file mode 100644 index 0000000000..d8cc5622e1 --- /dev/null +++ b/smtk/extension/qt/task/qtTaskViewConfiguration.h @@ -0,0 +1,99 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_extension_qtTaskViewConfiguration_h +#define smtk_extension_qtTaskViewConfiguration_h + +#include "smtk/extension/qt/task/qtTaskArc.h" +#include "smtk/task/State.h" + +#include + +#include + +namespace smtk +{ +namespace view +{ +class Configuration; +} +namespace extension +{ + +/**\brief An object to hold view configuration settings. + * + * This class extracts settings used to render the task view from a + * `view::Configuration` instance and makes them quickly accessible + * to the Qt classes that render tasks. + */ +class SMTKQTEXT_EXPORT qtTaskViewConfiguration +{ +public: + qtTaskViewConfiguration(const smtk::view::Configuration& viewConfig); + + QColor backgroundFillColor() const { return m_backgroundFillColor; } + QColor backgroundGridColor() const { return m_backgroundGridColor; } + + QColor activeTaskColor() const { return m_activeTaskColor; } + + QColor colorForArc(qtTaskArc::ArcType arcType) const + { + return m_colorForArc[static_cast(arcType)]; + } + QColor colorForState(smtk::task::State state) const + { + return m_colorForState[static_cast(state)]; + } + + qreal nodeWidth() const { return m_nodeWidth; } + qreal nodeRadius() const { return m_nodeRadius; } + qreal nodeHeadlineHeight() const { return m_nodeHeadlineHeight; } + qreal nodeHeadlinePadding() const { return m_nodeHeadlinePadding; } + qreal nodeBorderThickness() const { return m_nodeBorderThickness; } + int nodeFontSize() const { return m_nodeFontSize; } + int nodeLayer() const { return m_nodeLayer; } + + qreal arcWidth() const { return m_arcWidth; } + qreal arcOutline() const { return m_arcOutline; } + int arcLayer() const { return m_arcLayer; } + + qreal arrowStemLength() const { return m_arrowStemLength; } + qreal arrowHeadLength() const { return m_arrowHeadLength; } + qreal arrowTipAspectRatio() const { return m_arrowTipAspectRatio; } + +protected: + QColor m_backgroundFillColor; + QColor m_backgroundGridColor; + + QColor m_activeTaskColor; + + std::array(qtTaskArc::ArcType::Adaptor) + 1> m_colorForArc; + std::array(smtk::task::State::Completed) + 1> m_colorForState; + + qreal m_nodeWidth{ 300. }; + qreal m_nodeRadius{ 4. }; + qreal m_nodeHeadlineHeight{ 13 }; + qreal m_nodeHeadlinePadding{ 4. }; + qreal m_nodeBorderThickness{ 4. }; + int m_nodeFontSize{ 13 }; + int m_nodeLayer{ 10 }; + + qreal m_arcWidth{ 4. }; + qreal m_arcOutline{ 1. }; + int m_arcLayer{ 5 }; + + qreal m_arrowStemLength{ 16. }; // The length of the path guaranteed to be a straight line. + qreal m_arrowHeadLength{ 12. }; // The length of the arrow head along the linear stem. + qreal m_arrowTipAspectRatio{ 2. }; // The width of the arrow head as a fraction of head length. +}; + +} // namespace extension +} // namespace smtk + +#endif // smtk_extension_qtTaskViewConfiguration_h diff --git a/smtk/operation/operators/ReadResource.cxx b/smtk/operation/operators/ReadResource.cxx index 5c68bc12e1..1a8b69ef1e 100644 --- a/smtk/operation/operators/ReadResource.cxx +++ b/smtk/operation/operators/ReadResource.cxx @@ -151,6 +151,9 @@ ReadResource::Result ReadResource::operateInternal() return this->createResult(smtk::operation::Operation::Outcome::FAILED); } + // Pass our application state in to the read operation: + readOperation->setManagers(this->managers()); + // Set the local reader's filename field. smtk::attribute::FileItem::Ptr readerFileItem = readOperation->parameters()->findFile( readerGroup.fileItemNameForOperation(readOperation->index())); diff --git a/smtk/project/CMakeLists.txt b/smtk/project/CMakeLists.txt index f03cc53e97..ceb0c8c413 100644 --- a/smtk/project/CMakeLists.txt +++ b/smtk/project/CMakeLists.txt @@ -78,6 +78,11 @@ set(projectDependencies ${_projectDependencies} PARENT_SCOPE) # Install the headers smtk_public_headers(smtkCore ${projectHeaders}) +if (SMTK_ENABLE_PARAVIEW_SUPPORT) + set_property(GLOBAL APPEND + PROPERTY _smtk_plugin_files "${CMAKE_CURRENT_SOURCE_DIR}/plugin/paraview.plugin") +endif() + if (SMTK_ENABLE_PYTHON_WRAPPING) list(APPEND projectSrcs RegisterPythonProject.cxx) diff --git a/smtk/project/Manager.cxx b/smtk/project/Manager.cxx index 80b988ac36..b460614eb7 100644 --- a/smtk/project/Manager.cxx +++ b/smtk/project/Manager.cxx @@ -276,6 +276,10 @@ smtk::project::Project::Ptr Manager::create( // Create the project with the appropriate UUID project = metadata->create(id, m); this->add(metadata->index(), project); + if (project) + { + project->taskManager().setManagers(m); + } } return project; @@ -294,6 +298,10 @@ smtk::project::Project::Ptr Manager::create( { // Create the project with the appropriate UUID project = metadata->create(id, mm); + if (project) + { + project->taskManager().setManagers(mm); + } this->add(index, project); } diff --git a/smtk/project/operators/Read.cxx b/smtk/project/operators/Read.cxx index 5f64958ab4..580d6480dd 100644 --- a/smtk/project/operators/Read.cxx +++ b/smtk/project/operators/Read.cxx @@ -96,7 +96,7 @@ Read::Result Read::operateInternal() // Create a new project for the import boost::filesystem::path projectFilePath(filename); - auto project = this->projectManager()->create(j.at("type").get()); + auto project = this->projectManager()->create(j.at("type").get(), this->managers()); if (project == nullptr) { smtkErrorMacro(log(), "project of type " << j.at("type") << " was not created."); diff --git a/smtk/project/plugin/CMakeLists.txt b/smtk/project/plugin/CMakeLists.txt new file mode 100644 index 0000000000..b517c8ee2c --- /dev/null +++ b/smtk/project/plugin/CMakeLists.txt @@ -0,0 +1,9 @@ +smtk_add_plugin(smtkProjectPlugin + REGISTRAR smtk::project::Registrar + MANAGERS smtk::project::Manager + smtk::common::Managers + smtk::operation::Manager + smtk::resource::Manager + smtk::view::Manager + PARAVIEW_PLUGIN_ARGS + VERSION 1.0) diff --git a/smtk/project/plugin/paraview.plugin b/smtk/project/plugin/paraview.plugin new file mode 100644 index 0000000000..ce41a9a4d6 --- /dev/null +++ b/smtk/project/plugin/paraview.plugin @@ -0,0 +1,4 @@ +NAME + smtkProjectPlugin +DESCRIPTION + SMTK project support for ParaView diff --git a/smtk/session/mesh/testing/xml/OpenExodusFile.xml b/smtk/session/mesh/testing/xml/OpenExodusFile.xml index 0ae9b1d2e2..56648560a3 100644 --- a/smtk/session/mesh/testing/xml/OpenExodusFile.xml +++ b/smtk/session/mesh/testing/xml/OpenExodusFile.xml @@ -7,4 +7,5 @@ + diff --git a/smtk/task/CMakeLists.txt b/smtk/task/CMakeLists.txt index ddb7438ddc..261abe6a60 100644 --- a/smtk/task/CMakeLists.txt +++ b/smtk/task/CMakeLists.txt @@ -17,6 +17,8 @@ set(taskSrcs Instances.cxx Manager.cxx Registrar.cxx + UIState.cxx + UIStateGenerator.cxx adaptor/ResourceAndRole.cxx json/Configurator.cxx json/Helper.cxx @@ -31,6 +33,8 @@ set(taskHeaders Manager.h Registrar.h State.h + UIState.h + UIStateGenerator.h adaptor/Instances.h adaptor/ResourceAndRole.h json/Configurator.h diff --git a/smtk/task/FillOutAttributes.cxx b/smtk/task/FillOutAttributes.cxx index 65ad40f008..68dcf64230 100644 --- a/smtk/task/FillOutAttributes.cxx +++ b/smtk/task/FillOutAttributes.cxx @@ -182,27 +182,55 @@ smtk::common::Visit FillOutAttributes::visitAttributeSets(AttributeSetVisitor vi bool FillOutAttributes::initializeResources() { + // By default, assume we are unconfigured: bool foundResource = false; if (m_attributeSets.empty()) { return foundResource; } + if (auto resourceManager = m_managers->get()) { - auto resources = resourceManager->find(); - for (const auto& resource : resources) + // Iterate attribute sets to see if any are configured with valid + // attribute resource and/or attribute UUIDs. If so and if we can + // find the matching resource in the manager, then we can return true. + bool anyAutoconfigure = false; + for (const auto& attributeSet : m_attributeSets) { - const std::string& role = smtk::project::detail::role(resource); - for (auto& attributeSet : m_attributeSets) + if (attributeSet.m_autoconfigure) { - if ( - attributeSet.m_role.empty() || attributeSet.m_role == "*" || attributeSet.m_role == role) + anyAutoconfigure = true; + } + for (const auto& resourceEntry : attributeSet.m_resources) + { + if (auto rsrc = resourceManager->get(resourceEntry.first)) { - if (attributeSet.m_autoconfigure) + foundResource = true; + } + } + } + + // If any attribute sets were marked "autoconfigure" (meaning we should + // identify any attribute resources with the proper role), then iterate + // resources in the resource manager and configure as required. + if (anyAutoconfigure) + { + auto resources = resourceManager->find(); + for (const auto& resource : resources) + { + const std::string& role = smtk::project::detail::role(resource); + for (auto& attributeSet : m_attributeSets) + { + if ( + attributeSet.m_role.empty() || attributeSet.m_role == "*" || + attributeSet.m_role == role) { - foundResource = true; - auto it = attributeSet.m_resources.insert({ resource->id(), { {}, {} } }).first; - this->updateResourceEntry(*resource, attributeSet, it->second); + if (attributeSet.m_autoconfigure) + { + foundResource = true; + auto it = attributeSet.m_resources.insert({ resource->id(), { {}, {} } }).first; + this->updateResourceEntry(*resource, attributeSet, it->second); + } } } } @@ -422,7 +450,6 @@ bool FillOutAttributes::hasRelevantInfomation( } State FillOutAttributes::computeInternalState() const { - std::cerr << "Computing new state\n"; auto resourceManager = m_managers->get(); if (!resourceManager) { diff --git a/smtk/task/Manager.cxx b/smtk/task/Manager.cxx index 4161258b79..43d5147bf3 100644 --- a/smtk/task/Manager.cxx +++ b/smtk/task/Manager.cxx @@ -25,5 +25,14 @@ Manager::Manager() Manager::~Manager() = default; +nlohmann::json Manager::getStyle(const smtk::string::Token& styleClass) const +{ + if (this->m_styles.contains(styleClass.data())) + { + return this->m_styles.at(styleClass.data()); + } + return nlohmann::json(); +} + } // namespace task } // namespace smtk diff --git a/smtk/task/Manager.h b/smtk/task/Manager.h index 2e1eef868d..0d8a39d6dd 100644 --- a/smtk/task/Manager.h +++ b/smtk/task/Manager.h @@ -17,13 +17,17 @@ #include "smtk/common/Managers.h" #include "smtk/common/TypeName.h" +#include "smtk/string/Token.h" #include "smtk/task/Active.h" #include "smtk/task/Adaptor.h" #include "smtk/task/Instances.h" #include "smtk/task/Task.h" +#include "smtk/task/UIState.h" #include "smtk/task/adaptor/Instances.h" +#include "nlohmann/json.hpp" + #include #include #include @@ -76,11 +80,21 @@ public: smtk::common::Managers::Ptr managers() const { return m_managers.lock(); } void setManagers(const smtk::common::Managers::Ptr& managers) { m_managers = managers; } + /// Given a style key, return a style config. + nlohmann::json getStyle(const smtk::string::Token& styleClass) const; + nlohmann::json getStyles() const { return m_styles; }; + void setStyles(const nlohmann::json& styles) { m_styles = styles; } + + /// Store geometry changes from UI components + UIState& uiState() { return m_uiState; } + private: TaskInstances m_taskInstances; AdaptorInstances m_adaptorInstances; Active m_active; std::weak_ptr m_managers; + nlohmann::json m_styles; + UIState m_uiState; }; } // namespace task } // namespace smtk diff --git a/smtk/task/State.h b/smtk/task/State.h index 59e13566ad..9b8a8a6a73 100644 --- a/smtk/task/State.h +++ b/smtk/task/State.h @@ -45,8 +45,12 @@ inline std::string stateName(const State& s) } /// A type-conversion operation to cast strings to enumerants. -inline State stateEnum(const std::string& s) +inline State stateEnum(const std::string& s, bool* valid = nullptr) { + if (valid) + { + *valid = true; + } std::string stateName(s); std::transform(stateName.begin(), stateName.end(), stateName.begin(), [](unsigned char c) { return std::tolower(c); @@ -71,6 +75,10 @@ inline State stateEnum(const std::string& s) { return State::Completed; } + if (valid) + { + *valid = (stateName == "irrelevant"); + } return State::Irrelevant; } diff --git a/smtk/task/Task.h b/smtk/task/Task.h index e2e8f92685..1a9b81daf9 100644 --- a/smtk/task/Task.h +++ b/smtk/task/Task.h @@ -266,6 +266,9 @@ public: /// inherent for the task itself. State internalState() const { return m_internalState; } + /// Return the tasks's manager (or null if unmanaged). + Manager* manager() const { return m_manager.lock().get(); } + protected: friend SMTKCORE_EXPORT void workflowsOfTask(Task*, std::set&, std::set&); diff --git a/smtk/task/UIState.cxx b/smtk/task/UIState.cxx new file mode 100644 index 0000000000..8948324262 --- /dev/null +++ b/smtk/task/UIState.cxx @@ -0,0 +1,79 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#include "smtk/task/UIState.h" + +#include "smtk/io/Logger.h" +#include "smtk/task/Task.h" + +namespace smtk +{ +namespace task +{ + +void UIState::setData(std::shared_ptr task, const nlohmann::json& j) +{ + for (auto& entry : j.items()) + { + smtk::string::Token classToken = entry.key(); + m_data[classToken][task->id()] = entry.value(); + } +} + +nlohmann::json UIState::getData(smtk::string::Token classToken, smtk::task::Task* task) const +{ + auto classIter = m_data.find(classToken); + if (classIter == m_data.end()) + { + return nlohmann::json(); + } + + auto uiIter = classIter->second.find(task->id()); + if (uiIter == classIter->second.end()) + { + return nlohmann::json::object(); + } + + return uiIter->second; +} + +void UIState::updateJson(std::shared_ptr task, nlohmann::json& j) const +{ + auto jUI = nlohmann::json::object(); + if (j.contains("ui")) + { + jUI = j["ui"]; + } + + for (const auto& entry : m_generators) + { + jUI[entry.first] = entry.second->taskState(task); + } + + j["ui"] = jUI; +} + +void UIState::dump(std::ostream& os) +{ + os << '\n'; + auto classIter = m_data.begin(); + for (; classIter != m_data.end(); ++classIter) + { + os << classIter->first.data() << '\n'; + auto configIter = classIter->second.begin(); + for (; configIter != classIter->second.end(); ++configIter) + { + os << " " << configIter->first.data() << ", " << configIter->second << '\n'; + } + } + os << std::endl; +} + +} // namespace task +} // namespace smtk diff --git a/smtk/task/UIState.h b/smtk/task/UIState.h new file mode 100644 index 0000000000..5e8caab912 --- /dev/null +++ b/smtk/task/UIState.h @@ -0,0 +1,67 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_task_UIState_h +#define smtk_task_UIState_h + +#include "smtk/CoreExports.h" + +#include "smtk/string/Token.h" +#include "smtk/task/UIStateGenerator.h" + +#include "nlohmann/json.hpp" + +#include +#include +#include + +namespace smtk +{ +namespace task +{ +class Task; + +/**\brief Stores UI state data for task UI classes. */ +class SMTKCORE_EXPORT UIState +{ +public: + UIState() = default; + ~UIState() = default; + + /** \brief Stores "ui" object for specified task. */ + void setData(std::shared_ptr task, const nlohmann::json& j); + + /** \brief Returns "ui" data for given class and task. */ + nlohmann::json getData(smtk::string::Token classToken, smtk::task::Task* task) const; + + /** \brief Stores generator for given class name. */ + void setGenerator(const std::string& className, std::shared_ptr generator) + { + m_generators[className] = generator; + } + + /** \brief Updates "ui" object to include data from all generators. */ + void updateJson(std::shared_ptr task, nlohmann::json& j) const; + + /** \brief Writes contents of "ui" objects stored for each task and class */ + void dump(std::ostream& os); + +protected: + /** \brief Nested map of <, > for deserialized UI state data. */ + std::unordered_map> + m_data; + + /** \brief Map of for serializing UI state data. */ + std::unordered_map> m_generators; +}; + +} // namespace task +} // namespace smtk + +#endif diff --git a/smtk/task/UIStateGenerator.cxx b/smtk/task/UIStateGenerator.cxx new file mode 100644 index 0000000000..237c738755 --- /dev/null +++ b/smtk/task/UIStateGenerator.cxx @@ -0,0 +1,20 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#include "smtk/task/UIStateGenerator.h" + +namespace smtk +{ +namespace task +{ + +UIStateGenerator::UIStateGenerator() = default; + +} // namespace task +} // namespace smtk diff --git a/smtk/task/UIStateGenerator.h b/smtk/task/UIStateGenerator.h new file mode 100644 index 0000000000..d6262e5c3b --- /dev/null +++ b/smtk/task/UIStateGenerator.h @@ -0,0 +1,43 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_task_UIStateGenerator_h +#define smtk_task_UIStateGenerator_h + +#include "smtk/CoreExports.h" + +#include "smtk/task/Task.h" + +#include "nlohmann/json.hpp" + +namespace smtk +{ +namespace task +{ + +/**\brief Pure virtual class for read/write UI state data. */ + +class SMTKCORE_EXPORT UIStateGenerator +{ +public: + UIStateGenerator(); + ~UIStateGenerator() = default; + + /** \brief Returns state not tied to a particular task (e.g. background color). */ + virtual nlohmann::json globalState() const = 0; + /** \brief Returns state attached to a particular \a task. */ + virtual nlohmann::json taskState(const std::shared_ptr& task) const = 0; + +protected: +}; + +} // namespace task +} // namespace smtk + +#endif diff --git a/smtk/task/json/Configurator.h b/smtk/task/json/Configurator.h index d5e478f125..06c0f77db0 100644 --- a/smtk/task/json/Configurator.h +++ b/smtk/task/json/Configurator.h @@ -130,6 +130,11 @@ public: /// This will allocate a new ID if none exists. SwizzleId swizzleId(const ObjectType* object); + /// When deserializing an object, we have the swizzle ID assigned previously. + /// Accept the given ID; if it already exists, then return false and print + /// a warning. Otherwise, assign the given ID and return true. + bool setSwizzleId(const ObjectType* object, SwizzleId swizzle); + /// Return the pointer to an object given its swizzled ID (or null). ObjectType* unswizzle(SwizzleId objectId) const; diff --git a/smtk/task/json/Configurator.txx b/smtk/task/json/Configurator.txx index 69c0778a68..95bd0e8a20 100644 --- a/smtk/task/json/Configurator.txx +++ b/smtk/task/json/Configurator.txx @@ -13,6 +13,8 @@ #include "smtk/task/json/Configurator.h" #include "smtk/task/json/Helper.h" +#include "smtk/io/Logger.h" + namespace smtk { namespace task @@ -195,6 +197,37 @@ typename Configurator::SwizzleId Configurator::s return id; } +/// Assign a previously-provided swizzle ID to the object. +/// This will warn and return false if the ID already exists. +template +bool Configurator::setSwizzleId( + const ObjectType* object, + typename Configurator::SwizzleId swizzle) +{ + if (!object) + { + return false; + } + auto bit = m_swizzleBck.find(swizzle); + if (bit != m_swizzleBck.end()) + { + smtkWarningMacro( + smtk::io::Logger::instance(), + "Deserialized swizzle ID " << swizzle << " is already assigned to" + << "\"" << bit->second->title() << "\" " << bit->second + << ". Skipping."); + return false; + } + auto* ncobject = const_cast(object); // Need a non-const ObjectType in some cases. + m_swizzleFwd[ncobject] = swizzle; + m_swizzleBck[swizzle] = ncobject; + if (swizzle >= m_nextSwizzle) + { + m_nextSwizzle = swizzle + 1; + } + return true; +} + /// Return the pointer to an object given its swizzled ID (or null). template ObjectType* Configurator::unswizzle(SwizzleId objectId) const diff --git a/smtk/task/json/Helper.cxx b/smtk/task/json/Helper.cxx index 37666fbd92..8584b7a5bd 100644 --- a/smtk/task/json/Helper.cxx +++ b/smtk/task/json/Helper.cxx @@ -11,6 +11,7 @@ #include "smtk/task/json/Configurator.txx" #include "smtk/io/Logger.h" +#include "smtk/task/Manager.h" #include #include @@ -210,6 +211,17 @@ Task* Helper::activeSerializedTask() const return m_activeSerializedTask; } +void Helper::updateUIState(std::shared_ptr task, nlohmann::json& j) +{ + if (m_taskManager == nullptr) + { + return; + } + + auto& uiState = m_taskManager->uiState(); + uiState.updateJson(task, j); +} + } // namespace json } // namespace task } // namespace smtk diff --git a/smtk/task/json/Helper.h b/smtk/task/json/Helper.h index 019e65bd65..581cb808cd 100644 --- a/smtk/task/json/Helper.h +++ b/smtk/task/json/Helper.h @@ -135,6 +135,9 @@ public: void setActiveSerializedTask(Task* task); Task* activeSerializedTask() const; + // Insert UI states (if any) for given task + void updateUIState(std::shared_ptr task, nlohmann::json& j); + protected: Helper(); Helper(Manager*); diff --git a/smtk/task/json/jsonGatherResources.cxx b/smtk/task/json/jsonGatherResources.cxx index f6af8f7296..e134d0ae49 100644 --- a/smtk/task/json/jsonGatherResources.cxx +++ b/smtk/task/json/jsonGatherResources.cxx @@ -74,9 +74,11 @@ void from_json(const nlohmann::json& j, GatherResources::ResourceSet& resourceSe if (resource) { resourceSet.m_resources.insert(resource); + warnOnIds = false; } else { + warnOnIds = true; smtkWarningMacro( smtk::io::Logger::instance(), "Resource \"" << jsonId << "\" not found."); } @@ -87,6 +89,10 @@ void from_json(const nlohmann::json& j, GatherResources::ResourceSet& resourceSe warnOnIds = true; } } + else + { + warnOnIds = true; + } if (warnOnIds) { smtkWarningMacro( diff --git a/smtk/task/json/jsonManager.cxx b/smtk/task/json/jsonManager.cxx index 7e0c8a976c..ead0972785 100644 --- a/smtk/task/json/jsonManager.cxx +++ b/smtk/task/json/jsonManager.cxx @@ -48,19 +48,35 @@ void from_json(const nlohmann::json& jj, Manager& taskManager) auto taskId = jsonTask.at("id").get(); Task::Ptr task = jsonTask; taskMap[taskId] = task; - helper.tasks().swizzleId(task.get()); + helper.tasks().setSwizzleId(task.get(), taskId); } - // Do a second pass to deserialize dependencies. + // Do a second pass to deserialize dependencies and UI config. for (const auto& jsonTask : jj.at("tasks")) { + auto taskId = jsonTask.at("id").get(); + auto task = taskMap[taskId]; if (jsonTask.contains("dependencies")) { - auto taskId = jsonTask.at("id").get(); - auto task = taskMap[taskId]; auto taskDeps = helper.unswizzleDependencies(jsonTask.at("dependencies")); + // Make sure this is not its own dependencies + auto finder = taskDeps.find(task); + if (finder != taskDeps.end()) + { + smtkWarningMacro( + smtk::io::Logger::instance(), task->title() << " trying to set deps to itself"); + taskDeps.erase(task); + } + task->addDependencies(taskDeps); } + + // Get UI object + if (jsonTask.contains("ui")) + { + taskManager.uiState().setData(task, jsonTask["ui"]); + } } + // Now configure dependent tasks with adaptors if specified. // Note that tasks have already been deserialized, so the // helper's map from task-id to task-pointer is complete. @@ -90,6 +106,10 @@ void from_json(const nlohmann::json& jj, Manager& taskManager) } } } + if (jj.contains("styles")) + { + taskManager.setStyles(jj.at("styles")); + } // helper.clear(); } @@ -113,7 +133,11 @@ void to_json(nlohmann::json& jj, const Manager& manager) // Only serialize top-level tasks. (Tasks with children are responsible // for serializing their children). nlohmann::json jsonTask = task; - taskList.push_back(jsonTask); + if (!jsonTask.is_null()) + { + helper.updateUIState(task, jsonTask); + taskList.push_back(jsonTask); + } } return smtk::common::Visit::Continue; }); @@ -134,6 +158,7 @@ void to_json(nlohmann::json& jj, const Manager& manager) return smtk::common::Visit::Continue; }); jj["adaptors"] = adaptorList; + jj["styles"] = manager.getStyles(); } } // namespace task diff --git a/smtk/task/pybind11/PybindState.h b/smtk/task/pybind11/PybindState.h index 61a2bf020a..51ee7e46ae 100644 --- a/smtk/task/pybind11/PybindState.h +++ b/smtk/task/pybind11/PybindState.h @@ -34,7 +34,7 @@ inline void pybind11_init_smtk_task_stateName(py::module &m) inline void pybind11_init_smtk_task_stateEnum(py::module &m) { - m.def("stateEnum", &smtk::task::stateEnum, "", py::arg("s")); + m.def("stateEnum", &smtk::task::stateEnum, "", py::arg("s"), py::arg("matched")); } #endif diff --git a/smtk/task/testing/cxx/CMakeLists.txt b/smtk/task/testing/cxx/CMakeLists.txt index 34fe555948..176e26dbc9 100644 --- a/smtk/task/testing/cxx/CMakeLists.txt +++ b/smtk/task/testing/cxx/CMakeLists.txt @@ -3,6 +3,7 @@ set(unit_tests TestTaskBasics.cxx TestTaskGroup.cxx TestTaskJSON.cxx + TestTaskUIState.cxx ) find_package(Threads REQUIRED) diff --git a/smtk/task/testing/cxx/TestTaskUIState.cxx b/smtk/task/testing/cxx/TestTaskUIState.cxx new file mode 100644 index 0000000000..e115a5792e --- /dev/null +++ b/smtk/task/testing/cxx/TestTaskUIState.cxx @@ -0,0 +1,83 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= + +#include "smtk/common/Managers.h" +#include "smtk/plugin/Registry.h" +#include "smtk/resource/json/Helper.h" +#include "smtk/task/Manager.h" +#include "smtk/task/Registrar.h" +#include "smtk/task/Task.h" +#include "smtk/task/UIState.h" +#include "smtk/task/UIStateGenerator.h" +#include "smtk/task/json/Helper.h" +#include "smtk/task/json/jsonManager.h" +#include "smtk/task/json/jsonTask.h" + +#include "smtk/common/testing/cxx/helpers.h" + +#include "nlohmann/json.hpp" + +#include + +namespace +{ +class TaskUIStateGenerator : public smtk::task::UIStateGenerator +{ +public: + TaskUIStateGenerator() = default; + virtual ~TaskUIStateGenerator() = default; + + nlohmann::json globalState() const override { return nlohmann::json(); } + nlohmann::json taskState(const std::shared_ptr& task) const override + { + (void)task; + nlohmann::json state = R"({ "test": "passed" })"_json; + return state; + } +}; +} // namespace + +int TestTaskUIState(int, char*[]) +{ + // Create managers + auto managers = smtk::common::Managers::create(); + auto taskRegistry = smtk::plugin::addToManagers(managers); + + auto taskManager = smtk::task::Manager::create(); + auto taskTaskRegistry = smtk::plugin::addToManagers(taskManager); + + // Add UI state generator + std::shared_ptr gen(new TaskUIStateGenerator); + auto baseGen = std::dynamic_pointer_cast(gen); + taskManager->uiState().setGenerator("TaskUIStateGenerator", baseGen); + + // Create task + std::shared_ptr t1 = taskManager->taskInstances().create( + smtk::task::Task::Configuration{ { "title", "Task 1" } }, *taskManager, managers); + smtkTest(t1 != nullptr, "failed to create task."); + + // Generate json object + auto& resourceHelper = smtk::resource::json::Helper::instance(); + resourceHelper.setManagers(managers); + auto& taskHelper = + smtk::task::json::Helper::pushInstance(*taskManager, resourceHelper.managers()); + taskHelper.setManagers(resourceHelper.managers()); + nlohmann::json j = *taskManager; + smtk::task::json::Helper::popInstance(); + std::cout << j << std::endl; + + // Verify that ui content was generated + auto jtest = j["tasks"][0]["ui"]["TaskUIStateGenerator"]["test"]; + smtkTest(jtest.get() == "passed", "did not find \"passed\" field."); + + // Future: come up with test reading json object to initialize (second) task manager. + + return 0; +} -- GitLab