diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index 7a06a512eb..f3da6bc391 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -455,6 +455,7 @@ SET(Dialog_CPP_SRCS DownloadManager.cpp DocumentRecovery.cpp TaskElementColors.cpp + TaskMeasure.cpp DlgObjectSelection.cpp DlgAddProperty.cpp VectorListEditor.cpp @@ -494,6 +495,7 @@ SET(Dialog_HPP_SRCS DownloadManager.h DocumentRecovery.h TaskElementColors.h + TaskMeasure.h DlgObjectSelection.h DlgAddProperty.h VectorListEditor.h diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index bf39b60cef..f4cb50cd8c 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -76,6 +76,7 @@ #include "SelectionObject.h" #include "SoAxisCrossKit.h" #include "SoFCOffscreenRenderer.h" +#include "TaskMeasure.h" #include "TextureMapping.h" #include "Tools.h" #include "Tree.h" @@ -3189,6 +3190,38 @@ bool StdCmdMeasureDistance::isActive() return false; } +//=========================================================================== +// Std_Measure +// this is the Unified Measurement Facility Measure command +//=========================================================================== + + +DEF_STD_CMD_A(StdCmdMeasure) + +StdCmdMeasure::StdCmdMeasure() + :Command("Std_Measure") +{ + sGroup = "Measure"; + sMenuText = QT_TR_NOOP("&Measure"); + sToolTipText = QT_TR_NOOP("Measure a feature"); + sWhatsThis = "Std_Measure"; + sStatusTip = QT_TR_NOOP("Measure a feature"); + sPixmap = "umf-measurement"; +} + +void StdCmdMeasure::activated(int iMsg) +{ + Q_UNUSED(iMsg); + + TaskMeasure *task = new TaskMeasure(); + Gui::Control().showDialog(task); +} + + +bool StdCmdMeasure::isActive(){ + return true; +} + //=========================================================================== // Std_SceneInspector //=========================================================================== @@ -4117,6 +4150,7 @@ void CreateViewStdCommands() rcCmdMgr.addCommand(new StdCmdTreeCollapse()); rcCmdMgr.addCommand(new StdCmdTreeSelectAllInstances()); rcCmdMgr.addCommand(new StdCmdMeasureDistance()); + rcCmdMgr.addCommand(new StdCmdMeasure()); rcCmdMgr.addCommand(new StdCmdSceneInspector()); rcCmdMgr.addCommand(new StdCmdTextureMapping()); rcCmdMgr.addCommand(new StdCmdDemoMode()); diff --git a/src/Gui/Icons/resource.qrc b/src/Gui/Icons/resource.qrc index a5bf4c9b2c..614a660e4b 100644 --- a/src/Gui/Icons/resource.qrc +++ b/src/Gui/Icons/resource.qrc @@ -135,6 +135,7 @@ view-rotate-right.svg view-measurement.svg view-measurement-cross.svg + umf-measurement.svg Tree_Annotation.svg Tree_Dimension.svg Tree_Python.svg diff --git a/src/Gui/Icons/umf-measurement.svg b/src/Gui/Icons/umf-measurement.svg new file mode 100644 index 0000000000..e9971aed73 --- /dev/null +++ b/src/Gui/Icons/umf-measurement.svg @@ -0,0 +1,317 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Gui/TaskMeasure.cpp b/src/Gui/TaskMeasure.cpp new file mode 100644 index 0000000000..7d18b5593a --- /dev/null +++ b/src/Gui/TaskMeasure.cpp @@ -0,0 +1,388 @@ +/*************************************************************************** + * Copyright (c) 2023 David Friedli * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + **************************************************************************/ + + +#include "PreCompiled.h" + +#ifndef _PreComp_ +# include +# include +#endif + + +#include "TaskMeasure.h" + +#include "Control.h" +#include "MainWindow.h" +#include "Application.h" +#include "App/Document.h" +#include "App/DocumentObjectGroup.h" +#include + +#include +#include + +using namespace Gui; + + +TaskMeasure::TaskMeasure() +{ + qApp->installEventFilter(this); + + this->setButtonPosition(TaskMeasure::South); + auto taskbox = new Gui::TaskView::TaskBox(Gui::BitmapFactory().pixmap("umf-measurement"), tr("Measurement"), true, nullptr); + + // Create mode dropdown and add all registered measuretypes + modeSwitch = new QComboBox(); + modeSwitch->addItem(QString::fromLatin1("Auto")); + + for (App::MeasureType* mType : App::MeasureManager::getMeasureTypes()){ + modeSwitch->addItem(QString::fromLatin1(mType->label.c_str())); + } + + // Connect dropdown's change signal to our onModeChange slot + connect(modeSwitch, qOverload(&QComboBox::currentIndexChanged), this, &TaskMeasure::onModeChanged); + + // Result widget + valueResult = new QLineEdit(); + valueResult->setReadOnly(true); + + // Main layout + QBoxLayout *layout = taskbox->groupLayout(); + + QFormLayout* formLayout = new QFormLayout(); + formLayout->setHorizontalSpacing(10); + // Note: How can the split between columns be kept in the middle? + // formLayout->setFieldGrowthPolicy(QFormLayout::FieldGrowthPolicy::ExpandingFieldsGrow); + formLayout->setFormAlignment(Qt::AlignCenter); + + formLayout->addRow(QString::fromLatin1("Mode:"), modeSwitch); + formLayout->addRow(QString::fromLatin1("Result:"), valueResult); + layout->addLayout(formLayout); + + Content.emplace_back(taskbox); + + // engage the selectionObserver + attachSelection(); + + // Set selection style + Gui::Selection().setSelectionStyle(Gui::SelectionSingleton::SelectionStyle::GreedySelection); + + if(!App::GetApplication().getActiveTransaction()) + App::GetApplication().setActiveTransaction("Add Measurement"); + + + // Call invoke method delayed, otherwise the dialog might not be fully initialized + QTimer::singleShot(0, this, &TaskMeasure::invoke); +} + +TaskMeasure::~TaskMeasure(){ + Gui::Selection().setSelectionStyle(Gui::SelectionSingleton::SelectionStyle::NormalSelection); + detachSelection(); + qApp->removeEventFilter(this); +} + + +void TaskMeasure::modifyStandardButtons(QDialogButtonBox* box) { + + QPushButton* btn = box->button(QDialogButtonBox::Apply); + btn->setText(tr("Annotate")); + btn->setToolTip(tr("Press the Annotate button to add measurement to the document.")); + connect(btn, &QPushButton::released, this, &TaskMeasure::apply); + + // Disable button by default + btn->setEnabled(false); + btn = box->button(QDialogButtonBox::Abort); + btn->setText(QString::fromLatin1("Close")); + btn->setToolTip(tr("Press the Close button to exit.")); + + // Connect reset button + btn = box->button(QDialogButtonBox::Reset); + connect(btn, &QPushButton::released, this, &TaskMeasure::reset); +} + +bool canAnnotate(Measure::MeasureBase* obj) { + if (obj == nullptr) { + // null object, can't annotate this + return false; + } + + auto vpName = obj->getViewProviderName(); + // if there is not a vp, return false + if ((vpName == nullptr) || (vpName[0] == '\0')){ + return false; + } + + return true; +} + +void TaskMeasure::enableAnnotateButton(bool state) { + // if the task ui is not init yet we don't have a button box. + if (!this->buttonBox) { + return; + } + // Enable/Disable annotate button + auto btn = this->buttonBox->button(QDialogButtonBox::Apply); + btn->setEnabled(state); +} + +void TaskMeasure::setMeasureObject(Measure::MeasureBase* obj) { + _mMeasureObject = obj; +} + + +void TaskMeasure::update() { + + // Reset selection if the selected object is not valid + for(auto sel : Gui::Selection().getSelection()) { + App::DocumentObject* ob = sel.pObject; + App::DocumentObject* sub = ob->getSubObject(sel.SubName); + std::string mod = Base::Type::getModuleName(sub->getTypeId().getName()); + + if (!App::MeasureManager::hasMeasureHandler(mod.c_str())) { + Base::Console().Message("No measure handler available for geometry of module: %s\n", mod); + clearSelection(); + return; + } + } + + valueResult->setText(QString::asprintf("-")); + + // Get valid measure type + App::MeasureType *measureType(nullptr); + + + std::string mode = explicitMode ? modeSwitch->currentText().toStdString() : ""; + + App::MeasureSelection selection; + for (auto s : Gui::Selection().getSelection()) { + App::SubObjectT sub(s.pObject, s.SubName); + + App::MeasureSelectionItem item = { sub, Base::Vector3d(s.x, s.y, s.z) }; + selection.push_back(item); + } + + auto measureTypes = App::MeasureManager::getValidMeasureTypes(selection, mode); + if (measureTypes.size() > 0) { + measureType = measureTypes.front(); + } + + + if (!measureType) { + + // Note: If there's no valid measure type we might just restart the selection, + // however this requires enough coverage of measuretypes that we can access all of them + + // std::tuple sel = selection.back(); + // clearSelection(); + // addElement(measureModule.c_str(), get<0>(sel).c_str(), get<1>(sel).c_str()); + + // Reset measure object + if (!explicitMode) { + setModeSilent(nullptr); + } + removeObject(); + enableAnnotateButton(false); + return; + } + + // Update tool mode display + setModeSilent(measureType); + + if (!_mMeasureObject || measureType->measureObject != _mMeasureObject->getTypeId().getName()) { + // we don't already have a measureobject or it isn't the same type as the new one + removeObject(); + + App::Document *doc = App::GetApplication().getActiveDocument(); + if (measureType->isPython) { + Base::PyGILStateLocker lock; + auto pyMeasureClass = measureType->pythonClass; + + // Create a MeasurePython instance + auto featurePython = doc->addObject("Measure::MeasurePython", measureType->label.c_str()); + setMeasureObject((Measure::MeasureBase*)featurePython); + + // Create an instance of the pyMeasureClass, the classe's initializer sets the object as proxy + Py::Tuple args(1); + args.setItem(0, Py::asObject(featurePython->getPyObject())); + PyObject_CallObject(pyMeasureClass, args.ptr()); + } + else { + // Create measure object + setMeasureObject( + (Measure::MeasureBase*)doc->addObject(measureType->measureObject.c_str(), measureType->label.c_str()) + ); + } + } + + // we have a valid measure object so we can enable the annotate button + enableAnnotateButton(true); + + // Fill measure object's properties from selection + _mMeasureObject->parseSelection(selection); + + // Get result + valueResult->setText(_mMeasureObject->getResultString()); +} + +void TaskMeasure::close(){ + Control().closeDialog(); +} + + +void ensureGroup(Measure::MeasureBase* measurement) { + // Ensure measurement object is part of the measurements group + + const char* measurementGroupName = "Measurements"; + if (measurement == nullptr) { + return; + } + + App::Document* doc = App::GetApplication().getActiveDocument(); + App::DocumentObject* obj = doc->getObject(measurementGroupName); + if (!obj || !obj->isValid()) { + obj = doc->addObject("App::DocumentObjectGroup", measurementGroupName); + } + + auto group = static_cast(obj); + group->addObject(measurement); +} + + +// Runs after the dialog is created +void TaskMeasure::invoke() { + update(); +} + +bool TaskMeasure::apply(){ + ensureGroup(_mMeasureObject); + _mMeasureObject = nullptr; + reset(); + + // Commit transaction + App::GetApplication().closeActiveTransaction(); + App::GetApplication().setActiveTransaction("Add Measurement"); + return false; +} + +bool TaskMeasure::reject(){ + removeObject(); + close(); + + // Abort transaction + App::GetApplication().closeActiveTransaction(true); + return false; +} + +void TaskMeasure::reset() { + // Reset tool state + this->clearSelection(); + + // Should the explicit mode also be reset? + // setModeSilent(nullptr); + // explicitMode = false; + + this->update(); +} + + +void TaskMeasure::removeObject() { + if (_mMeasureObject == nullptr) { + return; + } + if (_mMeasureObject->isRemoving() ) { + return; + } + _mMeasureObject->getDocument()->removeObject (_mMeasureObject->getNameInDocument()); + setMeasureObject(nullptr); +} + +bool TaskMeasure::hasSelection(){ + return !Gui::Selection().getSelection().empty(); +} + +void TaskMeasure::clearSelection(){ + Gui::Selection().clearSelection(); +} + +void TaskMeasure::onSelectionChanged(const Gui::SelectionChanges& msg) +{ + // Skip non-relevant events + if (msg.Type != SelectionChanges::AddSelection && msg.Type != SelectionChanges::RmvSelection + && msg.Type != SelectionChanges::SetSelection && msg.Type != SelectionChanges::ClrSelection) { + + return; + } + + update(); +} + +bool TaskMeasure::eventFilter(QObject* obj, QEvent* event) { + + if (event->type() == QEvent::KeyPress) { + auto keyEvent = static_cast(event); + + if (keyEvent->key() == Qt::Key_Escape) { + + if (this->hasSelection()) { + this->reset(); + } else { + this->reject(); + } + + return true; + } + + if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) { + this->apply(); + return true; + } + } + + return TaskDialog::eventFilter(obj, event); +} + +void TaskMeasure::onModeChanged(int index) { + explicitMode = (index != 0); + this->update(); +} + +void TaskMeasure::setModeSilent(App::MeasureType* mode) { + modeSwitch->blockSignals(true); + + if (mode == nullptr) { + modeSwitch->setCurrentIndex(0); + } + else { + modeSwitch->setCurrentText(QString::fromLatin1(mode->label.c_str())); + } + modeSwitch->blockSignals(false); +} + +// Get explicitly set measure type from the mode switch +App::MeasureType* TaskMeasure::getMeasureType() { + for (App::MeasureType* mType : App::MeasureManager::getMeasureTypes()) { + if (mType->label.c_str() == modeSwitch->currentText().toLatin1()) { + return mType; + } + } + return nullptr; +} diff --git a/src/Gui/TaskMeasure.h b/src/Gui/TaskMeasure.h new file mode 100644 index 0000000000..ed92e6ab6e --- /dev/null +++ b/src/Gui/TaskMeasure.h @@ -0,0 +1,87 @@ +/*************************************************************************** + * Copyright (c) 2023 David Friedli * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + **************************************************************************/ + + +#include +#include +#include +#include + +#include +#include + +#include + +#include "TaskView/TaskDialog.h" +#include "TaskView/TaskView.h" +#include "Selection.h" + +namespace Gui { + +class TaskMeasure : public TaskView::TaskDialog, public Gui::SelectionObserver { + +public: + TaskMeasure(); + ~TaskMeasure() override; + + void modifyStandardButtons(QDialogButtonBox* box) override; + QDialogButtonBox::StandardButtons getStandardButtons() const override { + return QDialogButtonBox::Apply | QDialogButtonBox::Abort | QDialogButtonBox::Reset; + } + + void invoke(); + void update(); + void close(); + bool apply(); + bool reject() override; + void reset(); + + bool hasSelection(); + void clearSelection(); + bool eventFilter(QObject* obj, QEvent* event) override; + void setMeasureObject(Measure::MeasureBase* obj); + +private: + QColumnView* dialog{nullptr}; + + void onSelectionChanged(const Gui::SelectionChanges& msg) override; + + Measure::MeasureBase *_mMeasureObject = nullptr; + + QLineEdit* valueResult{nullptr}; + QLabel* labelResult{nullptr}; + QComboBox* modeSwitch{nullptr}; + + void removeObject(); + void onModeChanged(int index); + void setModeSilent(App::MeasureType* mode); + App::MeasureType* getMeasureType(); + void enableAnnotateButton(bool state); + + // List of measure types + std::vector measureObjects; + + // Stores if the mode is explicitly set by the user or implicitly through the selection + bool explicitMode = false; + +}; + +} // namespace Gui diff --git a/src/Gui/Workbench.cpp b/src/Gui/Workbench.cpp index 09e54d4e4b..e284b42bb7 100644 --- a/src/Gui/Workbench.cpp +++ b/src/Gui/Workbench.cpp @@ -814,7 +814,7 @@ ToolBarItem* StdWorkbench::setupToolBars() const view->setCommand("View"); *view << "Std_ViewFitAll" << "Std_ViewFitSelection" << "Std_ViewGroup" << "Separator" << "Std_DrawStyle" << "Std_TreeViewActions" - << "Separator" << "Std_MeasureDistance"; + << "Separator" << "Std_MeasureDistance" << "Std_Measure"; // Individual views auto individualViews = new ToolBarItem(root, ToolBarItem::DefaultVisibility::Hidden);