/*************************************************************************** * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Gui; namespace { constexpr auto taskMeasureSettingsGroup = "TaskMeasure"; constexpr auto taskMeasureShowDeltaSettingsName = "ShowDelta"; constexpr auto taskMeasureAutoSaveSettingsName = "AutoSave"; constexpr auto taskMeasureGreedySelection = "GreedySelection"; using SelectionStyle = Gui::SelectionSingleton::SelectionStyle; } // namespace TaskMeasure::TaskMeasure() { qApp->installEventFilter(this); this->setButtonPosition(TaskMeasure::South); auto taskbox = new Gui::TaskView::TaskBox(Gui::BitmapFactory().pixmap("umf-measurement"), tr("Measurement"), true, nullptr); QSettings settings; settings.beginGroup(QLatin1String(taskMeasureSettingsGroup)); delta = settings.value(QLatin1String(taskMeasureShowDeltaSettingsName), true).toBool(); mAutoSave = settings.value(QLatin1String(taskMeasureAutoSaveSettingsName), mAutoSave).toBool(); if (settings.value(QLatin1String(taskMeasureGreedySelection), false).toBool()) { Gui::Selection().setSelectionStyle(SelectionStyle::GreedySelection); } else { Gui::Selection().setSelectionStyle(SelectionStyle::NormalSelection); } settings.endGroup(); showDelta = new QCheckBox(); showDelta->setChecked(delta); showDeltaLabel = new QLabel(tr("Show Delta:")); #if QT_VERSION >= QT_VERSION_CHECK(6,7,0) connect(showDelta, &QCheckBox::checkStateChanged, this, &TaskMeasure::showDeltaChanged); #else connect(showDelta, &QCheckBox::stateChanged, this, &TaskMeasure::showDeltaChanged); #endif autoSaveAction = new QAction(tr("Auto Save")); autoSaveAction->setCheckable(true); autoSaveAction->setChecked(mAutoSave); autoSaveAction->setToolTip(tr("Auto saving of the last measurement when starting a new " "measurement. Use SHIFT to temporarily invert the behaviour.")); connect(autoSaveAction, &QAction::triggered, this, &TaskMeasure::autoSaveChanged); newMeasurementBehaviourAction = new QAction(tr("Additive Selection")); newMeasurementBehaviourAction->setCheckable(true); newMeasurementBehaviourAction->setChecked(Gui::Selection().getSelectionStyle() == SelectionStyle::GreedySelection); newMeasurementBehaviourAction->setToolTip( tr("If checked, new selection will be added to the measurement. If unchecked, CTRL must be " "pressed to add a " "selection to the current measurement otherwise a new measurement will be started")); connect(newMeasurementBehaviourAction, &QAction::triggered, this, &TaskMeasure::newMeasurementBehaviourChanged); mSettings = new QToolButton(); mSettings->setToolTip(tr("Settings")); mSettings->setIcon(QIcon(QStringLiteral(":/icons/dialogs/Sketcher_Settings.svg"))); auto* menu = new QMenu(mSettings); menu->setToolTipsVisible(true); mSettings->setMenu(menu); menu->addAction(autoSaveAction); menu->addAction(newMeasurementBehaviourAction); connect(mSettings, &QToolButton::clicked, mSettings, &QToolButton::showMenu); // Create mode dropdown and add all registered measuretypes modeSwitch = new QComboBox(); modeSwitch->addItem(QStringLiteral("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); auto* settingsLayout = new QHBoxLayout(); settingsLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding)); settingsLayout->addWidget(mSettings); formLayout->addRow(QStringLiteral(""), settingsLayout); formLayout->addRow(tr("Mode:"), modeSwitch); formLayout->addRow(showDeltaLabel, showDelta); formLayout->addRow(tr("Result:"), valueResult); layout->addLayout(formLayout); Content.emplace_back(taskbox); // engage the selectionObserver attachSelection(); if (!App::GetApplication().getActiveTransaction()) { App::GetApplication().setActiveTransaction("Add Measurement"); } setAutoCloseOnDeletedDocument(true); // Call invoke method delayed, otherwise the dialog might not be fully initialized QTimer::singleShot(0, this, &TaskMeasure::invoke); } TaskMeasure::~TaskMeasure() { Gui::Selection().setSelectionStyle(SelectionStyle::NormalSelection); detachSelection(); qApp->removeEventFilter(this); } void TaskMeasure::modifyStandardButtons(QDialogButtonBox* box) { QPushButton* btn = box->button(QDialogButtonBox::Apply); btn->setText(tr("Save")); btn->setToolTip(tr("Save the measurement in the active document.")); connect(btn, &QPushButton::released, this, qOverload<>(&TaskMeasure::apply)); // Disable button by default btn->setEnabled(false); btn = box->button(QDialogButtonBox::Abort); btn->setText(tr("Close")); btn->setToolTip(tr("Close the measurement task.")); // Connect reset button btn = box->button(QDialogButtonBox::Reset); connect(btn, &QPushButton::released, this, &TaskMeasure::reset); } 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); } Measure::MeasureBase* TaskMeasure::createObject(const App::MeasureType* measureType) { App::Document* doc = App::GetApplication().getActiveDocument(); if (!doc) { return nullptr; } if (measureType->isPython) { Base::PyGILStateLocker lock; auto pyMeasureClass = measureType->pythonClass; // Create a MeasurePython instance _mMeasureObject = doc->addObject(measureType->label.c_str()); // Create an instance of the pyMeasureClass, the classe's initializer sets the object as // proxy Py::Tuple args(1); args.setItem(0, Py::asObject(_mMeasureObject->getPyObject())); PyObject* result = PyObject_CallObject(pyMeasureClass, args.ptr()); Py_XDECREF(result); } else { // Create measure object _mMeasureObject = dynamic_cast( doc->addObject(measureType->measureObject.c_str(), measureType->label.c_str())); } return _mMeasureObject; } void TaskMeasure::update() { App::Document* doc = App::GetApplication().getActiveDocument(); // 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); // Resolve App::Link if (auto link = freecad_cast(sub)) { sub = link->getLinkedObject(true); } 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("-")); std::string mode = explicitMode ? modeSwitch->currentText().toStdString() : ""; App::MeasureSelection selection; for (auto s : Gui::Selection().getSelection(doc->getName(), ResolveMode::NoResolve)) { App::SubObjectT sub(s.pObject, s.SubName); App::MeasureSelectionItem item = {sub, Base::Vector3d(s.x, s.y, s.z)}; selection.push_back(item); } // Get valid measure type App::MeasureType* measureType = nullptr; 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() || _mMeasureObject->getDocument() != doc) { // we don't already have a measureobject or it isn't the same type as the new one removeObject(); createObject(measureType); } // 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()); // Initialite the measurement's viewprovider initViewObject(); } void TaskMeasure::initViewObject() { Gui::Document* guiDoc = Gui::Application::Instance->activeDocument(); if (!guiDoc) { return; } Gui::ViewProvider* viewObject = guiDoc->getViewProvider(_mMeasureObject); if (!viewObject) { return; } // Init the position of the annotation dynamic_cast(viewObject)->positionAnno(_mMeasureObject); // Set the ShowDelta Property if it exists on the measurements view object auto* prop = viewObject->getPropertyByName("ShowDelta"); setDeltaPossible(prop != nullptr); if (prop) { prop->setValue(showDelta->isChecked()); viewObject->update(prop); } } void TaskMeasure::close() { Control().closeDialog(); } void TaskMeasure::ensureGroup(Measure::MeasureBase* measurement) { // Ensure measurement object is part of the measurements group const char* measurementGroupName = "Measurements"; if (measurement == nullptr) { return; } App::Document* doc = measurement->getDocument(); App::DocumentObject* obj = doc->getObject(measurementGroupName); if (!obj || !obj->isValid() || !obj->isDerivedFrom()) { obj = doc->addObject(measurementGroupName, true, "MeasureGui::ViewProviderMeasureGroup"); } auto group = static_cast(obj); group->addObject(measurement); } // Runs after the dialog is created void TaskMeasure::invoke() { update(); } bool TaskMeasure::apply() { return apply(true); } bool TaskMeasure::apply(bool reset) { ensureGroup(_mMeasureObject); _mMeasureObject = nullptr; if (reset) { this->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()); _mMeasureObject = 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; } // If the control modifier is pressed, the object is just added to the current measurement // If the control modifier is not pressed, a new measurement will be started. If autosave is on, // the old measurement will be saved otherwise discharded. Shift inverts the autosave behaviour // temporarily const auto modifier = QGuiApplication::keyboardModifiers(); const bool ctrl = (modifier & Qt::ControlModifier) > 0; const bool shift = (modifier & Qt::ShiftModifier) > 0; // shift inverts the current state temporarily const auto autosave = (mAutoSave && !shift) || (!mAutoSave && shift); if ((!ctrl && Selection().getSelectionStyle() == SelectionStyle::NormalSelection) || (ctrl && Selection().getSelectionStyle() == SelectionStyle::GreedySelection)) { if (autosave && this->buttonBox->button(QDialogButtonBox::Apply)->isEnabled()) { apply(false); } } 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) { // Save object. Indirectly dependent on whether the apply button is enabled // enabled if valid measurement object. this->buttonBox->button(QDialogButtonBox::Apply)->click(); return true; } } return TaskDialog::eventFilter(obj, event); } void TaskMeasure::setDeltaPossible(bool possible) { showDelta->setVisible(possible); showDeltaLabel->setVisible(possible); } void TaskMeasure::onModeChanged(int index) { explicitMode = (index != 0); this->update(); } void TaskMeasure::showDeltaChanged(int checkState) { delta = checkState == Qt::CheckState::Checked; QSettings settings; settings.beginGroup(QLatin1String(taskMeasureSettingsGroup)); settings.setValue(QLatin1String(taskMeasureShowDeltaSettingsName), delta); settings.endGroup(); settings.sync(); // immediate write to the settings file this->update(); } void TaskMeasure::autoSaveChanged(bool checked) { mAutoSave = checked; QSettings settings; settings.beginGroup(QLatin1String(taskMeasureSettingsGroup)); settings.setValue(QLatin1String(taskMeasureAutoSaveSettingsName), mAutoSave); settings.endGroup(); } void TaskMeasure::newMeasurementBehaviourChanged(bool checked) { QSettings settings; settings.beginGroup(QLatin1String(taskMeasureSettingsGroup)); if (!checked) { Gui::Selection().setSelectionStyle(SelectionStyle::NormalSelection); settings.setValue(QLatin1String(taskMeasureGreedySelection), false); } else { Gui::Selection().setSelectionStyle(SelectionStyle::GreedySelection); settings.setValue(QLatin1String(taskMeasureGreedySelection), true); } settings.endGroup(); } 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; }