diff --git a/src/translations/Dyt_zh_CN.ts b/src/translations/Dyt_zh_CN.ts index b0e5580f..b6311aa8 100644 --- a/src/translations/Dyt_zh_CN.ts +++ b/src/translations/Dyt_zh_CN.ts @@ -559,27 +559,27 @@ MainFrame - + Dyt - + file manager - + simu manager - + play manager - + system manager @@ -592,132 +592,132 @@ - + model elements - + attribte - + Main View - + Wave Curve - + Speed Curve - + 3D Curve - + Target number - + Signal-to-noise ratio - + Azimuth line of sight - + Pitch gaze angle - + azimuth - + Pitch angle - + attribute - + Doppler - + course - + Speed - + longitude - + latitude - + distance - + velocity - + Radial dimensions - + Target RCS - + Report Table - + Signal Indicator Lamp - + ParamSetting - + bat File @@ -972,26 +972,26 @@ - - + + Curve[%1] - - + + Surface[%1] - - + + Table[%1] - - + + Light[%1] @@ -1067,48 +1067,48 @@ QtConeWaveComponentManager - - + + ConeWaveComponent - + Height - + Radius - + waveCount - + waveSpeed - + baseColor - + waveColor - + ringBrightAlpha - + ringDarkAlpha @@ -1214,28 +1214,28 @@ QtDashedLineComponentManager - - + + DashedLineComponent - + Start - + End - + Radius - + Color @@ -1251,17 +1251,17 @@ QtEntityPropertyManager - + Name - + Visible - + Transform @@ -1343,13 +1343,13 @@ QtMeshComponetManager - - + + MeshComponent - + Mesh @@ -1398,13 +1398,13 @@ QtPathComponentManager - - + + PathComponent - + Path @@ -1654,87 +1654,92 @@ QtWorkspacePropertyManager - + Name - + Description - + Timestep - + SimMatlab - + MatlabParam - + WavePath - + ReportPath - + RDPath - + + CommondPath + + + + Count - + Curve[%1] - + Surface[%1] - + Table[%1] - + Light[%1] - + Curves - + Surfaces - + Tables - + Lights @@ -1762,6 +1767,29 @@ + + SimuRunMenu + + + no workspace + + + + + no commands + + + + + Commands + + + + + unnamed + + + SimuRunMenuClass diff --git a/src/ui/MainFrame.cpp b/src/ui/MainFrame.cpp index e351078d..0e69b60b 100644 --- a/src/ui/MainFrame.cpp +++ b/src/ui/MainFrame.cpp @@ -25,6 +25,8 @@ #include "ui/Menu/ChartPlotMenu.h" // lz 20140914 #include "common/SpdLogger.h" +#include "ui/Menu/SimuRunMenu.h" + #include "ui_MainFrame.h" #include "viewer/OsgWidget.h" @@ -104,8 +106,12 @@ void MainFrame::InitUI() { //AddMenuWidget("view_manager", tr("view manager"), new ViewManagerMenu(this)); //AddMenuWidget("plan_manager", tr("plan manager"), new PlanManagerMenu(this)); //AddMenuWidget("dynamic_display", tr("dynamic display"), new DynamicDisplayMenu(this)); - ChartPlotMenu* chartMenu = new ChartPlotMenu(this); - AddMenuWidget("simu_manager", tr("simu manager"), chartMenu); + // ChartPlotMenu* chartMenu = new ChartPlotMenu(this); + // AddMenuWidget("simu_manager", tr("simu manager"), chartMenu); + + // Command buttons menu based on parsed commands + SimuRunMenu* simuRunMenu = new SimuRunMenu(this); + AddMenuWidget("simu_manager", tr("simu manager"), simuRunMenu); PlayManagerMenu* playMenu = new PlayManagerMenu(this); AddMenuWidget("play_manager", tr("play manager"), playMenu); diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index 95a34c94..6c54995b 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -27,6 +27,9 @@ #include "Matlab/MatlabObject.h" +// 曲线面板管理器 +#include "Panel/DataPanelManager.h" + #include "ui_MainWindow.h" #include "viewer/OsgWidget.h" #include "DockTitleBar.h" @@ -211,6 +214,13 @@ void MainWindow::InitUI() { // InitDockLayout(); + // 初始化数据面板管理器 + dataPanelManager_ = new DataPanelManager(this, this); + + // 连接工作空间变化信号 + connect(&WorkSpaceManager::Get(), &WorkSpaceManager::WorkSpaceChanged, + dataPanelManager_, &DataPanelManager::OnWorkspaceChanged); + // Restore previous UI layout if available UiLayoutManager::Restore(this, 1); @@ -230,6 +240,13 @@ void MainWindow::InitUI() { void MainWindow::UninitUI() { // Save layout state before tearing down widgets UiLayoutManager::Save(this, 1); + + // 清理数据面板管理器 + if (dataPanelManager_) { + delete dataPanelManager_; + dataPanelManager_ = nullptr; + } + if (qtOsgViewWidget_) { qtOsgViewWidget_->Uninitialize(); delete qtOsgViewWidget_; diff --git a/src/ui/MainWindow.h b/src/ui/MainWindow.h index 65c484e6..110276bb 100644 --- a/src/ui/MainWindow.h +++ b/src/ui/MainWindow.h @@ -10,6 +10,8 @@ namespace Ui { class MainWindow; } +class DataPanelManager; + class MainWindow : public QMainWindow { Q_OBJECT @@ -37,6 +39,14 @@ public: return surfaceDlg_; } + /** + * @brief 获取数据面板管理器 + * @return 数据面板管理器指针 + */ + DataPanelManager* GetDataPanelManager() const { + return dataPanelManager_; + } + void slotShowUISetting(); public slots: @@ -68,6 +78,9 @@ private: class CodeEdtUI* matlabFileDlg_{ nullptr }; class AddParamSetting* addParamDlg_{ nullptr }; + // 数据面板管理器 + DataPanelManager* dataPanelManager_{ nullptr }; + QMap m_mapDockWidget; ChartXMLMgr m_mgrChart; diff --git a/src/ui/Menu/SimuRunMenu.cpp b/src/ui/Menu/SimuRunMenu.cpp index cb555e12..1edb0365 100644 --- a/src/ui/Menu/SimuRunMenu.cpp +++ b/src/ui/Menu/SimuRunMenu.cpp @@ -1,10 +1,107 @@ -#include "SimuRunMenu.h" +#include "ui/Menu/SimuRunMenu.h" + +#include +#include +#include + +#include "workspace/WorkSpaceManager.h" +#include "workspace/WorkSpace.h" +#include "workspace/CommandManager.h" SimuRunMenu::SimuRunMenu(QWidget *parent) - : QWidget(parent) -{ - ui.setupUi(this); + : QWidget(parent) { + ui.setupUi(this); + + // Create a vertical layout to host grouped command rows + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(9, 0, 0, 0); + layout->setSpacing(6); + setLayout(layout); + + // Refresh when workspace changes + connect(&WorkSpaceManager::Get(), &WorkSpaceManager::WorkSpaceChanged, + this, &SimuRunMenu::OnWorkspaceChanged); + + // Initial population + RefreshButtons(); } -SimuRunMenu::~SimuRunMenu() -{} +SimuRunMenu::~SimuRunMenu() { +} + +void SimuRunMenu::OnWorkspaceChanged(WorkSpace* ws) { + Q_UNUSED(ws); + RefreshButtons(); +} + +void SimuRunMenu::RefreshButtons() { + // Clear existing buttons + if (auto* layout = qobject_cast(this->layout())) { + while (layout->count() > 0) { + QLayoutItem* item = layout->takeAt(0); + if (item) { + if (auto* w = item->widget()) { + w->deleteLater(); + } + delete item; + } + } + } + + auto* ws = WorkSpaceManager::Get().GetCurrent(); + if (!ws) { + // Show hint when no workspace + if (auto* layout = qobject_cast(this->layout())) { + auto* hint = new QLabel(tr("no workspace"), this); + layout->addWidget(hint); + } + return; + } + + CommandManager mgr; + const auto items = mgr.ListCommands(ws); + if (items.empty()) { + if (auto* layout = qobject_cast(this->layout())) { + auto* hint = new QLabel(tr("no commands"), this); + layout->addWidget(hint); + } + return; + } + + // Render all commands in a single group without trigger distinction + CreateGroup(tr("Commands"), items); +} + +void SimuRunMenu::CreateGroup(const QString& title, + const std::vector& items) { + auto* root = qobject_cast(this->layout()); + if (!root) return; + + auto* label = new QLabel(title, this); + root->addWidget(label); + + auto* row = new QHBoxLayout(); + row->setSpacing(6); + for (const auto& item : items) { + auto* btn = new QToolButton(this); + btn->setText(item.name.isEmpty() ? tr("unnamed") : item.name); + QString tip = item.descript; + if (!item.program.isEmpty()) { + tip += QStringLiteral("\nprog: ") + item.program; + } + if (!item.path.isEmpty()) { + tip += QStringLiteral("\npath: ") + item.path; + } + btn->setToolTip(tip); + row->addWidget(btn); + + connect(btn, &QToolButton::clicked, this, [name = item.name]() { + auto* wsCur = WorkSpaceManager::Get().GetCurrent(); + if (!wsCur) return; + CommandManager execMgr; + execMgr.ExecuteByName(wsCur, name); + }); + } + row->addStretch(1); + root->addLayout(row); +} diff --git a/src/ui/Menu/SimuRunMenu.h b/src/ui/Menu/SimuRunMenu.h index 28ac1222..fb1630b9 100644 --- a/src/ui/Menu/SimuRunMenu.h +++ b/src/ui/Menu/SimuRunMenu.h @@ -2,15 +2,25 @@ #include #include "ui_SimuRunMenu.h" +#include "workspace/CommandManager.h" +#include "workspace/CommandExecutor.h" -class SimuRunMenu : public QWidget -{ - Q_OBJECT +class SimuRunMenu : public QWidget { + Q_OBJECT public: - SimuRunMenu(QWidget *parent = nullptr); - ~SimuRunMenu(); + SimuRunMenu(QWidget *parent = nullptr); + ~SimuRunMenu(); + +private slots: + void OnWorkspaceChanged(class WorkSpace* ws); private: - Ui::SimuRunMenuClass ui; + void RefreshButtons(); + void CreateGroup(const QString& title, + const std::vector& items); + + +private: + Ui::SimuRunMenuClass ui; }; diff --git a/src/ui/Panel/CurvePanel.cpp b/src/ui/Panel/CurvePanel.cpp new file mode 100644 index 00000000..865222c8 --- /dev/null +++ b/src/ui/Panel/CurvePanel.cpp @@ -0,0 +1,48 @@ +#include "ui/Panel/CurvePanel.h" +#include "ui/DockWidget.h" +#include "common/SpdLogger.h" + +#include +#include +#include + +CurvePanel::CurvePanel(int index, const QString& filePath, QWidget* parent) + : DataPanel(index, FileEntryType::Curve, filePath, parent) +{ + LOG_INFO("Created CurvePanel {} for file: {}", index, filePath.toStdString()); +} + +CurvePanel::~CurvePanel() +{ + LOG_INFO("Destroyed CurvePanel {}", GetIndex()); +} + +void CurvePanel::RefreshPanel() +{ + // Implement curve-specific refresh logic here + // For now, just call the base class implementation + DataPanel::RefreshPanel(); + + LOG_INFO("Refreshed CurvePanel {}", GetIndex()); +} + +void CurvePanel::InitUI() +{ + // Create basic layout + QVBoxLayout* layout = new QVBoxLayout(this); + + // Add placeholder label showing panel information + QLabel* infoLabel = new QLabel(QString("Curve Panel %1\nFile: %2\n\nCurve Drawing Area\nPlease inherit this class to implement specific drawing functionality") + .arg(GetIndex()) + .arg(QFileInfo(GetFilePath()).fileName())); + infoLabel->setAlignment(Qt::AlignCenter); + infoLabel->setStyleSheet("QLabel { color: #666; font-size: 12px; padding: 20px; }"); + + layout->addWidget(infoLabel); + setLayout(layout); +} + +QString CurvePanel::GetTypeDisplayName() const +{ + return "Curve"; +} \ No newline at end of file diff --git a/src/ui/Panel/CurvePanel.h b/src/ui/Panel/CurvePanel.h new file mode 100644 index 00000000..1fa7e231 --- /dev/null +++ b/src/ui/Panel/CurvePanel.h @@ -0,0 +1,55 @@ +#pragma once + +#include "DataPanel.h" + +/** + * @file CurvePanel.h + * @brief Curve Panel Class + * Specialized panel for curve data visualization and manipulation + */ + +/** + * @brief Curve panel class + * Specialized panel for curve data, inherits from DataPanel + */ +class CurvePanel : public DataPanel +{ + Q_OBJECT + +public: + /** + * @brief Constructor + * @param index Panel index + * @param filePath Associated file path + * @param parent Parent widget + */ + explicit CurvePanel(int index, const QString& filePath, QWidget* parent = nullptr); + + /** + * @brief Destructor + */ + virtual ~CurvePanel(); + + /** + * @brief Get file type + * @return File type (always Curve for this class) + */ + FileEntryType GetFileType() const override { return FileEntryType::Curve; } + + /** + * @brief Refresh panel content + */ + void RefreshPanel() override; + +protected: + /** + * @brief Initialize UI for curve-specific layout + */ + void InitUI() override; + + /** + * @brief Get type display name + * @return Display name for curve type + */ + QString GetTypeDisplayName() const override; +}; \ No newline at end of file diff --git a/src/ui/Panel/DataPanel.cpp b/src/ui/Panel/DataPanel.cpp new file mode 100644 index 00000000..fa90efeb --- /dev/null +++ b/src/ui/Panel/DataPanel.cpp @@ -0,0 +1,75 @@ +#include "ui/Panel/DataPanel.h" +#include "ui/DockWidget.h" +#include "common/SpdLogger.h" + +#include +#include +#include +#include + +DataPanel::DataPanel(int index, FileEntryType fileType, const QString& filePath, QWidget* parent) + : QWidget(parent) + , index_(index) + , fileType_(fileType) + , filePath_(filePath) + , title_() + , dockWidget_(nullptr) +{ + title_ = GenerateTitle(); + InitUI(); + + LOG_INFO("Created DataPanel {} for {} file: {}", index_, FileEntryTypeToString(fileType_), filePath_.toStdString()); +} + +DataPanel::~DataPanel() +{ + LOG_INFO("Destroyed DataPanel {} ({})", index_, FileEntryTypeToString(fileType_)); +} + +void DataPanel::closeEvent(QCloseEvent* event) +{ + emit PanelClosed(); + event->accept(); +} + +void DataPanel::InitUI() +{ + // Create basic layout + QVBoxLayout* layout = new QVBoxLayout(this); + + // Add placeholder label showing panel information + QString typeDisplayName = GetTypeDisplayName(); + QLabel* infoLabel = new QLabel(QString("Panel %1 (%2)\nFile: %3\n\n%4 Data Area\nPlease inherit this class to implement specific functionality") + .arg(index_) + .arg(typeDisplayName) + .arg(QFileInfo(filePath_).fileName()) + .arg(typeDisplayName)); + infoLabel->setAlignment(Qt::AlignCenter); + infoLabel->setStyleSheet("QLabel { color: #666; font-size: 12px; padding: 20px; }"); + + layout->addWidget(infoLabel); + setLayout(layout); +} + +QString DataPanel::GenerateTitle() +{ + QFileInfo fileInfo(filePath_); + QString typeDisplayName = GetTypeDisplayName(); + return QString("%1 Panel %2 - %3").arg(typeDisplayName).arg(index_).arg(fileInfo.baseName()); +} + +QString DataPanel::GetTypeDisplayName() const +{ + switch (fileType_) { + case FileEntryType::Curve: + return "Curve"; + case FileEntryType::Surface: + return "Surface"; + case FileEntryType::Table: + return "Table"; + case FileEntryType::Light: + return "Light"; + default: + return "Unknown"; + } +} \ No newline at end of file diff --git a/src/ui/Panel/DataPanel.h b/src/ui/Panel/DataPanel.h new file mode 100644 index 00000000..611a7879 --- /dev/null +++ b/src/ui/Panel/DataPanel.h @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include "workspace/FileEntry.h" + +class DockWidget; + +/** + * @file DataPanel.h + * @brief Data Panel Base Class + * Provides panel framework structure for different data types, specific functionality implemented by derived classes + */ + +/** + * @brief Data panel base class + * Provides panel framework structure for different data types, specific functionality implemented by derived classes + */ +class DataPanel : public QWidget +{ + Q_OBJECT + +public: + /** + * @brief Constructor + * @param index Panel index + * @param fileType File type + * @param filePath Associated file path + * @param parent Parent widget + */ + explicit DataPanel(int index, FileEntryType fileType, const QString& filePath, QWidget* parent = nullptr); + + /** + * @brief Destructor + */ + virtual ~DataPanel(); + + /** + * @brief Get panel index + * @return Panel index + */ + int GetIndex() const { return index_; } + + /** + * @brief Get file type (virtual function, implemented by derived classes) + * @return File type + */ + virtual FileEntryType GetFileType() const { return fileType_; } + + /** + * @brief Get file path + * @return File path + */ + QString GetFilePath() const { return filePath_; } + + /** + * @brief Get panel title + * @return Panel title + */ + QString GetTitle() const { return title_; } + + /** + * @brief Set dock widget reference + * @param dockWidget Dock widget pointer + */ + void SetDockWidget(DockWidget* dockWidget) { dockWidget_ = dockWidget; } + + /** + * @brief Get dock widget reference + * @return Dock widget pointer + */ + DockWidget* GetDockWidget() const { return dockWidget_; } + + /** + * @brief Refresh panel content (virtual function, implemented by derived classes) + */ + virtual void RefreshPanel() {} + +signals: + /** + * @brief Panel close signal + */ + void PanelClosed(); + +protected: + /** + * @brief Close event handler + * @param event Close event + */ + void closeEvent(QCloseEvent* event) override; + + /** + * @brief Initialize UI (virtual function, derived classes implement specific layout) + */ + virtual void InitUI(); + + /** + * @brief Generate panel title + * @return Generated title + */ + virtual QString GenerateTitle(); + + /** + * @brief Get type display name (virtual function, implemented by derived classes) + * @return Display name for the file type + */ + virtual QString GetTypeDisplayName() const; + +private: + int index_; // Panel index + FileEntryType fileType_; // File type + QString filePath_; // Associated file path + QString title_; // Panel title + DockWidget* dockWidget_; // Dock widget reference +}; \ No newline at end of file diff --git a/src/ui/Panel/DataPanelFactory.cpp b/src/ui/Panel/DataPanelFactory.cpp new file mode 100644 index 00000000..2e59f7a7 --- /dev/null +++ b/src/ui/Panel/DataPanelFactory.cpp @@ -0,0 +1,67 @@ +#include "DataPanelFactory.h" +#include "DataPanel.h" +#include "CurvePanel.h" +#include "common/SpdLogger.h" + +// Forward declarations for future panel types +// #include "SurfacePanel.h" +// #include "TablePanel.h" +// #include "LightPanel.h" + +DataPanel* DataPanelFactory::CreatePanel(int index, FileEntryType fileType, const QString& filePath, QWidget* parent) +{ + switch (fileType) { + case FileEntryType::Curve: + // For now, create CurvePanel which should inherit from DataPanel + // TODO: Update CurvePanel to inherit from DataPanel + return new CurvePanel(index, filePath, parent); + + case FileEntryType::Surface: + // TODO: Implement SurfacePanel + LOG_WARN("SurfacePanel not implemented yet, creating base DataPanel"); + return new DataPanel(index, fileType, filePath, parent); + + case FileEntryType::Table: + // TODO: Implement TablePanel + LOG_WARN("TablePanel not implemented yet, creating base DataPanel"); + return new DataPanel(index, fileType, filePath, parent); + + case FileEntryType::Light: + // TODO: Implement LightPanel + LOG_WARN("LightPanel not implemented yet, creating base DataPanel"); + return new DataPanel(index, fileType, filePath, parent); + + default: + LOG_ERROR("Unsupported file type: {}", static_cast(fileType)); + return nullptr; + } +} + +bool DataPanelFactory::IsTypeSupported(FileEntryType fileType) +{ + switch (fileType) { + case FileEntryType::Curve: + case FileEntryType::Surface: + case FileEntryType::Table: + case FileEntryType::Light: + return true; + default: + return false; + } +} + +QString DataPanelFactory::GetTypeDisplayName(FileEntryType fileType) +{ + switch (fileType) { + case FileEntryType::Curve: + return "Curve"; + case FileEntryType::Surface: + return "Surface"; + case FileEntryType::Table: + return "Table"; + case FileEntryType::Light: + return "Light"; + default: + return "Unknown"; + } +} \ No newline at end of file diff --git a/src/ui/Panel/DataPanelFactory.h b/src/ui/Panel/DataPanelFactory.h new file mode 100644 index 00000000..a72b631c --- /dev/null +++ b/src/ui/Panel/DataPanelFactory.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include "workspace/FileEntry.h" + +class DataPanel; +class QWidget; + +/** + * @file DataPanelFactory.h + * @brief Data Panel Factory + * Creates appropriate panel instances based on file type + */ + +/** + * @brief Data panel factory class + * Creates appropriate panel instances based on file type using factory pattern + */ +class DataPanelFactory +{ +public: + /** + * @brief Create panel based on file type + * @param index Panel index + * @param fileType File type + * @param filePath File path + * @param parent Parent widget + * @return Created panel pointer (caller takes ownership) + */ + static DataPanel* CreatePanel(int index, FileEntryType fileType, const QString& filePath, QWidget* parent = nullptr); + + /** + * @brief Check if file type is supported + * @param fileType File type to check + * @return True if supported, false otherwise + */ + static bool IsTypeSupported(FileEntryType fileType); + + /** + * @brief Get display name for file type + * @param fileType File type + * @return Display name + */ + static QString GetTypeDisplayName(FileEntryType fileType); + +private: + // Private constructor to prevent instantiation + DataPanelFactory() = default; +}; \ No newline at end of file diff --git a/src/ui/Panel/DataPanelManager.cpp b/src/ui/Panel/DataPanelManager.cpp new file mode 100644 index 00000000..8764e78b --- /dev/null +++ b/src/ui/Panel/DataPanelManager.cpp @@ -0,0 +1,296 @@ +#include "DataPanelManager.h" +#include "DataPanel.h" +#include "DataPanelFactory.h" +#include "ui/DockWidget.h" +#include "ui/DockTitleBar.h" +#include "ui/MainWindow.h" +#include "workspace/FileEntry.h" +#include "common/SpdLogger.h" + +#include +#include + +const QString DataPanelManager::PANEL_OBJECT_NAME_PREFIX = "DataPanel_"; + +DataPanelManager::DataPanelManager(MainWindow* mainWindow, QObject* parent) + : QObject(parent) + , mainWindow_(mainWindow) + , currentWorkspace_(nullptr) +{ + LOG_INFO("DataPanelManager initialized"); +} + +DataPanelManager::~DataPanelManager() +{ + ClearAllPanels(); + LOG_INFO("DataPanelManager destroyed"); +} + +void DataPanelManager::SetWorkspace(WorkSpace* workspace) +{ + if (currentWorkspace_ == workspace) { + return; + } + + // Disconnect old workspace signal connections + if (currentWorkspace_) { + disconnect(currentWorkspace_, nullptr, this, nullptr); + } + + currentWorkspace_ = workspace; + + // Connect new workspace signals + if (currentWorkspace_) { + connect(currentWorkspace_, &WorkSpace::FilesChanged, this, &DataPanelManager::OnFilesChanged); + } + + // Update all panel types + if (currentWorkspace_) { + UpdatePanelsForType(FileEntryType::Curve); + UpdatePanelsForType(FileEntryType::Surface); + UpdatePanelsForType(FileEntryType::Table); + UpdatePanelsForType(FileEntryType::Light); + } else { + ClearAllPanels(); + } +} + +int DataPanelManager::GetActivePanelCount(FileEntryType fileType) const +{ + if (static_cast(fileType) == -1) { + // Return total count + return dataPanels_.size(); + } + + // Count panels of specific type + int count = 0; + for (auto it = dataPanels_.begin(); it != dataPanels_.end(); ++it) { + if (it.value()->GetFileType() == fileType) { + count++; + } + } + return count; +} + +QList DataPanelManager::GetPanelsOfType(FileEntryType fileType) const +{ + QList panels; + for (auto it = dataPanels_.begin(); it != dataPanels_.end(); ++it) { + if (it.value()->GetFileType() == fileType) { + panels.append(it.value()); + } + } + return panels; +} + +void DataPanelManager::OnWorkspaceChanged(WorkSpace* workspace) +{ + SetWorkspace(workspace); +} + +void DataPanelManager::OnFilesChanged(FileEntryType type) +{ + // Only respond to supported file types + if (!DataPanelFactory::IsTypeSupported(type)) { + return; + } + + UpdatePanelsForType(type); +} + +void DataPanelManager::OnPanelClosed() +{ + DataPanel* panel = qobject_cast(sender()); + if (panel) { + RemovePanel(panel); + } +} + +void DataPanelManager::UpdatePanelsForType(FileEntryType fileType) +{ + if (!currentWorkspace_) { + ClearPanelsOfType(fileType); + return; + } + + // Get files of specified type from current workspace + std::vector files = currentWorkspace_->GetFileEntries(fileType); + + // Limit to maximum panels per type + const int maxPanels = qMin(static_cast(files.size()), GetMaxPanelCount()); + + // Find panels to remove (excess panels of this type) + QStringList keysToRemove; + for (auto it = dataPanels_.begin(); it != dataPanels_.end(); ++it) { + DataPanel* panel = it.value(); + if (panel->GetFileType() == fileType && panel->GetIndex() >= maxPanels) { + keysToRemove.append(it.key()); + } + } + + // Remove excess panels + for (const QString& key : keysToRemove) { + RemovePanel(dataPanels_[key]); + } + + // Create or update panels + for (int i = 0; i < maxPanels; ++i) { + const FileEntry& fileEntry = files[i]; + QString filePath = currentWorkspace_->GetFileEntryAbsPath(fileEntry.type, i); + QString panelKey = QString("%1_%2").arg(FileEntryTypeToString(fileType)).arg(i); + + if (dataPanels_.contains(panelKey)) { + // Check if file path has changed + DataPanel* existingPanel = dataPanels_[panelKey]; + if (existingPanel->GetFilePath() != filePath) { + // File path changed, recreate panel + RemovePanel(existingPanel); + CreateDataPanel(fileType, filePath); + } else { + // File path unchanged, refresh data + existingPanel->RefreshPanel(); + } + } else { + // Create new panel + CreateDataPanel(fileType, filePath); + } + } +} + +DataPanel* DataPanelManager::CreateDataPanel(FileEntryType fileType, const QString& filePath) +{ + if (GetActivePanelCount(fileType) >= GetMaxPanelCount()) { + LOG_WARN("Cannot create more {} panels, maximum count reached: {}", + FileEntryTypeToString(fileType), GetMaxPanelCount()); + return nullptr; + } + + // Find next available index for this file type + int index = FindNextAvailableIndex(fileType); + QString panelKey = QString("%1_%2").arg(FileEntryTypeToString(fileType)).arg(index); + + // Create dock widget + DockWidget* dockWidget = new DockWidget(mainWindow_); + dockWidget->setObjectName(GeneratePanelObjectName(fileType, index)); + + // Set title bar + DockTitleBar* titleBar = new DockTitleBar(dockWidget); + dockWidget->SetDockWidgetTitleBar(titleBar); + + // Connect signals + connect(dockWidget, &DockWidget::dockLocationChanged, this, [this](Qt::DockWidgetArea area) { + // Handle dock location changes if needed + }); + + // Add to main window + mainWindow_->addDockWidget(Qt::RightDockWidgetArea, dockWidget); + + // Create panel using factory + DataPanel* panel = DataPanelFactory::CreatePanel(index, fileType, filePath, dockWidget); + if (!panel) { + LOG_ERROR("Failed to create panel for type: {}", FileEntryTypeToString(fileType)); + dockWidget->deleteLater(); + return nullptr; + } + + dockWidget->setWidget(panel); + + // Set panel's dock widget reference + panel->SetDockWidget(dockWidget); + + // Connect panel signals + connect(panel, &DataPanel::PanelClosed, this, &DataPanelManager::OnPanelClosed); + + // Save references + dataPanels_[panelKey] = panel; + dockWidgets_[panelKey] = dockWidget; + + LOG_INFO("Created {} panel {} for file: {}", + FileEntryTypeToString(fileType), index, filePath.toStdString()); + return panel; +} + +void DataPanelManager::RemovePanel(DataPanel* panel) +{ + if (!panel) { + return; + } + + // Find panel key + QString panelKey; + for (auto it = dataPanels_.begin(); it != dataPanels_.end(); ++it) { + if (it.value() == panel) { + panelKey = it.key(); + break; + } + } + + if (panelKey.isEmpty()) { + return; + } + + // Remove dock widget + if (dockWidgets_.contains(panelKey)) { + DockWidget* dockWidget = dockWidgets_[panelKey]; + mainWindow_->removeDockWidget(dockWidget); + dockWidget->deleteLater(); + } + + // Remove from mappings + dataPanels_.remove(panelKey); + dockWidgets_.remove(panelKey); + + // Delete panel + panel->deleteLater(); + + LOG_INFO("Removed {} panel {}", + FileEntryTypeToString(panel->GetFileType()), panel->GetIndex()); +} + +void DataPanelManager::ClearAllPanels() +{ + // Get copy of all keys + QStringList keys = dataPanels_.keys(); + + // Remove panels one by one + for (const QString& key : keys) { + RemovePanel(dataPanels_[key]); + } + + LOG_INFO("Cleared all data panels"); +} + +void DataPanelManager::ClearPanelsOfType(FileEntryType fileType) +{ + QStringList keysToRemove; + for (auto it = dataPanels_.begin(); it != dataPanels_.end(); ++it) { + if (it.value()->GetFileType() == fileType) { + keysToRemove.append(it.key()); + } + } + + for (const QString& key : keysToRemove) { + RemovePanel(dataPanels_[key]); + } + + LOG_INFO("Cleared all {} panels", FileEntryTypeToString(fileType)); +} + +QString DataPanelManager::GeneratePanelObjectName(FileEntryType fileType, int index) const +{ + return QString("%1%2_%3").arg(PANEL_OBJECT_NAME_PREFIX) + .arg(FileEntryTypeToString(fileType)) + .arg(index); +} + +int DataPanelManager::FindNextAvailableIndex(FileEntryType fileType) const +{ + int index = 0; + QString baseKey = QString("%1_").arg(FileEntryTypeToString(fileType)); + + while (dataPanels_.contains(baseKey + QString::number(index))) { + index++; + } + + return index; +} \ No newline at end of file diff --git a/src/ui/Panel/DataPanelManager.h b/src/ui/Panel/DataPanelManager.h new file mode 100644 index 00000000..084f4a33 --- /dev/null +++ b/src/ui/Panel/DataPanelManager.h @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include +#include "workspace/WorkSpace.h" +#include "workspace/FileEntry.h" + +class DockWidget; +class DataPanel; +class MainWindow; + +/** + * @file DataPanelManager.h + * @brief Data Panel Manager + * Manages data panels created based on workspace files, supports multiple file types + */ + +class DataPanelManager : public QObject +{ + Q_OBJECT + +public: + explicit DataPanelManager(MainWindow* mainWindow, QObject* parent = nullptr); + ~DataPanelManager(); + + /** + * @brief Set current workspace + * @param workspace Workspace pointer + */ + void SetWorkspace(WorkSpace* workspace); + + /** + * @brief Get current active panel count for specific type + * @param fileType File type (if not specified, returns total count) + * @return Panel count + */ + int GetActivePanelCount(FileEntryType fileType = static_cast(-1)) const; + + /** + * @brief Get maximum panel count per type + * @return Maximum panel count + */ + int GetMaxPanelCount() const { return 9; } + + /** + * @brief Get panels of specific type + * @param fileType File type + * @return List of panels for the specified type + */ + QList GetPanelsOfType(FileEntryType fileType) const; + +public slots: + /** + * @brief Handle workspace changes + * @param workspace New workspace + */ + void OnWorkspaceChanged(WorkSpace* workspace); + + /** + * @brief Handle file changes + * @param type File type + */ + void OnFilesChanged(FileEntryType type); + + /** + * @brief Handle panel close event + */ + void OnPanelClosed(); + +private: + /** + * @brief Update panels for specific file type + * @param fileType File type to update + */ + void UpdatePanelsForType(FileEntryType fileType); + + /** + * @brief Create new data panel + * @param fileType File type + * @param filePath File path + * @return Created panel pointer + */ + DataPanel* CreateDataPanel(FileEntryType fileType, const QString& filePath); + + /** + * @brief Remove panel + * @param panel Panel pointer + */ + void RemovePanel(DataPanel* panel); + + /** + * @brief Clear all panels + */ + void ClearAllPanels(); + + /** + * @brief Clear panels of specific type + * @param fileType File type + */ + void ClearPanelsOfType(FileEntryType fileType); + + /** + * @brief Generate panel object name + * @param fileType File type + * @param index Panel index + * @return Object name + */ + QString GeneratePanelObjectName(FileEntryType fileType, int index) const; + + /** + * @brief Find next available index for file type + * @param fileType File type + * @return Next available index + */ + int FindNextAvailableIndex(FileEntryType fileType) const; + +private: + MainWindow* mainWindow_; // Main window pointer + WorkSpace* currentWorkspace_; // Current workspace + QMap dataPanels_; // Panel mapping (key -> panel) + QMap dockWidgets_; // Dock widget mapping (key -> dock widget) + + static const QString PANEL_OBJECT_NAME_PREFIX; // Panel object name prefix +}; \ No newline at end of file diff --git a/src/ui/PropertyBrowser/qtpropertymanager.cpp b/src/ui/PropertyBrowser/qtpropertymanager.cpp index 0de1686e..2b4d5162 100644 --- a/src/ui/PropertyBrowser/qtpropertymanager.cpp +++ b/src/ui/PropertyBrowser/qtpropertymanager.cpp @@ -7920,6 +7920,7 @@ public: QMap m_properyToWavePath; QMap m_properyToReportPath; QMap m_properyToRDPath; + QMap m_properyToCommondPath; QMap m_nameToPropery; QMap m_descriptionToPropery; @@ -7929,6 +7930,7 @@ public: QMap m_wavePathToPropery; QMap m_reportPathToPropery; QMap m_rdPathToPropery; + QMap m_commondPathToPropery; // Grouped file entries: Curve QMap m_properyToCurveGroup; @@ -8000,6 +8002,10 @@ void QtWorkspacePropertyManagerPrivate::slotStringChanged(QtProperty* property, QWorkspaceAttribute c = m_values[prop]; c.SetRDPath(value); q_ptr->setValue(prop, c); + } else if (QtProperty* prop = m_commondPathToPropery.value(property, 0)) { + QWorkspaceAttribute c = m_values[prop]; + c.SetCommondFilePath(value); + q_ptr->setValue(prop, c); } else if (QtProperty* prop = m_curvePathToPropery.value(property, 0)) { QWorkspaceAttribute c = m_values[prop]; int idx = m_curvePathIndex.value(property, 0); @@ -8213,6 +8219,7 @@ void QtWorkspacePropertyManager::setValue(QtProperty* property, const QWorkspace d_ptr->m_filesProperyManager->setValue(d_ptr->m_properyToWavePath[property], value.GetWavePath()); d_ptr->m_filesProperyManager->setValue(d_ptr->m_properyToReportPath[property], value.GetReportPath()); d_ptr->m_filesProperyManager->setValue(d_ptr->m_properyToRDPath[property], value.GetRDPath()); + d_ptr->m_filesProperyManager->setValue(d_ptr->m_properyToCommondPath[property], value.GetCommondFilePath()); auto syncGroup = [&](FileEntryType type, QMap& propToGroup, @@ -8334,6 +8341,14 @@ void QtWorkspacePropertyManager::initializeProperty(QtProperty* property) { d_ptr->m_rdPathToPropery[prop] = property; property->addSubProperty(prop); + // Command XML path + prop = d_ptr->m_filesProperyManager->addProperty(); + prop->setPropertyName(tr("CommondPath")); + d_ptr->m_filesProperyManager->setValueOnly(prop, val.GetCommondFilePath()); + d_ptr->m_properyToCommondPath[property] = prop; + d_ptr->m_commondPathToPropery[prop] = property; + property->addSubProperty(prop); + // Add grouped file sections auto addGroup = [&](FileEntryType type, const QString& groupName, QMap& propToGroup, @@ -8455,6 +8470,13 @@ void QtWorkspacePropertyManager::uninitializeProperty(QtProperty* property) { } d_ptr->m_properyToRDPath.remove(property); + prop = d_ptr->m_commondPathToPropery[property]; + if (prop) { + d_ptr->m_commondPathToPropery.remove(prop); + delete prop; + } + d_ptr->m_properyToCommondPath.remove(property); + // Cleanup grouped file properties auto cleanupGroup = [&](QMap& propToGroup, QMap& groupToProp, diff --git a/src/ui/PropertyBrowser/qtworkspaceattribute.cpp b/src/ui/PropertyBrowser/qtworkspaceattribute.cpp index d4b7f89f..e9c42cbb 100644 --- a/src/ui/PropertyBrowser/qtworkspaceattribute.cpp +++ b/src/ui/PropertyBrowser/qtworkspaceattribute.cpp @@ -174,6 +174,24 @@ const QString QWorkspaceAttribute::GetRDPath() const return workspace_->GetRDPath(); } +void QWorkspaceAttribute::SetCommondFilePath(const QString& path) +{ + if (nullptr == workspace_) { + return; + } + workspace_->SetCommondFilePath(path); +} + +const QString QWorkspaceAttribute::GetCommondFilePath() const +{ + if (nullptr == workspace_) { + return ""; + } + + // 只返回文件名,不包含完整路径 + return workspace_->GetCommondFilePath(); +} + std::vector QWorkspaceAttribute::GetFileEntries(FileEntryType type) const { if (nullptr == workspace_) { return {}; diff --git a/src/ui/PropertyBrowser/qtworkspaceattribute.h b/src/ui/PropertyBrowser/qtworkspaceattribute.h index 397a3ccc..f1add88a 100644 --- a/src/ui/PropertyBrowser/qtworkspaceattribute.h +++ b/src/ui/PropertyBrowser/qtworkspaceattribute.h @@ -80,6 +80,10 @@ public: void SetRDPath(const QString& path); const QString GetRDPath() const; + // Command XML path + void SetCommondFilePath(const QString& path); + const QString GetCommondFilePath() const; + // Grouped files API std::vector GetFileEntries(FileEntryType type) const; void SetFileEntryCount(FileEntryType type, int count); diff --git a/src/workspace/CommandManager.cpp b/src/workspace/CommandManager.cpp index f5313b9d..353161c2 100644 --- a/src/workspace/CommandManager.cpp +++ b/src/workspace/CommandManager.cpp @@ -21,7 +21,7 @@ void CommandManager::Reload(WorkSpace* ws) { onLoad_.clear(); if (!ws) return; - const QString cmdPath = ws->GetCommondFilePath(); + const QString cmdPath = QString("%1/%2").arg(ws->GetDir(), ws->GetCommondFilePath()); if (cmdPath.isEmpty()) { LOG_INFO("no command xml configured"); return; @@ -114,4 +114,32 @@ void CommandManager::Execute(WorkSpace* ws, WorkSpace::CommandWhen when) { for (auto& exec : list) { exec->Execute(ws, when); } +} + +std::vector CommandManager::ListCommands(WorkSpace* ws) { + std::vector items; + Reload(ws); + for (auto& exec : onCreate_) { + items.push_back(exec->Get()); + } + for (auto& exec : onLoad_) { + items.push_back(exec->Get()); + } + return items; +} + +bool CommandManager::ExecuteByName(WorkSpace* ws, const QString& name) { + Reload(ws); + auto matchAndRun = [&](std::vector>& list, WorkSpace::CommandWhen when) -> bool { + for (auto& exec : list) { + if (exec->Get().name == name) { + exec->Execute(ws, when); + return true; + } + } + return false; + }; + if (matchAndRun(onCreate_, WorkSpace::CommandWhen::OnCreate)) return true; + if (matchAndRun(onLoad_, WorkSpace::CommandWhen::OnLoad)) return true; + return false; } \ No newline at end of file diff --git a/src/workspace/CommandManager.h b/src/workspace/CommandManager.h index 178f2fa3..3dbf9170 100644 --- a/src/workspace/CommandManager.h +++ b/src/workspace/CommandManager.h @@ -11,6 +11,11 @@ public: void Reload(WorkSpace* ws); void Execute(WorkSpace* ws, WorkSpace::CommandWhen when); + // List parsed commands (no trigger distinction for UI consumption) + std::vector ListCommands(WorkSpace* ws); + // Execute a single command by its name (exact match) + bool ExecuteByName(WorkSpace* ws, const QString& name); + private: std::vector> onCreate_; std::vector> onLoad_; diff --git a/src/workspace/WorkSpace.cpp b/src/workspace/WorkSpace.cpp index b971b926..511520cd 100644 --- a/src/workspace/WorkSpace.cpp +++ b/src/workspace/WorkSpace.cpp @@ -54,11 +54,6 @@ void WorkSpace::SetCommondFilePath(const QString& path) { commondPath_ = fileInfo.fileName(); } -const QString WorkSpace::GetCommondFilePath() const { - QString path = QString("%1/%2").arg(GetDir(), commondPath_); - return path; -} - void WorkSpace::SetSimMatlab(const QString& path) { QFileInfo fileInfo(path); QString dirPath = QString("%1/%2").arg(GetDir(), fileInfo.fileName()); diff --git a/src/workspace/WorkSpace.h b/src/workspace/WorkSpace.h index e649b2ba..6205bfd1 100644 --- a/src/workspace/WorkSpace.h +++ b/src/workspace/WorkSpace.h @@ -52,7 +52,9 @@ public: } void SetCommondFilePath(const QString& path); - const QString GetCommondFilePath() const; + const QString GetCommondFilePath() const { + return commondPath_; + } // Execute command xml according to trigger enum class CommandWhen { OnCreate, OnLoad }; @@ -183,5 +185,6 @@ private: public: std::uint64_t GetFilesSeq() const { return filesSeq_; } friend class WorkSpaceXMLWrite; + friend class WorkSpaceXMLParse; }; diff --git a/src/workspace/WorkSpaceManager.cpp b/src/workspace/WorkSpaceManager.cpp index 582e244f..7b85fea4 100644 --- a/src/workspace/WorkSpaceManager.cpp +++ b/src/workspace/WorkSpaceManager.cpp @@ -190,7 +190,7 @@ void WorkSpaceManager::OnFrame() { QString WorkSpaceManager::GetDefaultWorkSpaceName() { #if _DEBUG - const QString iniFile = QString("%1workspace/config.ini").arg(QString(CONFIG_PATH)).arg(skin); + const QString iniFile = QString("%1workspace/config.ini").arg(QString(CONFIG_PATH)); #else const QString appDirPath = QApplication::applicationDirPath(); const QString iniFile = QString("%1/config/workspace/config.ini").arg(appDirPath); diff --git a/src/workspace/WorkSpaceXMLParse.cpp b/src/workspace/WorkSpaceXMLParse.cpp index f4089e24..0af5857d 100644 --- a/src/workspace/WorkSpaceXMLParse.cpp +++ b/src/workspace/WorkSpaceXMLParse.cpp @@ -80,10 +80,27 @@ bool WorkSpaceXMLParse::ParseLamp(const tinyxml2::XMLElement* element) { LOG_WARN("element not has path"); return false; } - + return workSpace_->SetLampPath(path); } +bool WorkSpaceXMLParse::ParseCommond(const tinyxml2::XMLElement* element) { + if (nullptr == element) { + LOG_WARN("commond element is nullptr"); + return false; + } + + const char* path = element->Attribute("path"); + if (nullptr == path) { + LOG_WARN("commond element not has path"); + return false; + } + + // Set the command file path using the filename stored in XML + workSpace_->commondPath_ = path; + return true; +} + bool WorkSpaceXMLParse::ParseFiles(const tinyxml2::XMLElement* element) { if (nullptr == element) { LOG_WARN("element is nullptr"); @@ -252,6 +269,8 @@ bool WorkSpaceXMLParse::Load(const QString& dyt) { ParseTimestep(xmlElement); } else if (0 == strcmp(name, "lamp")) { ParseLamp(xmlElement); + } else if (0 == strcmp(name, "commond")) { + ParseCommond(xmlElement); } else if (0 == strcmp(name, "charts")) { ParseChart(xmlElement); diff --git a/src/workspace/WorkSpaceXMLParse.h b/src/workspace/WorkSpaceXMLParse.h index 626af5b2..8a52bf91 100644 --- a/src/workspace/WorkSpaceXMLParse.h +++ b/src/workspace/WorkSpaceXMLParse.h @@ -31,6 +31,7 @@ private: bool ParseScene(const tinyxml2::XMLElement* element); bool ParseTimestep(const tinyxml2::XMLElement* element); bool ParseLamp(const tinyxml2::XMLElement* element); + bool ParseCommond(const tinyxml2::XMLElement* element); bool ParseEntities(const tinyxml2::XMLElement* element); bool ParseChart(const tinyxml2::XMLElement* element); bool ParseReport(const tinyxml2::XMLElement* element); diff --git a/src/workspace/WorkSpaceXMLWrite.cpp b/src/workspace/WorkSpaceXMLWrite.cpp index dc4e2625..4e4d00f6 100644 --- a/src/workspace/WorkSpaceXMLWrite.cpp +++ b/src/workspace/WorkSpaceXMLWrite.cpp @@ -12,6 +12,8 @@ #include "workspace/WorkSpaceManager.h" #include "workspace/FileEntry.h" +#include + WorkSpaceXMLWrite::WorkSpaceXMLWrite(WorkSpace* workspace, QObject* parent) noexcept : QObject(parent) , workSpace_(workspace) {} @@ -36,6 +38,7 @@ bool WorkSpaceXMLWrite::Save(const QString& path) { SaveChart(scene, &doc); SaveTimeStep(scene); SaveLamp(scene); + SaveCommond(scene); SaveFiles(scene, &doc); tinyxml2::XMLElement* entitiesXml = scene->InsertNewChildElement("entities"); @@ -58,6 +61,8 @@ bool WorkSpaceXMLWrite::SaveScene(tinyxml2::XMLElement* scene) { scene->SetAttribute("describe", workSpace_->GetDescribe().toStdString().c_str()); scene->SetAttribute("uuid", workSpace_->GetUUid().toStdString().c_str()); scene->SetAttribute("viewpoint", StringUtils::ViewpointToString(workSpace_->GetHomeViewpoint()).c_str()); + scene->SetAttribute("commondPath", ""); + return true; } @@ -72,12 +77,22 @@ bool WorkSpaceXMLWrite::SaveTimeStep(tinyxml2::XMLElement* scene) { } bool WorkSpaceXMLWrite::SaveLamp(tinyxml2::XMLElement* scene) { - LampStatus* lampStatus = workSpace_->GetLampStatus(); - if (nullptr == lampStatus) { - return false; + tinyxml2::XMLElement* lamp = scene->InsertNewChildElement("lamp"); + const QString lampPath = workSpace_->GetLampStatus()->GetPath(); + if (!lampPath.isEmpty()) { + lamp->SetAttribute("path", lampPath.toStdString().c_str()); + } + return true; +} + +bool WorkSpaceXMLWrite::SaveCommond(tinyxml2::XMLElement* scene) { + const QString commondPath = workSpace_->GetCommondFilePath(); + if (!commondPath.isEmpty()) { + tinyxml2::XMLElement* commond = scene->InsertNewChildElement("commond"); + // Extract just the filename from the full path for storage + QFileInfo fileInfo(commondPath); + commond->SetAttribute("path", fileInfo.fileName().toStdString().c_str()); } - tinyxml2::XMLElement* timestepXml = scene->InsertNewChildElement("lamp"); - timestepXml->SetAttribute("path", lampStatus->GetPath().toStdString().c_str()); return true; } diff --git a/src/workspace/WorkSpaceXMLWrite.h b/src/workspace/WorkSpaceXMLWrite.h index 2e3785e8..06299cf9 100644 --- a/src/workspace/WorkSpaceXMLWrite.h +++ b/src/workspace/WorkSpaceXMLWrite.h @@ -19,6 +19,7 @@ protected: bool SaveScene(tinyxml2::XMLElement* scene); bool SaveTimeStep(tinyxml2::XMLElement* scene); bool SaveLamp(tinyxml2::XMLElement* scene); + bool SaveCommond(tinyxml2::XMLElement* scene); bool SaveEntities(tinyxml2::XMLElement* scene, tinyxml2::XMLDocument* doc); bool SaveChart(tinyxml2::XMLElement* scene, tinyxml2::XMLDocument* doc); bool SaveFiles(tinyxml2::XMLElement* scene, tinyxml2::XMLDocument* doc); diff --git a/workspace/command.xml b/workspace/command.xml new file mode 100644 index 00000000..87a47783 --- /dev/null +++ b/workspace/command.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file