This is to make it shorter and easier to use. QT does the same thing with their qobject_cast.
5895 lines
207 KiB
C++
5895 lines
207 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 "PreCompiled.h"
|
|
|
|
#ifndef _PreComp_
|
|
# 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>
|
|
#endif
|
|
|
|
#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.getAttributeAsInteger("count");
|
|
for (int i = 0; i < count; ++i) {
|
|
reader.readElement("Expand");
|
|
auto& entry = (*this)[reader.getAttribute("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;
|
|
}
|
|
|
|
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->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..."));
|
|
this->searchObjectsAction->setStatusTip(tr("Search for objects"));
|
|
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);
|
|
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("Activate 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("Show a description column for items. An item's description can be set by pressing F2 (or your OS's edit button) or 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("Show 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().count() > 0) {
|
|
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();
|
|
// the dependent can in turn have dependents, thus add them recursively
|
|
for (auto itDepend = subObjectList.begin(); itDepend != subObjectList.end(); ++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());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<TreeWidget::SelInfo> TreeWidget::getSelection(App::Document* doc)
|
|
{
|
|
std::vector<SelInfo> ret;
|
|
|
|
TreeWidget* tree = instance();
|
|
if (!tree || !tree->isSelectionAttached()) {
|
|
for (auto pTree : Instances)
|
|
if (pTree->isSelectionAttached()) {
|
|
tree = pTree;
|
|
break;
|
|
}
|
|
}
|
|
if (!tree)
|
|
return ret;
|
|
|
|
if (tree->selectTimer->isActive())
|
|
tree->onSelectTimer();
|
|
else
|
|
tree->_updateStatus(false);
|
|
|
|
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 = dynamic_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 = dynamic_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 = dynamic_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 = dynamic_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 = dynamic_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.count(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 = dynamic_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.count(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)
|
|
{
|
|
// object item selected
|
|
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("Show 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("Create a group"));
|
|
|
|
this->relabelObjectAction->setText(tr("Rename"));
|
|
this->relabelObjectAction->setStatusTip(tr("Rename object"));
|
|
|
|
this->finishEditingAction->setText(tr("Finish editing"));
|
|
this->finishEditingAction->setStatusTip(tr("Finish 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("Close the document"));
|
|
|
|
#ifdef Q_OS_MAC
|
|
this->openFileLocationAction->setText(tr("Reveal in Finder"));
|
|
this->openFileLocationAction->setStatusTip(tr("Reveal the current file location in Finder"));
|
|
#else
|
|
this->openFileLocationAction->setText(tr("Open File Location"));
|
|
this->openFileLocationAction->setStatusTip(tr("Open the current file location"));
|
|
#endif
|
|
|
|
this->reloadDocAction->setText(tr("Reload document"));
|
|
this->reloadDocAction->setStatusTip(tr("Reload a partially loaded document"));
|
|
|
|
this->skipRecomputeAction->setText(tr("Skip recomputes"));
|
|
this->skipRecomputeAction->setStatusTip(tr("Enable or disable recomputations of document"));
|
|
|
|
this->allowPartialRecomputeAction->setText(tr("Allow partial recomputes"));
|
|
this->allowPartialRecomputeAction->setStatusTip(
|
|
tr("Enable or disable recomputating editing object when 'skip recomputation' is enabled"));
|
|
|
|
this->markRecomputeAction->setText(tr("Mark to recompute"));
|
|
this->markRecomputeAction->setStatusTip(tr("Mark this object to be recomputed"));
|
|
this->markRecomputeAction->setIcon(BitmapFactory().iconFromTheme("Std_MarkToRecompute"));
|
|
|
|
this->recomputeObjectAction->setText(tr("Recompute object"));
|
|
this->recomputeObjectAction->setStatusTip(tr("Recompute 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 (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 = dynamic_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());
|
|
|
|
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);
|
|
|
|
bool lock = blockSelection(true);
|
|
for (auto cit = items.begin(), citNext = cit; cit != items.end(); cit = citNext) {
|
|
++citNext;
|
|
(*cit)->myOwner = nullptr;
|
|
delete* cit;
|
|
}
|
|
blockSelection(lock);
|
|
|
|
// 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);
|
|
|
|
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 = dynamic_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.count(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.count(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)
|
|
{
|
|
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);
|
|
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"
|