"Professional CMake" book suggest the following: "Targets should build successfully with or without compiler support for precompiled headers. It should be considered an optimization, not a requirement. In particular, do not explicitly include a precompile header (e.g. stdafx.h) in the source code, let CMake force-include an automatically generated precompile header on the compiler command line instead. This is more portable across the major compilers and is likely to be easier to maintain. It will also avoid warnings being generated from certain code checking tools like iwyu (include what you use)." Therefore, removed the "#include <PreCompiled.h>" from sources, also there is no need for the "#ifdef _PreComp_" anymore
5956 lines
209 KiB
C++
5956 lines
209 KiB
C++
/***************************************************************************
|
|
* Copyright (c) 2004 Jürgen Riegel <juergen.riegel@web.de> *
|
|
* *
|
|
* This file is part of the FreeCAD CAx development system. *
|
|
* *
|
|
* This library is free software; you can redistribute it and/or *
|
|
* modify it under the terms of the GNU Library General Public *
|
|
* License as published by the Free Software Foundation; either *
|
|
* version 2 of the License, or (at your option) any later version. *
|
|
* *
|
|
* This library 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 Library General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU Library General Public *
|
|
* License along with this library; see the file COPYING.LIB. If not, *
|
|
* write to the Free Software Foundation, Inc., 59 Temple Place, *
|
|
* Suite 330, Boston, MA 02111-1307, USA *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
|
|
|
|
|
|
# include <QAction>
|
|
# include <QActionGroup>
|
|
# include <QApplication>
|
|
# include <QContextMenuEvent>
|
|
# include <QCursor>
|
|
# include <QDir>
|
|
# include <QFileInfo>
|
|
# include <QHeaderView>
|
|
# include <QMenu>
|
|
# include <QMessageBox>
|
|
# include <QPainter>
|
|
# include <QPixmap>
|
|
# include <QProcess>
|
|
# include <QThread>
|
|
# include <QTimer>
|
|
# include <QToolTip>
|
|
# include <QVBoxLayout>
|
|
|
|
|
|
#include <Base/Console.h>
|
|
#include <Base/Reader.h>
|
|
#include <Base/Sequencer.h>
|
|
#include <Base/Tools.h>
|
|
#include <Base/Writer.h>
|
|
|
|
#include <Base/Color.h>
|
|
#include <App/Document.h>
|
|
#include <App/DocumentObjectGroup.h>
|
|
#include <App/AutoTransaction.h>
|
|
#include <App/GeoFeatureGroupExtension.h>
|
|
#include <App/Link.h>
|
|
|
|
#include "Tree.h"
|
|
#include "BitmapFactory.h"
|
|
#include "Command.h"
|
|
#include "Document.h"
|
|
#include "ExpressionCompleter.h"
|
|
#include "Macro.h"
|
|
#include "MainWindow.h"
|
|
#include "MenuManager.h"
|
|
#include "TreeParams.h"
|
|
#include "View3DInventor.h"
|
|
#include "ViewProviderDocumentObject.h"
|
|
#include "Widgets.h"
|
|
#include "Workbench.h"
|
|
|
|
|
|
FC_LOG_LEVEL_INIT("Tree", false, true, true)
|
|
|
|
#define _TREE_PRINT(_level,_func,_msg) \
|
|
_FC_PRINT(FC_LOG_INSTANCE,_level,_func, '['<<getTreeName()<<"] " << _msg)
|
|
#define TREE_MSG(_msg) _TREE_PRINT(FC_LOGLEVEL_MSG,notify<Base::LogStyle::Message>,_msg)
|
|
#define TREE_WARN(_msg) _TREE_PRINT(FC_LOGLEVEL_WARN,notify<Base::LogStyle::Warning>,_msg)
|
|
#define TREE_ERR(_msg) _TREE_PRINT(FC_LOGLEVEL_ERR,notify<Base::LogStyle::Error>,_msg)
|
|
#define TREE_LOG(_msg) _TREE_PRINT(FC_LOGLEVEL_LOG,notify<Base::LogStyle::Log>,_msg)
|
|
#define TREE_TRACE(_msg) _TREE_PRINT(FC_LOGLEVEL_TRACE,notify<Base::LogStyle::Log>,_msg)
|
|
|
|
using namespace Gui;
|
|
namespace sp = std::placeholders;
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
std::unique_ptr<QPixmap> TreeWidget::documentPixmap;
|
|
std::unique_ptr<QPixmap> TreeWidget::documentPartialPixmap;
|
|
static QBrush _TreeItemBackground;
|
|
std::set<TreeWidget*> TreeWidget::Instances;
|
|
static TreeWidget* _LastSelectedTreeWidget;
|
|
const int TreeWidget::DocumentType = 1000;
|
|
const int TreeWidget::ObjectType = 1001;
|
|
static bool _DraggingActive;
|
|
static bool _DragEventFilter;
|
|
|
|
static bool isVisibilityIconEnabled() {
|
|
return TreeParams::getVisibilityIcon();
|
|
}
|
|
|
|
static bool isOnlyNameColumnDisplayed() {
|
|
return TreeParams::getHideInternalNames()
|
|
&& TreeParams::getHideColumn();
|
|
}
|
|
|
|
static bool isSelectionCheckBoxesEnabled() {
|
|
return TreeParams::getCheckBoxesSelection();
|
|
}
|
|
|
|
void TreeParams::onItemBackgroundChanged()
|
|
{
|
|
if (getItemBackground()) {
|
|
Base::Color color;
|
|
color.setPackedValue(getItemBackground());
|
|
QColor col;
|
|
col.setRedF(color.r);
|
|
col.setGreenF(color.g);
|
|
col.setBlueF(color.b);
|
|
col.setAlphaF(color.a);
|
|
_TreeItemBackground = QBrush(col);
|
|
} else
|
|
_TreeItemBackground = QBrush();
|
|
refreshTreeViews();
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////
|
|
struct Stats {
|
|
#define DEFINE_STATS \
|
|
DEFINE_STAT(testStatus1) \
|
|
DEFINE_STAT(testStatus2) \
|
|
DEFINE_STAT(testStatus3) \
|
|
DEFINE_STAT(getIcon) \
|
|
DEFINE_STAT(setIcon) \
|
|
|
|
#define DEFINE_STAT(_name) \
|
|
FC_DURATION_DECLARE(_name);\
|
|
int _name##_count;
|
|
|
|
DEFINE_STATS
|
|
|
|
void init() {
|
|
#undef DEFINE_STAT
|
|
#define DEFINE_STAT(_name) \
|
|
FC_DURATION_INIT(_name);\
|
|
_name##_count = 0;
|
|
|
|
DEFINE_STATS
|
|
}
|
|
|
|
void print() {
|
|
#undef DEFINE_STAT
|
|
#define DEFINE_STAT(_name) FC_DURATION_MSG(_name, #_name " count: " << _name##_count);
|
|
DEFINE_STATS
|
|
}
|
|
|
|
#undef DEFINE_STAT
|
|
#define DEFINE_STAT(_name) \
|
|
void time_##_name(FC_TIME_POINT &t) {\
|
|
++_name##_count;\
|
|
FC_DURATION_PLUS(_name,t);\
|
|
}
|
|
|
|
DEFINE_STATS
|
|
};
|
|
|
|
//static Stats _Stats;
|
|
|
|
struct TimingInfo {
|
|
bool timed = false;
|
|
FC_TIME_POINT t;
|
|
FC_DURATION& d;
|
|
explicit TimingInfo(FC_DURATION& d)
|
|
:d(d)
|
|
{
|
|
_FC_TIME_INIT(t);
|
|
}
|
|
~TimingInfo() {
|
|
stop();
|
|
}
|
|
void stop() {
|
|
if (!timed) {
|
|
timed = true;
|
|
FC_DURATION_PLUS(d, t);
|
|
}
|
|
}
|
|
void reset() {
|
|
stop();
|
|
_FC_TIME_INIT(t);
|
|
}
|
|
};
|
|
|
|
// #define DO_TIMING
|
|
#ifdef DO_TIMING
|
|
#define _Timing(_idx,_name) ++_Stats._name##_count; TimingInfo _tt##_idx(_Stats._name)
|
|
#define Timing(_name) _Timing(0,_name)
|
|
#define _TimingStop(_idx,_name) _tt##_idx.stop();
|
|
#define TimingStop(_name) _TimingStop(0,_name);
|
|
#define TimingInit() _Stats.init();
|
|
#define TimingPrint() _Stats.print();
|
|
#else
|
|
#define _Timing(...) do{}while(0)
|
|
#define Timing(...) do{}while(0)
|
|
#define TimingInit() do{}while(0)
|
|
#define TimingPrint() do{}while(0)
|
|
#define _TimingStop(...) do{}while(0);
|
|
#define TimingStop(...) do{}while(0);
|
|
#endif
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
using DocumentObjectItems = std::set<DocumentObjectItem*>;
|
|
|
|
class Gui::DocumentObjectData {
|
|
public:
|
|
bool dirtyFlag {};
|
|
DocumentItem* docItem;
|
|
DocumentObjectItems items;
|
|
ViewProviderDocumentObject* viewObject;
|
|
DocumentObjectItem* rootItem{nullptr};
|
|
std::vector<App::DocumentObject*> children;
|
|
std::set<App::DocumentObject*> childSet;
|
|
bool removeChildrenFromRoot;
|
|
bool itemHidden;
|
|
std::string label;
|
|
std::string label2;
|
|
std::string internalName;
|
|
|
|
using Connection = boost::signals2::scoped_connection;
|
|
|
|
Connection connectIcon;
|
|
Connection connectTool;
|
|
Connection connectStat;
|
|
Connection connectHl;
|
|
|
|
DocumentObjectData(DocumentItem* docItem, ViewProviderDocumentObject* vpd)
|
|
: docItem(docItem)
|
|
, viewObject(vpd)
|
|
{
|
|
//NOLINTBEGIN
|
|
// Setup connections
|
|
connectIcon = viewObject->signalChangeIcon.connect(
|
|
std::bind(&DocumentObjectData::slotChangeIcon, this));
|
|
connectTool = viewObject->signalChangeToolTip.connect(
|
|
std::bind(&DocumentObjectData::slotChangeToolTip, this, sp::_1));
|
|
connectStat = viewObject->signalChangeStatusTip.connect(
|
|
std::bind(&DocumentObjectData::slotChangeStatusTip, this, sp::_1));
|
|
connectHl = viewObject->signalChangeHighlight.connect(
|
|
std::bind(&DocumentObjectData::slotChangeHighlight, this, sp::_1, sp::_2));
|
|
//NOLINTEND
|
|
|
|
removeChildrenFromRoot = viewObject->canRemoveChildrenFromRoot();
|
|
itemHidden = !viewObject->showInTree();
|
|
label = viewObject->getObject()->Label.getValue();
|
|
label2 = viewObject->getObject()->Label2.getValue();
|
|
internalName = viewObject->getObject()->getNameInDocument();
|
|
}
|
|
|
|
void insertItem(DocumentObjectItem* item)
|
|
{
|
|
items.insert(item);
|
|
dirtyFlag = true;
|
|
}
|
|
|
|
void removeItem(DocumentObjectItem* item)
|
|
{
|
|
auto it = items.find(item);
|
|
if (it == items.end()) {
|
|
assert(0);
|
|
}
|
|
else {
|
|
items.erase(it);
|
|
dirtyFlag = true;
|
|
}
|
|
}
|
|
|
|
const char* getTreeName() const {
|
|
return docItem->getTreeName();
|
|
}
|
|
|
|
void updateChildren(DocumentObjectDataPtr other) {
|
|
children = other->children;
|
|
childSet = other->childSet;
|
|
}
|
|
|
|
bool updateChildren(bool checkVisibility) {
|
|
auto newChildren = viewObject->claimChildren();
|
|
auto obj = viewObject->getObject();
|
|
std::set<App::DocumentObject*> newSet;
|
|
bool updated = false;
|
|
for (auto child : newChildren) {
|
|
auto childVp = docItem->getViewProvider(child);
|
|
if (!childVp)
|
|
continue;
|
|
if (child && child->isAttachedToDocument()) {
|
|
if (!newSet.insert(child).second) {
|
|
TREE_WARN("duplicate child item " << obj->getFullName()
|
|
<< '.' << child->getNameInDocument());
|
|
}
|
|
else if (!childSet.erase(child)) {
|
|
// this means new child detected
|
|
updated = true;
|
|
if (child->getDocument() == obj->getDocument() &&
|
|
child->getDocument() == docItem->document()->getDocument())
|
|
{
|
|
auto& parents = docItem->_ParentMap[child];
|
|
if (parents.insert(obj).second && child->Visibility.getValue()) {
|
|
bool showable = false;
|
|
for (auto parent : parents) {
|
|
if (!parent->hasChildElement()
|
|
&& parent->getLinkedObject(false) == parent)
|
|
{
|
|
showable = true;
|
|
break;
|
|
}
|
|
}
|
|
childVp->setShowable(showable);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (auto child : childSet) {
|
|
if (newSet.find(child) == newSet.end()) {
|
|
// this means old child removed
|
|
updated = true;
|
|
auto mapIt = docItem->_ParentMap.find(child);
|
|
|
|
// If 'child' is not part of the map then it has already been deleted
|
|
// in _slotDeleteObject.
|
|
if (mapIt != docItem->_ParentMap.end()) {
|
|
docItem->_ParentMap[child].erase(obj);
|
|
|
|
auto childVp = docItem->getViewProvider(child);
|
|
if (childVp && child->getDocument() == obj->getDocument())
|
|
childVp->setShowable(docItem->isObjectShowable(child));
|
|
}
|
|
}
|
|
}
|
|
// We still need to check the order of the children
|
|
updated = updated || children != newChildren;
|
|
children.swap(newChildren);
|
|
childSet.swap(newSet);
|
|
|
|
if (updated && checkVisibility) {
|
|
for (auto child : children) {
|
|
auto childVp = docItem->getViewProvider(child);
|
|
if (childVp && child->getDocument() == obj->getDocument())
|
|
childVp->setShowable(docItem->isObjectShowable(child));
|
|
}
|
|
}
|
|
return updated;
|
|
}
|
|
|
|
void testStatus(bool resetStatus = false) {
|
|
QIcon icon, icon2;
|
|
for (auto item : items)
|
|
item->testStatus(resetStatus, icon, icon2);
|
|
}
|
|
|
|
void slotChangeIcon() {
|
|
testStatus(true);
|
|
}
|
|
|
|
void slotChangeToolTip(const QString& tip) {
|
|
for (auto item : items)
|
|
item->setToolTip(0, tip);
|
|
}
|
|
|
|
void slotChangeStatusTip(const QString& tip) {
|
|
for (auto item : items)
|
|
item->setStatusTip(0, tip);
|
|
}
|
|
|
|
void slotChangeHighlight(bool set, Gui::HighlightMode mode) {
|
|
for (auto item : items)
|
|
item->setHighlight(set, mode);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
class DocumentItem::ExpandInfo :
|
|
public std::unordered_map<std::string, DocumentItem::ExpandInfoPtr>
|
|
{
|
|
public:
|
|
void restore(Base::XMLReader& reader) {
|
|
int level = reader.level();
|
|
int count = reader.getAttribute<long>("count");
|
|
for (int i = 0; i < count; ++i) {
|
|
reader.readElement("Expand");
|
|
auto& entry = (*this)[reader.getAttribute<const char*>("name")];
|
|
if (!reader.hasAttribute("count"))
|
|
continue;
|
|
entry.reset(new ExpandInfo);
|
|
entry->restore(reader);
|
|
}
|
|
reader.readEndElement("Expand", level - 1);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
namespace Gui {
|
|
/**
|
|
* TreeWidget item delegate for editing
|
|
*/
|
|
class TreeWidgetItemDelegate: public QStyledItemDelegate {
|
|
typedef QStyledItemDelegate inherited;
|
|
|
|
// Beware, big scary hack incoming!
|
|
//
|
|
// This is artificial QTreeWidget that is not rendered and its sole goal is to be the source
|
|
// of style information that can be manipulated using QSS. From Qt6.5 tree branches also
|
|
// have rendered background using ::item sub-control. Whole row also gets background from
|
|
// the same sub-control. Only way to prevent this is to disable background of ::item,
|
|
// this however limits our ability to style tree items. As solution we create this widget
|
|
// that will be for painter to read information and draw proper backgrounds only when asked.
|
|
//
|
|
// More information: https://github.com/FreeCAD/FreeCAD/pull/13807
|
|
QTreeView *artificial;
|
|
|
|
QRect calculateItemRect(const QStyleOptionViewItem &option) const;
|
|
|
|
public:
|
|
explicit TreeWidgetItemDelegate(QObject* parent=nullptr);
|
|
|
|
virtual QWidget* createEditor(QWidget *parent,
|
|
const QStyleOptionViewItem &, const QModelIndex &index) const;
|
|
|
|
virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
|
|
|
|
virtual void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const;
|
|
|
|
virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
|
|
};
|
|
|
|
} // namespace Gui
|
|
|
|
TreeWidgetItemDelegate::TreeWidgetItemDelegate(QObject* parent)
|
|
: QStyledItemDelegate(parent)
|
|
{
|
|
artificial = new QTreeView(qobject_cast<QWidget*>(parent));
|
|
artificial->setObjectName(QStringLiteral("DocumentTreeItems"));
|
|
artificial->setFixedSize(0, 0); // ensure that it does not render
|
|
}
|
|
|
|
|
|
QRect TreeWidgetItemDelegate::calculateItemRect(const QStyleOptionViewItem &option) const
|
|
{
|
|
auto tree = static_cast<TreeWidget*>(parent());
|
|
auto style = tree->style();
|
|
|
|
QRect rect = option.rect;
|
|
|
|
const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &option, artificial) + 1;
|
|
|
|
// 2 margin for text, 2 margin for decoration (icon) = 4 times margin
|
|
int width = 4 * margin
|
|
+ option.fontMetrics.boundingRect(option.text).width()
|
|
+ option.decorationSize.width()
|
|
+ TreeParams::getItemBackgroundPadding()
|
|
;
|
|
|
|
if (TreeParams::getCheckBoxesSelection()) {
|
|
// another 2 margin for checkbox
|
|
width += 2 * margin
|
|
+ style->pixelMetric(QStyle::PM_IndicatorWidth)
|
|
+ style->pixelMetric(QStyle::PM_LayoutHorizontalSpacing);
|
|
}
|
|
|
|
if (width < rect.width()) {
|
|
rect.setWidth(width);
|
|
}
|
|
|
|
return rect;
|
|
}
|
|
|
|
void TreeWidgetItemDelegate::paint(QPainter *painter,
|
|
const QStyleOptionViewItem &option, const QModelIndex &index) const
|
|
{
|
|
QStyleOptionViewItem opt = option;
|
|
initStyleOption(&opt, index);
|
|
|
|
auto tree = static_cast<TreeWidget*>(parent());
|
|
auto style = tree->style();
|
|
|
|
// If only the first column is shown, we'll trim the color background when
|
|
// rendering as transparent overlay.
|
|
bool trimColumnSize = isOnlyNameColumnDisplayed();
|
|
|
|
if (index.column() == 0) {
|
|
if (tree->testAttribute(Qt::WA_NoSystemBackground)
|
|
&& (trimColumnSize || (opt.backgroundBrush.style() == Qt::NoBrush
|
|
&& _TreeItemBackground.style() != Qt::NoBrush)))
|
|
{
|
|
QRect rect = calculateItemRect(option);
|
|
|
|
if (trimColumnSize && opt.backgroundBrush.style() == Qt::NoBrush) {
|
|
painter->fillRect(rect, _TreeItemBackground);
|
|
} else if (!opt.state.testFlag(QStyle::State_Selected)) {
|
|
painter->fillRect(rect, _TreeItemBackground);
|
|
}
|
|
}
|
|
}
|
|
style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, artificial);
|
|
}
|
|
|
|
void TreeWidgetItemDelegate::initStyleOption(QStyleOptionViewItem *option,
|
|
const QModelIndex &index) const
|
|
{
|
|
inherited::initStyleOption(option, index);
|
|
|
|
auto tree = static_cast<TreeWidget*>(parent());
|
|
auto item = tree->itemFromIndex(index);
|
|
|
|
if (!item) {
|
|
return;
|
|
}
|
|
|
|
option->textElideMode = Qt::ElideMiddle;
|
|
auto mousePos = option->widget->mapFromGlobal(QCursor::pos());
|
|
auto isHovered = option->rect.contains(mousePos);
|
|
if (!isHovered) {
|
|
option->state &= ~QStyle::State_MouseOver;
|
|
}
|
|
|
|
QSize size = option->icon.actualSize(QSize(0xffff, 0xffff));
|
|
|
|
if (size.height() > 0) {
|
|
option->decorationSize = QSize(
|
|
size.width() * TreeWidget::iconSize() / size.height(),
|
|
TreeWidget::iconSize()
|
|
);
|
|
}
|
|
|
|
if (isOnlyNameColumnDisplayed()) {
|
|
option->rect = calculateItemRect(*option);
|
|
|
|
// we need to extend this shape a bit, 3px on each side
|
|
// this value was obtained experimentally
|
|
option->rect.setWidth(option->rect.width() + 3 * 2);
|
|
}
|
|
}
|
|
|
|
class DynamicQLineEdit : public ExpLineEdit
|
|
{
|
|
public:
|
|
DynamicQLineEdit(QWidget *parent = nullptr) : ExpLineEdit(parent) {}
|
|
|
|
QSize sizeHint() const override
|
|
{
|
|
QSize size = QLineEdit::sizeHint();
|
|
QFontMetrics fm(font());
|
|
int availableWidth = parentWidget()->width() - geometry().x(); // Calculate available width
|
|
int margin = 2 * (style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1)
|
|
+ 2 * style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing)
|
|
+ TreeParams::getItemBackgroundPadding();
|
|
size.setWidth(std::min(fm.horizontalAdvance(text()) + margin , availableWidth));
|
|
return size;
|
|
}
|
|
|
|
// resize on key presses
|
|
void keyPressEvent(QKeyEvent *event) override
|
|
{
|
|
ExpLineEdit::keyPressEvent(event);
|
|
setMinimumWidth(sizeHint().width());
|
|
}
|
|
|
|
};
|
|
|
|
QWidget* TreeWidgetItemDelegate::createEditor(
|
|
QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const
|
|
{
|
|
auto ti = static_cast<QTreeWidgetItem*>(index.internalPointer());
|
|
if (ti->type() != TreeWidget::ObjectType || index.column() > 1)
|
|
return nullptr;
|
|
auto item = static_cast<DocumentObjectItem*>(ti);
|
|
App::DocumentObject* obj = item->object()->getObject();
|
|
auto& prop = index.column() ? obj->Label2 : obj->Label;
|
|
|
|
std::ostringstream str;
|
|
str << "Change " << obj->getNameInDocument() << '.' << prop.getName();
|
|
App::GetApplication().setActiveTransaction(str.str().c_str());
|
|
FC_LOG("create editor transaction " << App::GetApplication().getActiveTransaction());
|
|
|
|
DynamicQLineEdit *editor;
|
|
if(TreeParams::getLabelExpression()) {
|
|
DynamicQLineEdit *le = new DynamicQLineEdit(parent);
|
|
le->setAutoApply(true);
|
|
le->setFrame(false);
|
|
le->bind(App::ObjectIdentifier(prop));
|
|
editor = le;
|
|
} else {
|
|
editor = new DynamicQLineEdit(parent);
|
|
}
|
|
editor->setReadOnly(prop.isReadOnly());
|
|
return editor;
|
|
}
|
|
|
|
QSize TreeWidgetItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
|
|
{
|
|
QSize size = QStyledItemDelegate::sizeHint(option, index);
|
|
int spacing = std::max(0, static_cast<int>(TreeParams::getItemSpacing()));
|
|
size.setHeight(size.height() + spacing);
|
|
return size;
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TreeWidget::TreeWidget(const char* name, QWidget* parent)
|
|
: QTreeWidget(parent), SelectionObserver(true, ResolveMode::NoResolve)
|
|
, contextItem(nullptr)
|
|
, searchObject(nullptr)
|
|
, searchDoc(nullptr)
|
|
, searchContextDoc(nullptr)
|
|
, editingItem(nullptr)
|
|
, currentDocItem(nullptr)
|
|
, myName(name)
|
|
{
|
|
Instances.insert(this);
|
|
if (!_LastSelectedTreeWidget)
|
|
_LastSelectedTreeWidget = this;
|
|
|
|
this->setDragEnabled(true);
|
|
this->setAcceptDrops(true);
|
|
this->setColumnCount(3);
|
|
this->setItemDelegate(new TreeWidgetItemDelegate(this));
|
|
this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
|
|
this->showHiddenAction = new QAction(this);
|
|
this->showHiddenAction->setCheckable(true);
|
|
connect(this->showHiddenAction, &QAction::triggered,
|
|
this, &TreeWidget::onShowHidden);
|
|
|
|
this->toggleVisibilityInTreeAction = new QAction(this);
|
|
connect(this->toggleVisibilityInTreeAction, &QAction::triggered,
|
|
this, &TreeWidget::onToggleVisibilityInTree);
|
|
|
|
this->createGroupAction = new QAction(this);
|
|
connect(this->createGroupAction, &QAction::triggered,
|
|
this, &TreeWidget::onCreateGroup);
|
|
|
|
this->relabelObjectAction = new QAction(this);
|
|
#ifndef Q_OS_MAC
|
|
this->relabelObjectAction->setShortcut(Qt::Key_F2);
|
|
#endif
|
|
connect(this->relabelObjectAction, &QAction::triggered,
|
|
this, &TreeWidget::onRelabelObject);
|
|
|
|
this->finishEditingAction = new QAction(this);
|
|
connect(this->finishEditingAction, &QAction::triggered,
|
|
this, &TreeWidget::onFinishEditing);
|
|
|
|
this->selectDependentsAction = new QAction(this);
|
|
connect(this->selectDependentsAction, &QAction::triggered,
|
|
this, &TreeWidget::onSelectDependents);
|
|
|
|
this->closeDocAction = new QAction(this);
|
|
connect(this->closeDocAction, &QAction::triggered,
|
|
this, &TreeWidget::onCloseDoc);
|
|
|
|
this->reloadDocAction = new QAction(this);
|
|
connect(this->reloadDocAction, &QAction::triggered,
|
|
this, &TreeWidget::onReloadDoc);
|
|
|
|
this->skipRecomputeAction = new QAction(this);
|
|
this->skipRecomputeAction->setCheckable(true);
|
|
connect(this->skipRecomputeAction, &QAction::toggled,
|
|
this, &TreeWidget::onSkipRecompute);
|
|
this->skipRecomputeCommand = Gui::Application::Instance->commandManager().getCommandByName("Std_ToggleSkipRecompute");
|
|
|
|
this->allowPartialRecomputeAction = new QAction(this);
|
|
this->allowPartialRecomputeAction->setCheckable(true);
|
|
connect(this->allowPartialRecomputeAction, &QAction::toggled,
|
|
this, &TreeWidget::onAllowPartialRecompute);
|
|
|
|
this->markRecomputeAction = new QAction(this);
|
|
connect(this->markRecomputeAction, &QAction::triggered,
|
|
this, &TreeWidget::onMarkRecompute);
|
|
|
|
this->recomputeObjectAction = new QAction(this);
|
|
connect(this->recomputeObjectAction, &QAction::triggered,
|
|
this, &TreeWidget::onRecomputeObject);
|
|
this->searchObjectsAction = new QAction(this);
|
|
this->searchObjectsAction->setText(tr("Search Objects"));
|
|
this->searchObjectsAction->setStatusTip(tr("Searches for objects in the tree"));
|
|
connect(this->searchObjectsAction, &QAction::triggered,
|
|
this, &TreeWidget::onSearchObjects);
|
|
|
|
this->openFileLocationAction = new QAction(this);
|
|
connect(this->openFileLocationAction, &QAction::triggered,
|
|
this, &TreeWidget::onOpenFileLocation);
|
|
|
|
//NOLINTBEGIN
|
|
// Setup connections
|
|
connectNewDocument = Application::Instance->signalNewDocument.connect(std::bind(&TreeWidget::slotNewDocument, this, sp::_1, sp::_2));
|
|
connectDelDocument = Application::Instance->signalDeleteDocument.connect(std::bind(&TreeWidget::slotDeleteDocument, this, sp::_1));
|
|
connectRenDocument = Application::Instance->signalRenameDocument.connect(std::bind(&TreeWidget::slotRenameDocument, this, sp::_1));
|
|
connectActDocument = Application::Instance->signalActiveDocument.connect(std::bind(&TreeWidget::slotActiveDocument, this, sp::_1));
|
|
connectRelDocument = Application::Instance->signalRelabelDocument.connect(std::bind(&TreeWidget::slotRelabelDocument, this, sp::_1));
|
|
connectShowHidden = Application::Instance->signalShowHidden.connect(std::bind(&TreeWidget::slotShowHidden, this, sp::_1));
|
|
|
|
// Gui::Document::signalChangedObject informs the App::Document property
|
|
// change, not view provider's own property, which is what the signal below
|
|
// for
|
|
connectChangedViewObj = Application::Instance->signalChangedObject.connect(
|
|
std::bind(&TreeWidget::slotChangedViewObject, this, sp::_1, sp::_2));
|
|
//NOLINTEND
|
|
|
|
setupResizableColumn(this);
|
|
this->header()->setStretchLastSection(true);
|
|
QObject::connect(this->header(), &QHeaderView::sectionResized, [](int idx, int, int newSize) {
|
|
if (idx == 1)
|
|
TreeParams::setColumnSize2(newSize);
|
|
else if (idx == 2)
|
|
TreeParams::setColumnSize3(newSize);
|
|
else
|
|
TreeParams::setColumnSize1(newSize);
|
|
});
|
|
|
|
// Add the first main label
|
|
this->rootItem = invisibleRootItem();
|
|
this->expandItem(this->rootItem);
|
|
this->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
|
|
|
this->setMouseTracking(true); // needed for itemEntered() to work
|
|
|
|
|
|
this->preselectTimer = new QTimer(this);
|
|
this->preselectTimer->setSingleShot(true);
|
|
|
|
this->statusTimer = new QTimer(this);
|
|
this->statusTimer->setSingleShot(false);
|
|
|
|
this->selectTimer = new QTimer(this);
|
|
this->selectTimer->setSingleShot(true);
|
|
|
|
connect(this->statusTimer, &QTimer::timeout, this, &TreeWidget::onUpdateStatus);
|
|
connect(this, &QTreeWidget::itemEntered, this, &TreeWidget::onItemEntered);
|
|
connect(this, &QTreeWidget::itemCollapsed, this, &TreeWidget::onItemCollapsed);
|
|
connect(this, &QTreeWidget::itemExpanded, this, &TreeWidget::onItemExpanded);
|
|
connect(this, &QTreeWidget::itemSelectionChanged,
|
|
this, &TreeWidget::onItemSelectionChanged);
|
|
connect(this, &QTreeWidget::itemChanged, this, &TreeWidget::onItemChanged);
|
|
connect(this->preselectTimer, &QTimer::timeout, this, &TreeWidget::onPreSelectTimer);
|
|
connect(this->selectTimer, &QTimer::timeout, this, &TreeWidget::onSelectTimer);
|
|
preselectTime.start();
|
|
|
|
setupText();
|
|
if (!documentPixmap) {
|
|
documentPixmap = std::make_unique<QPixmap>(Gui::BitmapFactory().pixmap("Document"));
|
|
QIcon icon(*documentPixmap);
|
|
documentPartialPixmap = std::make_unique<QPixmap>(icon.pixmap(documentPixmap->size(), QIcon::Disabled));
|
|
}
|
|
setColumnHidden(1, TreeParams::getHideColumn());
|
|
setColumnHidden(2, TreeParams::getHideInternalNames());
|
|
header()->setVisible(!TreeParams::getHideColumn() || !TreeParams::getHideInternalNames());
|
|
TreeParams::onFontSizeChanged();
|
|
}
|
|
|
|
TreeWidget::~TreeWidget()
|
|
{
|
|
connectNewDocument.disconnect();
|
|
connectDelDocument.disconnect();
|
|
connectRenDocument.disconnect();
|
|
connectActDocument.disconnect();
|
|
connectRelDocument.disconnect();
|
|
connectShowHidden.disconnect();
|
|
connectChangedViewObj.disconnect();
|
|
Instances.erase(this);
|
|
if (_LastSelectedTreeWidget == this)
|
|
_LastSelectedTreeWidget = nullptr;
|
|
}
|
|
|
|
const char* TreeWidget::getTreeName() const {
|
|
return myName.c_str();
|
|
}
|
|
|
|
// reimpelement to select only objects in the active document
|
|
void TreeWidget::selectAll() {
|
|
auto gdoc = Application::Instance->getDocument(
|
|
App::GetApplication().getActiveDocument());
|
|
if (!gdoc)
|
|
return;
|
|
auto itDoc = DocumentMap.find(gdoc);
|
|
if (itDoc == DocumentMap.end())
|
|
return;
|
|
if (TreeParams::getRecordSelection())
|
|
Gui::Selection().selStackPush();
|
|
Gui::Selection().clearSelection();
|
|
Gui::Selection().setSelection(gdoc->getDocument()->getName(), gdoc->getDocument()->getObjects());
|
|
}
|
|
|
|
bool TreeWidget::isObjectShowable(App::DocumentObject* obj) {
|
|
if (!obj || !obj->isAttachedToDocument())
|
|
return true;
|
|
Gui::Document* doc = Application::Instance->getDocument(obj->getDocument());
|
|
if (!doc)
|
|
return true;
|
|
if (Instances.empty())
|
|
return true;
|
|
auto tree = *Instances.begin();
|
|
auto it = tree->DocumentMap.find(doc);
|
|
if (it != tree->DocumentMap.end())
|
|
return it->second->isObjectShowable(obj);
|
|
return true;
|
|
}
|
|
|
|
static bool _DisableCheckTopParent;
|
|
|
|
void TreeWidget::checkTopParent(App::DocumentObject*& obj, std::string& subname) {
|
|
if (_DisableCheckTopParent)
|
|
return;
|
|
if (!Instances.empty() && obj && obj->isAttachedToDocument()) {
|
|
auto tree = *Instances.begin();
|
|
auto it = tree->DocumentMap.find(Application::Instance->getDocument(obj->getDocument()));
|
|
if (it != tree->DocumentMap.end()) {
|
|
if (tree->statusTimer->isActive()) {
|
|
bool locked = tree->blockSelection(true);
|
|
tree->_updateStatus(false);
|
|
tree->blockSelection(locked);
|
|
}
|
|
auto parent = it->second->getTopParent(obj, subname);
|
|
if (parent)
|
|
obj = parent;
|
|
}
|
|
}
|
|
}
|
|
|
|
void TreeWidget::resetItemSearch() {
|
|
if (!searchObject)
|
|
return;
|
|
auto it = ObjectTable.find(searchObject);
|
|
if (it != ObjectTable.end()) {
|
|
for (auto& data : it->second) {
|
|
if (!data)
|
|
continue;
|
|
for (auto item : data->items)
|
|
static_cast<DocumentObjectItem*>(item)->restoreBackground();
|
|
}
|
|
}
|
|
searchObject = nullptr;
|
|
}
|
|
|
|
void TreeWidget::startItemSearch(QLineEdit* edit) {
|
|
resetItemSearch();
|
|
searchDoc = nullptr;
|
|
searchContextDoc = nullptr;
|
|
auto sels = selectedItems();
|
|
if (sels.size() == 1) {
|
|
if (sels.front()->type() == DocumentType) {
|
|
searchDoc = static_cast<DocumentItem*>(sels.front())->document();
|
|
}
|
|
else if (sels.front()->type() == ObjectType) {
|
|
auto item = static_cast<DocumentObjectItem*>(sels.front());
|
|
searchDoc = item->object()->getDocument();
|
|
searchContextDoc = item->getOwnerDocument()->document();
|
|
}
|
|
}
|
|
else
|
|
searchDoc = Application::Instance->activeDocument();
|
|
|
|
App::DocumentObject* obj = nullptr;
|
|
if (searchContextDoc && !searchContextDoc->getDocument()->getObjects().empty())
|
|
obj = searchContextDoc->getDocument()->getObjects().front();
|
|
else if (searchDoc && !searchDoc->getDocument()->getObjects().empty())
|
|
obj = searchDoc->getDocument()->getObjects().front();
|
|
|
|
if (obj)
|
|
static_cast<ExpressionLineEdit*>(edit)->setDocumentObject(obj);
|
|
}
|
|
|
|
void TreeWidget::itemSearch(const QString& text, bool select) {
|
|
resetItemSearch();
|
|
|
|
auto docItem = getDocumentItem(searchDoc);
|
|
if (!docItem) {
|
|
docItem = getDocumentItem(Application::Instance->activeDocument());
|
|
if (!docItem) {
|
|
FC_TRACE("item search no document");
|
|
resetItemSearch();
|
|
return;
|
|
}
|
|
}
|
|
|
|
auto doc = docItem->document()->getDocument();
|
|
const auto& objs = doc->getObjects();
|
|
if (objs.empty()) {
|
|
FC_TRACE("item search no objects");
|
|
return;
|
|
}
|
|
std::string txt(text.toUtf8().constData());
|
|
try {
|
|
if (txt.empty())
|
|
return;
|
|
if (txt.find("<<") == std::string::npos) {
|
|
auto pos = txt.find('.');
|
|
if (pos == std::string::npos)
|
|
txt += '.';
|
|
else if (pos != txt.size() - 1) {
|
|
txt.insert(pos + 1, "<<");
|
|
if (txt.back() != '.')
|
|
txt += '.';
|
|
txt += ">>.";
|
|
}
|
|
}
|
|
else if (txt.back() != '.')
|
|
txt += '.';
|
|
txt += "_self";
|
|
auto path = App::ObjectIdentifier::parse(objs.front(), txt);
|
|
if (path.getPropertyName() != "_self") {
|
|
FC_TRACE("Object " << txt << " not found in " << doc->getName());
|
|
return;
|
|
}
|
|
auto obj = path.getDocumentObject();
|
|
if (!obj) {
|
|
FC_TRACE("Object " << txt << " not found in " << doc->getName());
|
|
return;
|
|
}
|
|
std::string subname = path.getSubObjectName();
|
|
App::DocumentObject* parent = nullptr;
|
|
if (searchContextDoc) {
|
|
auto it = DocumentMap.find(searchContextDoc);
|
|
if (it != DocumentMap.end()) {
|
|
parent = it->second->getTopParent(obj, subname);
|
|
if (parent) {
|
|
obj = parent;
|
|
docItem = it->second;
|
|
doc = docItem->document()->getDocument();
|
|
}
|
|
}
|
|
}
|
|
if (!parent) {
|
|
parent = docItem->getTopParent(obj, subname);
|
|
while (!parent) {
|
|
if (docItem->document()->getDocument() == obj->getDocument()) {
|
|
// this shouldn't happen
|
|
FC_LOG("Object " << txt << " not found in " << doc->getName());
|
|
return;
|
|
}
|
|
auto it = DocumentMap.find(Application::Instance->getDocument(obj->getDocument()));
|
|
if (it == DocumentMap.end())
|
|
return;
|
|
docItem = it->second;
|
|
parent = docItem->getTopParent(obj, subname);
|
|
}
|
|
obj = parent;
|
|
}
|
|
auto item = docItem->findItemByObject(true, obj, subname.c_str());
|
|
if (!item) {
|
|
FC_TRACE("item " << txt << " not found in " << doc->getName());
|
|
return;
|
|
}
|
|
scrollToItem(item);
|
|
Selection().setPreselect(obj->getDocument()->getName(),
|
|
obj->getNameInDocument(), subname.c_str(), 0, 0, 0,
|
|
SelectionChanges::MsgSource::TreeView);
|
|
if (select) {
|
|
Gui::Selection().selStackPush();
|
|
Gui::Selection().clearSelection();
|
|
Gui::Selection().addSelection(obj->getDocument()->getName(),
|
|
obj->getNameInDocument(), subname.c_str());
|
|
Gui::Selection().selStackPush();
|
|
}
|
|
else {
|
|
searchObject = item->object()->getObject();
|
|
item->setBackground(0, QColor(255, 255, 0, 100));
|
|
}
|
|
FC_TRACE("found item " << txt);
|
|
}
|
|
catch (...)
|
|
{
|
|
FC_TRACE("item " << txt << " search exception in " << doc->getName());
|
|
}
|
|
}
|
|
|
|
Gui::Document* TreeWidget::selectedDocument() {
|
|
for (auto tree : Instances) {
|
|
if (!tree->isVisible())
|
|
continue;
|
|
auto sels = tree->selectedItems();
|
|
if (sels.size() == 1 && sels[0]->type() == DocumentType)
|
|
return static_cast<DocumentItem*>(sels[0])->document();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void TreeWidget::updateStatus(bool delay) {
|
|
for (auto tree : Instances)
|
|
tree->_updateStatus(delay);
|
|
}
|
|
|
|
void TreeWidget::_updateStatus(bool delay) {
|
|
// When running from a different thread Qt will raise a warning
|
|
// when trying to start the QTimer
|
|
if (Q_UNLIKELY(thread() != QThread::currentThread())) {
|
|
return;
|
|
}
|
|
|
|
if (!delay) {
|
|
if (!ChangedObjects.empty() || !NewObjects.empty())
|
|
onUpdateStatus();
|
|
return;
|
|
}
|
|
int timeout = TreeParams::getStatusTimeout();
|
|
if (timeout < 0)
|
|
timeout = 1;
|
|
statusTimer->start(timeout);
|
|
}
|
|
|
|
void TreeWidget::contextMenuEvent(QContextMenuEvent* e)
|
|
{
|
|
// ask workbenches and view provider, ...
|
|
MenuItem view;
|
|
Gui::Application::Instance->setupContextMenu("Tree", &view);
|
|
|
|
view << "Std_Properties" << "Separator" << "Std_Expressions";
|
|
Workbench::createLinkMenu(&view);
|
|
|
|
QMenu contextMenu;
|
|
|
|
QMenu subMenu;
|
|
QMenu editMenu;
|
|
QActionGroup subMenuGroup(&subMenu);
|
|
subMenuGroup.setExclusive(true);
|
|
connect(&subMenuGroup, &QActionGroup::triggered,
|
|
this, &TreeWidget::onActivateDocument);
|
|
MenuManager::getInstance()->setupContextMenu(&view, contextMenu);
|
|
|
|
// get the current item
|
|
this->contextItem = itemAt(e->pos());
|
|
|
|
if (this->contextItem && this->contextItem->type() == DocumentType) {
|
|
auto docitem = static_cast<DocumentItem*>(this->contextItem);
|
|
App::Document* doc = docitem->document()->getDocument();
|
|
|
|
// It's better to let user decide whether and how to activate
|
|
// the current document, such as by double-clicking.
|
|
// App::GetApplication().setActiveDocument(doc);
|
|
|
|
showHiddenAction->setChecked(docitem->showHidden());
|
|
contextMenu.addAction(this->showHiddenAction);
|
|
contextMenu.addAction(this->openFileLocationAction);
|
|
contextMenu.addAction(this->searchObjectsAction);
|
|
contextMenu.addAction(this->closeDocAction);
|
|
if (doc->testStatus(App::Document::PartialDoc))
|
|
contextMenu.addAction(this->reloadDocAction);
|
|
else {
|
|
for (auto d : doc->getDependentDocuments()) {
|
|
if (d->testStatus(App::Document::PartialDoc)) {
|
|
contextMenu.addAction(this->reloadDocAction);
|
|
break;
|
|
}
|
|
}
|
|
contextMenu.addAction(this->selectDependentsAction);
|
|
if (doc == App::GetApplication().getActiveDocument() && this->skipRecomputeCommand != nullptr) {
|
|
// if active document is selected, use Command
|
|
this->skipRecomputeCommand->addTo(&contextMenu);
|
|
} else {
|
|
// if other document is selected or Command load fails, edit selected Document directly
|
|
this->skipRecomputeAction->setChecked(doc->testStatus(App::Document::SkipRecompute));
|
|
contextMenu.addAction(this->skipRecomputeAction);
|
|
}
|
|
this->allowPartialRecomputeAction->setChecked(doc->testStatus(App::Document::AllowPartialRecompute));
|
|
if (doc->testStatus(App::Document::SkipRecompute))
|
|
contextMenu.addAction(this->allowPartialRecomputeAction);
|
|
contextMenu.addAction(this->markRecomputeAction);
|
|
contextMenu.addAction(this->createGroupAction);
|
|
}
|
|
contextMenu.addSeparator();
|
|
}
|
|
else if (this->contextItem && this->contextItem->type() == ObjectType) {
|
|
auto objitem = static_cast<DocumentObjectItem*>
|
|
(this->contextItem);
|
|
|
|
// check that the selection is not across several documents
|
|
bool acrossDocuments = false;
|
|
auto SelectedObjectsList = Selection().getCompleteSelection();
|
|
// get the object's document as reference
|
|
App::Document* doc = objitem->object()->getObject()->getDocument();
|
|
for (auto it = SelectedObjectsList.begin(); it != SelectedObjectsList.end(); ++it) {
|
|
if ((*it).pDoc != doc) {
|
|
acrossDocuments = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
showHiddenAction->setChecked(doc->ShowHidden.getValue());
|
|
contextMenu.addAction(this->showHiddenAction);
|
|
contextMenu.addAction(this->toggleVisibilityInTreeAction);
|
|
|
|
if (!acrossDocuments) { // is only sensible for selections within one document
|
|
if (objitem->object()->getObject()->isDerivedFrom<App::DocumentObjectGroup>())
|
|
contextMenu.addAction(this->createGroupAction);
|
|
// if there are dependent objects in the selection, add context menu to add them to selection
|
|
if (CheckForDependents())
|
|
contextMenu.addAction(this->selectDependentsAction);
|
|
}
|
|
|
|
contextMenu.addSeparator();
|
|
contextMenu.addAction(this->markRecomputeAction);
|
|
contextMenu.addAction(this->recomputeObjectAction);
|
|
contextMenu.addSeparator();
|
|
|
|
// relabeling is only possible for a single selected document
|
|
if (SelectedObjectsList.size() == 1)
|
|
contextMenu.addAction(this->relabelObjectAction);
|
|
|
|
auto selItems = this->selectedItems();
|
|
// if only one item is selected, setup the edit menu
|
|
if (selItems.size() == 1) {
|
|
objitem->object()->setupContextMenu(&editMenu, this, SLOT(onStartEditing()));
|
|
QList<QAction*> editAct = editMenu.actions();
|
|
if (!editAct.isEmpty()) {
|
|
QAction* topact = contextMenu.actions().constFirst();
|
|
for (QList<QAction*>::iterator it = editAct.begin(); it != editAct.end(); ++it)
|
|
contextMenu.insertAction(topact, *it);
|
|
QAction* first = editAct.front();
|
|
contextMenu.setDefaultAction(first);
|
|
if (objitem->object()->isEditing())
|
|
contextMenu.insertAction(topact, this->finishEditingAction);
|
|
contextMenu.insertSeparator(topact);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// add a submenu to active a document if two or more exist
|
|
std::vector<App::Document*> docs = App::GetApplication().getDocuments();
|
|
if (docs.size() >= 2) {
|
|
contextMenu.addSeparator();
|
|
App::Document* activeDoc = App::GetApplication().getActiveDocument();
|
|
subMenu.setTitle(tr("Activate Document"));
|
|
contextMenu.addMenu(&subMenu);
|
|
QAction* active = nullptr;
|
|
for (auto it = docs.begin(); it != docs.end(); ++it) {
|
|
QString label = QString::fromUtf8((*it)->Label.getValue());
|
|
QAction* action = subMenuGroup.addAction(label);
|
|
action->setCheckable(true);
|
|
action->setStatusTip(tr("Activates document %1").arg(label));
|
|
action->setData(QByteArray((*it)->getName()));
|
|
if (*it == activeDoc) active = action;
|
|
}
|
|
|
|
if (active)
|
|
active->setChecked(true);
|
|
subMenu.addActions(subMenuGroup.actions());
|
|
}
|
|
|
|
// add a submenu to present the settings of the tree.
|
|
QMenu settingsMenu;
|
|
settingsMenu.setTitle(tr("Tree Settings"));
|
|
contextMenu.addSeparator();
|
|
contextMenu.addMenu(&settingsMenu);
|
|
|
|
QAction* action = new QAction(tr("Show Description"), this);
|
|
QAction* internalNameAction = new QAction(tr("Show Internal Name"), this);
|
|
action->setStatusTip(tr("Shows a description column for items. An item's description can be set by by editing the 'label2' property."));
|
|
action->setCheckable(true);
|
|
|
|
ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/TreeView");
|
|
action->setChecked(!hGrp->GetBool("HideColumn", true));
|
|
|
|
settingsMenu.addAction(action);
|
|
QObject::connect(action, &QAction::triggered, this, [this, action, internalNameAction, hGrp]() {
|
|
bool show = action->isChecked();
|
|
hGrp->SetBool("HideColumn", !show);
|
|
setColumnHidden(1, !show);
|
|
header()->setVisible(action->isChecked()||internalNameAction->isChecked());
|
|
});
|
|
|
|
|
|
internalNameAction->setStatusTip(tr("Shows an internal name column for items."));
|
|
internalNameAction->setCheckable(true);
|
|
|
|
internalNameAction->setChecked(!hGrp->GetBool("HideInternalNames", true));
|
|
|
|
settingsMenu.addAction(internalNameAction);
|
|
|
|
QObject::connect(internalNameAction, &QAction::triggered, this, [this, action, internalNameAction, hGrp]() {
|
|
bool show = internalNameAction->isChecked();
|
|
hGrp->SetBool("HideInternalNames", !show);
|
|
setColumnHidden(2, !show);
|
|
header()->setVisible(action->isChecked()||internalNameAction->isChecked());
|
|
});
|
|
|
|
if (!contextMenu.actions().isEmpty()) {
|
|
try {
|
|
contextMenu.exec(QCursor::pos());
|
|
}
|
|
catch (Base::Exception& e) {
|
|
e.reportException();
|
|
}
|
|
catch (std::exception& e) {
|
|
FC_ERR("C++ exception: " << e.what());
|
|
}
|
|
catch (...) {
|
|
FC_ERR("Unknown exception");
|
|
}
|
|
contextItem = nullptr;
|
|
}
|
|
}
|
|
|
|
void TreeWidget::hideEvent(QHideEvent* ev) {
|
|
QTreeWidget::hideEvent(ev);
|
|
}
|
|
|
|
void TreeWidget::showEvent(QShowEvent* ev) {
|
|
QTreeWidget::showEvent(ev);
|
|
}
|
|
|
|
void TreeWidget::onCreateGroup()
|
|
{
|
|
QString name = tr("Group");
|
|
App::AutoTransaction trans("Create group");
|
|
if (this->contextItem->type() == DocumentType) {
|
|
auto docitem = static_cast<DocumentItem*>(this->contextItem);
|
|
App::Document* doc = docitem->document()->getDocument();
|
|
QString cmd = QStringLiteral("App.getDocument(\"%1\").addObject"
|
|
"(\"App::DocumentObjectGroup\",\"Group\").Label=\"%2\"")
|
|
.arg(QString::fromLatin1(doc->getName()), name);
|
|
Gui::Command::runCommand(Gui::Command::App, cmd.toUtf8());
|
|
}
|
|
else if (this->contextItem->type() == ObjectType) {
|
|
auto objitem = static_cast<DocumentObjectItem*>
|
|
(this->contextItem);
|
|
App::DocumentObject* obj = objitem->object()->getObject();
|
|
App::Document* doc = obj->getDocument();
|
|
QString cmd = QStringLiteral("App.getDocument(\"%1\").getObject(\"%2\")"
|
|
".newObject(\"App::DocumentObjectGroup\",\"Group\").Label=\"%3\"")
|
|
.arg(QString::fromLatin1(doc->getName()),
|
|
QString::fromLatin1(obj->getNameInDocument()),
|
|
name);
|
|
Gui::Command::runCommand(Gui::Command::App, cmd.toUtf8());
|
|
}
|
|
}
|
|
|
|
void TreeWidget::onRelabelObject()
|
|
{
|
|
QTreeWidgetItem* item = currentItem();
|
|
if (item)
|
|
editItem(item);
|
|
}
|
|
|
|
void TreeWidget::onStartEditing()
|
|
{
|
|
auto action = qobject_cast<QAction*>(sender());
|
|
if (action) {
|
|
if (this->contextItem && this->contextItem->type() == ObjectType) {
|
|
auto objitem = static_cast<DocumentObjectItem*>
|
|
(this->contextItem);
|
|
int edit = action->data().toInt();
|
|
|
|
App::DocumentObject* obj = objitem->object()->getObject();
|
|
if (!obj || !obj->isAttachedToDocument())
|
|
return;
|
|
auto doc = const_cast<Document*>(objitem->getOwnerDocument()->document());
|
|
MDIView* view = doc->getActiveView();
|
|
if (view) getMainWindow()->setActiveWindow(view);
|
|
|
|
editingItem = objitem;
|
|
if (!doc->setEdit(objitem->object(), edit))
|
|
editingItem = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
void TreeWidget::onFinishEditing()
|
|
{
|
|
if (this->contextItem && this->contextItem->type() == ObjectType) {
|
|
auto objitem = static_cast<DocumentObjectItem*>
|
|
(this->contextItem);
|
|
App::DocumentObject* obj = objitem->object()->getObject();
|
|
if (!obj)
|
|
return;
|
|
Gui::Document* doc = Gui::Application::Instance->getDocument(obj->getDocument());
|
|
doc->commitCommand();
|
|
doc->resetEdit();
|
|
doc->getDocument()->recompute();
|
|
}
|
|
}
|
|
|
|
// check if selection has dependent objects
|
|
bool TreeWidget::CheckForDependents()
|
|
{
|
|
// if the selected object is a document
|
|
if (this->contextItem && this->contextItem->type() == DocumentType) {
|
|
return true;
|
|
}
|
|
// it can be an object
|
|
else {
|
|
QList<QTreeWidgetItem*> items = this->selectedItems();
|
|
for (QList<QTreeWidgetItem*>::iterator it = items.begin(); it != items.end(); ++it) {
|
|
if ((*it)->type() == ObjectType) {
|
|
auto objitem = static_cast<DocumentObjectItem*>(*it);
|
|
App::DocumentObject* obj = objitem->object()->getObject();
|
|
// get dependents
|
|
auto subObjectList = obj->getOutList();
|
|
if (!subObjectList.empty())
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// adds an App::DocumentObject* and its dependent objects to the selection
|
|
void TreeWidget::addDependentToSelection(App::Document* doc, App::DocumentObject* docObject)
|
|
{
|
|
// add the docObject to the selection
|
|
Selection().addSelection(doc->getName(), docObject->getNameInDocument());
|
|
// get the dependent
|
|
auto subObjectList = docObject->getOutList();
|
|
for (auto itDepend : subObjectList) {
|
|
if (!Selection().isSelected(itDepend)) {
|
|
addDependentToSelection(doc, itDepend);
|
|
}
|
|
}
|
|
}
|
|
|
|
// add dependents of the selected tree object to selection
|
|
void TreeWidget::onSelectDependents()
|
|
{
|
|
// We only have this context menu entry if the selection is within one document but it
|
|
// might be not the active document. Therefore get the document not here but later by casting.
|
|
App::Document* doc;
|
|
|
|
// if the selected object is a document
|
|
if (this->contextItem && this->contextItem->type() == DocumentType) {
|
|
auto docitem = static_cast<DocumentItem*>(this->contextItem);
|
|
doc = docitem->document()->getDocument();
|
|
std::vector<App::DocumentObject*> obj = doc->getObjects();
|
|
for (auto it = obj.begin(); it != obj.end(); ++it)
|
|
Selection().addSelection(doc->getName(), (*it)->getNameInDocument());
|
|
}
|
|
// it can be an object
|
|
else {
|
|
QList<QTreeWidgetItem*> items = this->selectedItems();
|
|
for (QList<QTreeWidgetItem*>::iterator it = items.begin(); it != items.end(); ++it) {
|
|
if ((*it)->type() == ObjectType) {
|
|
auto objitem = static_cast<DocumentObjectItem*>(*it);
|
|
doc = objitem->object()->getObject()->getDocument();
|
|
App::DocumentObject* obj = objitem->object()->getObject();
|
|
// the dependents can also have dependents, thus add them recursively via a separate void
|
|
addDependentToSelection(doc, obj);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TreeWidget::onSkipRecompute(bool on)
|
|
{
|
|
// if a document item is selected then touch all objects
|
|
if (this->contextItem && this->contextItem->type() == DocumentType) {
|
|
auto docitem = static_cast<DocumentItem*>(this->contextItem);
|
|
App::Document* doc = docitem->document()->getDocument();
|
|
doc->setStatus(App::Document::SkipRecompute, on);
|
|
}
|
|
}
|
|
|
|
void TreeWidget::onAllowPartialRecompute(bool on)
|
|
{
|
|
// if a document item is selected then touch all objects
|
|
if (this->contextItem && this->contextItem->type() == DocumentType) {
|
|
auto docitem = static_cast<DocumentItem*>(this->contextItem);
|
|
App::Document* doc = docitem->document()->getDocument();
|
|
doc->setStatus(App::Document::AllowPartialRecompute, on);
|
|
}
|
|
}
|
|
|
|
void TreeWidget::onMarkRecompute()
|
|
{
|
|
// if a document item is selected then touch all objects
|
|
if (this->contextItem && this->contextItem->type() == DocumentType) {
|
|
auto docitem = static_cast<DocumentItem*>(this->contextItem);
|
|
App::Document* doc = docitem->document()->getDocument();
|
|
std::vector<App::DocumentObject*> obj = doc->getObjects();
|
|
for (auto it = obj.begin(); it != obj.end(); ++it)
|
|
(*it)->enforceRecompute();
|
|
}
|
|
// mark all selected objects
|
|
else {
|
|
QList<QTreeWidgetItem*> items = this->selectedItems();
|
|
for (QList<QTreeWidgetItem*>::iterator it = items.begin(); it != items.end(); ++it) {
|
|
if ((*it)->type() == ObjectType) {
|
|
auto objitem = static_cast<DocumentObjectItem*>(*it);
|
|
App::DocumentObject* obj = objitem->object()->getObject();
|
|
obj->enforceRecompute();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TreeWidget::onRecomputeObject() {
|
|
std::vector<App::DocumentObject*> objs;
|
|
const auto items = selectedItems();
|
|
for (auto ti : items) {
|
|
if (ti->type() == ObjectType) {
|
|
auto objitem = static_cast<DocumentObjectItem*>(ti);
|
|
objs.push_back(objitem->object()->getObject());
|
|
objs.back()->enforceRecompute();
|
|
}
|
|
}
|
|
if (objs.empty())
|
|
return;
|
|
App::AutoTransaction committer("Recompute object");
|
|
objs.front()->getDocument()->recompute(objs, true);
|
|
}
|
|
|
|
|
|
DocumentItem* TreeWidget::getDocumentItem(const Gui::Document* doc) const {
|
|
auto it = DocumentMap.find(doc);
|
|
if (it != DocumentMap.end())
|
|
return it->second;
|
|
return nullptr;
|
|
}
|
|
|
|
void TreeWidget::selectAllInstances(const ViewProviderDocumentObject& vpd) {
|
|
if (!isSelectionAttached())
|
|
return;
|
|
|
|
if (selectTimer->isActive())
|
|
onSelectTimer();
|
|
else
|
|
_updateStatus(false);
|
|
|
|
for (const auto& v : DocumentMap)
|
|
v.second->selectAllInstances(vpd);
|
|
}
|
|
|
|
static int &treeIconSize()
|
|
{
|
|
static int _treeIconSize = -1;
|
|
|
|
if (_treeIconSize < 0)
|
|
_treeIconSize = TreeParams::getIconSize();
|
|
return _treeIconSize;
|
|
}
|
|
|
|
int TreeWidget::iconHeight() const
|
|
{
|
|
return treeIconSize();
|
|
}
|
|
|
|
void TreeWidget::setIconHeight(int height)
|
|
{
|
|
if (treeIconSize() == height)
|
|
return;
|
|
|
|
treeIconSize() = height;
|
|
if (treeIconSize() <= 0)
|
|
treeIconSize() = std::max(10, iconSize());
|
|
|
|
for(auto tree : Instances)
|
|
tree->setIconSize(QSize(treeIconSize(), treeIconSize()));
|
|
}
|
|
|
|
int TreeWidget::iconSize() {
|
|
static int defaultSize;
|
|
if (defaultSize == 0) {
|
|
auto tree = instance();
|
|
if(tree) {
|
|
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
|
|
defaultSize = tree->viewOptions().decorationSize.width();
|
|
#else
|
|
QStyleOptionViewItem opt;
|
|
tree->initViewItemOption(&opt);
|
|
defaultSize = opt.decorationSize.width();
|
|
#endif
|
|
}
|
|
else {
|
|
defaultSize = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize);
|
|
}
|
|
}
|
|
if (treeIconSize() > 0)
|
|
return std::max(10, treeIconSize());
|
|
return defaultSize;
|
|
}
|
|
|
|
TreeWidget* TreeWidget::instance() {
|
|
auto res = _LastSelectedTreeWidget;
|
|
if (res && res->isVisible())
|
|
return res;
|
|
for (auto inst : Instances) {
|
|
if (!res) res = inst;
|
|
if (inst->isVisible())
|
|
return inst;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
void TreeWidget::setupResizableColumn(TreeWidget *tree) {
|
|
auto mode = TreeParams::getResizableColumn()?
|
|
QHeaderView::Interactive : QHeaderView::ResizeToContents;
|
|
for(auto inst : Instances) {
|
|
if(!tree || tree==inst) {
|
|
inst->header()->setSectionResizeMode(0, mode);
|
|
inst->header()->setSectionResizeMode(1, mode);
|
|
inst->header()->setSectionResizeMode(2, mode);
|
|
if (TreeParams::getResizableColumn()) {
|
|
QSignalBlocker blocker(inst);
|
|
if (TreeParams::getColumnSize1() > 0)
|
|
inst->header()->resizeSection(0, TreeParams::getColumnSize1());
|
|
if (TreeParams::getColumnSize2() > 0)
|
|
inst->header()->resizeSection(1, TreeParams::getColumnSize2());
|
|
if (TreeParams::getColumnSize3() > 0)
|
|
inst->header()->resizeSection(2, TreeParams::getColumnSize3());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TreeWidget* TreeWidget::getTreeForSelection()
|
|
{
|
|
TreeWidget* tree = instance();
|
|
if (!tree || !tree->isSelectionAttached()) {
|
|
for (auto pTree : Instances)
|
|
if (pTree->isSelectionAttached()) {
|
|
tree = pTree;
|
|
break;
|
|
}
|
|
}
|
|
if (!tree) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (tree->selectTimer->isActive()) {
|
|
tree->onSelectTimer();
|
|
}
|
|
else {
|
|
tree->_updateStatus(false);
|
|
}
|
|
|
|
return tree;
|
|
}
|
|
|
|
std::vector<Document*> TreeWidget::getSelectedDocuments()
|
|
{
|
|
std::vector<Document*> ret;
|
|
TreeWidget* tree = getTreeForSelection();
|
|
|
|
if (!tree) {
|
|
return ret;
|
|
}
|
|
|
|
const auto items = tree->selectedItems();
|
|
for (auto ti : items) {
|
|
if (ti->type() != DocumentType)
|
|
continue;
|
|
auto item = static_cast<DocumentItem*>(ti);
|
|
auto doc = item->document();
|
|
if (!doc || !doc->getDocument()) {
|
|
FC_WARN("skip invalid document");
|
|
continue;
|
|
}
|
|
ret.push_back(doc);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
std::vector<TreeWidget::SelInfo> TreeWidget::getSelection(App::Document* doc)
|
|
{
|
|
std::vector<SelInfo> ret;
|
|
TreeWidget* tree = getTreeForSelection();
|
|
|
|
if (!tree) {
|
|
return ret;
|
|
}
|
|
|
|
const auto items = tree->selectedItems();
|
|
for (auto ti : items) {
|
|
if (ti->type() != ObjectType)
|
|
continue;
|
|
auto item = static_cast<DocumentObjectItem*>(ti);
|
|
auto vp = item->object();
|
|
auto obj = vp->getObject();
|
|
if (!obj || !obj->isAttachedToDocument()) {
|
|
FC_WARN("skip invalid object");
|
|
continue;
|
|
}
|
|
if (doc && obj->getDocument() != doc) {
|
|
FC_LOG("skip objects not from current document");
|
|
continue;
|
|
}
|
|
ViewProviderDocumentObject* parentVp = nullptr;
|
|
auto parent = item->getParentItem();
|
|
if (parent) {
|
|
parentVp = parent->object();
|
|
if (!parentVp->getObject()->isAttachedToDocument()) {
|
|
FC_WARN("skip '" << obj->getFullName() << "' with invalid parent");
|
|
continue;
|
|
}
|
|
}
|
|
ret.emplace_back();
|
|
auto& sel = ret.back();
|
|
sel.topParent = nullptr;
|
|
std::ostringstream ss;
|
|
item->getSubName(ss, sel.topParent);
|
|
if (!sel.topParent)
|
|
sel.topParent = obj;
|
|
else
|
|
ss << obj->getNameInDocument() << '.';
|
|
sel.subname = ss.str();
|
|
sel.parentVp = parentVp;
|
|
sel.vp = vp;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void TreeWidget::selectAllLinks(App::DocumentObject* obj) {
|
|
if (!isSelectionAttached())
|
|
return;
|
|
|
|
if (!obj || !obj->isAttachedToDocument()) {
|
|
TREE_ERR("invalid object");
|
|
return;
|
|
}
|
|
|
|
if (selectTimer->isActive())
|
|
onSelectTimer();
|
|
else
|
|
_updateStatus(false);
|
|
|
|
for (auto link : App::GetApplication().getLinksTo(obj, App::GetLinkRecursive))
|
|
{
|
|
if (!link || !link->isAttachedToDocument()) {
|
|
TREE_ERR("invalid linked object");
|
|
continue;
|
|
}
|
|
auto vp = freecad_cast<ViewProviderDocumentObject*>(
|
|
Application::Instance->getViewProvider(link));
|
|
if (!vp) {
|
|
TREE_ERR("invalid view provider of the linked object");
|
|
continue;
|
|
}
|
|
for (auto& v : DocumentMap)
|
|
v.second->selectAllInstances(*vp);
|
|
}
|
|
}
|
|
|
|
void TreeWidget::onSearchObjects()
|
|
{
|
|
Q_EMIT emitSearchObjects();
|
|
}
|
|
|
|
void TreeWidget::onActivateDocument(QAction* active)
|
|
{
|
|
// activate the specified document
|
|
QByteArray docname = active->data().toByteArray();
|
|
Gui::Document* doc = Application::Instance->getDocument((const char*)docname);
|
|
if (doc && !doc->setActiveView())
|
|
doc->setActiveView(nullptr, View3DInventor::getClassTypeId());
|
|
}
|
|
|
|
Qt::DropActions TreeWidget::supportedDropActions() const
|
|
{
|
|
return Qt::LinkAction | Qt::CopyAction | Qt::MoveAction;
|
|
}
|
|
|
|
bool TreeWidget::event(QEvent* e)
|
|
{
|
|
return QTreeWidget::event(e);
|
|
}
|
|
|
|
bool TreeWidget::eventFilter(QObject*, QEvent* ev) {
|
|
switch (ev->type()) {
|
|
case QEvent::KeyPress:
|
|
case QEvent::KeyRelease: {
|
|
auto ke = static_cast<QKeyEvent*>(ev);
|
|
if (ke->key() != Qt::Key_Escape) {
|
|
// Qt 5 only recheck key modifier on mouse move, so generate a fake
|
|
// event to trigger drag cursor change
|
|
auto mouseEvent = new QMouseEvent(QEvent::MouseMove,
|
|
mapFromGlobal(QCursor::pos()), QCursor::pos(), Qt::NoButton,
|
|
QApplication::mouseButtons(), QApplication::queryKeyboardModifiers());
|
|
QApplication::postEvent(this, mouseEvent);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
namespace Gui {
|
|
|
|
bool isTreeViewDragging()
|
|
{
|
|
return _DraggingActive;
|
|
}
|
|
|
|
} // namespace Gui
|
|
|
|
void TreeWidget::keyPressEvent(QKeyEvent* event)
|
|
{
|
|
if (event->matches(QKeySequence::Find)) {
|
|
event->accept();
|
|
onSearchObjects();
|
|
return;
|
|
}
|
|
else if (event->modifiers() == Qt::AltModifier) {
|
|
if (event->key() == Qt::Key_Left) {
|
|
for (auto& item : selectedItems()) {
|
|
item->setExpanded(false);
|
|
}
|
|
event->accept();
|
|
return;
|
|
}
|
|
else if (event->key() == Qt::Key_Right) {
|
|
for (auto& item : selectedItems()) {
|
|
item->setExpanded(true);
|
|
}
|
|
event->accept();
|
|
return;
|
|
}
|
|
else if (event->key() == Qt::Key_Up) {
|
|
for (auto& item : selectedItems()) {
|
|
item->setExpanded(true);
|
|
for (auto& child : childrenOfItem(*item)) {
|
|
child->setExpanded(false);
|
|
}
|
|
}
|
|
event->accept();
|
|
return;
|
|
}
|
|
else if (event->key() == Qt::Key_Down) {
|
|
for (auto& item : selectedItems()) {
|
|
item->setExpanded(true);
|
|
for (auto& child : childrenOfItem(*item)) {
|
|
child->setExpanded(true);
|
|
}
|
|
}
|
|
event->accept();
|
|
return;
|
|
}
|
|
}
|
|
|
|
QTreeWidget::keyPressEvent(event);
|
|
}
|
|
|
|
void TreeWidget::mousePressEvent(QMouseEvent* event)
|
|
{
|
|
if (isVisibilityIconEnabled()) {
|
|
QTreeWidgetItem* item = itemAt(event->pos());
|
|
if (item && item->type() == TreeWidget::ObjectType && event->button() == Qt::LeftButton) {
|
|
auto objitem = static_cast<DocumentObjectItem*>(item);
|
|
|
|
// Mouse position relative to viewport
|
|
auto mousePos = event->pos();
|
|
|
|
// Rect occupied by the item relative to viewport
|
|
auto iconRect = visualItemRect(objitem);
|
|
|
|
auto style = this->style();
|
|
|
|
// If the checkboxes are visible, these are displayed before the icon
|
|
// and we have to compensate for its width.
|
|
if (isSelectionCheckBoxesEnabled()) {
|
|
int checkboxWidth = style->pixelMetric(QStyle::PM_IndicatorWidth)
|
|
+ style->pixelMetric(QStyle::PM_LayoutHorizontalSpacing);
|
|
iconRect.adjust(checkboxWidth, 0, 0, 0);
|
|
}
|
|
|
|
int const margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1;
|
|
iconRect.adjust(margin, 0, 0, 0);
|
|
|
|
// We are interested in the first icon (visibility icon)
|
|
iconRect.setWidth(iconSize());
|
|
|
|
// If the visibility icon was clicked, toggle the DocumentObject visibility
|
|
if (iconRect.contains(mousePos)) {
|
|
auto obj = objitem->object()->getObject();
|
|
char const* objname = obj->getNameInDocument();
|
|
|
|
App::DocumentObject* parent = nullptr;
|
|
std::ostringstream subName;
|
|
objitem->getSubName(subName, parent);
|
|
|
|
// Try the ElementVisible API, if that is not supported toggle the Visibility property
|
|
int visible = -1;
|
|
if (parent) {
|
|
visible = parent->isElementVisible(objname);
|
|
}
|
|
if (parent && visible >= 0) {
|
|
parent->setElementVisible(objname, !visible);
|
|
} else {
|
|
visible = obj->Visibility.getValue();
|
|
obj->Visibility.setValue(!visible);
|
|
}
|
|
|
|
// to prevent selection of the item via QTreeWidget::mousePressEvent
|
|
event->accept();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
QTreeWidget::mousePressEvent(event);
|
|
}
|
|
|
|
void TreeWidget::mouseDoubleClickEvent(QMouseEvent* event)
|
|
{
|
|
QTreeWidgetItem* item = itemAt(event->pos());
|
|
if (!item)
|
|
return;
|
|
|
|
try {
|
|
if (item->type() == TreeWidget::DocumentType) {
|
|
Gui::Document* doc = static_cast<DocumentItem*>(item)->document();
|
|
if (!doc)
|
|
return;
|
|
if (doc->getDocument()->testStatus(App::Document::PartialDoc)) {
|
|
contextItem = item;
|
|
onReloadDoc();
|
|
return;
|
|
}
|
|
if (!doc->setActiveView())
|
|
doc->setActiveView(nullptr, View3DInventor::getClassTypeId());
|
|
}
|
|
else if (item->type() == TreeWidget::ObjectType) {
|
|
auto objitem = static_cast<DocumentObjectItem*>(item);
|
|
ViewProviderDocumentObject* vp = objitem->object();
|
|
|
|
objitem->getOwnerDocument()->document()->setActiveView(vp);
|
|
auto manager = Application::Instance->macroManager();
|
|
auto lines = manager->getLines();
|
|
|
|
std::ostringstream ss;
|
|
ss << Command::getObjectCmd(vp->getObject())
|
|
<< ".ViewObject.doubleClicked()";
|
|
|
|
const char* commandText = vp->getTransactionText();
|
|
if (commandText) {
|
|
auto editDoc = Application::Instance->editDocument();
|
|
App::AutoTransaction committer(commandText, true);
|
|
|
|
if (!vp->doubleClicked())
|
|
QTreeWidget::mouseDoubleClickEvent(event);
|
|
else if (lines == manager->getLines())
|
|
manager->addLine(MacroManager::Gui, ss.str().c_str());
|
|
|
|
// If the double click starts an editing, let the transaction persist
|
|
if (!editDoc && Application::Instance->editDocument())
|
|
committer.setEnable(false);
|
|
}
|
|
else {
|
|
if (!vp->doubleClicked())
|
|
QTreeWidget::mouseDoubleClickEvent(event);
|
|
else if (lines == manager->getLines())
|
|
manager->addLine(MacroManager::Gui, ss.str().c_str());
|
|
}
|
|
}
|
|
}
|
|
catch (Base::Exception& e) {
|
|
e.reportException();
|
|
}
|
|
catch (std::exception& e) {
|
|
FC_ERR("C++ exception: " << e.what());
|
|
}
|
|
catch (...) {
|
|
FC_ERR("Unknown exception");
|
|
}
|
|
}
|
|
|
|
void TreeWidget::startDragging() {
|
|
if (state() != NoState)
|
|
return;
|
|
if (selectedItems().empty())
|
|
return;
|
|
|
|
setState(DraggingState);
|
|
startDrag(model()->supportedDragActions());
|
|
setState(NoState);
|
|
stopAutoScroll();
|
|
}
|
|
|
|
void TreeWidget::startDrag(Qt::DropActions supportedActions)
|
|
{
|
|
Base::StateLocker guard(_DraggingActive);
|
|
QTreeWidget::startDrag(supportedActions);
|
|
if (_DragEventFilter) {
|
|
_DragEventFilter = false;
|
|
qApp->removeEventFilter(this);
|
|
}
|
|
}
|
|
|
|
bool TreeWidget::dropMimeData(QTreeWidgetItem* parent, int index,
|
|
const QMimeData* data, Qt::DropAction action)
|
|
{
|
|
return QTreeWidget::dropMimeData(parent, index, data, action);
|
|
}
|
|
|
|
void TreeWidget::dragEnterEvent(QDragEnterEvent* event)
|
|
{
|
|
QTreeWidget::dragEnterEvent(event);
|
|
}
|
|
|
|
void TreeWidget::dragLeaveEvent(QDragLeaveEvent* event)
|
|
{
|
|
QTreeWidget::dragLeaveEvent(event);
|
|
}
|
|
|
|
|
|
struct ItemInfo {
|
|
std::string doc;
|
|
std::string obj;
|
|
std::string parentDoc;
|
|
std::string parent;
|
|
std::string ownerDoc;
|
|
std::string owner;
|
|
std::string subname;
|
|
std::string topDoc;
|
|
std::string topObj;
|
|
std::string topSubname;
|
|
std::vector<std::string> subs;
|
|
bool dragging = false;
|
|
};
|
|
|
|
struct ItemInfo2 {
|
|
std::string doc;
|
|
std::string obj;
|
|
std::string parentDoc;
|
|
std::string parent;
|
|
std::string topDoc;
|
|
std::string topObj;
|
|
std::string topSubname;
|
|
};
|
|
|
|
namespace {
|
|
class DropHandler
|
|
{
|
|
public:
|
|
static std::vector<std::pair<DocumentObjectItem*, std::vector<std::string> > > filterItems(const QList<QTreeWidgetItem*>& sels, QTreeWidgetItem* targetItem)
|
|
{
|
|
std::vector<std::pair<DocumentObjectItem*, std::vector<std::string> > > items;
|
|
items.reserve(sels.size());
|
|
for (auto ti : sels) {
|
|
if (ti->type() != TreeWidget::ObjectType)
|
|
continue;
|
|
// ignore child elements if the parent is selected
|
|
if (sels.contains(ti->parent()))
|
|
continue;
|
|
if (ti == targetItem)
|
|
continue;
|
|
auto item = static_cast<DocumentObjectItem*>(ti);
|
|
items.emplace_back();
|
|
auto& info = items.back();
|
|
info.first = item;
|
|
const auto& subnames = item->getSubNames();
|
|
info.second.insert(info.second.end(), subnames.begin(), subnames.end());
|
|
}
|
|
|
|
return items;
|
|
}
|
|
static App::PropertyPlacement* getPlacement(const ItemInfo& info, const App::DocumentObject* obj, Base::Matrix4D& mat)
|
|
{
|
|
App::PropertyPlacement* propPlacement = nullptr;
|
|
if (!info.topObj.empty()) {
|
|
auto doc = App::GetApplication().getDocument(info.topDoc.c_str());
|
|
if (doc) {
|
|
auto topObj = doc->getObject(info.topObj.c_str());
|
|
if (topObj) {
|
|
auto sobj = topObj->getSubObject(info.topSubname.c_str(), nullptr, &mat);
|
|
if (sobj == obj) {
|
|
propPlacement = freecad_cast<App::PropertyPlacement*>(
|
|
obj->getPropertyByName("Placement"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
propPlacement = freecad_cast<App::PropertyPlacement*>(
|
|
obj->getPropertyByName("Placement"));
|
|
if (propPlacement)
|
|
mat = propPlacement->getValue().toMatrix();
|
|
}
|
|
|
|
return propPlacement;
|
|
}
|
|
};
|
|
|
|
QPoint getPos(QEvent* event) {
|
|
if (auto* dragMoveEvent = dynamic_cast<QDragMoveEvent*>(event)) {
|
|
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
|
|
return dragMoveEvent->pos();
|
|
#else
|
|
return dragMoveEvent->position().toPoint();
|
|
#endif
|
|
}
|
|
|
|
else if (auto* dropEvent = dynamic_cast<QDropEvent*>(event)) {
|
|
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
|
|
return dropEvent->pos();
|
|
#else
|
|
return dropEvent->position().toPoint();
|
|
#endif
|
|
}
|
|
|
|
// For unsupported event types or if casting fails
|
|
return QPoint(-1, -1);
|
|
}
|
|
|
|
Qt::DropAction getDropAction(int size, const int type)
|
|
{
|
|
if (QApplication::keyboardModifiers() == Qt::ControlModifier) {
|
|
return Qt::CopyAction;
|
|
}
|
|
else if (QApplication::keyboardModifiers() == Qt::AltModifier
|
|
&& (size == 1 || type == TreeWidget::DocumentType)) {
|
|
return Qt::LinkAction;
|
|
}
|
|
else {
|
|
return Qt::MoveAction;
|
|
}
|
|
}
|
|
}
|
|
|
|
void TreeWidget::dragMoveEvent(QDragMoveEvent* event)
|
|
{
|
|
// Qt5 does not change drag cursor in response to modifier key press,
|
|
// because QDrag installs a event filter that eats up key event. We install
|
|
// a filter after Qt and generate fake mouse move event in response to key
|
|
// press event, which triggers QDrag to update its cursor
|
|
if (!_DragEventFilter) {
|
|
_DragEventFilter = true;
|
|
qApp->installEventFilter(this);
|
|
}
|
|
|
|
QTreeWidget::dragMoveEvent(event);
|
|
if (!event->isAccepted()) {
|
|
//return;
|
|
// QTreeWidget::dragMoveEvent is rejecting the event when in between items
|
|
// at DocumentItem root level. Which is preventing reordering. To work around
|
|
// we accept for now, then reject below if targetItem not found.
|
|
event->accept();
|
|
}
|
|
|
|
|
|
TargetItemInfo targetInfo = getTargetInfo(event);
|
|
QTreeWidgetItem* targetItem = targetInfo.targetItem;
|
|
if (!targetItem) {
|
|
event->ignore();
|
|
return;
|
|
}
|
|
|
|
auto items = selectedItems();
|
|
|
|
auto da = getDropAction(items.size(), targetItem->type());
|
|
event->setDropAction(da);
|
|
|
|
if (targetItem->type() == TreeWidget::DocumentType) {
|
|
leaveEvent(nullptr);
|
|
}
|
|
else if (targetItem->type() == TreeWidget::ObjectType) {
|
|
onItemEntered(targetItem);
|
|
|
|
auto targetItemObj = static_cast<DocumentObjectItem*>(targetItem);
|
|
Gui::ViewProviderDocumentObject* vp = targetItemObj->object();
|
|
|
|
// if we are in between or if target doesn't accept drops then the target is the parent
|
|
if (da == Qt::MoveAction && (targetInfo.inThresholdZone || !vp->canDropObjects())) {
|
|
targetInfo.targetItem = targetInfo.targetItem->parent();
|
|
if (targetInfo.targetItem->type() == TreeWidget::DocumentType) {
|
|
leaveEvent(nullptr);
|
|
return;
|
|
}
|
|
|
|
targetItemObj = static_cast<DocumentObjectItem*>(targetInfo.targetItem);
|
|
vp = targetItemObj->object();
|
|
|
|
if (!vp) {
|
|
TREE_TRACE("cannot drop");
|
|
return;
|
|
}
|
|
}
|
|
try {
|
|
if (da != Qt::LinkAction && !vp->canDropObjects()) {
|
|
if (!(event->possibleActions() & Qt::LinkAction) || items.size() != 1) {
|
|
TREE_TRACE("Cannot drop here");
|
|
event->ignore();
|
|
return;
|
|
}
|
|
}
|
|
for (auto ti : items) {
|
|
if (ti->type() != TreeWidget::ObjectType) {
|
|
TREE_TRACE("cannot drop");
|
|
event->ignore();
|
|
return;
|
|
}
|
|
auto item = static_cast<DocumentObjectItem*>(ti);
|
|
|
|
auto obj = item->object()->getObject();
|
|
|
|
if (da == Qt::MoveAction) {
|
|
// Check if item can be dragged from his parent
|
|
auto parentItem = item->getParentItem();
|
|
if (parentItem && !(parentItem->object()->canDragObjects() && parentItem->object()->canDragObject(obj)))
|
|
{
|
|
TREE_TRACE("Cannot drag object");
|
|
event->ignore();
|
|
return;
|
|
}
|
|
}
|
|
|
|
std::ostringstream str;
|
|
auto owner = item->getRelativeParent(str, targetItemObj);
|
|
auto subname = str.str();
|
|
|
|
// let the view provider decide to accept the object or ignore it
|
|
if (da != Qt::LinkAction && !vp->canDropObjectEx(obj, owner, subname.c_str(), item->mySubs)) {
|
|
TREE_TRACE("cannot drop " << obj->getFullName() << ' ' << (owner ? owner->getFullName() : "<No Owner>") << '.' << subname);
|
|
event->ignore();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
catch (Base::Exception& e) {
|
|
e.reportException();
|
|
event->ignore();
|
|
}
|
|
catch (std::exception& e) {
|
|
FC_ERR("C++ exception: " << e.what());
|
|
event->ignore();
|
|
}
|
|
catch (...) {
|
|
FC_ERR("Unknown exception");
|
|
event->ignore();
|
|
}
|
|
}
|
|
else {
|
|
leaveEvent(nullptr);
|
|
event->ignore();
|
|
}
|
|
}
|
|
|
|
TreeWidget::TargetItemInfo TreeWidget::getTargetInfo(QEvent* ev)
|
|
{
|
|
TargetItemInfo targetInfo;
|
|
|
|
QPoint pos = getPos(ev);
|
|
if (pos == QPoint(-1, -1)) {
|
|
return {}; // Return an empty struct
|
|
}
|
|
|
|
targetInfo.targetItem = itemAt(pos);
|
|
// not dropped onto an item or one of the source items is also the destination item
|
|
if (!targetInfo.targetItem || targetInfo.targetItem->isSelected()) {
|
|
return {};
|
|
}
|
|
targetInfo.underMouseItem = targetInfo.targetItem;
|
|
|
|
if (targetInfo.targetItem->type() == TreeWidget::ObjectType) {
|
|
auto targetItemObj = static_cast<DocumentObjectItem*>(targetInfo.targetItem);
|
|
targetInfo.targetDoc = targetItemObj->getOwnerDocument()->document()->getDocument();
|
|
}
|
|
else if (targetInfo.targetItem->type() == TreeWidget::DocumentType) {
|
|
auto targetDocItem = static_cast<DocumentItem*>(targetInfo.targetItem);
|
|
targetInfo.targetDoc = targetDocItem->document()->getDocument();
|
|
}
|
|
else {
|
|
return {};
|
|
}
|
|
|
|
// Calculate the position of the mouse relative to the item's rectangle
|
|
QRect itemRect = visualItemRect(targetInfo.targetItem);
|
|
int mouseY = pos.y();
|
|
int itemMidPoint = itemRect.top() + itemRect.height() / 2;
|
|
int threshold = itemRect.height() * 0.20; // 20% of the item's height as threshold
|
|
|
|
targetInfo.inBottomHalf = mouseY > itemMidPoint;
|
|
targetInfo.inThresholdZone = ((mouseY < itemRect.top() + threshold) && !targetInfo.targetItem->isExpanded())
|
|
|| (mouseY > itemRect.top() + itemRect.height() - threshold);
|
|
return targetInfo;
|
|
}
|
|
|
|
bool TreeWidget::dropInDocument(QDropEvent* event, TargetItemInfo& targetInfo,
|
|
std::vector<TreeWidget::ObjectItemSubname> items)
|
|
{
|
|
std::string errMsg;
|
|
auto da = event->dropAction();
|
|
bool touched = false;
|
|
|
|
std::vector<ItemInfo2> infos;
|
|
infos.reserve(items.size());
|
|
bool syncPlacement = TreeParams::getSyncPlacement();
|
|
|
|
App::AutoTransaction committer(
|
|
da == Qt::LinkAction ? "Link object" :
|
|
da == Qt::CopyAction ? "Copy object" : "Move object");
|
|
|
|
// check if items can be dragged
|
|
for (auto& v : items) {
|
|
auto item = v.first;
|
|
auto obj = item->object()->getObject();
|
|
auto parentItem = item->getParentItem();
|
|
if (parentItem) {
|
|
bool allParentsOK = canDragFromParents(parentItem, obj, nullptr);
|
|
|
|
if (!allParentsOK || !parentItem->object()->canDragObjects() || !parentItem->object()->canDragObject(obj)) {
|
|
committer.close(true);
|
|
TREE_ERR("'" << obj->getFullName() << "' cannot be dragged out of '" << parentItem->object()->getObject()->getFullName() << "'");
|
|
return false;
|
|
}
|
|
}
|
|
else if (da != Qt::MoveAction || item->myOwner != targetInfo.targetItem) {
|
|
// We will not drag item out of parent if either, 1) modifier
|
|
// key is held, or 2) the dragging item is not inside the
|
|
// dropping document tree.
|
|
parentItem = nullptr;
|
|
}
|
|
infos.emplace_back();
|
|
auto& info = infos.back();
|
|
info.doc = obj->getDocument()->getName();
|
|
info.obj = obj->getNameInDocument();
|
|
if (parentItem) {
|
|
auto parent = parentItem->object()->getObject();
|
|
info.parentDoc = parent->getDocument()->getName();
|
|
info.parent = parent->getNameInDocument();
|
|
}
|
|
if (syncPlacement) {
|
|
std::ostringstream ss;
|
|
App::DocumentObject* topParent = nullptr;
|
|
item->getSubName(ss, topParent);
|
|
if (topParent) {
|
|
info.topDoc = topParent->getDocument()->getName();
|
|
info.topObj = topParent->getNameInDocument();
|
|
ss << obj->getNameInDocument() << '.';
|
|
info.topSubname = ss.str();
|
|
}
|
|
}
|
|
}
|
|
// Because the existence of subname, we must de-select the drag the
|
|
// object manually. Just do a complete clear here for simplicity
|
|
Selection().selStackPush();
|
|
Selection().clearCompleteSelection();
|
|
|
|
// Open command
|
|
auto manager = Application::Instance->macroManager();
|
|
try {
|
|
std::vector<App::DocumentObject*> droppedObjs;
|
|
for (auto& info : infos) {
|
|
auto doc = App::GetApplication().getDocument(info.doc.c_str());
|
|
if (!doc) continue;
|
|
auto obj = doc->getObject(info.obj.c_str());
|
|
auto vpc = freecad_cast<ViewProviderDocumentObject*>(Application::Instance->getViewProvider(obj));
|
|
if (!vpc) {
|
|
FC_WARN("Cannot find dragging object " << info.obj);
|
|
continue;
|
|
}
|
|
|
|
Base::Matrix4D mat;
|
|
App::PropertyPlacement* propPlacement = nullptr;
|
|
if (syncPlacement) {
|
|
if (!info.topObj.empty()) {
|
|
auto doc = App::GetApplication().getDocument(info.topDoc.c_str());
|
|
if (doc) {
|
|
auto topObj = doc->getObject(info.topObj.c_str());
|
|
if (topObj) {
|
|
auto sobj = topObj->getSubObject(info.topSubname.c_str(), nullptr, &mat);
|
|
if (sobj == obj) {
|
|
propPlacement = dynamic_cast<App::PropertyPlacement*>(obj->getPropertyByName("Placement"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
propPlacement = dynamic_cast<App::PropertyPlacement*>(obj->getPropertyByName("Placement"));
|
|
if (propPlacement) {
|
|
mat = propPlacement->getValue().toMatrix();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (da == Qt::LinkAction) {
|
|
std::string name = targetInfo.targetDoc->getUniqueObjectName("Link");
|
|
FCMD_DOC_CMD(targetInfo.targetDoc, "addObject('App::Link','" << name << "').setLink("
|
|
<< Command::getObjectCmd(obj) << ")");
|
|
auto link = targetInfo.targetDoc->getObject(name.c_str());
|
|
if (!link)
|
|
continue;
|
|
FCMD_OBJ_CMD(link, "Label='" << obj->getLinkedObject(true)->Label.getValue() << "'");
|
|
propPlacement = dynamic_cast<App::PropertyPlacement*>(link->getPropertyByName("Placement"));
|
|
if (propPlacement)
|
|
propPlacement->setValueIfChanged(Base::Placement(mat));
|
|
droppedObjs.push_back(link);
|
|
}
|
|
else if (!info.parent.empty()) {
|
|
auto parentDoc = App::GetApplication().getDocument(info.parentDoc.c_str());
|
|
if (!parentDoc) {
|
|
FC_WARN("Canont find document " << info.parentDoc);
|
|
continue;
|
|
}
|
|
auto parent = parentDoc->getObject(info.parent.c_str());
|
|
auto vpp = freecad_cast<ViewProviderDocumentObject*>(Application::Instance->getViewProvider(parent));
|
|
if (!vpp) {
|
|
FC_WARN("Cannot find dragging object's parent " << info.parent);
|
|
continue;
|
|
}
|
|
|
|
std::ostringstream ss;
|
|
ss << Command::getObjectCmd(vpp->getObject()) << ".ViewObject.dragObject(" << Command::getObjectCmd(obj) << ')';
|
|
auto lines = manager->getLines();
|
|
vpp->dragObject(obj);
|
|
if (manager->getLines() == lines) {
|
|
manager->addLine(MacroManager::Gui, ss.str().c_str());
|
|
}
|
|
|
|
//make sure it is not part of a geofeaturegroup anymore.
|
|
//When this has happen we need to handle all removed
|
|
//objects
|
|
auto grp = App::GeoFeatureGroupExtension::getGroupOfObject(obj);
|
|
if (grp) {
|
|
FCMD_OBJ_CMD(grp, "removeObject(" << Command::getObjectCmd(obj) << ")");
|
|
}
|
|
|
|
// check if the object has been deleted
|
|
obj = doc->getObject(info.obj.c_str());
|
|
if (!obj || !obj->isAttachedToDocument()) {
|
|
continue;
|
|
}
|
|
droppedObjs.push_back(obj);
|
|
if (propPlacement) {
|
|
propPlacement->setValueIfChanged(Base::Placement(mat));
|
|
}
|
|
}
|
|
else {
|
|
std::ostringstream ss;
|
|
ss << "App.getDocument('" << targetInfo.targetDoc->getName() << "')."
|
|
<< (da == Qt::CopyAction ? "copyObject(" : "moveObject(")
|
|
<< Command::getObjectCmd(obj) << ", True)";
|
|
App::DocumentObject* res = nullptr;
|
|
if (da == Qt::CopyAction) {
|
|
auto copied = targetInfo.targetDoc->copyObject({ obj }, true);
|
|
if (!copied.empty()) {
|
|
res = copied.back();
|
|
}
|
|
}
|
|
else if (da == Qt::MoveAction && obj->getDocument() == targetInfo.targetDoc) {
|
|
// Moving a object within the document root.
|
|
// Do not set 'res' as changing the placement is not desired: #13690
|
|
droppedObjs.push_back(obj);
|
|
}
|
|
else {
|
|
// Moving a object from another document.
|
|
res = targetInfo.targetDoc->moveObject(obj, true);
|
|
}
|
|
if (res) {
|
|
propPlacement = dynamic_cast<App::PropertyPlacement*>( res->getPropertyByName("Placement"));
|
|
if (propPlacement) {
|
|
propPlacement->setValueIfChanged(Base::Placement(mat));
|
|
}
|
|
droppedObjs.push_back(res);
|
|
}
|
|
manager->addLine(MacroManager::App, ss.str().c_str());
|
|
}
|
|
}
|
|
touched = true;
|
|
Base::FlagToggler<> guard(_DisableCheckTopParent);
|
|
Selection().setSelection(targetInfo.targetDoc->getName(), droppedObjs);
|
|
|
|
// If moved, then we sort objects properly.
|
|
if (da == Qt::MoveAction) {
|
|
sortDroppedObjects(targetInfo, droppedObjs);
|
|
}
|
|
}
|
|
catch (const Base::Exception& e) {
|
|
e.reportException();
|
|
errMsg = e.what();
|
|
}
|
|
catch (std::exception& e) {
|
|
FC_ERR("C++ exception: " << e.what());
|
|
errMsg = e.what();
|
|
}
|
|
catch (...) {
|
|
FC_ERR("Unknown exception");
|
|
errMsg = "Unknown exception";
|
|
}
|
|
if (!errMsg.empty()) {
|
|
committer.close(true);
|
|
QMessageBox::critical(getMainWindow(), QObject::tr("Drag & drop failed"), QString::fromUtf8(errMsg.c_str()));
|
|
return false;
|
|
}
|
|
return touched;
|
|
}
|
|
|
|
bool TreeWidget::dropInObject(QDropEvent* event, TargetItemInfo& targetInfo,
|
|
std::vector<TreeWidget::ObjectItemSubname> items)
|
|
{
|
|
std::string errMsg;
|
|
auto da = event->dropAction();
|
|
bool touched = false;
|
|
|
|
// add object to group
|
|
auto targetItemObj = static_cast<DocumentObjectItem*>(targetInfo.targetItem);
|
|
Gui::ViewProviderDocumentObject* vp = targetItemObj->object();
|
|
|
|
if (!vp || !vp->getObject() || !vp->getObject()->isAttachedToDocument()) {
|
|
TREE_TRACE("invalid object");
|
|
return false;
|
|
}
|
|
|
|
// if we are in between or if target doesn't accept drops then the target is the parent
|
|
if (da == Qt::MoveAction && (targetInfo.inThresholdZone || !vp->canDropObjects())) {
|
|
targetInfo.targetItem = targetInfo.targetItem->parent();
|
|
if (targetInfo.targetItem->type() == TreeWidget::DocumentType) {
|
|
return dropInDocument(event, targetInfo, items);
|
|
}
|
|
|
|
targetItemObj = static_cast<DocumentObjectItem*>(targetInfo.targetItem);
|
|
vp = targetItemObj->object();
|
|
|
|
if (!vp || !vp->getObject() || !vp->getObject()->isAttachedToDocument()) {
|
|
TREE_TRACE("invalid object");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (da != Qt::LinkAction && !vp->canDropObjects()) {
|
|
if (!(event->possibleActions() & Qt::LinkAction) || items.size() != 1) {
|
|
TREE_TRACE("Cannot drop objects");
|
|
return false; // no group like object
|
|
}
|
|
}
|
|
|
|
App::DocumentObject* targetObj = targetItemObj->object()->getObject();
|
|
std::ostringstream targetSubname;
|
|
App::DocumentObject* targetParent = nullptr;
|
|
targetItemObj->getSubName(targetSubname, targetParent);
|
|
Selection().selStackPush();
|
|
Selection().clearCompleteSelection();
|
|
if (targetParent) {
|
|
targetSubname << vp->getObject()->getNameInDocument() << '.';
|
|
Selection().addSelection(targetParent->getDocument()->getName(), targetParent->getNameInDocument(), targetSubname.str().c_str());
|
|
}
|
|
else {
|
|
targetParent = targetObj;
|
|
Selection().addSelection(targetParent->getDocument()->getName(), targetParent->getNameInDocument());
|
|
}
|
|
|
|
// Open command
|
|
App::AutoTransaction committer("Drop object");
|
|
|
|
bool syncPlacement = TreeParams::getSyncPlacement() && targetItemObj->isGroup();
|
|
bool setSelection = true;
|
|
std::vector<App::DocumentObject*> draggedObjects;
|
|
std::vector<std::pair<App::DocumentObject*, std::string> > droppedObjects;
|
|
std::vector<ItemInfo> infos;
|
|
// Only keep text names here, because you never know when doing drag
|
|
// and drop some object may delete other objects.
|
|
infos.reserve(items.size());
|
|
for (auto& v : items) {
|
|
infos.emplace_back();
|
|
auto& info = infos.back();
|
|
auto item = v.first;
|
|
App::DocumentObject* obj = item->object()->getObject();
|
|
|
|
std::ostringstream str;
|
|
App::DocumentObject* topParent = nullptr;
|
|
auto owner = item->getRelativeParent(str, targetItemObj, &topParent, &info.topSubname);
|
|
if (syncPlacement && topParent) {
|
|
info.topDoc = topParent->getDocument()->getName();
|
|
info.topObj = topParent->getNameInDocument();
|
|
}
|
|
info.subname = str.str();
|
|
info.doc = obj->getDocument()->getName();
|
|
info.obj = obj->getNameInDocument();
|
|
if (owner) {
|
|
info.ownerDoc = owner->getDocument()->getName();
|
|
info.owner = owner->getNameInDocument();
|
|
}
|
|
|
|
info.subs.swap(v.second);
|
|
|
|
// check if items can be dragged
|
|
if (da == Qt::MoveAction && item->myOwner == targetItemObj->myOwner && vp->canDragAndDropObject(obj)) {
|
|
auto parentItem = item->getParentItem();
|
|
if (!parentItem) {
|
|
info.dragging = true;
|
|
}
|
|
else {
|
|
|
|
bool allParentsOK = canDragFromParents(parentItem, obj, targetObj);
|
|
|
|
if (allParentsOK) {
|
|
auto vpp = parentItem->object();
|
|
info.dragging = true;
|
|
info.parent = vpp->getObject()->getNameInDocument();
|
|
info.parentDoc = vpp->getObject()->getDocument()->getName();
|
|
}
|
|
else {
|
|
committer.close(true);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (da != Qt::LinkAction
|
|
&& !vp->canDropObjectEx(obj, owner, info.subname.c_str(), item->mySubs))
|
|
{
|
|
if (event->possibleActions() & Qt::LinkAction) {
|
|
if (items.size() > 1) {
|
|
committer.close(true);
|
|
TREE_TRACE("Cannot replace with more than one object");
|
|
return false;
|
|
}
|
|
auto ext = vp->getObject()->getExtensionByType<App::LinkBaseExtension>(true);
|
|
if ((!ext || !ext->getLinkedObjectProperty()) && !targetItemObj->getParentItem()) {
|
|
committer.close(true);
|
|
TREE_TRACE("Cannot replace without parent");
|
|
return false;
|
|
}
|
|
da = Qt::LinkAction;
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
std::set<App::DocumentObject*> inList;
|
|
auto parentObj = targetObj;
|
|
if (da == Qt::LinkAction && targetItemObj->getParentItem()) {
|
|
parentObj = targetItemObj->getParentItem()->object()->getObject();
|
|
}
|
|
inList = parentObj->getInListEx(true);
|
|
inList.insert(parentObj);
|
|
|
|
std::string target = targetObj->getNameInDocument();
|
|
auto targetDoc = targetObj->getDocument();
|
|
for (auto& info : infos) {
|
|
auto& subname = info.subname;
|
|
targetObj = targetDoc->getObject(target.c_str());
|
|
vp = freecad_cast<ViewProviderDocumentObject*>( Application::Instance->getViewProvider(targetObj));
|
|
if (!vp) {
|
|
FC_ERR("Cannot find drop target object " << target);
|
|
break;
|
|
}
|
|
|
|
auto doc = App::GetApplication().getDocument(info.doc.c_str());
|
|
if (!doc) {
|
|
FC_WARN("Cannot find document " << info.doc);
|
|
continue;
|
|
}
|
|
auto obj = doc->getObject(info.obj.c_str());
|
|
auto vpc = freecad_cast<ViewProviderDocumentObject*>(Application::Instance->getViewProvider(obj));
|
|
if (!vpc) {
|
|
FC_WARN("Cannot find dragging object " << info.obj);
|
|
continue;
|
|
}
|
|
|
|
ViewProviderDocumentObject* vpp = nullptr;
|
|
if (da != Qt::LinkAction && !info.parentDoc.empty()) {
|
|
auto parentDoc = App::GetApplication().getDocument(info.parentDoc.c_str());
|
|
if (parentDoc) {
|
|
auto parent = parentDoc->getObject(info.parent.c_str());
|
|
vpp = freecad_cast<ViewProviderDocumentObject*>(Application::Instance->getViewProvider(parent));
|
|
}
|
|
if (!vpp) {
|
|
FC_WARN("Cannot find dragging object's parent " << info.parent);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
App::DocumentObject* owner = nullptr;
|
|
if (!info.ownerDoc.empty()) {
|
|
auto ownerDoc = App::GetApplication().getDocument(info.ownerDoc.c_str());
|
|
if (ownerDoc)
|
|
owner = ownerDoc->getObject(info.owner.c_str());
|
|
if (!owner) {
|
|
FC_WARN("Cannot find dragging object's top parent " << info.owner);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
Base::Matrix4D mat;
|
|
App::PropertyPlacement* propPlacement = nullptr;
|
|
if (syncPlacement) {
|
|
propPlacement = DropHandler::getPlacement(info, obj, mat);
|
|
}
|
|
|
|
auto dropParent = targetParent;
|
|
|
|
auto manager = Application::Instance->macroManager();
|
|
std::ostringstream ss;
|
|
if (vpp) {
|
|
auto lines = manager->getLines();
|
|
ss << Command::getObjectCmd(vpp->getObject())
|
|
<< ".ViewObject.dragObject(" << Command::getObjectCmd(obj) << ')';
|
|
vpp->dragObject(obj);
|
|
if (manager->getLines() == lines)
|
|
manager->addLine(MacroManager::Gui, ss.str().c_str());
|
|
owner = nullptr;
|
|
subname.clear();
|
|
ss.str("");
|
|
|
|
obj = doc->getObject(info.obj.c_str());
|
|
if (!obj || !obj->isAttachedToDocument()) {
|
|
FC_WARN("Dropping object deleted: " << info.doc << '#' << info.obj);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (da == Qt::MoveAction) {
|
|
// Try to adjust relative links to avoid cyclic dependency, may
|
|
// throw exception if failed
|
|
ss.str("");
|
|
ss << Command::getObjectCmd(obj) << ".adjustRelativeLinks("
|
|
<< Command::getObjectCmd(targetObj) << ")";
|
|
manager->addLine(MacroManager::Gui, ss.str().c_str());
|
|
|
|
std::set<App::DocumentObject*> visited;
|
|
if (obj->adjustRelativeLinks(inList, &visited)) {
|
|
inList = parentObj->getInListEx(true);
|
|
inList.insert(parentObj);
|
|
|
|
// TODO: link adjustment and placement adjustment does
|
|
// not work together at the moment.
|
|
propPlacement = nullptr;
|
|
}
|
|
}
|
|
|
|
if (inList.contains(obj)) {
|
|
FC_THROWM(Base::RuntimeError, "Dependency loop detected for " << obj->getFullName());
|
|
}
|
|
|
|
|
|
std::string dropName;
|
|
ss.str("");
|
|
if (da == Qt::LinkAction) {
|
|
auto parentItem = targetItemObj->getParentItem();
|
|
if (parentItem) {
|
|
ss << Command::getObjectCmd(
|
|
parentItem->object()->getObject(), nullptr, ".replaceObject(", true)
|
|
<< Command::getObjectCmd(targetObj) << ","
|
|
<< Command::getObjectCmd(obj) << ")";
|
|
|
|
std::ostringstream ss;
|
|
|
|
dropParent = nullptr;
|
|
parentItem->getSubName(ss, dropParent);
|
|
if (dropParent)
|
|
ss << parentItem->object()->getObject()->getNameInDocument() << '.';
|
|
else
|
|
dropParent = parentItem->object()->getObject();
|
|
ss << obj->getNameInDocument() << '.';
|
|
dropName = ss.str();
|
|
}
|
|
else {
|
|
TREE_WARN("ignore replace operation without parent");
|
|
continue;
|
|
}
|
|
|
|
Gui::Command::runCommand(Gui::Command::App, ss.str().c_str());
|
|
|
|
}
|
|
else {
|
|
ss << Command::getObjectCmd(vp->getObject())
|
|
<< ".ViewObject.dropObject(" << Command::getObjectCmd(obj);
|
|
if (owner) {
|
|
ss << "," << Command::getObjectCmd(owner)
|
|
<< ",'" << subname << "',[";
|
|
}
|
|
else
|
|
ss << ",None,'',[";
|
|
for (auto& sub : info.subs)
|
|
ss << "'" << sub << "',";
|
|
ss << "])";
|
|
auto lines = manager->getLines();
|
|
dropName = vp->dropObjectEx(obj, owner, subname.c_str(), info.subs);
|
|
if (manager->getLines() == lines)
|
|
manager->addLine(MacroManager::Gui, ss.str().c_str());
|
|
if (!dropName.empty())
|
|
dropName = targetSubname.str() + dropName;
|
|
}
|
|
|
|
touched = true;
|
|
|
|
// Construct the subname pointing to the dropped object
|
|
if (dropName.empty()) {
|
|
auto pos = targetSubname.tellp();
|
|
targetSubname << obj->getNameInDocument() << '.' << std::ends;
|
|
dropName = targetSubname.str();
|
|
targetSubname.seekp(pos);
|
|
}
|
|
|
|
Base::Matrix4D newMat;
|
|
auto sobj = dropParent->getSubObject(dropName.c_str(), nullptr, &newMat);
|
|
if (!sobj) {
|
|
FC_LOG("failed to find dropped object "
|
|
<< dropParent->getFullName() << '.' << dropName);
|
|
setSelection = false;
|
|
continue;
|
|
}
|
|
|
|
if (da != Qt::CopyAction && propPlacement) {
|
|
// try to adjust placement
|
|
if ((info.dragging && sobj == obj) ||
|
|
(!info.dragging && sobj->getLinkedObject(false) == obj))
|
|
{
|
|
if (!info.dragging)
|
|
propPlacement = freecad_cast<App::PropertyPlacement*>(
|
|
sobj->getPropertyByName("Placement"));
|
|
if (propPlacement) {
|
|
newMat *= propPlacement->getValue().inverse().toMatrix();
|
|
newMat.inverseGauss();
|
|
Base::Placement pla(newMat * mat);
|
|
propPlacement->setValueIfChanged(pla);
|
|
}
|
|
}
|
|
}
|
|
droppedObjects.emplace_back(dropParent, dropName);
|
|
draggedObjects.push_back(obj);
|
|
}
|
|
Base::FlagToggler<> guard(_DisableCheckTopParent);
|
|
if (setSelection && !droppedObjects.empty()) {
|
|
Selection().selStackPush();
|
|
Selection().clearCompleteSelection();
|
|
for (auto& v : droppedObjects) {
|
|
Selection().addSelection(v.first->getDocument()->getName(),
|
|
v.first->getNameInDocument(), v.second.c_str());
|
|
}
|
|
Selection().selStackPush();
|
|
}
|
|
|
|
// If moved, then we sort objects properly.
|
|
if (da == Qt::MoveAction && vp->acceptReorderingObjects()) {
|
|
sortDroppedObjects(targetInfo, draggedObjects);
|
|
}
|
|
}
|
|
catch (const Base::Exception& e) {
|
|
e.reportException();
|
|
errMsg = e.what();
|
|
}
|
|
catch (std::exception& e) {
|
|
FC_ERR("C++ exception: " << e.what());
|
|
errMsg = e.what();
|
|
}
|
|
catch (...) {
|
|
FC_ERR("Unknown exception");
|
|
errMsg = "Unknown exception";
|
|
}
|
|
if (!errMsg.empty()) {
|
|
committer.close(true);
|
|
QMessageBox::critical(getMainWindow(), QObject::tr("Drag & drop failed"),
|
|
QString::fromUtf8(errMsg.c_str()));
|
|
return false;
|
|
}
|
|
return touched;
|
|
}
|
|
|
|
bool TreeWidget::canDragFromParents(DocumentObjectItem* parentItem, App::DocumentObject* obj, App::DocumentObject* target)
|
|
{
|
|
// We query all the parents recursively. (for cases like assembly/group/part)
|
|
bool allParentsOK = true;
|
|
while (parentItem) {
|
|
if (!parentItem->object()->canDragObjectToTarget(obj, target)) {
|
|
allParentsOK = false;
|
|
break;
|
|
}
|
|
parentItem = parentItem->getParentItem();
|
|
}
|
|
|
|
return allParentsOK;
|
|
}
|
|
|
|
void TreeWidget::dropEvent(QDropEvent* event)
|
|
{
|
|
//FIXME: This should actually be done inside dropMimeData
|
|
|
|
TargetItemInfo targetInfo = getTargetInfo(event);
|
|
if (!targetInfo.targetItem) {
|
|
return;
|
|
}
|
|
|
|
// filter out the selected items we cannot handle
|
|
std::vector<ObjectItemSubname> items;
|
|
items = DropHandler::filterItems(selectedItems(), targetInfo.targetItem);
|
|
if (items.empty()) {
|
|
return; // nothing needs to be done
|
|
}
|
|
|
|
event->setDropAction(getDropAction(items.size(), targetInfo.targetItem->type()));
|
|
|
|
bool touched = false;
|
|
if (targetInfo.targetItem->type() == TreeWidget::ObjectType) {
|
|
touched = dropInObject(event, targetInfo, items);
|
|
}
|
|
else if (targetInfo.targetItem->type() == TreeWidget::DocumentType) {
|
|
touched = dropInDocument(event, targetInfo, items);
|
|
}
|
|
|
|
if (touched && TreeParams::getRecomputeOnDrop()) {
|
|
targetInfo.targetDoc->recompute();
|
|
}
|
|
if (touched && TreeParams::getSyncView()) {
|
|
auto gdoc = Application::Instance->getDocument(targetInfo.targetDoc);
|
|
if (gdoc)
|
|
gdoc->setActiveView();
|
|
}
|
|
}
|
|
|
|
void TreeWidget::sortDroppedObjects(TargetItemInfo& targetInfo, std::vector<App::DocumentObject*> draggedObjects)
|
|
{
|
|
if (targetInfo.targetItem == targetInfo.underMouseItem) {
|
|
return;
|
|
}
|
|
auto underMouseItemObj = static_cast<DocumentObjectItem*>(targetInfo.underMouseItem);
|
|
auto underMouseObj = underMouseItemObj->object()->getObject();
|
|
std::vector<App::DocumentObject*> sortedObjList;
|
|
std::vector<App::DocumentObject*> objList;
|
|
|
|
auto sortIntoList = [&sortedObjList, &draggedObjects, underMouseObj, &targetInfo](const std::vector<App::DocumentObject*>& objects) {
|
|
for (auto* obj : objects) {
|
|
if (obj == underMouseObj) {
|
|
if (targetInfo.inBottomHalf) {
|
|
sortedObjList.push_back(obj);
|
|
}
|
|
|
|
for (auto* draggedObj : draggedObjects) {
|
|
sortedObjList.push_back(draggedObj);
|
|
}
|
|
|
|
if (!targetInfo.inBottomHalf) {
|
|
sortedObjList.push_back(obj);
|
|
}
|
|
}
|
|
else {
|
|
if (std::ranges::find(draggedObjects, obj) == draggedObjects.end()) {
|
|
sortedObjList.push_back(obj);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
if (targetInfo.targetItem->type() == TreeWidget::ObjectType) {
|
|
// To update the order of items of groups such as App::Part, we just need to change the order in the Group property
|
|
auto targetItemObj = static_cast<DocumentObjectItem*>(targetInfo.targetItem);
|
|
App::DocumentObject* targetObj = targetItemObj->object()->getObject();
|
|
|
|
auto propGroup = freecad_cast<App::PropertyLinkList*>(targetObj->getPropertyByName("Group"));
|
|
if (!propGroup) {
|
|
return;
|
|
}
|
|
|
|
objList = propGroup->getValue();
|
|
sortIntoList(objList); // Move dropped objects to correct position
|
|
propGroup->setValue(sortedObjList);
|
|
}
|
|
else if (targetInfo.targetItem->type() == TreeWidget::DocumentType) {
|
|
Gui::Document* guiDoc = Gui::Application::Instance->getDocument(targetInfo.targetDoc->getName());
|
|
objList = guiDoc->getTreeRootObjects();
|
|
// First we need to sort objList by treeRank.
|
|
std::sort(objList.begin(), objList.end(),
|
|
[](App::DocumentObject* a, App::DocumentObject* b) {
|
|
auto vpA = dynamic_cast<Gui::ViewProviderDocumentObject*>(Gui::Application::Instance->getViewProvider(a));
|
|
auto vpB = dynamic_cast<Gui::ViewProviderDocumentObject*>(Gui::Application::Instance->getViewProvider(b));
|
|
if (vpA && vpB) {
|
|
return vpA->getTreeRank() < vpB->getTreeRank();
|
|
}
|
|
return false; // Keep the original order if either vpA or vpB is nullptr
|
|
});
|
|
|
|
// Then we move dropped objects to their correct position
|
|
sortIntoList(objList);
|
|
|
|
// Then we set the tree rank
|
|
for (size_t i = 0; i < sortedObjList.size(); ++i) {
|
|
auto vp = freecad_cast<ViewProviderDocumentObject*>(Application::Instance->getViewProvider(sortedObjList[i]));
|
|
vp->setTreeRank(i);
|
|
}
|
|
|
|
// Lastly we refresh the tree
|
|
static_cast<DocumentItem*>(targetInfo.targetItem)->sortObjectItems();
|
|
}
|
|
}
|
|
|
|
void TreeWidget::drawRow(QPainter* painter, const QStyleOptionViewItem& options, const QModelIndex& index) const
|
|
{
|
|
QTreeWidget::drawRow(painter, options, index);
|
|
}
|
|
|
|
void TreeWidget::slotNewDocument(const Gui::Document& Doc, bool isMainDoc)
|
|
{
|
|
if (Doc.getDocument()->testStatus(App::Document::TempDoc))
|
|
return;
|
|
auto item = new DocumentItem(&Doc, this->rootItem);
|
|
if (isMainDoc)
|
|
this->expandItem(item);
|
|
item->setIcon(0, *documentPixmap);
|
|
item->setText(0, QString::fromUtf8(Doc.getDocument()->Label.getValue()));
|
|
DocumentMap[&Doc] = item;
|
|
}
|
|
|
|
|
|
void TreeWidget::onReloadDoc() {
|
|
if (!this->contextItem || this->contextItem->type() != DocumentType)
|
|
return;
|
|
auto docitem = static_cast<DocumentItem*>(this->contextItem);
|
|
App::Document* doc = docitem->document()->getDocument();
|
|
std::string name = doc->FileName.getValue();
|
|
Application::Instance->reopen(doc);
|
|
for (auto& v : DocumentMap) {
|
|
if (name == v.first->getDocument()->FileName.getValue()) {
|
|
scrollToItem(v.second);
|
|
App::GetApplication().setActiveDocument(v.first->getDocument());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void TreeWidget::onCloseDoc()
|
|
{
|
|
if (!this->contextItem || this->contextItem->type() != DocumentType)
|
|
return;
|
|
try {
|
|
auto docitem = static_cast<DocumentItem*>(this->contextItem);
|
|
Gui::Document* gui = docitem->document();
|
|
App::Document* doc = gui->getDocument();
|
|
if (gui->canClose(true, true))
|
|
Command::doCommand(Command::Doc, "App.closeDocument(\"%s\")", doc->getName());
|
|
}
|
|
catch (const Base::Exception& e) {
|
|
e.reportException();
|
|
}
|
|
catch (std::exception& e) {
|
|
FC_ERR("C++ exception: " << e.what());
|
|
}
|
|
catch (...) {
|
|
FC_ERR("Unknown exception");
|
|
}
|
|
}
|
|
|
|
void TreeWidget::onOpenFileLocation()
|
|
{
|
|
auto docitem = static_cast<DocumentItem*>(this->contextItem);
|
|
App::Document* doc = docitem->document()->getDocument();
|
|
std::string name = doc->FileName.getValue();
|
|
|
|
const QFileInfo fileInfo(QString::fromStdString(name));
|
|
if (!fileInfo.exists()) {
|
|
QMessageBox::warning(this, tr("Error"), tr("File does not exist."));
|
|
return;
|
|
}
|
|
|
|
const QString filePath = fileInfo.canonicalPath();
|
|
bool success = false;
|
|
|
|
#if defined(Q_OS_MAC)
|
|
success = QProcess::startDetached(QStringLiteral("open"), {filePath});
|
|
#elif defined(Q_OS_WIN)
|
|
QStringList param;
|
|
if (!fileInfo.isDir()) {
|
|
param += QStringLiteral("/select,");
|
|
}
|
|
param += QDir::toNativeSeparators(filePath);
|
|
success = QProcess::startDetached(QStringLiteral("explorer.exe"), param);
|
|
#else
|
|
success = QProcess::startDetached(QStringLiteral("xdg-open"), {filePath});
|
|
#endif
|
|
|
|
if (!success) {
|
|
QMessageBox::warning(this, tr("Error"), tr("Failed to open directory."));
|
|
}
|
|
}
|
|
|
|
void TreeWidget::slotRenameDocument(const Gui::Document& Doc)
|
|
{
|
|
// do nothing here
|
|
Q_UNUSED(Doc);
|
|
}
|
|
|
|
void TreeWidget::slotChangedViewObject(const Gui::ViewProvider& vp, const App::Property& prop)
|
|
{
|
|
if (!App::GetApplication().isRestoring()
|
|
&& vp.isDerivedFrom<ViewProviderDocumentObject>())
|
|
{
|
|
const auto& vpd = static_cast<const ViewProviderDocumentObject&>(vp);
|
|
if (&prop == &vpd.ShowInTree) {
|
|
ChangedObjects.emplace(vpd.getObject(), 0);
|
|
_updateStatus();
|
|
}
|
|
}
|
|
}
|
|
|
|
void TreeWidget::slotTouchedObject(const App::DocumentObject& obj) {
|
|
ChangedObjects.emplace(const_cast<App::DocumentObject*>(&obj), 0);
|
|
_updateStatus();
|
|
}
|
|
|
|
void TreeWidget::slotShowHidden(const Gui::Document& Doc)
|
|
{
|
|
auto it = DocumentMap.find(&Doc);
|
|
if (it != DocumentMap.end())
|
|
it->second->updateItemsVisibility(it->second, it->second->showHidden());
|
|
}
|
|
|
|
void TreeWidget::slotRelabelDocument(const Gui::Document& Doc)
|
|
{
|
|
auto it = DocumentMap.find(&Doc);
|
|
if (it != DocumentMap.end()) {
|
|
it->second->setText(0, QString::fromUtf8(Doc.getDocument()->Label.getValue()));
|
|
}
|
|
}
|
|
|
|
void TreeWidget::slotActiveDocument(const Gui::Document& Doc)
|
|
{
|
|
auto jt = DocumentMap.find(&Doc);
|
|
if (jt == DocumentMap.end())
|
|
return; // signal is emitted before the item gets created
|
|
int displayMode = TreeParams::getDocumentMode();
|
|
for (auto it = DocumentMap.begin();
|
|
it != DocumentMap.end(); ++it)
|
|
{
|
|
QFont f = it->second->font(0);
|
|
f.setBold(it == jt);
|
|
it->second->setHidden(0 == displayMode && it != jt);
|
|
if (2 == displayMode) {
|
|
it->second->setExpanded(it == jt);
|
|
}
|
|
// this must be done as last step
|
|
it->second->setFont(0, f);
|
|
}
|
|
}
|
|
|
|
struct UpdateDisabler {
|
|
QWidget& widget;
|
|
int& blocked;
|
|
bool visible{false};
|
|
bool focus{false};
|
|
|
|
// Note! DO NOT block signal here, or else
|
|
// QTreeWidgetItem::setChildIndicatorPolicy() does not work
|
|
UpdateDisabler(QWidget& w, int& blocked)
|
|
: widget(w), blocked(blocked)
|
|
{
|
|
if (++blocked > 1)
|
|
return;
|
|
focus = widget.hasFocus();
|
|
visible = widget.isVisible();
|
|
if (visible) {
|
|
// setUpdatesEnabled(false) does not seem to speed up anything.
|
|
// setVisible(false) on the other hand makes QTreeWidget::setData
|
|
// (i.e. any change to QTreeWidgetItem) faster by 10+ times.
|
|
//
|
|
// widget.setUpdatesEnabled(false);
|
|
|
|
widget.setVisible(false);
|
|
}
|
|
}
|
|
~UpdateDisabler() {
|
|
if (blocked <= 0 || --blocked != 0)
|
|
return;
|
|
|
|
if (visible) {
|
|
widget.setVisible(true);
|
|
if (focus)
|
|
widget.setFocus();
|
|
}
|
|
}
|
|
};
|
|
|
|
void TreeWidget::onUpdateStatus()
|
|
{
|
|
if (this->state() == DraggingState || App::GetApplication().isRestoring()) {
|
|
_updateStatus();
|
|
return;
|
|
}
|
|
|
|
for (auto& v : DocumentMap) {
|
|
if (v.first->isPerformingTransaction()) {
|
|
// We have to delay item creation until undo/redo is done, because the
|
|
// object re-creation while in transaction may break tree view item
|
|
// update logic. For example, a parent object re-created before its
|
|
// children, but the parent's link property already contains all the
|
|
// (detached) children.
|
|
_updateStatus();
|
|
return;
|
|
}
|
|
}
|
|
|
|
FC_LOG("begin update status");
|
|
|
|
UpdateDisabler disabler(*this, updateBlocked);
|
|
|
|
std::vector<App::DocumentObject*> errors;
|
|
|
|
// Use a local copy in case of nested calls
|
|
auto localNewObjects = NewObjects;
|
|
NewObjects.clear();
|
|
|
|
// Checking for new objects
|
|
for (auto& v : localNewObjects) {
|
|
auto doc = App::GetApplication().getDocument(v.first.c_str());
|
|
if (!doc)
|
|
continue;
|
|
auto gdoc = Application::Instance->getDocument(doc);
|
|
if (!gdoc)
|
|
continue;
|
|
auto docItem = getDocumentItem(gdoc);
|
|
if (!docItem)
|
|
continue;
|
|
for (auto id : v.second) {
|
|
auto obj = doc->getObjectByID(id);
|
|
if (!obj)
|
|
continue;
|
|
if (obj->isError())
|
|
errors.push_back(obj);
|
|
if (docItem->ObjectMap.contains(obj))
|
|
continue;
|
|
auto vpd = freecad_cast<ViewProviderDocumentObject*>(gdoc->getViewProvider(obj));
|
|
if (vpd)
|
|
docItem->createNewItem(*vpd);
|
|
}
|
|
}
|
|
|
|
// Use a local copy in case of nested calls
|
|
auto localChangedObjects = ChangedObjects;
|
|
ChangedObjects.clear();
|
|
|
|
// Update children of changed objects
|
|
for (auto& v : localChangedObjects) {
|
|
auto obj = v.first;
|
|
|
|
auto iter = ObjectTable.find(obj);
|
|
if (iter == ObjectTable.end())
|
|
continue;
|
|
|
|
if (v.second.test(CS_Error) && obj->isError())
|
|
errors.push_back(obj);
|
|
|
|
if (!iter->second.empty()) {
|
|
auto data = *iter->second.begin();
|
|
bool itemHidden = !data->viewObject->showInTree();
|
|
if (data->itemHidden != itemHidden) {
|
|
for (auto& data : iter->second) {
|
|
data->itemHidden = itemHidden;
|
|
if (data->docItem->showHidden())
|
|
continue;
|
|
for (auto item : data->items)
|
|
item->setHidden(itemHidden);
|
|
}
|
|
}
|
|
}
|
|
|
|
updateChildren(iter->first, iter->second, v.second.test(CS_Output), false);
|
|
}
|
|
|
|
FC_LOG("update item status");
|
|
TimingInit();
|
|
for (auto pos = DocumentMap.begin(); pos != DocumentMap.end(); ++pos) {
|
|
pos->second->testStatus();
|
|
}
|
|
TimingPrint();
|
|
|
|
// Checking for just restored documents
|
|
for (auto& v : DocumentMap) {
|
|
auto docItem = v.second;
|
|
|
|
for (auto obj : docItem->PopulateObjects)
|
|
docItem->populateObject(obj);
|
|
docItem->PopulateObjects.clear();
|
|
|
|
auto doc = v.first->getDocument();
|
|
|
|
if (!docItem->connectChgObject.connected()) {
|
|
//NOLINTBEGIN
|
|
docItem->connectChgObject = docItem->document()->signalChangedObject.connect(
|
|
std::bind(&TreeWidget::slotChangeObject, this, sp::_1, sp::_2));
|
|
docItem->connectTouchedObject = doc->signalTouchedObject.connect(
|
|
std::bind(&TreeWidget::slotTouchedObject, this, sp::_1));
|
|
//NOLINTEND
|
|
}
|
|
|
|
if (doc->testStatus(App::Document::PartialDoc))
|
|
docItem->setIcon(0, *documentPartialPixmap);
|
|
else if (docItem->_ExpandInfo) {
|
|
for (auto& entry : *docItem->_ExpandInfo) {
|
|
const char* name = entry.first.c_str();
|
|
bool legacy = name[0] == '*';
|
|
if (legacy)
|
|
++name;
|
|
auto obj = doc->getObject(name);
|
|
if (!obj)
|
|
continue;
|
|
auto iter = docItem->ObjectMap.find(obj);
|
|
if (iter == docItem->ObjectMap.end())
|
|
continue;
|
|
if (iter->second->rootItem)
|
|
docItem->restoreItemExpansion(entry.second, iter->second->rootItem);
|
|
else if (legacy && !iter->second->items.empty()) {
|
|
auto item = *iter->second->items.begin();
|
|
item->setExpanded(true);
|
|
}
|
|
}
|
|
}
|
|
docItem->_ExpandInfo.reset();
|
|
}
|
|
|
|
if (Selection().hasSelection() && !selectTimer->isActive() && !this->isSelectionBlocked()) {
|
|
this->blockSelection(true);
|
|
currentDocItem = nullptr;
|
|
for (auto& v : DocumentMap) {
|
|
v.second->setSelected(false);
|
|
v.second->selectItems();
|
|
}
|
|
this->blockSelection(false);
|
|
}
|
|
|
|
auto activeDocItem = getDocumentItem(Application::Instance->activeDocument());
|
|
|
|
QTreeWidgetItem* errItem = nullptr;
|
|
for (auto obj : errors) {
|
|
DocumentObjectDataPtr data;
|
|
if (activeDocItem) {
|
|
auto it = activeDocItem->ObjectMap.find(obj);
|
|
if (it != activeDocItem->ObjectMap.end())
|
|
data = it->second;
|
|
}
|
|
if (!data) {
|
|
auto docItem = getDocumentItem(
|
|
Application::Instance->getDocument(obj->getDocument()));
|
|
if (docItem) {
|
|
auto it = docItem->ObjectMap.find(obj);
|
|
if (it != docItem->ObjectMap.end())
|
|
data = it->second;
|
|
}
|
|
}
|
|
if (data) {
|
|
auto item = data->rootItem;
|
|
if (!item && !data->items.empty()) {
|
|
item = *data->items.begin();
|
|
data->docItem->showItem(item, false, true);
|
|
}
|
|
if (!errItem)
|
|
errItem = item;
|
|
}
|
|
}
|
|
if (errItem)
|
|
scrollToItem(errItem);
|
|
|
|
updateGeometries();
|
|
statusTimer->stop();
|
|
|
|
FC_LOG("done update status");
|
|
}
|
|
|
|
void TreeWidget::onItemEntered(QTreeWidgetItem* item)
|
|
{
|
|
if (item && item->type() == TreeWidget::ObjectType) {
|
|
auto objItem = static_cast<DocumentObjectItem*>(item);
|
|
objItem->displayStatusInfo();
|
|
|
|
if (TreeParams::getPreSelection()) {
|
|
int timeout = TreeParams::getPreSelectionDelay();
|
|
if (timeout < 0)
|
|
timeout = 1;
|
|
if (preselectTime.elapsed() < timeout)
|
|
onPreSelectTimer();
|
|
else {
|
|
timeout = TreeParams::getPreSelectionTimeout();
|
|
if (timeout < 0)
|
|
timeout = 1;
|
|
preselectTimer->start(timeout);
|
|
Selection().rmvPreselect();
|
|
}
|
|
}
|
|
}
|
|
else if (TreeParams::getPreSelection())
|
|
Selection().rmvPreselect();
|
|
}
|
|
|
|
void TreeWidget::leaveEvent(QEvent* event)
|
|
{
|
|
Q_UNUSED(event)
|
|
if (!updateBlocked && TreeParams::getPreSelection()) {
|
|
preselectTimer->stop();
|
|
Selection().rmvPreselect();
|
|
}
|
|
}
|
|
|
|
void TreeWidget::onPreSelectTimer() {
|
|
if (!TreeParams::getPreSelection())
|
|
return;
|
|
auto item = itemAt(viewport()->mapFromGlobal(QCursor::pos()));
|
|
if (!item || item->type() != TreeWidget::ObjectType)
|
|
return;
|
|
|
|
preselectTime.restart();
|
|
auto objItem = static_cast<DocumentObjectItem*>(item);
|
|
auto vp = objItem->object();
|
|
auto obj = vp->getObject();
|
|
std::ostringstream ss;
|
|
App::DocumentObject* parent = nullptr;
|
|
objItem->getSubName(ss, parent);
|
|
if (!parent)
|
|
parent = obj;
|
|
else if (!obj->redirectSubName(ss, parent, nullptr))
|
|
ss << obj->getNameInDocument() << '.';
|
|
Selection().setPreselect(parent->getDocument()->getName(), parent->getNameInDocument(),
|
|
ss.str().c_str(), 0, 0, 0, SelectionChanges::MsgSource::TreeView);
|
|
}
|
|
|
|
void TreeWidget::onItemCollapsed(QTreeWidgetItem* item)
|
|
{
|
|
// object item collapsed
|
|
if (item && item->type() == TreeWidget::ObjectType) {
|
|
static_cast<DocumentObjectItem*>(item)->setExpandedStatus(false);
|
|
}
|
|
}
|
|
|
|
void TreeWidget::onItemExpanded(QTreeWidgetItem* item)
|
|
{
|
|
// object item expanded
|
|
if (item && item->type() == TreeWidget::ObjectType) {
|
|
auto objItem = static_cast<DocumentObjectItem*>(item);
|
|
objItem->setExpandedStatus(true);
|
|
objItem->getOwnerDocument()->populateItem(objItem, false, false);
|
|
}
|
|
}
|
|
|
|
void TreeWidget::scrollItemToTop()
|
|
{
|
|
auto doc = Application::Instance->activeDocument();
|
|
for (auto tree : Instances) {
|
|
if (!tree->isSelectionAttached() || tree->isSelectionBlocked())
|
|
continue;
|
|
|
|
tree->_updateStatus(false);
|
|
|
|
if (doc && Gui::Selection().hasSelection(doc->getDocument()->getName(), ResolveMode::NoResolve)) {
|
|
auto it = tree->DocumentMap.find(doc);
|
|
if (it != tree->DocumentMap.end()) {
|
|
bool lock = tree->blockSelection(true);
|
|
it->second->selectItems(DocumentItem::SR_FORCE_EXPAND);
|
|
tree->blockSelection(lock);
|
|
}
|
|
}
|
|
else {
|
|
tree->blockSelection(true);
|
|
for (int i = 0; i < tree->rootItem->childCount(); i++) {
|
|
auto docItem = dynamic_cast<DocumentItem*>(tree->rootItem->child(i));
|
|
if (!docItem)
|
|
continue;
|
|
auto doc = docItem->document()->getDocument();
|
|
if (Gui::Selection().hasSelection(doc->getName())) {
|
|
tree->currentDocItem = docItem;
|
|
docItem->selectItems(DocumentItem::SR_FORCE_EXPAND);
|
|
tree->currentDocItem = nullptr;
|
|
break;
|
|
}
|
|
}
|
|
tree->blockSelection(false);
|
|
}
|
|
tree->selectTimer->stop();
|
|
tree->_updateStatus(false);
|
|
}
|
|
}
|
|
|
|
void TreeWidget::expandSelectedItems(TreeItemMode mode)
|
|
{
|
|
if (!isSelectionAttached())
|
|
return;
|
|
|
|
const auto items = selectedItems();
|
|
for (auto item : items) {
|
|
switch (mode) {
|
|
case TreeItemMode::ExpandPath: {
|
|
QTreeWidgetItem* parentItem = item->parent();
|
|
while (parentItem) {
|
|
parentItem->setExpanded(true);
|
|
parentItem = parentItem->parent();
|
|
}
|
|
item->setExpanded(true);
|
|
break;
|
|
}
|
|
case TreeItemMode::ExpandItem:
|
|
item->setExpanded(true);
|
|
break;
|
|
case TreeItemMode::CollapseItem:
|
|
item->setExpanded(false);
|
|
break;
|
|
case TreeItemMode::ToggleItem:
|
|
if (item->isExpanded())
|
|
item->setExpanded(false);
|
|
else
|
|
item->setExpanded(true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void TreeWidget::setupText()
|
|
{
|
|
this->headerItem()->setText(0, tr("Labels & Attributes"));
|
|
this->headerItem()->setText(1, tr("Description"));
|
|
this->headerItem()->setText(2, tr("Internal name"));
|
|
|
|
this->showHiddenAction->setText(tr("Show Items Hidden in Tree View"));
|
|
this->showHiddenAction->setStatusTip(tr("Shows items that are marked as 'hidden' in the tree view"));
|
|
|
|
this->toggleVisibilityInTreeAction->setText(tr("Toggle Visibility in Tree View"));
|
|
this->toggleVisibilityInTreeAction->setStatusTip(tr("Toggles the visibility of selected items in the tree view"));
|
|
|
|
this->createGroupAction->setText(tr("Create Group"));
|
|
this->createGroupAction->setStatusTip(tr("Creates a group"));
|
|
|
|
this->relabelObjectAction->setText(tr("Rename"));
|
|
this->relabelObjectAction->setStatusTip(tr("Renames object"));
|
|
|
|
this->finishEditingAction->setText(tr("Finish Editing"));
|
|
this->finishEditingAction->setStatusTip(tr("Finishes editing object"));
|
|
|
|
this->selectDependentsAction->setText(tr("Add Dependent Objects to Selection"));
|
|
this->selectDependentsAction->setStatusTip(tr("Adds all dependent objects to the selection"));
|
|
|
|
this->closeDocAction->setText(tr("Close Document"));
|
|
this->closeDocAction->setStatusTip(tr("Closes the document"));
|
|
|
|
#ifdef Q_OS_MAC
|
|
this->openFileLocationAction->setText(tr("Reveal in Finder"));
|
|
this->openFileLocationAction->setStatusTip(tr("Reveals the current file location in Finder"));
|
|
#else
|
|
this->openFileLocationAction->setText(tr("Open File Location"));
|
|
this->openFileLocationAction->setStatusTip(tr("Opens the current file location"));
|
|
#endif
|
|
|
|
this->reloadDocAction->setText(tr("Reload Document"));
|
|
this->reloadDocAction->setStatusTip(tr("Reloads a partially loaded document"));
|
|
|
|
this->skipRecomputeAction->setText(tr("Skip Recomputes"));
|
|
this->skipRecomputeAction->setStatusTip(tr("Enables or disables the recomputations of document"));
|
|
|
|
this->allowPartialRecomputeAction->setText(tr("Allow Partial Recomputes"));
|
|
this->allowPartialRecomputeAction->setStatusTip(
|
|
tr("Enables or disables the recomputating editing object when 'skip recomputation' is enabled"));
|
|
|
|
this->markRecomputeAction->setText(tr("Mark to Recompute"));
|
|
this->markRecomputeAction->setStatusTip(tr("Marks this object to be recomputed"));
|
|
this->markRecomputeAction->setIcon(BitmapFactory().iconFromTheme("Std_MarkToRecompute"));
|
|
|
|
this->recomputeObjectAction->setText(tr("Recompute Object"));
|
|
this->recomputeObjectAction->setStatusTip(tr("Recomputes the selected object"));
|
|
this->recomputeObjectAction->setIcon(BitmapFactory().iconFromTheme("view-refresh"));
|
|
}
|
|
|
|
void TreeWidget::syncView(ViewProviderDocumentObject* vp)
|
|
{
|
|
if (currentDocItem && TreeParams::getSyncView()) {
|
|
bool focus = hasFocus();
|
|
currentDocItem->document()->setActiveView(vp);
|
|
if (focus)
|
|
setFocus();
|
|
}
|
|
}
|
|
|
|
void TreeWidget::onShowHidden()
|
|
{
|
|
if (!this->contextItem)
|
|
return;
|
|
DocumentItem* docItem = nullptr;
|
|
if (this->contextItem->type() == DocumentType)
|
|
docItem = static_cast<DocumentItem*>(contextItem);
|
|
else if (this->contextItem->type() == ObjectType)
|
|
docItem = static_cast<DocumentObjectItem*>(contextItem)->getOwnerDocument();
|
|
if (docItem)
|
|
docItem->setShowHidden(showHiddenAction->isChecked());
|
|
}
|
|
|
|
void TreeWidget::onToggleVisibilityInTree()
|
|
{
|
|
const auto items = selectedItems();
|
|
for (auto item : items) {
|
|
if (item->type() == ObjectType) {
|
|
auto objectItem = static_cast<DocumentObjectItem*>(item);
|
|
auto object = objectItem->object();
|
|
|
|
// toggle value
|
|
bool showInTree = !object->showInTree();
|
|
|
|
// update object
|
|
object->ShowInTree.setValue(showInTree);
|
|
|
|
// update GUI
|
|
auto ownerDocument = objectItem->getOwnerDocument();
|
|
bool hidden = !ownerDocument->showHidden() && !showInTree;
|
|
objectItem->setHidden(hidden);
|
|
if (hidden) {
|
|
objectItem->setSelected(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TreeWidget::changeEvent(QEvent* e)
|
|
{
|
|
if (e->type() == QEvent::LanguageChange)
|
|
setupText();
|
|
|
|
QTreeWidget::changeEvent(e);
|
|
}
|
|
|
|
void TreeWidget::onItemSelectionChanged()
|
|
{
|
|
if (!this->isSelectionAttached()
|
|
|| this->isSelectionBlocked()
|
|
|| updateBlocked)
|
|
return;
|
|
|
|
_LastSelectedTreeWidget = this;
|
|
|
|
// block tmp. the connection to avoid to notify us ourself
|
|
bool lock = this->blockSelection(true);
|
|
|
|
if (preselectTimer->isActive()) {
|
|
// block preselect after selecting
|
|
preselectTimer->stop();
|
|
}
|
|
|
|
if (selectTimer->isActive())
|
|
onSelectTimer();
|
|
else
|
|
_updateStatus(false);
|
|
|
|
auto selItems = selectedItems();
|
|
|
|
// do not allow document item multi-selection
|
|
if (!selItems.empty()) {
|
|
auto firstType = selItems.back()->type();
|
|
for (auto it = selItems.begin(); it != selItems.end();) {
|
|
auto item = *it;
|
|
if ((firstType == ObjectType && item->type() != ObjectType)
|
|
|| (firstType == DocumentType && item != selItems.back()))
|
|
{
|
|
item->setSelected(false);
|
|
it = selItems.erase(it);
|
|
}
|
|
else
|
|
++it;
|
|
}
|
|
}
|
|
|
|
if (selItems.size() <= 1) {
|
|
if (TreeParams::getRecordSelection())
|
|
Gui::Selection().selStackPush();
|
|
|
|
// This special handling to deal with possible discrepancy of
|
|
// Gui.Selection and Tree view selection because of newly added
|
|
// DocumentObject::redirectSubName()
|
|
Selection().clearCompleteSelection();
|
|
DocumentObjectItem* item = nullptr;
|
|
if (!selItems.empty()) {
|
|
if (selItems.front()->type() == ObjectType)
|
|
item = static_cast<DocumentObjectItem*>(selItems.front());
|
|
else if (selItems.front()->type() == DocumentType) {
|
|
auto ditem = static_cast<DocumentItem*>(selItems.front());
|
|
if (TreeParams::getSyncView()) {
|
|
bool focus = hasFocus();
|
|
ditem->document()->setActiveView();
|
|
if (focus)
|
|
setFocus();
|
|
}
|
|
// For triggering property editor refresh
|
|
Gui::Selection().signalSelectionChanged(SelectionChanges());
|
|
}
|
|
}
|
|
for (auto& v : DocumentMap) {
|
|
currentDocItem = v.second;
|
|
v.second->clearSelection(item);
|
|
currentDocItem = nullptr;
|
|
}
|
|
if (TreeParams::getRecordSelection())
|
|
Gui::Selection().selStackPush();
|
|
}
|
|
else {
|
|
for (auto pos = DocumentMap.begin(); pos != DocumentMap.end(); ++pos) {
|
|
currentDocItem = pos->second;
|
|
pos->second->updateSelection(pos->second);
|
|
currentDocItem = nullptr;
|
|
}
|
|
if (TreeParams::getRecordSelection())
|
|
Gui::Selection().selStackPush(true, true);
|
|
}
|
|
|
|
this->blockSelection(lock);
|
|
}
|
|
|
|
void TreeWidget::synchronizeSelectionCheckBoxes() {
|
|
const bool useCheckBoxes = isSelectionCheckBoxesEnabled();
|
|
for (auto tree : TreeWidget::Instances) {
|
|
QSignalBlocker blocker(tree);
|
|
for (QTreeWidgetItemIterator it(tree); *it; ++it) {
|
|
auto item = *it;
|
|
if (item->type() == ObjectType) {
|
|
if (useCheckBoxes)
|
|
item->setCheckState(0, item->isSelected() ? Qt::Checked : Qt::Unchecked);
|
|
else
|
|
item->setData(0, Qt::CheckStateRole, QVariant());
|
|
}
|
|
}
|
|
tree->resizeColumnToContents(0);
|
|
}
|
|
}
|
|
|
|
void TreeWidget::updateVisibilityIcons() {
|
|
for (auto tree : TreeWidget::Instances) {
|
|
QSignalBlocker blocker(tree);
|
|
for (QTreeWidgetItemIterator it(tree); *it; ++it) {
|
|
auto item = *it;
|
|
if (item->type() == ObjectType) {
|
|
auto objitem = static_cast<DocumentObjectItem*>(item);
|
|
objitem->testStatus(true);
|
|
}
|
|
}
|
|
tree->resizeColumnToContents(0);
|
|
}
|
|
}
|
|
|
|
QList<QTreeWidgetItem*> TreeWidget::childrenOfItem(const QTreeWidgetItem& item) const {
|
|
QList children = QList<QTreeWidgetItem*>();
|
|
|
|
// check item is in this tree
|
|
if (!this->indexFromItem(&item).isValid())
|
|
return children;
|
|
|
|
for (int i = 0; i < item.childCount(); i++) {
|
|
children.append(item.child(i));
|
|
}
|
|
return children;
|
|
}
|
|
|
|
void TreeWidget::onItemChanged(QTreeWidgetItem* item, int column) {
|
|
if (column == 0 && isSelectionCheckBoxesEnabled()) {
|
|
bool selected = item->isSelected();
|
|
bool checked = item->checkState(0) == Qt::Checked;
|
|
if (checked != selected) {
|
|
item->setSelected(checked);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TreeWidget::onSelectTimer() {
|
|
|
|
_updateStatus(false);
|
|
|
|
bool syncSelect = TreeParams::getSyncSelection();
|
|
bool locked = this->blockSelection(true);
|
|
if (Selection().hasSelection()) {
|
|
for (auto& v : DocumentMap) {
|
|
v.second->setSelected(false);
|
|
currentDocItem = v.second;
|
|
v.second->selectItems(syncSelect ? DocumentItem::SR_EXPAND : DocumentItem::SR_SELECT);
|
|
currentDocItem = nullptr;
|
|
}
|
|
}
|
|
else {
|
|
for (auto& v : DocumentMap)
|
|
v.second->clearSelection();
|
|
}
|
|
this->blockSelection(locked);
|
|
selectTimer->stop();
|
|
return;
|
|
}
|
|
|
|
void TreeWidget::onSelectionChanged(const SelectionChanges& msg)
|
|
{
|
|
// When running from a different thread Qt will raise a warning
|
|
// when trying to start the QTimer
|
|
if (Q_UNLIKELY(thread() != QThread::currentThread())) {
|
|
return;
|
|
}
|
|
|
|
switch (msg.Type)
|
|
{
|
|
case SelectionChanges::AddSelection:
|
|
case SelectionChanges::RmvSelection:
|
|
case SelectionChanges::SetSelection:
|
|
case SelectionChanges::ClrSelection: {
|
|
int timeout = TreeParams::getSelectionTimeout();
|
|
if (timeout <= 0)
|
|
timeout = 1;
|
|
selectTimer->start(timeout);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
/* TRANSLATOR Gui::TreePanel */
|
|
|
|
TreePanel::TreePanel(const char* name, QWidget* parent)
|
|
: QWidget(parent)
|
|
{
|
|
this->treeWidget = new TreeWidget(name, this);
|
|
int indent = TreeParams::getIndentation();
|
|
if (indent)
|
|
this->treeWidget->setIndentation(indent);
|
|
|
|
auto pLayout = new QVBoxLayout(this);
|
|
pLayout->setSpacing(0);
|
|
pLayout->setContentsMargins(0, 0, 0, 0);
|
|
pLayout->addWidget(this->treeWidget);
|
|
connect(this->treeWidget, &TreeWidget::emitSearchObjects,
|
|
this, &TreePanel::showEditor);
|
|
|
|
this->searchBox = new Gui::ExpressionLineEdit(this, true);
|
|
static_cast<ExpressionLineEdit*>(this->searchBox)->setExactMatch(Gui::ExpressionParameter::instance()->isExactMatch());
|
|
pLayout->addWidget(this->searchBox);
|
|
this->searchBox->hide();
|
|
this->searchBox->installEventFilter(this);
|
|
this->searchBox->setPlaceholderText(tr("Search"));
|
|
connect(this->searchBox, &QLineEdit::returnPressed,
|
|
this, &TreePanel::accept);
|
|
connect(this->searchBox, &QLineEdit::textChanged,
|
|
this, &TreePanel::itemSearch);
|
|
}
|
|
|
|
TreePanel::~TreePanel() = default;
|
|
|
|
void TreePanel::accept()
|
|
{
|
|
QString text = this->searchBox->text();
|
|
hideEditor();
|
|
this->treeWidget->setFocus();
|
|
this->treeWidget->itemSearch(text, true);
|
|
}
|
|
|
|
bool TreePanel::eventFilter(QObject* obj, QEvent* ev)
|
|
{
|
|
if (obj != this->searchBox)
|
|
return false;
|
|
|
|
if (ev->type() == QEvent::KeyPress) {
|
|
bool consumed = false;
|
|
int key = static_cast<QKeyEvent*>(ev)->key();
|
|
switch (key) {
|
|
case Qt::Key_Escape:
|
|
hideEditor();
|
|
consumed = true;
|
|
treeWidget->setFocus();
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return consumed;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void TreePanel::showEditor()
|
|
{
|
|
this->searchBox->show();
|
|
this->searchBox->setFocus();
|
|
this->treeWidget->startItemSearch(searchBox);
|
|
}
|
|
|
|
void TreePanel::hideEditor()
|
|
{
|
|
static_cast<ExpressionLineEdit*>(this->searchBox)->setDocumentObject(nullptr);
|
|
this->searchBox->clear();
|
|
this->searchBox->hide();
|
|
this->treeWidget->resetItemSearch();
|
|
auto sels = this->treeWidget->selectedItems();
|
|
if (!sels.empty())
|
|
this->treeWidget->scrollToItem(sels.front());
|
|
}
|
|
|
|
void TreePanel::itemSearch(const QString& text)
|
|
{
|
|
this->treeWidget->itemSearch(text, false);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
/* TRANSLATOR Gui::TreeDockWidget */
|
|
|
|
TreeDockWidget::TreeDockWidget(Gui::Document* pcDocument, QWidget* parent)
|
|
: DockWindow(pcDocument, parent)
|
|
{
|
|
setWindowTitle(tr("Tree View"));
|
|
auto panel = new TreePanel("TreeView", this);
|
|
auto pLayout = new QGridLayout(this);
|
|
pLayout->setSpacing(0);
|
|
pLayout->setContentsMargins(0, 0, 0, 0);
|
|
pLayout->addWidget(panel, 0, 0);
|
|
}
|
|
|
|
TreeDockWidget::~TreeDockWidget() = default;
|
|
|
|
void TreeWidget::selectLinkedObject(App::DocumentObject* linked) {
|
|
if (!isSelectionAttached() || isSelectionBlocked())
|
|
return;
|
|
|
|
auto linkedVp = freecad_cast<ViewProviderDocumentObject*>(
|
|
Application::Instance->getViewProvider(linked));
|
|
if (!linkedVp) {
|
|
TREE_ERR("invalid linked view provider");
|
|
return;
|
|
}
|
|
auto linkedDoc = getDocumentItem(linkedVp->getDocument());
|
|
if (!linkedDoc) {
|
|
TREE_ERR("cannot find document of linked object");
|
|
return;
|
|
}
|
|
|
|
if (selectTimer->isActive())
|
|
onSelectTimer();
|
|
else
|
|
_updateStatus(false);
|
|
|
|
auto it = linkedDoc->ObjectMap.find(linked);
|
|
if (it == linkedDoc->ObjectMap.end()) {
|
|
TREE_ERR("cannot find tree item of linked object");
|
|
return;
|
|
}
|
|
auto linkedItem = it->second->rootItem;
|
|
if (!linkedItem)
|
|
linkedItem = *it->second->items.begin();
|
|
|
|
if (linkedDoc->showItem(linkedItem, true))
|
|
scrollToItem(linkedItem);
|
|
|
|
if (linkedDoc->document()->getDocument() != App::GetApplication().getActiveDocument()) {
|
|
bool focus = hasFocus();
|
|
linkedDoc->document()->setActiveView(linkedItem->object());
|
|
if (focus)
|
|
setFocus();
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
DocumentItem::DocumentItem(const Gui::Document* doc, QTreeWidgetItem* parent)
|
|
: QTreeWidgetItem(parent, TreeWidget::DocumentType), pDocument(const_cast<Gui::Document*>(doc))
|
|
{
|
|
//NOLINTBEGIN
|
|
// Setup connections
|
|
connectNewObject = doc->signalNewObject.connect(std::bind(&DocumentItem::slotNewObject, this, sp::_1));
|
|
connectDelObject = doc->signalDeletedObject.connect(
|
|
std::bind(&TreeWidget::slotDeleteObject, getTree(), sp::_1));
|
|
if (!App::GetApplication().isRestoring()) {
|
|
connectChgObject = doc->signalChangedObject.connect(
|
|
std::bind(&TreeWidget::slotChangeObject, getTree(), sp::_1, sp::_2));
|
|
connectTouchedObject = doc->getDocument()->signalTouchedObject.connect(
|
|
std::bind(&TreeWidget::slotTouchedObject, getTree(), sp::_1));
|
|
}
|
|
connectEdtObject = doc->signalInEdit.connect(std::bind(&DocumentItem::slotInEdit, this, sp::_1));
|
|
connectResObject = doc->signalResetEdit.connect(std::bind(&DocumentItem::slotResetEdit, this, sp::_1));
|
|
connectHltObject = doc->signalHighlightObject.connect(
|
|
std::bind(&DocumentItem::slotHighlightObject, this, sp::_1, sp::_2, sp::_3, sp::_4, sp::_5));
|
|
connectExpObject = doc->signalExpandObject.connect(
|
|
std::bind(&DocumentItem::slotExpandObject, this, sp::_1, sp::_2, sp::_3, sp::_4));
|
|
connectScrObject = doc->signalScrollToObject.connect(std::bind(&DocumentItem::slotScrollToObject, this, sp::_1));
|
|
auto adoc = doc->getDocument();
|
|
connectRecomputed = adoc->signalRecomputed.connect(std::bind(&DocumentItem::slotRecomputed, this, sp::_1, sp::_2));
|
|
connectRecomputedObj = adoc->signalRecomputedObject.connect(
|
|
std::bind(&DocumentItem::slotRecomputedObject, this, sp::_1));
|
|
//NOLINTEND
|
|
|
|
setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable/*|Qt::ItemIsEditable*/);
|
|
|
|
treeName = getTree()->getTreeName();
|
|
}
|
|
|
|
DocumentItem::~DocumentItem()
|
|
{
|
|
connectNewObject.disconnect();
|
|
connectDelObject.disconnect();
|
|
connectChgObject.disconnect();
|
|
connectTouchedObject.disconnect();
|
|
connectEdtObject.disconnect();
|
|
connectResObject.disconnect();
|
|
connectHltObject.disconnect();
|
|
connectExpObject.disconnect();
|
|
connectScrObject.disconnect();
|
|
connectRecomputed.disconnect();
|
|
connectRecomputedObj.disconnect();
|
|
}
|
|
|
|
TreeWidget* DocumentItem::getTree() const {
|
|
return static_cast<TreeWidget*>(treeWidget());
|
|
}
|
|
|
|
const char* DocumentItem::getTreeName() const {
|
|
return treeName;
|
|
}
|
|
|
|
#define FOREACH_ITEM(_item, _obj) \
|
|
auto _it = ObjectMap.end();\
|
|
if(_obj.getObject() && _obj.getObject()->isAttachedToDocument())\
|
|
_it = ObjectMap.find(_obj.getObject());\
|
|
if(_it != ObjectMap.end()) {\
|
|
for(auto _item : _it->second->items) {
|
|
|
|
#define FOREACH_ITEM_ALL(_item) \
|
|
for(const auto& _v : ObjectMap) {\
|
|
for(auto _item : _v.second->items) {
|
|
|
|
#define END_FOREACH_ITEM }}
|
|
|
|
|
|
void DocumentItem::slotInEdit(const Gui::ViewProviderDocumentObject& v)
|
|
{
|
|
(void)v;
|
|
|
|
ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(
|
|
"User parameter:BaseApp/Preferences/TreeView");
|
|
unsigned long col = hGrp->GetUnsigned("TreeEditColor", 563609599);
|
|
QColor color(Base::Color::fromPackedRGB<QColor>(col));
|
|
|
|
if (!getTree()->editingItem) {
|
|
auto doc = Application::Instance->editDocument();
|
|
if (!doc)
|
|
return;
|
|
ViewProviderDocumentObject* parentVp = nullptr;
|
|
std::string subname;
|
|
auto vp = doc->getInEdit(&parentVp, &subname);
|
|
if (!parentVp)
|
|
parentVp = freecad_cast<ViewProviderDocumentObject*>(vp);
|
|
if (parentVp)
|
|
getTree()->editingItem = findItemByObject(true, parentVp->getObject(), subname.c_str());
|
|
}
|
|
|
|
if (getTree()->editingItem)
|
|
getTree()->editingItem->setBackground(0, color);
|
|
else {
|
|
FOREACH_ITEM(item, v)
|
|
item->setBackground(0, color);
|
|
END_FOREACH_ITEM
|
|
}
|
|
}
|
|
|
|
void DocumentItem::slotResetEdit(const Gui::ViewProviderDocumentObject& v)
|
|
{
|
|
auto tree = getTree();
|
|
FOREACH_ITEM_ALL(item)
|
|
if (tree->editingItem) {
|
|
if (item == tree->editingItem) {
|
|
item->setData(0, Qt::BackgroundRole, QVariant());
|
|
break;
|
|
}
|
|
}
|
|
else if (item->object() == &v)
|
|
item->setData(0, Qt::BackgroundRole, QVariant());
|
|
END_FOREACH_ITEM
|
|
tree->editingItem = nullptr;
|
|
}
|
|
|
|
void DocumentItem::slotNewObject(const Gui::ViewProviderDocumentObject& obj) {
|
|
if (!obj.getObject() || !obj.getObject()->isAttachedToDocument()) {
|
|
FC_ERR("view provider not attached");
|
|
return;
|
|
}
|
|
getTree()->NewObjects[pDocument->getDocument()->getName()].push_back(obj.getObject()->getID());
|
|
getTree()->_updateStatus();
|
|
}
|
|
|
|
bool DocumentItem::createNewItem(const Gui::ViewProviderDocumentObject& obj,
|
|
QTreeWidgetItem* parent, int index, DocumentObjectDataPtr data)
|
|
{
|
|
if (!obj.getObject() ||
|
|
!obj.getObject()->isAttachedToDocument() ||
|
|
obj.getObject()->testStatus(App::PartialObject))
|
|
return false;
|
|
|
|
if (!data) {
|
|
auto& pdata = ObjectMap[obj.getObject()];
|
|
if (!pdata) {
|
|
pdata = std::make_shared<DocumentObjectData>(
|
|
this, const_cast<ViewProviderDocumentObject*>(&obj));
|
|
auto& entry = getTree()->ObjectTable[obj.getObject()];
|
|
if (!entry.empty())
|
|
pdata->updateChildren(*entry.begin());
|
|
else
|
|
pdata->updateChildren(true);
|
|
entry.insert(pdata);
|
|
}
|
|
else if (pdata->rootItem && !parent) {
|
|
Base::Console().warning("DocumentItem::slotNewObject: Cannot add view provider twice.\n");
|
|
return false;
|
|
}
|
|
data = pdata;
|
|
}
|
|
|
|
auto item = new DocumentObjectItem(this, data);
|
|
if (!parent || parent == this) {
|
|
parent = this;
|
|
data->rootItem = item;
|
|
if (index < 0)
|
|
index = findRootIndex(obj.getObject());
|
|
}
|
|
if (index < 0)
|
|
parent->addChild(item);
|
|
else
|
|
parent->insertChild(index, item);
|
|
assert(item->parent() == parent);
|
|
item->setText(0, QString::fromUtf8(data->label.c_str()));
|
|
if (!data->label2.empty())
|
|
item->setText(1, QString::fromUtf8(data->label2.c_str()));
|
|
item->setText(2, QString::fromUtf8(data->internalName.c_str()));
|
|
if (!obj.showInTree() && !showHidden())
|
|
item->setHidden(true);
|
|
item->testStatus(true);
|
|
|
|
populateItem(item);
|
|
return true;
|
|
}
|
|
|
|
ViewProviderDocumentObject* DocumentItem::getViewProvider(App::DocumentObject* obj) {
|
|
return freecad_cast<ViewProviderDocumentObject*>(
|
|
Application::Instance->getViewProvider(obj));
|
|
}
|
|
|
|
void TreeWidget::slotDeleteDocument(const Gui::Document& Doc)
|
|
{
|
|
NewObjects.erase(Doc.getDocument()->getName());
|
|
auto it = DocumentMap.find(&Doc);
|
|
if (it != DocumentMap.end()) {
|
|
UpdateDisabler disabler(*this, updateBlocked);
|
|
auto docItem = it->second;
|
|
for (auto& v : docItem->ObjectMap) {
|
|
for (auto item : v.second->items)
|
|
item->myOwner = nullptr;
|
|
auto obj = v.second->viewObject->getObject();
|
|
if (obj->getDocument() == Doc.getDocument()) {
|
|
_slotDeleteObject(*v.second->viewObject, docItem);
|
|
continue;
|
|
}
|
|
auto it = ObjectTable.find(obj);
|
|
assert(it != ObjectTable.end());
|
|
assert(it->second.size() > 1);
|
|
it->second.erase(v.second);
|
|
}
|
|
this->rootItem->takeChild(this->rootItem->indexOfChild(docItem));
|
|
delete docItem;
|
|
DocumentMap.erase(it);
|
|
}
|
|
}
|
|
|
|
void TreeWidget::slotDeleteObject(const Gui::ViewProviderDocumentObject& view) {
|
|
_slotDeleteObject(view, nullptr);
|
|
}
|
|
|
|
void TreeWidget::_slotDeleteObject(const Gui::ViewProviderDocumentObject& view, DocumentItem* deletingDoc)
|
|
{
|
|
auto obj = view.getObject();
|
|
auto itEntry = ObjectTable.find(obj);
|
|
if (itEntry == ObjectTable.end())
|
|
return;
|
|
|
|
if (itEntry->second.empty()) {
|
|
ObjectTable.erase(itEntry);
|
|
return;
|
|
}
|
|
|
|
TREE_LOG("delete object " << obj->getFullName());
|
|
|
|
// Block all selection signals during deletion to prevent cascading selection change events
|
|
// during item creation or deletion
|
|
bool lock = blockSelection(true);
|
|
bool needUpdate = false;
|
|
|
|
for (const auto& data : itEntry->second) {
|
|
DocumentItem* docItem = data->docItem;
|
|
if (docItem == deletingDoc)
|
|
continue;
|
|
|
|
auto doc = docItem->document()->getDocument();
|
|
auto& items = data->items;
|
|
|
|
if (obj->getDocument() == doc)
|
|
docItem->_ParentMap.erase(obj);
|
|
|
|
for (auto cit = items.begin(), citNext = cit; cit != items.end(); cit = citNext) {
|
|
++citNext;
|
|
(*cit)->myOwner = nullptr;
|
|
delete* cit;
|
|
}
|
|
|
|
// Check for any child of the deleted object that is not in the tree, and put it
|
|
// under document item.
|
|
for (auto child : data->children) {
|
|
auto childVp = docItem->getViewProvider(child);
|
|
if (!childVp || child->getDocument() != doc)
|
|
continue;
|
|
docItem->_ParentMap[child].erase(obj);
|
|
auto cit = docItem->ObjectMap.find(child);
|
|
if (cit == docItem->ObjectMap.end() || cit->second->items.empty()) {
|
|
if (docItem->createNewItem(*childVp))
|
|
needUpdate = true;
|
|
}
|
|
else {
|
|
auto childItem = *cit->second->items.begin();
|
|
if (childItem->requiredAtRoot(false)) {
|
|
if (docItem->createNewItem(*childItem->object(), docItem, -1, childItem->myData))
|
|
needUpdate = true;
|
|
}
|
|
}
|
|
childVp->setShowable(docItem->isObjectShowable(child));
|
|
}
|
|
docItem->ObjectMap.erase(obj);
|
|
}
|
|
ObjectTable.erase(itEntry);
|
|
|
|
// Restore signal state
|
|
blockSelection(lock);
|
|
|
|
if (needUpdate)
|
|
_updateStatus();
|
|
}
|
|
|
|
bool DocumentItem::populateObject(App::DocumentObject* obj) {
|
|
// make sure at least one of the item corresponding to obj is populated
|
|
auto it = ObjectMap.find(obj);
|
|
if (it == ObjectMap.end())
|
|
return false;
|
|
auto& items = it->second->items;
|
|
if (items.empty())
|
|
return false;
|
|
for (auto item : items) {
|
|
if (item->populated)
|
|
return true;
|
|
}
|
|
TREE_LOG("force populate object " << obj->getFullName());
|
|
auto item = *items.begin();
|
|
item->populated = true;
|
|
populateItem(item, true);
|
|
return true;
|
|
}
|
|
|
|
void DocumentItem::populateItem(DocumentObjectItem* item, bool refresh, bool delay)
|
|
{
|
|
(void)delay;
|
|
|
|
if (item->populated && !refresh)
|
|
return;
|
|
|
|
// Lazy loading policy: We will create an item for each children object if
|
|
// a) the item is expanded, or b) there is at least one free child, i.e.
|
|
// child originally located at root.
|
|
|
|
item->setChildIndicatorPolicy(item->myData->children.empty() ?
|
|
QTreeWidgetItem::DontShowIndicator : QTreeWidgetItem::ShowIndicator);
|
|
|
|
if (!item->populated && !item->isExpanded()) {
|
|
bool doPopulate = false;
|
|
|
|
bool external = item->object()->getDocument() != item->getOwnerDocument()->document();
|
|
if (external)
|
|
return;
|
|
auto obj = item->object()->getObject();
|
|
auto linked = obj->getLinkedObject(true);
|
|
if (linked && linked->getDocument() != obj->getDocument())
|
|
return;
|
|
for (auto child : item->myData->children) {
|
|
auto it = ObjectMap.find(child);
|
|
if (it == ObjectMap.end() || it->second->items.empty()) {
|
|
auto vp = getViewProvider(child);
|
|
if (!vp) continue;
|
|
doPopulate = true;
|
|
break;
|
|
}
|
|
if (item->myData->removeChildrenFromRoot) {
|
|
if (it->second->rootItem) {
|
|
doPopulate = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!doPopulate)
|
|
return;
|
|
}
|
|
|
|
item->populated = true;
|
|
bool checkHidden = !showHidden();
|
|
bool updated = false;
|
|
|
|
int i = -1;
|
|
// iterate through the claimed children, and try to synchronize them with the
|
|
// children tree item with the same order of appearance.
|
|
int childCount = item->childCount();
|
|
for (auto child : item->myData->children) {
|
|
|
|
++i; // the current index of the claimed child
|
|
|
|
bool found = false;
|
|
for (int j = i; j < childCount; ++j) {
|
|
QTreeWidgetItem* ci = item->child(j);
|
|
if (ci->type() != TreeWidget::ObjectType)
|
|
continue;
|
|
|
|
auto childItem = static_cast<DocumentObjectItem*>(ci);
|
|
if (childItem->object()->getObject() != child)
|
|
continue;
|
|
|
|
found = true;
|
|
if (j != i) { // fix index if it is changed
|
|
childItem->setHighlight(false);
|
|
item->removeChild(ci);
|
|
item->insertChild(i, ci);
|
|
assert(ci->parent() == item);
|
|
if (checkHidden)
|
|
updateItemsVisibility(ci, false);
|
|
}
|
|
|
|
// Check if the item just changed its policy of whether to remove
|
|
// children item from the root.
|
|
if (item->myData->removeChildrenFromRoot) {
|
|
if (childItem->myData->rootItem) {
|
|
assert(childItem != childItem->myData->rootItem);
|
|
bool lock = getTree()->blockSelection(true);
|
|
delete childItem->myData->rootItem;
|
|
getTree()->blockSelection(lock);
|
|
}
|
|
}
|
|
else if (childItem->requiredAtRoot()) {
|
|
createNewItem(*childItem->object(), this, -1, childItem->myData);
|
|
updated = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (found)
|
|
continue;
|
|
|
|
// This algo will be recursively applied to newly created child items
|
|
// through slotNewObject -> populateItem
|
|
|
|
auto it = ObjectMap.find(child);
|
|
if (it == ObjectMap.end() || it->second->items.empty()) {
|
|
auto vp = getViewProvider(child);
|
|
if (!vp || !createNewItem(*vp, item, i, it == ObjectMap.end() ? DocumentObjectDataPtr() : it->second))
|
|
--i;
|
|
else
|
|
updated = true;
|
|
continue;
|
|
}
|
|
|
|
if (!item->myData->removeChildrenFromRoot || !it->second->rootItem) {
|
|
DocumentObjectItem* childItem = *it->second->items.begin();
|
|
if (!createNewItem(*childItem->object(), item, i, it->second))
|
|
--i;
|
|
else
|
|
updated = true;
|
|
}
|
|
else {
|
|
DocumentObjectItem* childItem = it->second->rootItem;
|
|
if (item == childItem || item->isChildOfItem(childItem)) {
|
|
TREE_ERR("Cyclic dependency in "
|
|
<< item->object()->getObject()->getFullName()
|
|
<< '.' << childItem->object()->getObject()->getFullName());
|
|
--i;
|
|
continue;
|
|
}
|
|
it->second->rootItem = nullptr;
|
|
childItem->setHighlight(false);
|
|
this->removeChild(childItem);
|
|
item->insertChild(i, childItem);
|
|
assert(childItem->parent() == item);
|
|
if (checkHidden)
|
|
updateItemsVisibility(childItem, false);
|
|
}
|
|
}
|
|
|
|
for (++i; item->childCount() > i;) {
|
|
QTreeWidgetItem* ci = item->child(i);
|
|
if (ci->type() == TreeWidget::ObjectType) {
|
|
auto childItem = static_cast<DocumentObjectItem*>(ci);
|
|
if (childItem->requiredAtRoot()) {
|
|
item->removeChild(childItem);
|
|
auto index = findRootIndex(childItem->object()->getObject());
|
|
if (index >= 0)
|
|
this->insertChild(index, childItem);
|
|
else
|
|
this->addChild(childItem);
|
|
assert(childItem->parent() == this);
|
|
if (checkHidden)
|
|
updateItemsVisibility(childItem, false);
|
|
childItem->myData->rootItem = childItem;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
bool lock = getTree()->blockSelection(true);
|
|
delete ci;
|
|
getTree()->blockSelection(lock);
|
|
}
|
|
if (updated)
|
|
getTree()->_updateStatus();
|
|
}
|
|
|
|
int DocumentItem::findRootIndex(App::DocumentObject* childObj) {
|
|
if (!TreeParams::getKeepRootOrder() || !childObj || !childObj->isAttachedToDocument())
|
|
return -1;
|
|
|
|
// Use view provider's tree rank to find correct place at the root level.
|
|
|
|
int count = this->childCount();
|
|
if (!count)
|
|
return -1;
|
|
|
|
int first, last;
|
|
|
|
auto getTreeRank = [](Gui::ViewProviderDocumentObject* vp) -> int {
|
|
if (vp->getTreeRank() == -1) {
|
|
vp->setTreeRank(vp->getObject()->getID());
|
|
}
|
|
return vp->getTreeRank();
|
|
};
|
|
|
|
auto vpc = freecad_cast<ViewProviderDocumentObject*>(Application::Instance->getViewProvider(childObj));
|
|
int childTreeRank = getTreeRank(vpc);
|
|
|
|
// find the last item
|
|
for (last = count - 1; last >= 0; --last) {
|
|
auto citem = this->child(last);
|
|
if (citem->type() == TreeWidget::ObjectType) {
|
|
auto vp = static_cast<DocumentObjectItem*>(citem)->object();
|
|
if (getTreeRank(vp) <= childTreeRank) {
|
|
return last + 1;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// find the first item
|
|
for (first = 0; first < count; ++first) {
|
|
auto citem = this->child(first);
|
|
if (citem->type() == TreeWidget::ObjectType) {
|
|
auto vp = static_cast<DocumentObjectItem*>(citem)->object();
|
|
if (getTreeRank(vp) > childTreeRank) {
|
|
return first;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// now do a binary search to find the lower bound, assuming the root level
|
|
// object is already in order
|
|
count = last - first;
|
|
int pos;
|
|
while (count > 0) {
|
|
int step = count / 2;
|
|
pos = first + step;
|
|
for (; pos <= last; ++pos) {
|
|
auto citem = this->child(pos);
|
|
if (citem->type() != TreeWidget::ObjectType)
|
|
continue;
|
|
auto vp = static_cast<DocumentObjectItem*>(citem)->object();
|
|
if (vp->getTreeRank() < childTreeRank) {
|
|
first = ++pos;
|
|
count -= step + 1;
|
|
}
|
|
else
|
|
count = step;
|
|
break;
|
|
}
|
|
if (pos > last)
|
|
return -1;
|
|
}
|
|
if (first > last)
|
|
return -1;
|
|
return first;
|
|
}
|
|
|
|
void DocumentItem::sortObjectItems()
|
|
{
|
|
QSignalBlocker guard(getTree());
|
|
|
|
std::vector<DocumentObjectItem*> sortedItems;
|
|
sortedItems.reserve(this->childCount());
|
|
|
|
for (int i = 0; i < this->childCount(); ++i) {
|
|
QTreeWidgetItem* treeItem = this->child(i);
|
|
if (treeItem->type() == TreeWidget::ObjectType) {
|
|
sortedItems.push_back(static_cast<DocumentObjectItem*>(treeItem));
|
|
}
|
|
}
|
|
|
|
std::stable_sort(sortedItems.begin(), sortedItems.end(),
|
|
[](DocumentObjectItem* a, DocumentObjectItem* b) {
|
|
return a->object()->getTreeRank() < b->object()->getTreeRank();
|
|
});
|
|
|
|
int sortedIndex = 0;
|
|
std::vector<bool> expansion;
|
|
for (int i = 0; i < this->childCount(); ++i) {
|
|
QTreeWidgetItem* treeItem = this->child(i);
|
|
if (treeItem->type() != TreeWidget::ObjectType) {
|
|
continue;
|
|
}
|
|
|
|
DocumentObjectItem* sortedItem = sortedItems[sortedIndex++];
|
|
if (sortedItem == treeItem) {
|
|
continue;
|
|
}
|
|
|
|
expansion.clear();
|
|
sortedItem->getExpandedSnapshot(expansion);
|
|
|
|
this->removeChild(sortedItem);
|
|
this->insertChild(i, sortedItem);
|
|
if (!showHidden()) {
|
|
updateItemsVisibility(sortedItem, false);
|
|
}
|
|
|
|
std::vector<bool>::const_iterator expFrom = expansion.cbegin();
|
|
sortedItem->applyExpandedSnapshot(expansion, expFrom);
|
|
}
|
|
}
|
|
|
|
void TreeWidget::slotChangeObject(
|
|
const Gui::ViewProviderDocumentObject& view, const App::Property& prop) {
|
|
|
|
auto obj = view.getObject();
|
|
if (!obj || !obj->isAttachedToDocument())
|
|
return;
|
|
|
|
auto itEntry = ObjectTable.find(obj);
|
|
if (itEntry == ObjectTable.end() || itEntry->second.empty())
|
|
return;
|
|
|
|
_updateStatus();
|
|
|
|
// Let's not waste time on the newly added Visibility property in
|
|
// DocumentObject.
|
|
if (&prop == &obj->Visibility)
|
|
return;
|
|
|
|
if (&prop == &obj->Label) {
|
|
const char* label = obj->Label.getValue();
|
|
auto firstData = *itEntry->second.begin();
|
|
if (firstData->label != label) {
|
|
for (const auto& data : itEntry->second) {
|
|
data->label = label;
|
|
auto displayName = QString::fromUtf8(label);
|
|
for (auto item : data->items)
|
|
item->setText(0, displayName);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (&prop == &obj->Label2) {
|
|
const char* label = obj->Label2.getValue();
|
|
auto firstData = *itEntry->second.begin();
|
|
if (firstData->label2 != label) {
|
|
for (const auto& data : itEntry->second) {
|
|
data->label2 = label;
|
|
auto displayName = QString::fromUtf8(label);
|
|
for (auto item : data->items)
|
|
item->setText(1, displayName);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
auto& s = ChangedObjects[obj];
|
|
if (prop.testStatus(App::Property::Output)
|
|
|| prop.testStatus(App::Property::NoRecompute))
|
|
{
|
|
s.set(CS_Output);
|
|
}
|
|
}
|
|
|
|
void TreeWidget::updateChildren(App::DocumentObject* obj,
|
|
const std::set<DocumentObjectDataPtr>& dataSet, bool propOutput, bool force)
|
|
{
|
|
bool childrenChanged = false;
|
|
std::vector<App::DocumentObject*> children;
|
|
bool removeChildrenFromRoot = true;
|
|
|
|
DocumentObjectDataPtr found;
|
|
for (auto data : dataSet) {
|
|
if (!found) {
|
|
found = data;
|
|
childrenChanged = found->updateChildren(force);
|
|
removeChildrenFromRoot = found->viewObject->canRemoveChildrenFromRoot();
|
|
if (!childrenChanged && found->removeChildrenFromRoot == removeChildrenFromRoot)
|
|
return;
|
|
}
|
|
else if (childrenChanged)
|
|
data->updateChildren(found);
|
|
data->removeChildrenFromRoot = removeChildrenFromRoot;
|
|
DocumentItem* docItem = data->docItem;
|
|
for (auto item : data->items)
|
|
docItem->populateItem(item, true);
|
|
}
|
|
|
|
if (force)
|
|
return;
|
|
|
|
if (childrenChanged && propOutput) {
|
|
// When a property is marked as output, it will not touch its object,
|
|
// and thus, its property change will not be propagated through
|
|
// recomputation. So we have to manually check for each links here.
|
|
for (auto link : App::GetApplication().getLinksTo(obj, App::GetLinkRecursive)) {
|
|
if (ChangedObjects.contains(link))
|
|
continue;
|
|
std::vector<App::DocumentObject*> linkedChildren;
|
|
DocumentObjectDataPtr found;
|
|
auto it = ObjectTable.find(link);
|
|
if (it == ObjectTable.end())
|
|
continue;
|
|
for (auto data : it->second) {
|
|
if (!found) {
|
|
found = data;
|
|
if (!found->updateChildren(false))
|
|
break;
|
|
}
|
|
data->updateChildren(found);
|
|
DocumentItem* docItem = data->docItem;
|
|
for (auto item : data->items)
|
|
docItem->populateItem(item, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (childrenChanged) {
|
|
if (!selectTimer->isActive())
|
|
onSelectionChanged(SelectionChanges());
|
|
|
|
//if the item is in a GeoFeatureGroup we may need to update that too, as the claim children
|
|
//of the geofeaturegroup depends on what the childs claim
|
|
auto grp = App::GeoFeatureGroupExtension::getGroupOfObject(obj);
|
|
if (grp && !ChangedObjects.contains(grp)) {
|
|
auto iter = ObjectTable.find(grp);
|
|
if (iter != ObjectTable.end())
|
|
updateChildren(grp, iter->second, true, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DocumentItem::slotHighlightObject(const Gui::ViewProviderDocumentObject& obj,
|
|
const Gui::HighlightMode& high, bool set, const App::DocumentObject* parent, const char* subname)
|
|
{
|
|
getTree()->_updateStatus(false);
|
|
if (parent && parent->getDocument() != document()->getDocument()) {
|
|
auto it = getTree()->DocumentMap.find(Application::Instance->getDocument(parent->getDocument()));
|
|
if (it != getTree()->DocumentMap.end())
|
|
it->second->slotHighlightObject(obj, high, set, parent, subname);
|
|
return;
|
|
}
|
|
FOREACH_ITEM(item, obj)
|
|
if (parent) {
|
|
App::DocumentObject* topParent = nullptr;
|
|
std::ostringstream ss;
|
|
item->getSubName(ss, topParent);
|
|
if (!topParent) {
|
|
if (parent != obj.getObject())
|
|
continue;
|
|
}
|
|
}
|
|
item->setHighlight(set, high);
|
|
if (parent)
|
|
return;
|
|
END_FOREACH_ITEM
|
|
}
|
|
|
|
static unsigned int countExpandedItem(const QTreeWidgetItem* item) {
|
|
unsigned int size = 0;
|
|
for (int i = 0, count = item->childCount(); i < count; ++i) {
|
|
auto citem = item->child(i);
|
|
if (citem->type() != TreeWidget::ObjectType || !citem->isExpanded())
|
|
continue;
|
|
auto obj = static_cast<const DocumentObjectItem*>(citem)->object()->getObject();
|
|
if (obj->isAttachedToDocument())
|
|
size += strlen(obj->getNameInDocument()) + countExpandedItem(citem);
|
|
}
|
|
return size;
|
|
}
|
|
|
|
unsigned int DocumentItem::getMemSize() const {
|
|
return countExpandedItem(this);
|
|
}
|
|
|
|
static void saveExpandedItem(Base::Writer& writer, const QTreeWidgetItem* item) {
|
|
int itemCount = 0;
|
|
for (int i = 0, count = item->childCount(); i < count; ++i) {
|
|
auto citem = item->child(i);
|
|
if (citem->type() != TreeWidget::ObjectType || !citem->isExpanded())
|
|
continue;
|
|
auto obj = static_cast<const DocumentObjectItem*>(citem)->object()->getObject();
|
|
if (obj->isAttachedToDocument())
|
|
++itemCount;
|
|
}
|
|
|
|
if (!itemCount) {
|
|
writer.Stream() << "/>" << std::endl;
|
|
return;
|
|
}
|
|
|
|
writer.Stream() << " count=\"" << itemCount << "\">" << std::endl;
|
|
writer.incInd();
|
|
for (int i = 0, count = item->childCount(); i < count; ++i) {
|
|
auto citem = item->child(i);
|
|
if (citem->type() != TreeWidget::ObjectType || !citem->isExpanded())
|
|
continue;
|
|
auto obj = static_cast<const DocumentObjectItem*>(citem)->object()->getObject();
|
|
if (obj->isAttachedToDocument()) {
|
|
writer.Stream() << writer.ind() << "<Expand name=\""
|
|
<< obj->getNameInDocument() << "\"";
|
|
saveExpandedItem(writer, static_cast<const DocumentObjectItem*>(citem));
|
|
}
|
|
}
|
|
writer.decInd();
|
|
writer.Stream() << writer.ind() << "</Expand>" << std::endl;
|
|
}
|
|
|
|
void DocumentItem::Save(Base::Writer& writer) const {
|
|
writer.Stream() << writer.ind() << "<Expand ";
|
|
saveExpandedItem(writer, this);
|
|
}
|
|
|
|
void DocumentItem::Restore(Base::XMLReader& reader) {
|
|
reader.readElement("Expand");
|
|
if (!reader.hasAttribute("count"))
|
|
return;
|
|
_ExpandInfo.reset(new ExpandInfo);
|
|
_ExpandInfo->restore(reader);
|
|
for (auto inst : TreeWidget::Instances) {
|
|
if (inst != getTree()) {
|
|
auto docItem = inst->getDocumentItem(document());
|
|
if (docItem)
|
|
docItem->_ExpandInfo = _ExpandInfo;
|
|
}
|
|
}
|
|
}
|
|
|
|
void DocumentItem::restoreItemExpansion(const ExpandInfoPtr& info, DocumentObjectItem* item) {
|
|
item->setExpanded(true);
|
|
if (!info)
|
|
return;
|
|
for (int i = 0, count = item->childCount(); i < count; ++i) {
|
|
auto citem = item->child(i);
|
|
if (citem->type() != TreeWidget::ObjectType)
|
|
continue;
|
|
auto obj = static_cast<DocumentObjectItem*>(citem)->object()->getObject();
|
|
if (!obj->isAttachedToDocument())
|
|
continue;
|
|
auto it = info->find(obj->getNameInDocument());
|
|
if (it != info->end())
|
|
restoreItemExpansion(it->second, static_cast<DocumentObjectItem*>(citem));
|
|
}
|
|
}
|
|
|
|
void DocumentItem::slotExpandObject(const Gui::ViewProviderDocumentObject& obj,
|
|
const Gui::TreeItemMode& mode, const App::DocumentObject* parent, const char* subname)
|
|
{
|
|
getTree()->_updateStatus(false);
|
|
|
|
if ((mode == TreeItemMode::ExpandItem ||
|
|
mode == TreeItemMode::ExpandPath) &&
|
|
obj.getDocument()->getDocument()->testStatus(App::Document::Restoring)) {
|
|
if (!_ExpandInfo)
|
|
_ExpandInfo.reset(new ExpandInfo);
|
|
_ExpandInfo->emplace(std::string("*") + obj.getObject()->getNameInDocument(), ExpandInfoPtr());
|
|
return;
|
|
}
|
|
|
|
if (parent && parent->getDocument() != document()->getDocument()) {
|
|
auto it = getTree()->DocumentMap.find(Application::Instance->getDocument(parent->getDocument()));
|
|
if (it != getTree()->DocumentMap.end())
|
|
it->second->slotExpandObject(obj, mode, parent, subname);
|
|
return;
|
|
}
|
|
|
|
FOREACH_ITEM(item, obj)
|
|
// All document object items must always have a parent, either another
|
|
// object item or document item. If not, then there is a bug somewhere
|
|
// else.
|
|
assert(item->parent());
|
|
|
|
switch (mode) {
|
|
case TreeItemMode::ExpandPath:
|
|
if (!parent) {
|
|
QTreeWidgetItem* parentItem = item->parent();
|
|
while (parentItem) {
|
|
parentItem->setExpanded(true);
|
|
parentItem = parentItem->parent();
|
|
}
|
|
item->setExpanded(true);
|
|
break;
|
|
}
|
|
// fall through
|
|
case TreeItemMode::ExpandItem:
|
|
if (!parent) {
|
|
if (item->parent()->isExpanded())
|
|
item->setExpanded(true);
|
|
}
|
|
else {
|
|
App::DocumentObject* topParent = nullptr;
|
|
std::ostringstream ss;
|
|
item->getSubName(ss, topParent);
|
|
if (!topParent) {
|
|
if (parent != obj.getObject())
|
|
continue;
|
|
}
|
|
else if (topParent != parent)
|
|
continue;
|
|
showItem(item, false, true);
|
|
item->setExpanded(true);
|
|
}
|
|
break;
|
|
case TreeItemMode::CollapseItem:
|
|
item->setExpanded(false);
|
|
break;
|
|
case TreeItemMode::ToggleItem:
|
|
if (item->isExpanded())
|
|
item->setExpanded(false);
|
|
else
|
|
item->setExpanded(true);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
if (item->isExpanded())
|
|
populateItem(item);
|
|
if (parent)
|
|
return;
|
|
END_FOREACH_ITEM
|
|
}
|
|
|
|
void DocumentItem::slotScrollToObject(const Gui::ViewProviderDocumentObject& obj)
|
|
{
|
|
if (!obj.getObject() || !obj.getObject()->isAttachedToDocument())
|
|
return;
|
|
auto it = ObjectMap.find(obj.getObject());
|
|
if (it == ObjectMap.end() || it->second->items.empty())
|
|
return;
|
|
auto item = it->second->rootItem;
|
|
if (!item)
|
|
item = *it->second->items.begin();
|
|
getTree()->_updateStatus(false);
|
|
getTree()->scrollToItem(item);
|
|
}
|
|
|
|
void DocumentItem::slotRecomputedObject(const App::DocumentObject& obj) {
|
|
if (obj.isValid())
|
|
return;
|
|
slotRecomputed(*obj.getDocument(), { const_cast<App::DocumentObject*>(&obj) });
|
|
}
|
|
|
|
void DocumentItem::slotRecomputed(const App::Document&, const std::vector<App::DocumentObject*>& objs) {
|
|
auto tree = getTree();
|
|
for (auto obj : objs) {
|
|
if (!obj->isValid())
|
|
tree->ChangedObjects[obj].set(TreeWidget::CS_Error);
|
|
}
|
|
if (!tree->ChangedObjects.empty())
|
|
tree->_updateStatus();
|
|
}
|
|
|
|
Gui::Document* DocumentItem::document() const
|
|
{
|
|
return this->pDocument;
|
|
}
|
|
|
|
void DocumentItem::testStatus()
|
|
{
|
|
for (const auto& v : ObjectMap)
|
|
v.second->testStatus();
|
|
}
|
|
|
|
void DocumentItem::setData(int column, int role, const QVariant& value)
|
|
{
|
|
if (role == Qt::EditRole) {
|
|
QString label = value.toString();
|
|
pDocument->getDocument()->Label.setValue((const char*)label.toUtf8());
|
|
}
|
|
|
|
QTreeWidgetItem::setData(column, role, value);
|
|
}
|
|
|
|
void DocumentItem::clearSelection(DocumentObjectItem* exclude)
|
|
{
|
|
// Block signals here otherwise we get a recursion and quadratic runtime
|
|
bool ok = treeWidget()->blockSignals(true);
|
|
FOREACH_ITEM_ALL(item);
|
|
_v.second->dirtyFlag = false;
|
|
if (item == exclude) {
|
|
if (item->selected > 0)
|
|
item->selected = -1;
|
|
else
|
|
item->selected = 0;
|
|
updateItemSelection(item);
|
|
// The set has been changed while calling updateItemSelection
|
|
// so that the iterator has become invalid -> Abort
|
|
if (_v.second->dirtyFlag) {
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
item->selected = 0;
|
|
item->mySubs.clear();
|
|
item->setSelected(false);
|
|
item->setCheckState(false);
|
|
}
|
|
END_FOREACH_ITEM;
|
|
treeWidget()->blockSignals(ok);
|
|
}
|
|
|
|
void DocumentItem::updateSelection(QTreeWidgetItem* ti, bool unselect) {
|
|
for (int i = 0, count = ti->childCount(); i < count; ++i) {
|
|
auto child = ti->child(i);
|
|
if (child && child->type() == TreeWidget::ObjectType) {
|
|
auto childItem = static_cast<DocumentObjectItem*>(child);
|
|
if (unselect) {
|
|
childItem->setSelected(false);
|
|
childItem->setCheckState(false);
|
|
}
|
|
updateItemSelection(childItem);
|
|
if (unselect && childItem->isGroup()) {
|
|
// If the child item being force unselected by its group parent
|
|
// is itself a group, propagate the unselection to its own
|
|
// children
|
|
updateSelection(childItem, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (unselect)
|
|
return;
|
|
for (int i = 0, count = ti->childCount(); i < count; ++i)
|
|
updateSelection(ti->child(i));
|
|
}
|
|
|
|
void DocumentItem::updateItemSelection(DocumentObjectItem* item)
|
|
{
|
|
// Note: In several places of this function the selection is altered and the notification of
|
|
// the selection observers can trigger a recreation of all DocumentObjectItem so that the
|
|
// passed 'item' can become a dangling pointer.
|
|
// Thus,'item' mustn't be accessed any more after altering the selection.
|
|
// For further details see the bug analysis of #13107
|
|
bool selected = item->isSelected();
|
|
bool checked = item->checkState(0) == Qt::Checked;
|
|
|
|
if (selected && !checked)
|
|
item->setCheckState(true);
|
|
|
|
if (!selected && checked)
|
|
item->setCheckState(false);
|
|
|
|
if ((selected && item->selected > 0) || (!selected && !item->selected)) {
|
|
return;
|
|
}
|
|
if (item->selected != -1)
|
|
item->mySubs.clear();
|
|
item->selected = selected;
|
|
|
|
auto obj = item->object()->getObject();
|
|
if (!obj || !obj->isAttachedToDocument())
|
|
return;
|
|
|
|
std::ostringstream str;
|
|
App::DocumentObject* topParent = nullptr;
|
|
item->getSubName(str, topParent);
|
|
if (topParent) {
|
|
if (!obj->redirectSubName(str, topParent, nullptr))
|
|
str << obj->getNameInDocument() << '.';
|
|
obj = topParent;
|
|
}
|
|
const char* objname = obj->getNameInDocument();
|
|
const char* docname = obj->getDocument()->getName();
|
|
const auto& subname = str.str();
|
|
|
|
#ifdef FC_DEBUG
|
|
if (!subname.empty()) {
|
|
assert(item->getParentItem());
|
|
}
|
|
#endif
|
|
|
|
if (!selected) {
|
|
Gui::Selection().rmvSelection(docname, objname, subname.c_str());
|
|
return;
|
|
}
|
|
|
|
auto vobj = item->object();
|
|
selected = false;
|
|
if (!item->mySubs.empty()) {
|
|
for (auto& sub : item->mySubs) {
|
|
if (Gui::Selection().addSelection(docname, objname, (subname + sub).c_str())) {
|
|
selected = true;
|
|
}
|
|
}
|
|
}
|
|
if (!selected) {
|
|
item->mySubs.clear();
|
|
if (!Gui::Selection().addSelection(docname, objname, subname.c_str())) {
|
|
// Safely re-access the item
|
|
DocumentObjectItem* item2 = findItem(vobj->getObject(), subname);
|
|
if (item2) {
|
|
item2->selected = 0;
|
|
item2->setSelected(false);
|
|
item2->setCheckState(false);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
getTree()->syncView(vobj);
|
|
}
|
|
|
|
App::DocumentObject* DocumentItem::getTopParent(App::DocumentObject* obj, std::string& subname) {
|
|
auto it = ObjectMap.find(obj);
|
|
if (it == ObjectMap.end() || it->second->items.empty())
|
|
return nullptr;
|
|
|
|
// already a top parent
|
|
if (it->second->rootItem)
|
|
return obj;
|
|
|
|
for (auto item : it->second->items) {
|
|
// non group object do not provide a coordinate system, hence its
|
|
// claimed child is still in the global coordinate space, so the
|
|
// child can still be considered a top level object
|
|
if (!item->isParentGroup())
|
|
return obj;
|
|
}
|
|
|
|
// If no top level item, find an item that is closest to the top level
|
|
std::multimap<int, DocumentObjectItem*> items;
|
|
for (auto item : it->second->items) {
|
|
int i = 0;
|
|
for (auto parent = item->parent(); parent; ++i, parent = parent->parent()) {
|
|
if (parent->isHidden())
|
|
i += 1000;
|
|
++i;
|
|
}
|
|
items.emplace(i, item);
|
|
}
|
|
|
|
App::DocumentObject* topParent = nullptr;
|
|
std::ostringstream ss;
|
|
items.begin()->second->getSubName(ss, topParent);
|
|
if (!topParent) {
|
|
// this shouldn't happen
|
|
FC_WARN("No top parent for " << obj->getFullName() << '.' << subname);
|
|
return obj;
|
|
}
|
|
ss << obj->getNameInDocument() << '.' << subname;
|
|
FC_LOG("Subname correction " << obj->getFullName() << '.' << subname
|
|
<< " -> " << topParent->getFullName() << '.' << ss.str());
|
|
subname = ss.str();
|
|
return topParent;
|
|
}
|
|
|
|
DocumentObjectItem *DocumentItem::findItem(App::DocumentObject* obj, const std::string& subname) const
|
|
{
|
|
auto it = ObjectMap.find(obj);
|
|
if (it == ObjectMap.end()) {
|
|
return nullptr;
|
|
}
|
|
|
|
// There is only one instance in the tree view
|
|
if (it->second->items.size() == 1) {
|
|
return *(it->second->items.begin());
|
|
}
|
|
|
|
// If there are multiple instances use the one with the same subname
|
|
DocumentObjectItem* item {};
|
|
for (auto jt : it->second->items) {
|
|
std::ostringstream str;
|
|
App::DocumentObject* topParent = nullptr;
|
|
jt->getSubName(str, topParent);
|
|
if (topParent) {
|
|
if (!obj->redirectSubName(str, topParent, nullptr)) {
|
|
str << obj->getNameInDocument() << '.';
|
|
}
|
|
}
|
|
|
|
if (subname == str.str()) {
|
|
item = jt;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
DocumentObjectItem* DocumentItem::findItemByObject(
|
|
bool sync, App::DocumentObject* obj, const char* subname, bool select)
|
|
{
|
|
if (!subname)
|
|
subname = "";
|
|
|
|
auto it = ObjectMap.find(obj);
|
|
if (it == ObjectMap.end() || it->second->items.empty())
|
|
return nullptr;
|
|
|
|
// prefer top level item of this object
|
|
if (it->second->rootItem)
|
|
return findItem(sync, it->second->rootItem, subname, select);
|
|
|
|
for (auto item : it->second->items) {
|
|
// non group object do not provide a coordinate system, hence its
|
|
// claimed child is still in the global coordinate space, so the
|
|
// child can still be considered a top level object
|
|
if (!item->isParentGroup())
|
|
return findItem(sync, item, subname, select);
|
|
}
|
|
|
|
// If no top level item, find an item that is closest to the top level
|
|
std::multimap<int, DocumentObjectItem*> items;
|
|
for (auto item : it->second->items) {
|
|
int i = 0;
|
|
for (auto parent = item->parent(); parent; ++i, parent = parent->parent())
|
|
++i;
|
|
items.emplace(i, item);
|
|
}
|
|
for (auto& v : items) {
|
|
auto item = findItem(sync, v.second, subname, select);
|
|
if (item)
|
|
return item;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
DocumentObjectItem* DocumentItem::findItem(
|
|
bool sync, DocumentObjectItem* item, const char* subname, bool select)
|
|
{
|
|
if (item->isHidden())
|
|
item->setHidden(false);
|
|
|
|
if (!subname || *subname == 0) {
|
|
if (select) {
|
|
item->selected += 2;
|
|
item->mySubs.clear();
|
|
}
|
|
return item;
|
|
}
|
|
|
|
TREE_TRACE("find next " << subname);
|
|
|
|
// try to find the next level object name
|
|
const char* nextsub = nullptr;
|
|
const char* dot = nullptr;
|
|
if ((dot = strchr(subname, '.')))
|
|
nextsub = dot + 1;
|
|
else {
|
|
if (select) {
|
|
item->selected += 2;
|
|
if (std::ranges::find(item->mySubs, subname) == item->mySubs.end()) {
|
|
item->mySubs.emplace_back(subname);
|
|
}
|
|
}
|
|
return item;
|
|
}
|
|
|
|
std::string name(subname, nextsub - subname);
|
|
auto obj = item->object()->getObject();
|
|
auto subObj = obj->getSubObject(name.c_str());
|
|
if (!subObj || subObj == obj) {
|
|
if (!subObj && !getTree()->searchDoc)
|
|
TREE_LOG("sub object not found " << item->getName() << '.' << name.c_str());
|
|
if (select) {
|
|
item->selected += 2;
|
|
if (std::ranges::find(item->mySubs, subname) == item->mySubs.end())
|
|
item->mySubs.emplace_back(subname);
|
|
}
|
|
return item;
|
|
}
|
|
|
|
if (select)
|
|
item->mySubs.clear();
|
|
|
|
if (!item->populated && sync) {
|
|
//force populate the item
|
|
item->populated = true;
|
|
populateItem(item, true);
|
|
}
|
|
|
|
for (int i = 0, count = item->childCount(); i < count; ++i) {
|
|
auto ti = item->child(i);
|
|
if (!ti || ti->type() != TreeWidget::ObjectType) continue;
|
|
auto child = static_cast<DocumentObjectItem*>(ti);
|
|
|
|
if (child->object()->getObject() == subObj)
|
|
return findItem(sync, child, nextsub, select);
|
|
}
|
|
|
|
// The sub object is not found. This could happen for geo group, since its
|
|
// children may be in more than one hierarchy down.
|
|
bool found = false;
|
|
DocumentObjectItem* res = nullptr;
|
|
auto it = ObjectMap.find(subObj);
|
|
if (it != ObjectMap.end()) {
|
|
for (auto child : it->second->items) {
|
|
if (child->isChildOfItem(item)) {
|
|
found = true;
|
|
res = findItem(sync, child, nextsub, select);
|
|
if (!select)
|
|
return res;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (select && !found) {
|
|
// The sub object is still not found. Maybe it is a non-object sub-element.
|
|
// Select the current object instead.
|
|
TREE_TRACE("element " << subname << " not found");
|
|
item->selected += 2;
|
|
if (std::ranges::find(item->mySubs, subname) == item->mySubs.end())
|
|
item->mySubs.emplace_back(subname);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
void DocumentItem::selectItems(SelectionReason reason) {
|
|
const auto& sels = Selection().getSelection(pDocument->getDocument()->getName(), ResolveMode::NoResolve);
|
|
|
|
bool sync = (sels.size() > 50 || reason == SR_SELECT) ? false : true;
|
|
|
|
for (const auto& sel : sels)
|
|
findItemByObject(sync, sel.pObject, sel.SubName, true);
|
|
|
|
DocumentObjectItem* newSelect = nullptr;
|
|
DocumentObjectItem* oldSelect = nullptr;
|
|
|
|
FOREACH_ITEM_ALL(item)
|
|
if (item->selected == 1) {
|
|
// this means it is the old selection and is not in the current
|
|
// selection
|
|
item->selected = 0;
|
|
item->mySubs.clear();
|
|
item->setSelected(false);
|
|
item->setCheckState(false);
|
|
}
|
|
else if (item->selected) {
|
|
if (sync) {
|
|
if (item->selected == 2 && showItem(item, false, reason == SR_FORCE_EXPAND)) {
|
|
// This means newly selected and can auto expand
|
|
if (!newSelect)
|
|
newSelect = item;
|
|
}
|
|
if (!newSelect && !oldSelect && !item->isHidden()) {
|
|
bool visible = true;
|
|
for (auto parent = item->parent(); parent; parent = parent->parent()) {
|
|
if (!parent->isExpanded() || parent->isHidden()) {
|
|
visible = false;
|
|
break;
|
|
}
|
|
}
|
|
if (visible)
|
|
oldSelect = item;
|
|
}
|
|
}
|
|
item->selected = 1;
|
|
item->setSelected(true);
|
|
item->setCheckState(true);
|
|
}
|
|
END_FOREACH_ITEM;
|
|
|
|
if (sync) {
|
|
if (!newSelect)
|
|
newSelect = oldSelect;
|
|
else
|
|
getTree()->syncView(newSelect->object());
|
|
if (newSelect)
|
|
getTree()->scrollToItem(newSelect);
|
|
}
|
|
}
|
|
|
|
void DocumentItem::populateParents(const ViewProvider* vp, ViewParentMap& parentMap) {
|
|
auto it = parentMap.find(vp);
|
|
if (it == parentMap.end())
|
|
return;
|
|
for (auto parent : it->second) {
|
|
auto it = ObjectMap.find(parent->getObject());
|
|
if (it == ObjectMap.end())
|
|
continue;
|
|
|
|
populateParents(parent, parentMap);
|
|
for (auto item : it->second->items) {
|
|
if (!item->isHidden() && !item->populated) {
|
|
item->populated = true;
|
|
populateItem(item, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void DocumentItem::selectAllInstances(const ViewProviderDocumentObject& vpd) {
|
|
ViewParentMap parentMap;
|
|
auto pObject = vpd.getObject();
|
|
if (ObjectMap.find(pObject) == ObjectMap.end())
|
|
return;
|
|
|
|
bool lock = getTree()->blockSelection(true);
|
|
|
|
// We are trying to select all items corresponding to a given view
|
|
// provider, i.e. all appearance of the object inside all its parent items
|
|
//
|
|
// Build a map of object to all its parent
|
|
for (auto& v : ObjectMap) {
|
|
if (v.second->viewObject == &vpd) continue;
|
|
for (auto child : v.second->viewObject->claimChildren()) {
|
|
auto vp = getViewProvider(child);
|
|
if (!vp) continue;
|
|
parentMap[vp].push_back(v.second->viewObject);
|
|
}
|
|
}
|
|
|
|
// now make sure all parent items are populated. In order to do that, we
|
|
// need to populate the oldest parent first
|
|
populateParents(&vpd, parentMap);
|
|
|
|
DocumentObjectItem* first = nullptr;
|
|
FOREACH_ITEM(item, vpd);
|
|
if (showItem(item, true) && !first)
|
|
first = item;
|
|
END_FOREACH_ITEM;
|
|
|
|
getTree()->blockSelection(lock);
|
|
if (first) {
|
|
treeWidget()->scrollToItem(first);
|
|
updateSelection();
|
|
}
|
|
}
|
|
|
|
bool DocumentItem::showHidden() const {
|
|
return pDocument->getDocument()->ShowHidden.getValue();
|
|
}
|
|
|
|
void DocumentItem::setShowHidden(bool show) {
|
|
pDocument->getDocument()->ShowHidden.setValue(show);
|
|
}
|
|
|
|
bool DocumentItem::showItem(DocumentObjectItem* item, bool select, bool force) {
|
|
auto parent = item->parent();
|
|
if (item->isHidden()) {
|
|
if (!force)
|
|
return false;
|
|
item->setHidden(false);
|
|
}
|
|
|
|
if (parent->type() == TreeWidget::ObjectType) {
|
|
if (!showItem(static_cast<DocumentObjectItem*>(parent), false))
|
|
return false;
|
|
auto pitem = static_cast<DocumentObjectItem*>(parent);
|
|
if (force || !pitem->object()->getObject()->testStatus(App::NoAutoExpand))
|
|
parent->setExpanded(true);
|
|
else if (!select)
|
|
return false;
|
|
}
|
|
else
|
|
parent->setExpanded(true);
|
|
|
|
if (select) {
|
|
item->setSelected(true);
|
|
item->setCheckState(true);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void DocumentItem::updateItemsVisibility(QTreeWidgetItem* item, bool show) {
|
|
if (item->type() == TreeWidget::ObjectType) {
|
|
auto objitem = static_cast<DocumentObjectItem*>(item);
|
|
objitem->setHidden(!show && !objitem->object()->showInTree());
|
|
}
|
|
for (int i = 0; i < item->childCount(); ++i)
|
|
updateItemsVisibility(item->child(i), show);
|
|
}
|
|
|
|
void DocumentItem::updateSelection() {
|
|
bool lock = getTree()->blockSelection(true);
|
|
updateSelection(this, false);
|
|
getTree()->blockSelection(lock);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
static int countItems;
|
|
|
|
DocumentObjectItem::DocumentObjectItem(DocumentItem* ownerDocItem, DocumentObjectDataPtr data)
|
|
: QTreeWidgetItem(TreeWidget::ObjectType)
|
|
, myOwner(ownerDocItem), myData(data), previousStatus(-1), selected(0), populated(false)
|
|
{
|
|
setFlags(flags() | Qt::ItemIsEditable | Qt::ItemIsUserCheckable);
|
|
setCheckState(false);
|
|
|
|
myData->insertItem(this);
|
|
++countItems;
|
|
TREE_LOG("Create item: " << countItems << ", " << object()->getObject()->getFullName());
|
|
}
|
|
|
|
DocumentObjectItem::~DocumentObjectItem()
|
|
{
|
|
--countItems;
|
|
TREE_LOG("Delete item: " << countItems << ", " << object()->getObject()->getFullName());
|
|
myData->removeItem(this);
|
|
|
|
if (myData->rootItem == this)
|
|
myData->rootItem = nullptr;
|
|
|
|
if (myOwner && myData->items.empty()) {
|
|
auto it = myOwner->_ParentMap.find(object()->getObject());
|
|
if (it != myOwner->_ParentMap.end() && !it->second.empty()) {
|
|
myOwner->PopulateObjects.push_back(*it->second.begin());
|
|
myOwner->getTree()->_updateStatus();
|
|
}
|
|
}
|
|
}
|
|
|
|
void DocumentObjectItem::restoreBackground() {
|
|
this->setBackground(0, this->bgBrush);
|
|
}
|
|
|
|
void DocumentObjectItem::setHighlight(bool set, Gui::HighlightMode high) {
|
|
QFont f = this->font(0);
|
|
auto highlight = [this, set](const QColor& col) {
|
|
if (set)
|
|
this->setBackground(0, col);
|
|
else
|
|
this->setBackground(0, QBrush());
|
|
this->bgBrush = this->background(0);
|
|
};
|
|
|
|
switch (high) {
|
|
case HighlightMode::Bold:
|
|
f.setBold(set);
|
|
break;
|
|
case HighlightMode::Italic:
|
|
f.setItalic(set);
|
|
break;
|
|
case HighlightMode::Underlined:
|
|
f.setUnderline(set);
|
|
break;
|
|
case HighlightMode::Overlined:
|
|
f.setOverline(set);
|
|
break;
|
|
case HighlightMode::StrikeOut:
|
|
f.setStrikeOut(set);
|
|
break;
|
|
case HighlightMode::Blue:
|
|
highlight(QColor(200, 200, 255));
|
|
break;
|
|
case HighlightMode::LightBlue:
|
|
highlight(QColor(230, 230, 255));
|
|
break;
|
|
case HighlightMode::UserDefined:
|
|
{
|
|
QColor color(230, 230, 255);
|
|
if (set) {
|
|
ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/TreeView");
|
|
bool bold = hGrp->GetBool("TreeActiveBold", true);
|
|
bool italic = hGrp->GetBool("TreeActiveItalic", false);
|
|
bool underlined = hGrp->GetBool("TreeActiveUnderlined", false);
|
|
bool overlined = hGrp->GetBool("TreeActiveOverlined", false);
|
|
f.setBold(bold);
|
|
f.setItalic(italic);
|
|
f.setUnderline(underlined);
|
|
f.setOverline(overlined);
|
|
|
|
unsigned long col = hGrp->GetUnsigned("TreeActiveColor", 1538528255);
|
|
color = Base::Color::fromPackedRGB<QColor>(col);
|
|
}
|
|
else {
|
|
f.setBold(false);
|
|
f.setItalic(false);
|
|
f.setUnderline(false);
|
|
f.setOverline(false);
|
|
}
|
|
highlight(color);
|
|
} break;
|
|
default:
|
|
break;
|
|
}
|
|
this->setFont(0, f);
|
|
}
|
|
|
|
const char* DocumentObjectItem::getTreeName() const
|
|
{
|
|
return myData->getTreeName();
|
|
}
|
|
|
|
Gui::ViewProviderDocumentObject* DocumentObjectItem::object() const
|
|
{
|
|
return myData->viewObject;
|
|
}
|
|
|
|
void DocumentObjectItem::testStatus(bool resetStatus)
|
|
{
|
|
QIcon icon, icon2;
|
|
testStatus(resetStatus, icon, icon2);
|
|
}
|
|
|
|
namespace {
|
|
enum Status {
|
|
Visible = 1 << 0,
|
|
Recompute = 1 << 1,
|
|
Error = 1 << 2,
|
|
Hidden = 1 << 3,
|
|
External = 1 << 4,
|
|
Freezed = 1 << 5
|
|
};
|
|
}
|
|
|
|
void DocumentObjectItem::testStatus(bool resetStatus, QIcon& icon1, QIcon& icon2)
|
|
{
|
|
// guard against calling this during destruction when tree widget may be nullptr
|
|
if (!treeWidget()) {
|
|
return;
|
|
}
|
|
|
|
App::DocumentObject* pObject = object()->getObject();
|
|
|
|
int visible = -1;
|
|
auto parentItem = getParentItem();
|
|
if (parentItem) {
|
|
Timing(testStatus1);
|
|
auto parent = parentItem->object()->getObject();
|
|
auto ext = parent->getExtensionByType<App::GroupExtension>(true, false);
|
|
if (!ext)
|
|
visible = parent->isElementVisible(pObject->getNameInDocument());
|
|
else {
|
|
// We are dealing with a plain group. It has special handling when
|
|
// linked, which allows it to have indpenedent visibility control.
|
|
// We need to go up the hierarchy and see if there is any link to
|
|
// it.
|
|
for (auto pp = parentItem->getParentItem(); pp; pp = pp->getParentItem()) {
|
|
auto obj = pp->object()->getObject();
|
|
if (!obj->hasExtension(App::GroupExtension::getExtensionClassTypeId(), false)) {
|
|
visible = pp->object()->getObject()->isElementVisible(pObject->getNameInDocument());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Timing(testStatus2);
|
|
|
|
if (visible < 0)
|
|
visible = object()->isShow() ? 1 : 0;
|
|
|
|
auto obj = object()->getObject();
|
|
auto linked = obj->getLinkedObject(false);
|
|
bool external = object()->getDocument() != getOwnerDocument()->document() ||
|
|
(linked && linked->getDocument() != obj->getDocument());
|
|
bool freezed = pObject->isFreezed();
|
|
|
|
int currentStatus =
|
|
((freezed ? 1 : 0) << 5) |
|
|
((external ? 1 : 0) << 4) |
|
|
((object()->showInTree() ? 0 : 1) << 3) |
|
|
((pObject->isError() ? 1 : 0) << 2) |
|
|
((pObject->isTouched() || pObject->mustExecute() == 1 ? 1 : 0) << 1) |
|
|
(visible ? 1 : 0);
|
|
|
|
TimingStop(testStatus2);
|
|
|
|
if (!resetStatus && previousStatus == currentStatus)
|
|
return;
|
|
|
|
_Timing(1, testStatus3);
|
|
|
|
previousStatus = currentStatus;
|
|
|
|
QIcon::Mode mode = QIcon::Normal;
|
|
if (currentStatus & Status::Visible) {
|
|
// Note: By default the foreground, i.e. text color is invalid
|
|
// to make use of the default color of the tree widget's palette.
|
|
// If we temporarily set this color to dark and reset to an invalid
|
|
// color again we cannot do it with setTextColor() or setForeground(),
|
|
// respectively, because for any reason the color would always switch
|
|
// to black which will lead to unreadable text if the system background
|
|
// hss already a dark color.
|
|
// However, it works if we set the appropriate role to an empty QVariant().
|
|
for (int column = 0; column < this->columnCount(); ++column) {
|
|
this->setData(column, Qt::ForegroundRole, QVariant());
|
|
}
|
|
}
|
|
else { // invisible
|
|
QStyleOptionViewItem opt;
|
|
// it can happen that a tree item is not attached to the tree widget (#0003025)
|
|
if (this->treeWidget())
|
|
opt.initFrom(this->treeWidget());
|
|
for (int column = 0; column < this->columnCount(); ++column) {
|
|
this->setForeground(column, opt.palette.color(QPalette::Disabled, QPalette::Text));
|
|
}
|
|
mode = QIcon::Disabled;
|
|
}
|
|
|
|
_TimingStop(1, testStatus3);
|
|
|
|
QIcon& icon = mode == QIcon::Normal ? icon1 : icon2;
|
|
|
|
if (icon.isNull()) {
|
|
Timing(getIcon);
|
|
QPixmap px;
|
|
if (currentStatus & Status::Error) {
|
|
static QPixmap pxError;
|
|
if (pxError.isNull()) {
|
|
// object is in error state
|
|
pxError = Gui::BitmapFactory().pixmapFromSvg("overlay_error", QSize(10, 10));
|
|
}
|
|
px = pxError;
|
|
}
|
|
else if (currentStatus & Status::Recompute) {
|
|
static QPixmap pxRecompute;
|
|
if (pxRecompute.isNull()) {
|
|
// object must be recomputed
|
|
pxRecompute = Gui::BitmapFactory().pixmapFromSvg("overlay_recompute", QSize(10, 10));
|
|
}
|
|
px = pxRecompute;
|
|
}
|
|
|
|
// get the original icon set
|
|
QIcon icon_org = object()->getIcon();
|
|
|
|
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
|
|
int w = getTree()->viewOptions().decorationSize.width();
|
|
#else
|
|
QStyleOptionViewItem opt;
|
|
getTree()->initViewItemOption(&opt);
|
|
int w = opt.decorationSize.width();
|
|
#endif
|
|
|
|
QPixmap pxOn, pxOff;
|
|
|
|
// if needed show small pixmap inside
|
|
if (!px.isNull()) {
|
|
pxOff = BitmapFactory().merge(icon_org.pixmap(w, w, mode, QIcon::Off),
|
|
px, BitmapFactoryInst::TopRight);
|
|
pxOn = BitmapFactory().merge(icon_org.pixmap(w, w, mode, QIcon::On),
|
|
px, BitmapFactoryInst::TopRight);
|
|
}
|
|
else {
|
|
pxOff = icon_org.pixmap(w, w, mode, QIcon::Off);
|
|
pxOn = icon_org.pixmap(w, w, mode, QIcon::On);
|
|
}
|
|
|
|
if (currentStatus & Status::Hidden) {
|
|
static QPixmap pxHidden;
|
|
if (pxHidden.isNull()) {
|
|
pxHidden = Gui::BitmapFactory().pixmapFromSvg("TreeItemVisible", QSize(10, 10));
|
|
}
|
|
pxOff = BitmapFactory().merge(pxOff, pxHidden, BitmapFactoryInst::TopLeft);
|
|
pxOn = BitmapFactory().merge(pxOn, pxHidden, BitmapFactoryInst::TopLeft);
|
|
}
|
|
|
|
if (currentStatus & Status::External) {
|
|
static QPixmap pxExternal;
|
|
constexpr int px = 12;
|
|
if (pxExternal.isNull()) {
|
|
pxExternal = Gui::BitmapFactory().pixmapFromSvg("LinkOverlay",
|
|
QSize(px, px));
|
|
}
|
|
pxOff = BitmapFactory().merge(pxOff, pxExternal, BitmapFactoryInst::BottomRight);
|
|
pxOn = BitmapFactory().merge(pxOn, pxExternal, BitmapFactoryInst::BottomRight);
|
|
}
|
|
|
|
if (currentStatus & Status::Freezed) {
|
|
static QPixmap pxFreeze;
|
|
if (pxFreeze.isNull()) {
|
|
// object is in freezed state
|
|
pxFreeze = Gui::BitmapFactory().pixmapFromSvg("Std_ToggleFreeze", QSize(16, 16));
|
|
}
|
|
pxOff = BitmapFactory().merge(pxOff, pxFreeze, BitmapFactoryInst::TopLeft);
|
|
pxOn = BitmapFactory().merge(pxOn, pxFreeze, BitmapFactoryInst::TopLeft);
|
|
}
|
|
|
|
icon.addPixmap(pxOn, QIcon::Normal, QIcon::On);
|
|
icon.addPixmap(pxOff, QIcon::Normal, QIcon::Off);
|
|
|
|
icon = object()->mergeColorfulOverlayIcons(icon);
|
|
|
|
if (isVisibilityIconEnabled()) {
|
|
static QPixmap pxVisible, pxInvisible;
|
|
if (pxVisible.isNull()) {
|
|
pxVisible = BitmapFactory().pixmap("TreeItemVisible");
|
|
}
|
|
if (pxInvisible.isNull()) {
|
|
pxInvisible = BitmapFactory().pixmap("TreeItemInvisible");
|
|
}
|
|
|
|
// Prepend the visibility pixmap to the final icon pixmaps and use these as the icon.
|
|
QIcon new_icon;
|
|
auto style = this->getTree()->style();
|
|
int const spacing = style->pixelMetric(QStyle::PM_LayoutHorizontalSpacing);
|
|
for (auto state: {QIcon::On, QIcon::Off}) {
|
|
QPixmap px_org = icon.pixmap(0xFFFF, 0xFFFF, QIcon::Normal, state);
|
|
|
|
QPixmap px(2*px_org.width() + spacing, px_org.height());
|
|
px.fill(Qt::transparent);
|
|
|
|
QPainter pt;
|
|
pt.begin(&px);
|
|
pt.setPen(Qt::NoPen);
|
|
if (object()->canToggleVisibility()) {
|
|
pt.drawPixmap(0, 0, px_org.width(), px_org.height(), (currentStatus & Status::Visible) ? pxVisible : pxInvisible);
|
|
}
|
|
pt.drawPixmap(px_org.width() + spacing, 0, px_org.width(), px_org.height(), px_org);
|
|
pt.end();
|
|
|
|
new_icon.addPixmap(px, QIcon::Normal, state);
|
|
}
|
|
icon = new_icon;
|
|
}
|
|
}
|
|
|
|
_Timing(2, setIcon);
|
|
this->setIcon(0, icon);
|
|
}
|
|
|
|
void DocumentObjectItem::displayStatusInfo()
|
|
{
|
|
App::DocumentObject* Obj = object()->getObject();
|
|
|
|
QString info = QApplication::translate(Obj->getTypeId().getName(), Obj->getStatusString());
|
|
|
|
if (Obj->mustExecute() == 1 && !Obj->isError())
|
|
info += TreeWidget::tr(" (but must be executed)");
|
|
|
|
QString status = TreeWidget::tr("%1, Internal name: %2")
|
|
.arg(info, QString::fromLatin1(Obj->getNameInDocument()));
|
|
|
|
if (!Obj->isError())
|
|
getMainWindow()->showMessage(status);
|
|
else {
|
|
getMainWindow()->showStatus(MainWindow::Err, status);
|
|
QTreeWidget* tree = this->treeWidget();
|
|
QPoint pos = tree->visualItemRect(this).topRight();
|
|
QToolTip::showText(tree->mapToGlobal(pos), info);
|
|
}
|
|
}
|
|
|
|
void DocumentObjectItem::setExpandedStatus(bool on)
|
|
{
|
|
if (getOwnerDocument()->document() == object()->getDocument())
|
|
object()->getObject()->setStatus(App::Expand, on);
|
|
}
|
|
|
|
void DocumentObjectItem::setData(int column, int role, const QVariant& value)
|
|
{
|
|
QVariant myValue(value);
|
|
if (role == Qt::EditRole && column <= 1) {
|
|
auto obj = object()->getObject();
|
|
auto& label = column ? obj->Label2 : obj->Label;
|
|
|
|
std::ostringstream str;
|
|
str << TreeWidget::tr("Rename").toStdString() << ' ' << getName() << '.' << label.getName();
|
|
|
|
// Explicitly open and commit a transaction since this is a single change here
|
|
// For more details: https://forum.freecad.org/viewtopic.php?f=3&t=72351
|
|
App::Document* doc = obj->getDocument();
|
|
doc->openTransaction(str.str().c_str());
|
|
label.setValue(value.toString().toUtf8().constData());
|
|
doc->commitTransaction();
|
|
|
|
myValue = QString::fromUtf8(label.getValue());
|
|
}
|
|
QTreeWidgetItem::setData(column, role, myValue);
|
|
}
|
|
|
|
bool DocumentObjectItem::isChildOfItem(DocumentObjectItem* item)
|
|
{
|
|
for (auto pitem = parent(); pitem; pitem = pitem->parent())
|
|
if (pitem == item)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
bool DocumentObjectItem::requiredAtRoot(bool excludeSelf) const {
|
|
if (myData->rootItem || object()->getDocument() != getOwnerDocument()->document())
|
|
return false;
|
|
bool checkMap = true;
|
|
for (auto item : myData->items) {
|
|
if (excludeSelf && item == this) continue;
|
|
auto pi = item->getParentItem();
|
|
if (!pi || pi->myData->removeChildrenFromRoot)
|
|
return false;
|
|
checkMap = false;
|
|
}
|
|
if (checkMap && myOwner) {
|
|
auto it = myOwner->_ParentMap.find(object()->getObject());
|
|
if (it != myOwner->_ParentMap.end()) {
|
|
// Reaching here means all items of this corresponding object is
|
|
// going to be deleted, but the object itself is not deleted and
|
|
// still being referred to by some parent item that is not expanded
|
|
// yet. So, we force populate at least one item of the parent
|
|
// object to make sure that there is at least one corresponding
|
|
// item for each object.
|
|
//
|
|
// PS: practically speaking, it won't hurt much to delete all the
|
|
// items, because the item will be auto created once the user
|
|
// expand its parent item. It only causes minor problems, such as,
|
|
// tree scroll to object command won't work properly.
|
|
|
|
for (auto parent : it->second) {
|
|
if (getOwnerDocument()->populateObject(parent))
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool DocumentObjectItem::isLink() const {
|
|
auto obj = object()->getObject();
|
|
auto linked = obj->getLinkedObject(false);
|
|
return linked && obj != linked;
|
|
}
|
|
|
|
bool DocumentObjectItem::isLinkFinal() const {
|
|
auto obj = object()->getObject();
|
|
auto linked = obj->getLinkedObject(false);
|
|
return linked && linked == linked->getLinkedObject(true);
|
|
}
|
|
|
|
|
|
bool DocumentObjectItem::isParentLink() const {
|
|
auto pi = getParentItem();
|
|
return pi && pi->isLink();
|
|
}
|
|
|
|
enum GroupType {
|
|
NotGroup = 0,
|
|
LinkGroup = 1,
|
|
PartGroup = 2,
|
|
SuperGroup = 3, //reversed for future
|
|
};
|
|
|
|
int DocumentObjectItem::isGroup() const {
|
|
auto obj = object()->getObject();
|
|
auto linked = obj->getLinkedObject(true);
|
|
if (linked && linked->hasExtension(
|
|
App::GeoFeatureGroupExtension::getExtensionClassTypeId()))
|
|
return PartGroup;
|
|
if (obj->hasChildElement())
|
|
return LinkGroup;
|
|
if (obj->hasExtension(App::GroupExtension::getExtensionClassTypeId(), false)) {
|
|
for (auto parent = getParentItem(); parent; parent = parent->getParentItem()) {
|
|
auto pobj = parent->object()->getObject();
|
|
if (pobj->hasExtension(App::GroupExtension::getExtensionClassTypeId(), false))
|
|
continue;
|
|
if (pobj->isElementVisible(obj->getNameInDocument()) >= 0)
|
|
return LinkGroup;
|
|
}
|
|
}
|
|
return NotGroup;
|
|
}
|
|
|
|
bool DocumentItem::isObjectShowable(App::DocumentObject* obj) {
|
|
auto itParents = _ParentMap.find(obj);
|
|
if (itParents == _ParentMap.end() || itParents->second.empty())
|
|
return true;
|
|
bool showable = true;
|
|
for (auto parent : itParents->second) {
|
|
if (parent->getDocument() != obj->getDocument())
|
|
continue;
|
|
if (!parent->hasChildElement()
|
|
&& parent->getLinkedObject(false) == parent)
|
|
return true;
|
|
showable = false;
|
|
}
|
|
return showable;
|
|
}
|
|
|
|
int DocumentObjectItem::isParentGroup() const {
|
|
auto pi = getParentItem();
|
|
return pi ? pi->isGroup() : 0;
|
|
}
|
|
|
|
DocumentObjectItem* DocumentObjectItem::getParentItem() const {
|
|
if (parent()->type() != TreeWidget::ObjectType)
|
|
return nullptr;
|
|
return static_cast<DocumentObjectItem*>(parent());
|
|
}
|
|
|
|
DocumentObjectItem* DocumentObjectItem::getNextSibling() const
|
|
{
|
|
QTreeWidgetItem* parent = this->parent();
|
|
if (parent) {
|
|
int index = parent->indexOfChild(const_cast<DocumentObjectItem*>(this));
|
|
if (index >= 0) {
|
|
while (++index < parent->childCount()) {
|
|
QTreeWidgetItem* sibling = parent->child(index);
|
|
if (sibling->type() == TreeWidget::ObjectType) {
|
|
return static_cast<DocumentObjectItem*>(sibling);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
DocumentObjectItem* DocumentObjectItem::getPreviousSibling() const
|
|
{
|
|
QTreeWidgetItem* parent = this->parent();
|
|
if (parent) {
|
|
int index = parent->indexOfChild(const_cast<DocumentObjectItem*>(this));
|
|
while (index > 0) {
|
|
QTreeWidgetItem* sibling = parent->child(--index);
|
|
if (sibling->type() == TreeWidget::ObjectType) {
|
|
return static_cast<DocumentObjectItem*>(sibling);
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
const char* DocumentObjectItem::getName() const {
|
|
const char* name = object()->getObject()->getNameInDocument();
|
|
return name ? name : "";
|
|
}
|
|
|
|
int DocumentObjectItem::getSubName(std::ostringstream& str, App::DocumentObject*& topParent) const
|
|
{
|
|
auto parent = getParentItem();
|
|
if (!parent)
|
|
return NotGroup;
|
|
int ret = parent->getSubName(str, topParent);
|
|
if (ret != SuperGroup) {
|
|
int group = parent->isGroup();
|
|
if (group == NotGroup) {
|
|
if (ret != PartGroup) {
|
|
// Handle this situation,
|
|
//
|
|
// LinkGroup
|
|
// |--PartExtrude
|
|
// |--Sketch
|
|
//
|
|
// This function traverse from top down, so, when seeing a
|
|
// non-group object 'PartExtrude', its following children should
|
|
// not be grouped, so must reset any previous parents here.
|
|
topParent = nullptr;
|
|
str.str(""); //reset the current subname
|
|
return NotGroup;
|
|
}
|
|
group = PartGroup;
|
|
}
|
|
ret = group;
|
|
}
|
|
|
|
auto obj = parent->object()->getObject();
|
|
if (!obj || !obj->isAttachedToDocument()) {
|
|
topParent = nullptr;
|
|
str.str("");
|
|
return NotGroup;
|
|
}
|
|
if (!topParent)
|
|
topParent = obj;
|
|
else if (!obj->redirectSubName(str, topParent, object()->getObject()))
|
|
str << obj->getNameInDocument() << '.';
|
|
return ret;
|
|
}
|
|
|
|
App::DocumentObject* DocumentObjectItem::getFullSubName(
|
|
std::ostringstream& str, DocumentObjectItem* parent) const
|
|
{
|
|
auto pi = getParentItem();
|
|
if (this == parent || !pi || (!parent && !pi->isGroup()))
|
|
return object()->getObject();
|
|
|
|
auto ret = pi->getFullSubName(str, parent);
|
|
str << getName() << '.';
|
|
return ret;
|
|
}
|
|
|
|
App::DocumentObject* DocumentObjectItem::getRelativeParent(
|
|
std::ostringstream& str, DocumentObjectItem* cousin,
|
|
App::DocumentObject** topParent, std::string* topSubname) const
|
|
{
|
|
std::ostringstream str2;
|
|
App::DocumentObject* top = nullptr, * top2 = nullptr;
|
|
getSubName(str, top);
|
|
if (topParent)
|
|
*topParent = top;
|
|
if (!top)
|
|
return nullptr;
|
|
if (topSubname)
|
|
*topSubname = str.str() + getName() + '.';
|
|
cousin->getSubName(str2, top2);
|
|
if (top != top2) {
|
|
str << getName() << '.';
|
|
return top;
|
|
}
|
|
|
|
auto subname = str.str();
|
|
auto subname2 = str2.str();
|
|
const char* sub = subname.c_str();
|
|
const char* sub2 = subname2.c_str();
|
|
while (true) {
|
|
const char* dot = strchr(sub, '.');
|
|
if (!dot) {
|
|
str.str("");
|
|
return nullptr;
|
|
}
|
|
const char* dot2 = strchr(sub2, '.');
|
|
if (!dot2 || dot - sub != dot2 - sub2 || strncmp(sub, sub2, dot - sub) != 0) {
|
|
auto substr = subname.substr(0, dot - subname.c_str() + 1);
|
|
auto ret = top->getSubObject(substr.c_str());
|
|
if (!top) {
|
|
FC_ERR("invalid subname " << top->getFullName() << '.' << substr);
|
|
str.str("");
|
|
return nullptr;
|
|
}
|
|
str.str("");
|
|
str << dot + 1 << getName() << '.';
|
|
return ret;
|
|
}
|
|
sub = dot + 1;
|
|
sub2 = dot2 + 1;
|
|
}
|
|
str.str("");
|
|
return nullptr;
|
|
}
|
|
|
|
void DocumentObjectItem::setCheckState(bool checked) {
|
|
if (isSelectionCheckBoxesEnabled())
|
|
QTreeWidgetItem::setCheckState(0, checked ? Qt::Checked : Qt::Unchecked);
|
|
else
|
|
setData(0, Qt::CheckStateRole, QVariant());
|
|
}
|
|
|
|
DocumentItem* DocumentObjectItem::getParentDocument() const {
|
|
return getTree()->getDocumentItem(object()->getDocument());
|
|
}
|
|
|
|
DocumentItem* DocumentObjectItem::getOwnerDocument() const {
|
|
return myOwner;
|
|
}
|
|
|
|
TreeWidget* DocumentObjectItem::getTree() const {
|
|
return static_cast<TreeWidget*>(treeWidget());
|
|
}
|
|
|
|
void DocumentObjectItem::getExpandedSnapshot(std::vector<bool>& snapshot) const
|
|
{
|
|
snapshot.push_back(isExpanded());
|
|
|
|
for (int i = 0; i < childCount(); ++i) {
|
|
static_cast<const DocumentObjectItem*>(child(i))->getExpandedSnapshot(snapshot);
|
|
}
|
|
}
|
|
|
|
void DocumentObjectItem::applyExpandedSnapshot(const std::vector<bool>& snapshot, std::vector<bool>::const_iterator& from)
|
|
{
|
|
setExpanded(*from++);
|
|
|
|
for (int i = 0; i < childCount(); ++i) {
|
|
static_cast<DocumentObjectItem*>(child(i))->applyExpandedSnapshot(snapshot, from);
|
|
}
|
|
}
|
|
|
|
#include "moc_Tree.cpp"
|