/** * RobloxTreeWidget.cpp * Copyright (c) 2013 ROBLOX Corp. All Rights Reserved. */ #include "stdafx.h" #include "RobloxTreeWidget.h" // Qt Headers #include #include #include #include #include #include #include #include #include // Roblox Headers #include "v8datamodel/DataModel.h" #include "v8datamodel/Selection.h" #include "v8datamodel/Workspace.h" #include "v8datamodel/ChangeHistory.h" #include "v8datamodel/GuiObject.h" #include "Script/ModuleScript.h" #include "script/script.h" #include "util/ScopedAssign.h" #include "util/BrickColor.h" #include "ReflectionMetaData.h" #include "v8datamodel/PlayerGui.h" // Roblox Studio Headers #include "CommonInsertWidget.h" #include "LuaSourceBuffer.h" #include "QtUtilities.h" #include "RobloxMainWindow.h" #include "RobloxContextualHelp.h" #include "RobloxCustomWidgets.h" #include "RobloxDocManager.h" #include "UpdateUIManager.h" #include "RobloxIDEDoc.h" #include "boost/algorithm/string.hpp" #include "util/RobloxGoogleAnalytics.h" #include "RobloxSettings.h" const static std::string classSearchString = "classname:"; const static std::string nameSearchString = "name:"; static int nearestWidgetItemIndex(RobloxTreeWidgetItem *pParentTWI, int first, int last, int key); static QItemSelection mergeModelIndexes(const QList &indexes); FASTINTVARIABLE(StudioTreeWidgetProcessingTime, 100) FASTINTVARIABLE(StudioTreeWidgetFilterTime, 30) FASTINTVARIABLE(StudioTreeWidgetEventProcessingTime, 10) FASTFLAGVARIABLE(StudioDE8774CrashFixEnabled, false) FASTFLAGVARIABLE(StudioPushTreeWidgetUpdatesToMainThread, false) FASTFLAGVARIABLE(StudioTreeWidgetCheckDeletingFlagWhenDoingUpdates, false) FASTFLAG(StudioNewWiki) FASTFLAG(StudioMimeDataContainsInstancePath) FASTFLAG(StudioSeparateActionByActivationMethod) FASTFLAG(TeamCreateOptimizeRemoteSelection) LOGGROUP(Explorer) bool DepthCompare::operator() (const RobloxTreeWidgetItem* lhs, const RobloxTreeWidgetItem* rhs) { if (lhs->getTreeWidgetDepth() == rhs->getTreeWidgetDepth()) return lhs < rhs; return rhs->getTreeWidgetDepth() < lhs->getTreeWidgetDepth(); } //-------------------------------------------------------------------------------------------- // TreeWidgetDelegate //-------------------------------------------------------------------------------------------- class TreeWidgetDelegate: public QStyledItemDelegate { public: TreeWidgetDelegate(QObject* parent) : QStyledItemDelegate(parent) { } enum Role { OtherUsersHighlightingRole = Qt::UserRole+1, }; private: QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { return QStyledItemDelegate::sizeHint(option, index); } void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { // do the normal paint QStyledItemDelegate::paint(painter, option, index); // now paint other users selection // TODO: once we finalize the display, get widget width and add "..." QRect newRect(option.rect.topRight(), QSize(15, option.rect.height())); PlayersColorCollection playersData = index.data(OtherUsersHighlightingRole).value(); for (PlayersColorCollection::const_iterator iter = playersData.constBegin(); iter != playersData.constEnd(); ++iter) { painter->fillRect(newRect, QtUtilities::toQColor(RBX::BrickColor(iter.value()).color3())); newRect.translate(15, 0); } } }; InstanceUpdateHandler::InstanceUpdateHandler(boost::shared_ptr pInstance) : m_pInstance(pInstance) { // Attach listeners if (FFlag::StudioPushTreeWidgetUpdatesToMainThread) { m_cChildAddedConnection = m_pInstance->getOrCreateChildAddedSignal()->connect(boost::bind(&InstanceUpdateHandler::childAddedSignalHandler, this, _1)); m_cChildRemovedConnection = m_pInstance->getOrCreateChildRemovedSignal()->connect(boost::bind(&InstanceUpdateHandler::childRemovedSignalHandler, this, _1)); m_cPropertyChangedConnection = m_pInstance->propertyChangedSignal.connect(boost::bind(&InstanceUpdateHandler::propertyChangedSignalHandler, this, _1)); } else { m_cChildAddedConnection = m_pInstance->getOrCreateChildAddedSignal()->connect(boost::bind(&InstanceUpdateHandler::onChildAdded, this, _1)); m_cChildRemovedConnection = m_pInstance->getOrCreateChildRemovedSignal()->connect(boost::bind(&InstanceUpdateHandler::onChildRemoved, this, _1)); m_cPropertyChangedConnection = m_pInstance->propertyChangedSignal.connect(boost::bind(&InstanceUpdateHandler::onPropertyChanged, this, _1)); } } InstanceUpdateHandler::~InstanceUpdateHandler() { m_cChildRemovedConnection.disconnect(); m_cChildAddedConnection.disconnect(); m_cPropertyChangedConnection.disconnect(); m_PendingItemsToAdd.clear(); } void InstanceUpdateHandler::populateChildren(RobloxTreeWidget* pTreeWidget) { shared_ptr inst = getInstance(); RobloxTreeWidgetItem* pParent = getItemParent(); for (size_t i = 0; i < inst->numChildren(); ++i) { if (RBX::Instance* pChildInst = inst->getChild(i)) { RobloxTreeWidgetItem* pChildItem = pTreeWidget->findItemFromInstance(pChildInst); if (pChildItem) { bool currentlySelected = false; if (RBX::Selection* selection = RBX::ServiceProvider::find(getInstance().get())) { for (RBX::Instances::const_iterator iter = selection->begin(); iter != selection->end(); ++iter) { if ((*iter).get() == pChildInst) { currentlySelected = true; break; } } } if (pParent) { int index = pParent->getIndexToInsertAt(shared_from(pChildInst)); pParent->insertChild(index < 0 ? 0 : index, pChildItem); } else if (pTreeWidget) { int index = pTreeWidget->getIndexToInsertAt(shared_from(pChildInst)); pTreeWidget->insertTopLevelItem(index < 0 ? 0 : index, pChildItem); } if (currentlySelected) pTreeWidget->addItemToSelection(pChildInst); pChildItem->populateChildren(pTreeWidget); } } } } bool InstanceUpdateHandler::onChildAdded(shared_ptr pChild) { FASTLOG1(FLog::Explorer, "InstanceUpdateHander::onChildAdded, child: %p", pChild.get()); if (!pChild || !RobloxTreeWidgetItem::isExplorerItem(pChild)) return false; FASTLOGS(FLog::Explorer, "Child name: %s", pChild->getName()); RobloxTreeWidget *pTreeWidget = getTreeWidget(); if (!pTreeWidget || pTreeWidget->isDeletionRequested()) return false; RobloxTreeWidgetItem *pTreeWidgetParentItem = pTreeWidget->findItemFromInstance(pChild->getParent()); if (pTreeWidgetParentItem) pTreeWidgetParentItem->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); QMutexLocker lock(pTreeWidget->treeWidgetMutex()); if (RobloxIDEDoc::getIsCloudEditSession() && RBX::Security::Context::current().identity == RBX::Security::Replicator_) pTreeWidget->markLocationToSuppressScrolling(); RobloxTreeWidgetItem* pChildItem = pTreeWidget->findItemFromInstance(pChild.get()); if (pChildItem && (pTreeWidgetParentItem || pTreeWidget)) { pTreeWidget->removeItemFromRemovalList(pChildItem); bool currentlySelected = pChildItem->removeItemFromTreeWidget(); if (pTreeWidgetParentItem) { int index = pTreeWidgetParentItem->getIndexToInsertAt(pChild); pTreeWidgetParentItem->insertChild(index < 0 ? 0 : index, pChildItem); } else if (pTreeWidget) { int index = pTreeWidget->getIndexToInsertAt(pChild); pTreeWidget->insertTopLevelItem(index < 0 ? 0 : index, pChildItem); } pChildItem->setExpandedOnce(false); if (currentlySelected) pTreeWidget->addItemToSelection(pChild.get()); } else { m_PendingItemsToAdd.insert(pChild); } if (pTreeWidgetParentItem && pTreeWidgetParentItem->isExpanded()) { pTreeWidgetParentItem->updateFilterItems(); } pTreeWidget->addToUpdateList(pTreeWidgetParentItem); return true; } bool InstanceUpdateHandler::onChildRemoved(shared_ptr pChild) { FASTLOG1(FLog::Explorer, "InstanceUpdateHander::onChildRemoved, child: %p", pChild.get()); if (!pChild) return false; FASTLOGS(FLog::Explorer, "Child name: %s", pChild->getName()); RobloxTreeWidget *pTreeWidget = getTreeWidget(); if (!pTreeWidget || pTreeWidget->isDeletionRequested()) return false; QMutexLocker lock(pTreeWidget->treeWidgetMutex()); m_PendingItemsToAdd.erase(pChild); m_FilterItemsToAdd.erase(pChild); RobloxTreeWidgetItem *pTreeWidgetItem = pTreeWidget->findItemFromInstance(pChild.get()); if (pTreeWidgetItem) { pTreeWidget->requestItemDelete(pTreeWidgetItem); if (pTreeWidgetItem == pTreeWidget->lastSelectedItem()) pTreeWidget->eraseLastSelectedItem(); if (RobloxIDEDoc::getIsCloudEditSession()) { if (RBX::Security::Context::current().identity == RBX::Security::Replicator_) pTreeWidget->markLocationToSuppressScrolling(); pTreeWidget->itemDeletionRequested(pTreeWidgetItem); } } return true; } void InstanceUpdateHandler::onPropertyChanged(const RBX::Reflection::PropertyDescriptor* pDescriptor) { bool isNameChanged = (*pDescriptor==RBX::Instance::desc_Name); if (isNameChanged) { RobloxTreeWidget *pTreeWidget = getTreeWidget(); if (pTreeWidget && !pTreeWidget->isDeletionRequested()) { QMutexLocker lock(pTreeWidget->treeWidgetMutex()); processPropertyChange(); } } else if (*pDescriptor==RBX::Instance::propParent) { if (RobloxTreeWidget* pTreeWidget = getTreeWidget()) if (!pTreeWidget->isFilterEmpty() && getInstance()->getParent()) pTreeWidget->filterWidget(); } } RobloxTreeWidgetItem* InstanceUpdateHandler::processChildAdd(shared_ptr pInstance) { RobloxTreeWidget *pTreeWidget = getTreeWidget(); RobloxTreeWidgetItem *pWidgetItem = NULL; if (pTreeWidget->findItemFromInstance(pInstance.get())) return NULL; if (getItemParent()) { int index = getItemParent()->getIndexToInsertAt(pInstance); pWidgetItem = new RobloxTreeWidgetItem((index > 0) ? index : 0, getItemParent(), pInstance); } else { int index = pTreeWidget->getIndexToInsertAt(pInstance); pWidgetItem = new RobloxTreeWidgetItem((index > 0) ? index : 0, pTreeWidget, pInstance); } if (pWidgetItem && pInstance->numChildren() > 0) pWidgetItem->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); if (pWidgetItem) pWidgetItem->setViewState(); pTreeWidget->updateSelectionState(pInstance.get()); m_FilterItemsToAdd.erase(pInstance); m_PendingItemsToAdd.erase(pInstance); return pWidgetItem; } bool InstanceUpdateHandler::processChildrenAdd() { RobloxTreeWidget *pTreeWidget = getTreeWidget(); RBXASSERT(pTreeWidget); if (!pTreeWidget) return false; InstanceList_SPTR* instanceList = pTreeWidget->isFilterEmpty() ? &m_PendingItemsToAdd : &m_FilterItemsToAdd; if (instanceList->empty()) return false; bool itemsAdded = false; //Lock for a few items QMutexLocker lock(pTreeWidget->treeWidgetMutex()); // Do a few items for ( int i = 0 ; i < 10 ; ++i ) { InstanceList_SPTR::iterator iter = instanceList->begin(); if (iter == instanceList->end()) return itemsAdded; shared_ptr pInstance = *iter; processChildAdd(pInstance); itemsAdded = true; } return itemsAdded; } void InstanceUpdateHandler::removeFromRemovalList(RobloxTreeWidgetItem *pTreeWidgetItem) { m_PendingItemsToRemove.erase(pTreeWidgetItem); int numChild = pTreeWidgetItem->childCount(); if (!numChild) return; int currentChild = 0; RobloxTreeWidgetItem *pCurrentItem = NULL; while (currentChild < numChild) { pCurrentItem = static_cast(pTreeWidgetItem->child(currentChild)); if (pCurrentItem) removeFromRemovalList(pCurrentItem); ++currentChild; } } void InstanceUpdateHandler::processChildRemove() { if (!m_PendingItemsToRemove.size()) return; RobloxTreeWidget *pTreeWidget = getTreeWidget(); if (FFlag::StudioDE8774CrashFixEnabled) { QMutexLocker lock(pTreeWidget->treeWidgetMutex()); while (m_PendingItemsToRemove.begin() != m_PendingItemsToRemove.end()) { RobloxTreeWidgetItem *pTreeWidgetItem = *m_PendingItemsToRemove.begin(); if (!pTreeWidgetItem) continue; pTreeWidgetItem->aboutToDelete(pTreeWidget); removeFromRemovalList(pTreeWidgetItem); delete pTreeWidgetItem; } } else { std::vector itemsToRemove; // Copy the items to a temp collection { QMutexLocker lock(pTreeWidget->treeWidgetMutex()); for (TreeWidgetItemList::iterator iter = m_PendingItemsToRemove.begin(); iter != m_PendingItemsToRemove.end(); ++iter) itemsToRemove.push_back(*iter); m_PendingItemsToRemove.clear(); } // Process them RobloxTreeWidgetItem *pTreeWidgetItem = NULL; for (std::vector::iterator iter = itemsToRemove.begin(); iter!=itemsToRemove.end(); ++iter) { pTreeWidgetItem = *iter; if (!pTreeWidgetItem) continue; pTreeWidgetItem->aboutToDelete(pTreeWidget); //if (pTreeWidgetItem->parent()) // pTreeWidgetItem->parent()->removeChild(pTreeWidgetItem); delete pTreeWidgetItem; } } } RobloxTreeWidgetItem::RobloxTreeWidgetItem(int index, RobloxTreeWidget *pTreeWidget, boost::shared_ptr pInstance) : InstanceUpdateHandler(pInstance) , m_ItemInfo(0) , m_queuedForDeletion(false) { initData(); m_treeWidgetDepth = 1; //add it to map in tree view for find pTreeWidget->addInstance(m_pInstance.get(), this); //add item to tree view pTreeWidget->insertTopLevelItem(index, this); } RobloxTreeWidgetItem::RobloxTreeWidgetItem(int index, RobloxTreeWidgetItem *pParentWidgetItem, boost::shared_ptr pInstance) : InstanceUpdateHandler(pInstance) , m_ItemInfo(0) , m_queuedForDeletion(false) { initData(); m_treeWidgetDepth = pParentWidgetItem->getTreeWidgetDepth() + 1; //add it to map in tree view for find (cannot use current item's getTreeWidget) pParentWidgetItem->getTreeWidget()->addInstance(m_pInstance.get(), this); //add item as child pParentWidgetItem->insertChild(index, this); } RobloxTreeWidgetItem::~RobloxTreeWidgetItem() { RobloxTreeWidget* pTreeWidget = getTreeWidget(); if (!pTreeWidget) { //May not be in treeWidget RobloxExplorerWidget* robloxExplorer = static_cast(UpdateUIManager::Instance().getExplorerWidget()); pTreeWidget = robloxExplorer->getTreeWidget(); } if (pTreeWidget) { pTreeWidget->removeItemFromRemovalList(this); pTreeWidget->eraseInstance(getInstance().get()); } } void RobloxTreeWidgetItem::setData(int column, int role, const QVariant & value) { //only editing of column 0 i.e. name change is to be handled if (column != 0 || role != Qt::EditRole) { QTreeWidgetItem::setData(column, role, value); return; } try { QString label = value.toString(); if (!label.isEmpty() && label != m_pInstance->getName().c_str()) { RBX::DataModel::LegacyLock lock(getTreeWidget()->dataModel(), RBX::DataModelJob::Write); m_pInstance->setName(label.toStdString()); // check to see if rename failed if ( m_pInstance->getName() == label.toStdString() ) { //now update the tree widget data QTreeWidgetItem::setData(column, role, value); //set waypoint RBX::ChangeHistoryService::requestWaypoint("Rename", getTreeWidget()->dataModel().get()); getTreeWidget()->dataModel()->setDirty(true); } } } catch(std::exception& exp) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "Label edit failed : %s", exp.what()); } } void RobloxTreeWidgetItem::initData() { setText(0, m_pInstance->getName().c_str()); int imageIndex = getImageIndex(m_pInstance); setIcon(0, QIcon(QtUtilities::getPixmap(":/images/ClassImages.PNG", imageIndex))); m_ItemType = getItemType(m_pInstance); setDirty(true); setExpandedOnce(false); setData(1, ExpandRole, QVariant(false)); setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable|Qt::ItemIsEditable|Qt::ItemIsDragEnabled|Qt::ItemIsDropEnabled); if (hasChildren()) setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); } bool RobloxTreeWidgetItem::hasChildren() { bool result = false; if (m_pInstance->getChildren()) { RBX::Instances childInstances = *m_pInstance->getChildren(); if (!childInstances.empty()) { for (RBX::Instances::const_iterator iter = childInstances.begin(); iter != childInstances.end(); ++iter) { if (isExplorerItem(*iter)) { result = true; break; } } } } return result; } void RobloxTreeWidgetItem::setViewState() { RobloxTreeWidget* pTreeWidget = getTreeWidget(); if (pTreeWidget->isFilterEmpty()) { setExpanded(data(1, ExpandRole).toBool()); setTextColor(0, Qt::black); setHidden(false); setDisabled(false); } else if (pTreeWidget->isFoundItem(getInstance())) { setExpanded(true); setTextColor(0, Qt::black); setHidden(false); setDisabled(false); } else if (pTreeWidget->isAncestorOfFoundItem(getInstance())) { setExpanded(true); setTextColor(0, Qt::gray); setHidden(false); setDisabled(false); } else { setHidden(true); setDisabled(true); } } RobloxTreeWidget *RobloxTreeWidgetItem::getTreeWidget() { return dynamic_cast(treeWidget()); } RobloxTreeWidgetItem* RobloxTreeWidgetItem::getItemParent() { return this; } void RobloxTreeWidgetItem::updateFilterItems() { m_FilterItemsToAdd.clear(); RobloxTreeWidget* pTreeWidget = getTreeWidget(); RBXASSERT(pTreeWidget); if (!pTreeWidget) return; if (!pTreeWidget->isFilterEmpty()) { for (std::set >::const_iterator iter = m_PendingItemsToAdd.begin(); iter != m_PendingItemsToAdd.end(); ++iter) if (pTreeWidget->isAncestorOfFoundItem(*iter) || pTreeWidget->isFoundItem(*iter)) m_FilterItemsToAdd.insert(*iter); } } void RobloxTreeWidgetItem::requestItemExpand() { RobloxTreeWidget *pTreeWidget = getTreeWidget(); RBXASSERT(pTreeWidget); if (!pTreeWidget) return; if (pTreeWidget->isFilterEmpty()) setData(1, ExpandRole, QVariant(true)); if (m_pInstance->getChildren()) { RBX::Instances childInstances = *m_pInstance->getChildren(); if (childInstances.empty()) return; //add child in pending queue if (!isExpandedOnce()) { QMutexLocker lock(pTreeWidget->treeWidgetMutex()); for (RBX::Instances::const_iterator iter = childInstances.begin(); iter != childInstances.end(); ++iter) if(!pTreeWidget->findItemFromInstance(iter->get()) &&RobloxTreeWidgetItem::isExplorerItem(*iter)) m_PendingItemsToAdd.insert(*iter); } } setExpandedOnce(true); updateFilterItems(); pTreeWidget->addToUpdateList(this); } void RobloxTreeWidgetItem::takeAllChildren() { while (childCount() > 0) { QTreeWidgetItem* firstItem = child(0); RobloxTreeWidgetItem* treeItem = dynamic_cast(firstItem); if (treeItem) treeItem->takeAllChildren(); this->takeChild(0); } } void RobloxTreeWidgetItem::addPlayerSelection(int playerId, int color) { PlayersColorCollection playersData = data(0, TreeWidgetDelegate::OtherUsersHighlightingRole).value(); if (playersData.find(playerId) == playersData.end()) { playersData[playerId] = color; setData(0, TreeWidgetDelegate::OtherUsersHighlightingRole, QVariant::fromValue(playersData)); } } void RobloxTreeWidgetItem::removePlayerSelection(int playerId) { PlayersColorCollection playersData = data(0, TreeWidgetDelegate::OtherUsersHighlightingRole).value(); if (playersData.remove(playerId) > 0) setData(0, TreeWidgetDelegate::OtherUsersHighlightingRole, QVariant::fromValue(playersData)); } bool RobloxTreeWidgetItem::removeItemFromTreeWidget() { RobloxTreeWidget *pTreeWidget = getTreeWidget(); if (!pTreeWidget) return false; bool currentlySelected = false; if (RBX::Selection* selection = RBX::ServiceProvider::find(getInstance().get())) { for (RBX::Instances::const_iterator iter = selection->begin(); iter != selection->end(); ++iter) { if (*iter == getInstance()) { currentlySelected = true; break; } } } if (currentlySelected) pTreeWidget->blockSignals(true); if (RobloxTreeWidgetItem* previousParent = dynamic_cast(static_cast(this)->parent())) { int childIndex = previousParent->indexOfChild(this); takeAllChildren(); previousParent->takeChild(childIndex); } else { int childIndex = pTreeWidget->indexOfTopLevelItem(this); takeAllChildren(); pTreeWidget->takeTopLevelItem(childIndex); } if (currentlySelected) pTreeWidget->blockSignals(false); return currentlySelected; } void RobloxTreeWidgetItem::aboutToDelete(RobloxTreeWidget* pTreeWidget) { m_queuedForDeletion = true; m_cChildAddedConnection.disconnect(); m_cChildRemovedConnection.disconnect(); m_cPropertyChangedConnection.disconnect(); pTreeWidget->eraseInstance(getInstance().get()); pTreeWidget->removeItemFromRemovalList(this); pTreeWidget->removeFromUpdateList(this); int numChild = childCount(); if (!numChild) { removeItemFromTreeWidget(); return; } int currentChild = 0; RobloxTreeWidgetItem *pCurrentItem = NULL; while (currentChild < numChild) { pCurrentItem = static_cast(child(currentChild)); if (pCurrentItem) { pCurrentItem->aboutToDelete(pTreeWidget); pCurrentItem->deleteLater(); } ++currentChild; } if (!FFlag::StudioDE8774CrashFixEnabled) pTreeWidget->removeFromUpdateList(this); removeItemFromTreeWidget(); } bool RobloxTreeWidgetItem::onChildAdded(shared_ptr pChild) { if (FFlag::StudioTreeWidgetCheckDeletingFlagWhenDoingUpdates && m_queuedForDeletion) return false; FASTLOG1(FLog::Explorer, "RobloxTreeWidgetItem::onChildAdded, child: %p", pChild.get()); RobloxTreeWidget *pTreeWidget = getTreeWidget(); if (!pTreeWidget) return false; FASTLOGS(FLog::Explorer, "Child name: %s", pChild->getName()); //if not expanded once then just set dirty flag if (!isExpandedOnce()) { if (RobloxTreeWidgetItem::isExplorerItem(pChild)) { setDirty(true); setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); pTreeWidget->requestViewportUpdate(); } return true; } if (!InstanceUpdateHandler::onChildAdded(pChild)) return false; if (isExpandedOnce()) pTreeWidget->addToUpdateList(this); return true; } bool RobloxTreeWidgetItem::onChildRemoved(shared_ptr pChild) { if (FFlag::StudioTreeWidgetCheckDeletingFlagWhenDoingUpdates && m_queuedForDeletion) return false; FASTLOG1(FLog::Explorer, "RobloxTreeWidgetItem::onChildRemoved, child: %p", pChild.get()); //if child item has been added before then let it get removed! if (!InstanceUpdateHandler::onChildRemoved(pChild)) return false; FASTLOGS(FLog::Explorer, "Child name: %s", pChild->getName()); RobloxTreeWidget* treeWidget = getTreeWidget(); if (!treeWidget) return false; if (!hasChildren()) { setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicatorWhenChildless); getTreeWidget()->requestViewportUpdate(); } //request for an update if (isExpandedOnce()) getTreeWidget()->addToUpdateList(this); return true; } bool RobloxTreeWidgetItem::hasChildrenToProcess() { if (RobloxTreeWidget* pWidget = getTreeWidget()) if (!pWidget->isFilterEmpty()) return !m_FilterItemsToAdd.empty(); return !m_PendingItemsToAdd.empty(); } void RobloxTreeWidgetItem::processPropertyChange() { setText(0, m_pInstance->getName().c_str()); } bool RobloxTreeWidgetItem::handleOpen() { if(!m_pInstance) return false; // Open script document return RobloxDocManager::Instance().openDoc(LuaSourceBuffer::fromInstance(m_pInstance)); } int RobloxTreeWidgetItem::getIndexToInsertAt(shared_ptr pInstance) { int numChild = childCount(); if (!numChild) return -1; int itemTypeToAdd = RobloxTreeWidgetItem::getItemType(pInstance); int nearestIndex = nearestWidgetItemIndex(this, 0, childCount()-1, itemTypeToAdd); RobloxTreeWidgetItem *pCurrentItem = static_cast(child(nearestIndex)); if (!pCurrentItem || (itemTypeToAdd != pCurrentItem->itemType())) return nearestIndex; //if the type is same then we need to insert in the order of item's name if ((pInstance->getName() < pCurrentItem->getInstance()->getName())) { while (nearestIndex >= 0) { pCurrentItem = static_cast(child(nearestIndex)); if (!pCurrentItem) break; if((itemTypeToAdd != pCurrentItem->itemType()) || (pInstance->getName() > pCurrentItem->getInstance()->getName())) { nearestIndex++; break; } --nearestIndex; } } else { while (nearestIndex < childCount()) { pCurrentItem = static_cast(child(nearestIndex)); if (!pCurrentItem || (itemTypeToAdd != pCurrentItem->itemType()) || (pInstance->getName() < pCurrentItem->getInstance()->getName())) break; ++nearestIndex; } } return nearestIndex; } bool RobloxTreeWidgetItem::isExplorerItem(const shared_ptr& pInstance) { // Assume everything that isn't a direct child of data model should be shown in explorer if (pInstance->getParent() != RBX::DataModel::get(pInstance.get())) { RBX::CoreGuiService* cgs = RBX::ServiceProvider::find(pInstance.get()); if (cgs && pInstance->isDescendantOf(cgs)) return false; return true; } // For everything else, check the class metadata boost::shared_ptr pReflection = RBX::Reflection::Metadata::Reflection::singleton(); RBX::Reflection::Metadata::Class* pClassData = pReflection ? pReflection->get(pInstance->getDescriptor(), true) : NULL; if(pClassData && pClassData->isExplorerItem()) return true; return false; } int RobloxTreeWidgetItem::getImageIndex(const shared_ptr& pInstance) { boost::shared_ptr pReflection = RBX::Reflection::Metadata::Reflection::singleton(); RBX::Reflection::Metadata::Class* pClassData = pReflection ? pReflection->get(pInstance->getDescriptor(), true) : NULL; if(pClassData) return pClassData->getExplorerImageIndex(); return 0; } int RobloxTreeWidgetItem::getItemType(const shared_ptr& pInstance) { boost::shared_ptr pReflection = RBX::Reflection::Metadata::Reflection::singleton(); RBX::Reflection::Metadata::Class* pClassData = pReflection->get(pInstance->getDescriptor(), true); return pClassData ? pClassData->getExplorerOrder() : 0; } RobloxTreeWidget::RobloxTreeWidget(boost::shared_ptr pDataModel) : InstanceUpdateHandler(pDataModel) , m_pDataModel(pDataModel) , m_treeWidgetMutex(QMutex::Recursive) , m_pInstanceSelectionHandler(NULL) , m_pRubberBand(NULL) , m_bIgnoreInstanceSelectionChanged(false) , m_bIgnoreItemSelectionChanged(false) , m_bUpdateRequested(false) , m_bViewportUpdateRequested(false) , m_bDeletionRequested(false) , m_currentFilter("") , m_filterRunning(false) , m_lastSelectedItem(NULL) , m_isActive(false) , m_savedMarkerItem(NULL) { setHeaderHidden(true); setUniformRowHeights(true); setDragEnabled(true); setDragDropMode(QAbstractItemView::DragDrop); setDropIndicatorShown(true); setAutoScroll(true); setAutoScrollMargin(32); setSelectionMode(QAbstractItemView::ExtendedSelection); setSelectionBehavior(QAbstractItemView::SelectItems); header()->setStretchLastSection(false); header()->setResizeMode(0,QHeaderView::ResizeToContents); // Set edit trigger only for SelectionClicked and EditKeyPressed setEditTriggers(QAbstractItemView::SelectedClicked | QAbstractItemView::EditKeyPressed); if (!UpdateUIManager::Instance().getMainWindow().isRibbonStyle()) { setStyleSheet(STRINGIFY( QTreeView { show-decoration-selected: 1; selection-color: white; } QTreeView::item { border: 1px solid transparent; } QTreeView::item:hover { border: 1px solid #BFCDE4; background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #E7EFFD, stop: 1 #CBDAF1); } QTreeView::item:selected { border: 1px solid #567DBC; } QTreeView::item:selected:active { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6EA1F1, stop: 1 #567DBC); } QTreeView::item:selected:!active { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6B9BE8, stop: 1 #577FBF); } ) ); } initTreeView(pDataModel); connect(this, SIGNAL(helpTopicChanged(const QString&)), &RobloxContextualHelpService::singleton(), SLOT(onHelpTopicChanged(const QString&))); m_cDescendantAddedConnection = pDataModel->getWorkspace()->getOrCreateDescendantAddedSignal()->connect(boost::bind(&RobloxTreeWidget::onDescendantAdded, this, _1)); } RobloxTreeWidget::~RobloxTreeWidget() { deActivate(); m_InstanceMap.clear(); m_itemsToUpdate.clear(); m_selectedItems.clear(); m_unSelectedItems.clear(); } void RobloxTreeWidget::addInstance(const RBX::Instance *pInstance, RobloxTreeWidgetItem *pTreeWidgetItem) { m_InstanceMap[pInstance] = pTreeWidgetItem; } void RobloxTreeWidget::initTreeView(boost::shared_ptr pDataModel) { if (!pDataModel->getChildren()) return; RBX::Instances instances; instances = *pDataModel->getChildren(); std::for_each(instances.begin(), instances.end(), boost::bind(&RobloxTreeWidget::createTreeRoot, this, _1)); } void RobloxTreeWidget::focusInEvent(QFocusEvent *event) { eraseLastSelectedItem(); QTreeWidget::focusInEvent(event); Q_EMIT focusGained(); UpdateUIManager::Instance().updateToolBars(); } void RobloxTreeWidget::createTreeRoot(const boost::shared_ptr pInstance) { if (RobloxTreeWidgetItem::isExplorerItem(pInstance)) { int index = getIndexToInsertAt(pInstance); RobloxTreeWidgetItem* item = new RobloxTreeWidgetItem((index > 0) ? index : 0, this, pInstance); } } RobloxTreeWidget* RobloxTreeWidget::getTreeWidget() { return this; } RBX::Selection* RobloxTreeWidget::getSelection() { return RBX::ServiceProvider::create(m_pDataModel.get()); } boost::shared_ptr RobloxTreeWidget::dataModel() { return m_pDataModel; } void RobloxTreeWidget::eraseInstance(RBX::Instance *pInstance) { QMutexLocker lock(treeWidgetMutex()); m_selectedItems.erase(pInstance); m_unSelectedItems.erase(pInstance); InstanceMap::iterator iter = m_InstanceMap.find(pInstance); if (iter != m_InstanceMap.end()) { if (m_lastSelectedItem == iter->second) eraseLastSelectedItem(); removeFromUpdateList(iter->second); m_InstanceMap.erase(iter); } } RobloxTreeWidgetItem* RobloxTreeWidget::findItemFromInstance(const RBX::Instance *pInstance) { InstanceMap::iterator iter = m_InstanceMap.find(pInstance); if (iter != m_InstanceMap.end()) return iter->second; return NULL; } void RobloxTreeWidget::scrollToInstance(RBX::Instance *pInstance) { if(!pInstance) return; if(isValidSelection(pInstance)) { RobloxTreeWidgetItem* item = findItemFromInstance(pInstance); if(item) scrollToItem(item); else if (RobloxTreeWidgetItem* parentItem = findItemFromInstance(pInstance->getParent())) parentItem->setExpanded(true); } } int RobloxTreeWidget::getIndexToInsertAt(shared_ptr pInstance) { int numChild = topLevelItemCount(); if (!numChild) return -1; int itemTypeToAdd = RobloxTreeWidgetItem::getItemType(pInstance), currentChild = 0; RobloxTreeWidgetItem *pCurrentItem = NULL; while (currentChild < numChild) { pCurrentItem = static_cast(topLevelItem(currentChild)); if (itemTypeToAdd < pCurrentItem->itemType()) break; if (itemTypeToAdd == pCurrentItem->itemType()) { if (pInstance->getName() < pCurrentItem->getInstance()->getName()) break; } ++currentChild; } return currentChild; } void RobloxTreeWidget::activate() { if (m_isActive) return; m_isActive = true; connect((QTreeWidget *)this, SIGNAL(itemSelectionChanged()), this, SLOT(onItemSelectionChanged())); connect((QTreeWidget *)this, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(onItemExpanded(QTreeWidgetItem*))); connect((QTreeWidget *)this, SIGNAL(itemCollapsed(QTreeWidgetItem*)), this, SLOT(onItemCollapsed(QTreeWidgetItem*))); connect((QTreeWidget *)this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), this, SLOT(onItemDoubleClicked(QTreeWidgetItem*, int))); if (m_pDataModel) { RBX::DataModel::LegacyLock lock(m_pDataModel, RBX::DataModelJob::Write); RBX::Selection* pSelection = m_pDataModel->create(); if (pSelection) m_cInstanceSelectionChanged = pSelection->selectionChanged.connect(boost::bind(&RobloxTreeWidget::onInstanceSelectionChanged, this, _1)); } } void RobloxTreeWidget::deActivate() { if (!m_isActive) m_isActive = false; disconnect((QTreeWidget *)this, SIGNAL(itemSelectionChanged()), this, SLOT(onItemSelectionChanged())); m_cInstanceSelectionChanged.disconnect(); disconnect((QTreeWidget *)this, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(onItemExpanded(QTreeWidgetItem*))); disconnect((QTreeWidget *)this, SIGNAL(itemCollapsed(QTreeWidgetItem*)), this, SLOT(onItemCollapsed(QTreeWidgetItem*))); disconnect((QTreeWidget *)this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), this, SLOT(onItemDoubleClicked(QTreeWidgetItem*, int))); } bool RobloxTreeWidget::onChildAdded(shared_ptr pChild) { if (FFlag::StudioTreeWidgetCheckDeletingFlagWhenDoingUpdates && m_bDeletionRequested) return false; if (!InstanceUpdateHandler::onChildAdded(pChild)) return false; requestUpdate(); return true; } bool RobloxTreeWidget::onChildRemoved(shared_ptr pChild) { if (FFlag::StudioTreeWidgetCheckDeletingFlagWhenDoingUpdates && m_bDeletionRequested) return false; if (!InstanceUpdateHandler::onChildRemoved(pChild)) return false; requestUpdate(); return true; } void RobloxTreeWidget::onDescendantAdded(shared_ptr pDescendant) { if (!isFilterEmpty()) { if (filterInstance(pDescendant) && filterAncestors(pDescendant)) requestUpdate(); } } void RobloxTreeWidget::contextMenuEvent(QContextMenuEvent * evt) { QMenu menu; QList commonActions; UpdateUIManager::Instance().commonContextMenuActions(commonActions); QAction *pAction = NULL; for (int ii=0; ii(currentItem()); parent = item->getInstance().get(); } if (!parent) parent = m_pDataModel->getWorkspace(); menu.addAction( QIcon(QtUtilities::getPixmap(":/images/ClassImages.PNG",1)), // part icon "Insert Part", this, SLOT(onInsertPart()) ); menu.addMenu(InsertObjectWidget::createMenu(parent,this,SLOT(onInsertObject()))); } menu.addAction(pAction); } else menu.addSeparator(); } menu.addSeparator(); menu.addAction(UpdateUIManager::Instance().getMainWindow().launchHelpForSelectionAction); //TODO: Add other tree view actions connect(&menu, SIGNAL(aboutToShow()), &UpdateUIManager::Instance(), SLOT(onMenuShow())); connect(&menu, SIGNAL(aboutToHide()), &UpdateUIManager::Instance(), SLOT(onMenuHide())); menu.exec(evt->globalPos()); } /** * Callback for user clicking on insert part in context menu menu. */ void RobloxTreeWidget::onInsertPart() { InsertObjectWidget::InsertObject("Part",m_pDataModel,InsertObjectWidget::InsertMode_TreeWidget); if (FFlag::StudioSeparateActionByActivationMethod) UpdateUIManager::Instance().getMainWindow().sendActionCounterEvent("insertPartMainContext"); } /** * Callback for user clicking on insert basic object in context menu sub menu. */ void RobloxTreeWidget::onInsertObject() { QAction* action = static_cast(sender()); QString className = action->text(); InsertObjectWidget::InsertObject(className,m_pDataModel,InsertObjectWidget::InsertMode_TreeWidget); if (FFlag::StudioSeparateActionByActivationMethod) UpdateUIManager::Instance().getMainWindow().sendActionCounterEvent(QString("insert%1Context").arg(className)); } void RobloxTreeWidget::keyPressEvent(QKeyEvent *evt) { if ( (state() != EditingState) && ((evt->key() == Qt::Key_Enter) || (evt->key() == Qt::Key_Return)) ) { RobloxTreeWidgetItem* pTreeWidgetItem = dynamic_cast(currentItem()); //handle appropriate document open if (pTreeWidgetItem && pTreeWidgetItem->handleOpen()) { //accept event evt->accept(); return; } } if (QKeySequence(evt->key() | evt->modifiers()) == UpdateUIManager::Instance().getMainWindow().zoomExtentsAction->shortcut()) { UpdateUIManager::Instance().getMainWindow().zoomExtentsAction->activate(QAction::Trigger); evt->accept(); return; } QTreeWidget::keyPressEvent(evt); } void RobloxTreeWidget::processChildrenRemoval() { TreeWidgetItemList::const_iterator iter = m_itemsToRemove.begin(); while (iter != m_itemsToRemove.end()) { if (RobloxTreeWidgetItem* pTreeWidgetItem = *iter) { pTreeWidgetItem->aboutToDelete(this); pTreeWidgetItem->deleteLater(); } else { m_itemsToRemove.erase(iter); } iter = m_itemsToRemove.begin(); } } void RobloxTreeWidget::requestItemDelete(RobloxTreeWidgetItem* item) { for (int i = 0; i < item->childCount(); ++i) if (RobloxTreeWidgetItem* childItem = dynamic_cast((item)->child(i))) requestItemDelete(childItem); m_itemsToRemove.insert(item); removeFromUpdateList(item); } void RobloxTreeWidget::onInstanceSelectionChanged(const RBX::SelectionChanged& evt) { if (evt.addedItem != NULL) if(RBX::Instance *pInstance = evt.addedItem.get()) if(RBX::GuiObject* guiObject = dynamic_cast(pInstance)) guiObject->setSelectionBox(true); if (evt.removedItem != NULL) if(RBX::Instance *pInstance = evt.removedItem.get()) if(RBX::GuiObject* guiObject = dynamic_cast(pInstance)) guiObject->setSelectionBox(false); if (m_bIgnoreInstanceSelectionChanged) return; RBX::ScopedAssign ignoreSelectionEvent(m_bIgnoreItemSelectionChanged, true); QMutexLocker lock(treeWidgetMutex()); bool isModificationRequired = false; if (evt.addedItem != NULL) { RBX::Instance* pInstance = evt.addedItem.get(); if (isValidSelection(pInstance)) { m_unSelectedItems.erase(pInstance); m_selectedItems.insert(pInstance); isModificationRequired = true; } } if (evt.removedItem != NULL) { RBX::Instance* pInstance = evt.removedItem.get(); m_selectedItems.erase(pInstance); m_unSelectedItems.insert(pInstance); isModificationRequired = true; } if (isModificationRequired) requestUpdate(); } void RobloxTreeWidget::onCloudEditSelectionChanged(int playerId) { RBXASSERT(QThread::currentThread() == qApp->thread()); if (!m_pPlayersDataManager) { RBXASSERT(false); return; } bool enabled = updatesEnabled(); setUpdatesEnabled(false); if (FFlag::TeamCreateOptimizeRemoteSelection) { shared_ptr oldSelection; PlayerSelectionMap::iterator iter = m_prevCloudEditSelection.find(playerId); if (iter != m_prevCloudEditSelection.end()) oldSelection = iter->second; else oldSelection.reset(new RBX::Instances()); RBX::DataModel::LegacyLock lock(m_pDataModel, RBX::DataModelJob::Write); RBX::Instances::const_iterator findIter; shared_ptr currentSelection = m_pPlayersDataManager->getLastKnownSelection(playerId); bool blockSignals = model()->blockSignals(true); for (auto& selectedItem : *currentSelection) { findIter = std::find(oldSelection->begin(), oldSelection->end(), selectedItem); if (findIter == oldSelection->end()) { if (RobloxTreeWidgetItem* item = findItemFromInstance(selectedItem.get())) item->addPlayerSelection(playerId, m_pPlayersDataManager->getPlayerColor(playerId)); } } for (auto& oldSelectedItem : *oldSelection) { findIter = std::find(currentSelection->begin(), currentSelection->end(), oldSelectedItem); if (findIter == currentSelection->end()) { if (RobloxTreeWidgetItem* item = findItemFromInstance(oldSelectedItem.get())) item->removePlayerSelection(playerId); } } model()->blockSignals(blockSignals); // TODO: check if we need to break it into smaller chunks to clear main thread asap? // save current selection so we can update unselected instances in next change m_prevCloudEditSelection[playerId] = currentSelection; setUpdatesEnabled(enabled); } else { RBX::Instances itemsToUnselect; PlayerSelectionMap::iterator iter = m_prevCloudEditSelection.find(playerId); if (iter != m_prevCloudEditSelection.end()) itemsToUnselect = *(iter->second); RBX::DataModel::LegacyLock lock(m_pDataModel, RBX::DataModelJob::Write); // TODO: check if we need to break it into smaller chunks to clear main thread asap? RBX::Instances::iterator findIter; shared_ptr currentSelection = m_pPlayersDataManager->getLastKnownSelection(playerId); for (auto& selectedItem : *currentSelection) { findIter = std::find(itemsToUnselect.begin(), itemsToUnselect.end(), selectedItem); if (findIter == itemsToUnselect.end()) { if (RobloxTreeWidgetItem* item = findItemFromInstance(selectedItem.get())) item->addPlayerSelection(playerId, m_pPlayersDataManager->getPlayerColor(playerId)); } else { itemsToUnselect.erase(findIter); } } for (auto& unselectedItem : itemsToUnselect) { if (RobloxTreeWidgetItem* item = findItemFromInstance(unselectedItem.get())) item->removePlayerSelection(playerId); } // save current selection so we can update unselected instances in next change m_prevCloudEditSelection[playerId] = currentSelection; setUpdatesEnabled(enabled); } } bool RobloxTreeWidget::isSearchTimeUp(const RBX::Time& startTime, int duration) { return (RBX::Time::now() - startTime).msec() > duration; } void RobloxTreeWidget::addFilterItemsToUpdateList() { for (std::set >::const_iterator iter = m_ancestorsOfFoundItems.begin(); iter != m_ancestorsOfFoundItems.end(); ++iter) if (RobloxTreeWidgetItem* item = findItemFromInstance(iter->get())) addToUpdateList(item); for (std::set >::const_iterator iter = m_foundItems.begin(); iter != m_foundItems.end(); ++iter) if (RobloxTreeWidgetItem* item = findItemFromInstance(iter->get())) addToUpdateList(item); } void RobloxTreeWidget::onFilterWidgetUpdate() { if (isHidden()) return; Q_EMIT startedProcessing(); FilterDeque_SPTR previousFilterStack = FilterDeque_SPTR(m_currentFilterStack.begin(), m_currentFilterStack.end()); m_currentFilterStack.clear(); { RBX::DataModel::LegacyLock lock(m_pDataModel, RBX::DataModelJob::Read); filterWidgetRecursive(dataModel(), previousFilterStack, RBX::Time::now()); } if (!m_currentFilterStack.empty()) { QTimer::singleShot(FInt::StudioTreeWidgetEventProcessingTime, this, SLOT(onFilterWidgetUpdate())); } else { m_filterRunning = false; addFilterItemsToUpdateList(); requestUpdate(); Q_EMIT filterSearchFinished(); } } bool RobloxTreeWidget::filterAncestors(shared_ptr instance) { if (!instance->getParent()) return true; if (filterAncestors(shared_from(instance->getParent()))) { if (!RobloxTreeWidgetItem::isExplorerItem(instance)) return false; if (filterInstance(instance)) m_foundItems.insert(instance); else m_ancestorsOfFoundItems.insert(instance); if (RobloxTreeWidgetItem* item = findItemFromInstance(instance.get())) addToUpdateList(item); return true; } return false; } void RobloxTreeWidget::filterWidget(const QString& searchString) { setWidgetFilter(searchString); filterWidget(); } void RobloxTreeWidget::filterWidget() { m_currentFilterStack.clear(); m_foundItems.clear(); m_ancestorsOfFoundItems.clear(); m_itemsToUpdate.clear(); for (int i = 0; i < invisibleRootItem()->childCount(); ++i) { invisibleRootItem()->child(i)->setDisabled(true); invisibleRootItem()->child(i)->setHidden(true); } if (!m_filterRunning) { m_filterRunning = true; QTimer::singleShot(0, this, SLOT(onFilterWidgetUpdate())); } } bool RobloxTreeWidget::filterInstance(shared_ptr instance) { for (ParamList::const_iterator iter = m_searchParamList.begin(); iter != m_searchParamList.end(); ++iter) { switch (iter->second) { case ParamAny: if (!boost::icontains(instance->getName(), iter->first) && !boost::icontains(instance->getClassNameStr(), iter->first)) return false; break; case ParamName: if (!boost::icontains(instance->getName(), iter->first)) return false; break; case ParamType: if (!boost::icontains(instance->getClassNameStr(), iter->first)) return false; break; } } return true; } bool RobloxTreeWidget::filterWidgetRecursive(shared_ptr instance, FilterDeque_SPTR& previousFilterStack, const RBX::Time& startTime) { if (!RobloxTreeWidgetItem::isExplorerItem(instance)) return false; bool parentOfShownItem = false; if (instance->getChildren()) { shared_ptr startingChild; bool stackFrontFound = true; //using previousFilterStack to start from last search location if (!previousFilterStack.empty()) { stackFrontFound = false; startingChild = previousFilterStack.front(); previousFilterStack.pop_front(); //if stack is broken, we continue through all children if (startingChild->getParent() != instance.get()) { previousFilterStack.clear(); stackFrontFound = true; } } for (RBX::Instances::const_iterator iter = instance->getChildren()->begin(); iter != instance->getChildren()->end(); ++iter) { if (!stackFrontFound && *iter == startingChild) stackFrontFound = true; m_currentFilterStack.push_back(*iter); if (stackFrontFound && filterWidgetRecursive(*iter, previousFilterStack, startTime)) parentOfShownItem = true; if (isSearchTimeUp(startTime, FInt::StudioTreeWidgetFilterTime)) break; m_currentFilterStack.pop_back(); } } else { previousFilterStack.clear(); } //This is a check to see if filter is empty or instance is the datamodel if (isFilterEmpty() || !instance->getParent()) { //Restore item to it's original state if (RobloxTreeWidgetItem* item = findItemFromInstance(instance.get())) item->setViewState(); return false; } if (filterInstance(instance)) { m_foundItems.insert(instance); return true; } else if (parentOfShownItem) { m_ancestorsOfFoundItems.insert(instance); return true; } else { //This is hiding the item if there is a filter if (RobloxTreeWidgetItem* item = findItemFromInstance(instance.get())) item->setViewState(); } return false; } void RobloxTreeWidget::setWidgetFilter(const QString& filterString) { m_currentFilter = filterString; QStringList filterList = filterString.split(" ", QString::SkipEmptyParts); bool nextIsName = false; bool nextIsType = false; m_searchParamList.clear(); for (QStringList::const_iterator iter = filterList.begin(); iter != filterList.end(); ++iter) { if (nextIsName) { nextIsName = false; m_searchParamList.push_back(StringParamPair(iter->toStdString(), ParamName)); } else if (nextIsType) { nextIsType = false; m_searchParamList.push_back(StringParamPair(iter->toStdString(), ParamType)); } else if (iter->startsWith(nameSearchString.c_str(), Qt::CaseInsensitive)) { QString rightSide = iter->right(iter->size() - nameSearchString.size()); if (rightSide.isEmpty()) nextIsName = true; else m_searchParamList.push_back(StringParamPair(rightSide.toStdString(), ParamName)); } else if (iter->startsWith(classSearchString.c_str(), Qt::CaseInsensitive)) { QString rightSide = iter->right(iter->size() - classSearchString.size()); if (rightSide.isEmpty()) nextIsType = true; else m_searchParamList.push_back(StringParamPair(rightSide.toStdString(), ParamType)); } else { m_searchParamList.push_back(StringParamPair(iter->toStdString(), ParamAny)); } } } bool RobloxTreeWidget::isFoundItem(shared_ptr instance) { return m_foundItems.count(instance) > 0; } bool RobloxTreeWidget::isAncestorOfFoundItem(shared_ptr instance) { return m_ancestorsOfFoundItems.count(instance) > 0; } bool RobloxTreeWidget::isValidSelection(RBX::Instance *pInstance) { if (!pInstance) return false; if (!pInstance->getParent()) return true; if (isValidSelection(pInstance->getParent())) { if (!RobloxTreeWidgetItem::isExplorerItem(shared_from(pInstance))) return false; return true; } return false; } void RobloxTreeWidget::onItemSelectionChanged() { if (m_bIgnoreItemSelectionChanged) return; RBX::Selection* pSelection(getSelection()); if (!pSelection) return; if (m_pInstanceSelectionHandler) { boost::shared_ptr selectedInstance; QList selectedItems = this->selectedItems(); if (selectedItems.size() > 0) { RobloxTreeWidgetItem *pTreeWidgetItem = dynamic_cast(selectedItems.at(0)); if (pTreeWidgetItem) selectedInstance = pTreeWidgetItem->getInstance(); } m_pInstanceSelectionHandler->onInstanceSelected(selectedInstance); return; } RBX::ScopedAssign ignoreSelectionEvent(m_bIgnoreInstanceSelectionChanged, true); RBX::DataModel::LegacyLock lock(m_pDataModel, RBX::DataModelJob::Write); QList selectedItems = this->selectedItems(); shared_ptr instances(new RBX::Instances()); for (QList::const_iterator iter = selectedItems.begin(); iter != selectedItems.end(); ++iter) { if (RobloxTreeWidgetItem* pTreeWidgetItem = dynamic_cast(*iter)) { if (shared_ptr instance = pTreeWidgetItem->getInstance()) instances->push_back(instance); } } pSelection->setSelection(instances); if (currentItem()) { RobloxTreeWidgetItem *pTreeWidgetItem = dynamic_cast(currentItem()); QString className = pTreeWidgetItem->getInstance()->getClassName().c_str(); if (FFlag::StudioNewWiki) className.prepend("API:Class/"); Q_EMIT(helpTopicChanged(className)); } //HACK: to get toolbars updated for object selection UpdateUIManager::Instance().updateToolBars(); } void RobloxTreeWidget::modifyItemsSelectionState(InstanceList_PTR &instancesList, bool select) { QList modelIndexes; for (InstanceList_PTR::const_iterator iter = instancesList.begin(); iter != instancesList.end(); ) { if (RobloxTreeWidgetItem *pTreeWidgetItem = findItemFromInstance(*iter)) { modelIndexes.push_back(indexFromItem(pTreeWidgetItem)); if (select) { m_lastSelectedItem = pTreeWidgetItem; instancesList.erase(iter++); } else { ++iter; } } else { ++iter; } } qStableSort(modelIndexes.begin(), modelIndexes.end()); selectionModel()->select(mergeModelIndexes(modelIndexes), select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); } void RobloxTreeWidget::onItemExpanded(QTreeWidgetItem *pItem) { if (!pItem) return; RobloxTreeWidgetItem* robloxTreeWidgetItem = static_cast(pItem); robloxTreeWidgetItem->requestItemExpand(); } void RobloxTreeWidget::onItemCollapsed(QTreeWidgetItem *pItem) { if (!pItem) return; if (isFilterEmpty()) pItem->setData(1, ExpandRole, QVariant(false)); } void RobloxTreeWidget::onItemDoubleClicked(QTreeWidgetItem *pWidgetItem, int) { if (!pWidgetItem) return; static_cast(pWidgetItem)->handleOpen(); } void RobloxTreeWidget::addToUpdateList(RobloxTreeWidgetItem *pTreeWidgetItem) { if (!pTreeWidgetItem) return; QMutexLocker lock(treeWidgetMutex()); if (pTreeWidgetItem->isItemPendingDeletion()) return; pTreeWidgetItem->setViewState(); if (existsInRemovalList(pTreeWidgetItem) || !pTreeWidgetItem->isInTreeWidget()) return; std::pair retIter = m_itemsToUpdate.insert(pTreeWidgetItem); if (retIter.second) requestUpdate(); } void RobloxTreeWidget::removeFromUpdateList(RobloxTreeWidgetItem *pTreeWidgetItem) { QMutexLocker lock(treeWidgetMutex()); m_itemsToUpdate.erase(pTreeWidgetItem); } void RobloxTreeWidget::requestUpdate() { if (m_bUpdateRequested) return; //qt will take ownership of the event so no need to delete QApplication::postEvent(this, new RobloxCustomEvent(TREE_WIDGET_UPDATE)); m_bUpdateRequested = true; } void RobloxTreeWidget::requestViewportUpdate() { if (m_bViewportUpdateRequested || m_bUpdateRequested) return; //qt will take ownership of the event so no need to delete QApplication::postEvent(this, new RobloxCustomEvent(TREE_WIDGET_VIEWPORT_UPDATE)); m_bViewportUpdateRequested = true; } void RobloxTreeWidget::updateSelectionState(RBX::Instance *pInstance) { RBX::Selection* pSelection(getSelection()); if (pSelection && pSelection->isSelected(pInstance)) { QMutexLocker lock(treeWidgetMutex()); m_unSelectedItems.erase(pInstance); m_selectedItems.insert(pInstance); } // If items are added after updating the highlighting state for CloudEdit mode, update now RBX::Instances::const_iterator selectedIter; for (auto& playersSelection : m_prevCloudEditSelection) { selectedIter = std::find(playersSelection.second->begin(), playersSelection.second->end(), shared_from(pInstance)); if (selectedIter != playersSelection.second->end()) { if (RobloxTreeWidgetItem* pTreeWidgetItem = findItemFromInstance(pInstance)) pTreeWidgetItem->addPlayerSelection(playersSelection.first, m_pPlayersDataManager->getPlayerColor(playersSelection.first)); } } } bool RobloxTreeWidget::event(QEvent* evt) { bool retVal = false; if(evt->type() == TREE_WIDGET_UPDATE) { onTreeWidgetUpdate(); m_bUpdateRequested = false; retVal = true; } else if(evt->type() == TREE_WIDGET_FILTER_UPDATE) { onFilterWidgetUpdate(); } else if(evt->type() == TREE_SCROLL_TO_INSTANCE) { RobloxCustomEventWithArg *pCustomEvent = dynamic_cast(evt); if (pCustomEvent) { boost::function* pFunctionObj = pCustomEvent->m_pEventArg; if (pFunctionObj) (*pFunctionObj)(); } retVal = true; } else if (evt->type() == TREE_WIDGET_VIEWPORT_UPDATE) { m_bViewportUpdateRequested = false; viewport()->update(); retVal = true; } else if ( evt->type() == QEvent::ShortcutOverride ) { retVal = QTreeWidget::event(evt); QKeyEvent* keyEvent = static_cast(evt); QChar key = keyEvent->key(); // capture single key alpha numerical presses - we want these to be considered // as shortcuts in the tree to jumped to items starting with that key, not global shortcuts if ( !(keyEvent->key() >= Qt::Key_F1 && keyEvent->key() <= Qt::Key_F35) && key.isLetterOrNumber() && keyEvent->modifiers() == Qt::NoModifier ) { keyEvent->accept(); retVal = true; } } else retVal = QTreeWidget::event(evt); return retVal; } void RobloxTreeWidget::onTreeWidgetUpdate() { if (isHidden() || m_filterRunning) return; QCoreApplication::removePostedEvents(this, TREE_WIDGET_VIEWPORT_UPDATE); //prevents screen flicker setUpdatesEnabled(false); RBX::ScopedAssign ignoreSelectionEvent(m_bIgnoreItemSelectionChanged, true); QMutexLocker treeLock(treeWidgetMutex()); if (!m_itemsToRemove.empty()) processChildrenRemoval(); int selectionSize = m_selectedItems.size(); //modify selection first, items not removed from list and items that have yet been populated { blockSignals(true); QMutexLocker lock(treeWidgetMutex()); if (!m_unSelectedItems.empty()) { modifyItemsSelectionState(m_unSelectedItems, false); m_unSelectedItems.clear(); } if (!m_selectedItems.empty()) modifyItemsSelectionState(m_selectedItems, true); selectionSize = m_selectedItems.size(); blockSignals(false); } bool itemsAdded = false; InstanceList_PTR itemsToSelect; RBX::Time startTime = RBX::Time::now(); std::vector recentlyAddedItems; //Processing Selection while (!m_selectedItems.empty() && isFilterEmpty() && !isSearchTimeUp(startTime, FInt::StudioTreeWidgetProcessingTime)) { RBX::Instance* pInstance = *m_selectedItems.begin(); RBX::Instance* pChild = pInstance; while (pChild->getParent() && !findItemFromInstance(pChild->getParent())) pChild = pChild->getParent(); RobloxTreeWidgetItem* newChildNode = NULL; if (RobloxTreeWidgetItem* pParentItem = findItemFromInstance(pChild->getParent())) { pParentItem->requestItemExpand(); newChildNode = pParentItem->processChildAdd(shared_from(pChild)); } else { m_selectedItems.erase(pInstance); } if (pChild == pInstance && newChildNode) { recentlyAddedItems.push_back(newChildNode); m_lastSelectedItem = newChildNode; m_selectedItems.erase(pInstance); } } //Processing items requesting update while (!m_itemsToUpdate.empty() && !isSearchTimeUp(startTime, FInt::StudioTreeWidgetProcessingTime)) { RobloxTreeWidgetItem* itemNode = *m_itemsToUpdate.begin(); if (itemNode) { itemNode->updateFilterItems(); if (itemNode->processChildrenAdd()) itemsAdded = true; if (!itemNode->hasChildrenToProcess()) { removeFromUpdateList(itemNode); itemNode->setDirty(false); } } } //recently added items if (!recentlyAddedItems.empty()) { QList modelIndexes; for (std::vector::const_iterator iter = recentlyAddedItems.begin(); iter != recentlyAddedItems.end(); ++iter) modelIndexes.push_back(indexFromItem(*iter)); qStableSort(modelIndexes.begin(), modelIndexes.end()); selectionModel()->select(mergeModelIndexes(modelIndexes), QItemSelectionModel::Select); } processChildrenAdd(); setUpdatesEnabled(true); m_bUpdateRequested = false; //again request for an update if (!m_itemsToRemove.empty() || !m_itemsToUpdate.empty() || itemsAdded || (!m_selectedItems.empty() && isFilterEmpty())) { QTimer::singleShot(0, this, SLOT(requestUpdate())); } else { if (m_lastSelectedItem) { scrollToItem(m_lastSelectedItem); eraseLastSelectedItem(); } if (m_savedMarkerItem) QMetaObject::invokeMethod(this, "scrollBackToLastMarkedLocation"); Q_EMIT finishedProcessing(); } } void RobloxTreeWidget::requestDelete() { m_bDeletionRequested = true; deActivate(); m_pInstance.reset(); m_pDataModel.reset(); setPlayersDataManager(boost::shared_ptr()); deleteLater(); } void RobloxTreeWidget::mousePressEvent(QMouseEvent * evt) { QTreeWidget::mousePressEvent(evt); RobloxTreeWidgetItem* pTreeWidgetItem = dynamic_cast(itemAt(evt->pos())); if (pTreeWidgetItem) { QString className = pTreeWidgetItem->getInstance()->getClassName().c_str(); if (FFlag::StudioNewWiki) className.prepend("API:Class/"); Q_EMIT(helpTopicChanged(className)); } // Check if ContextMenu was opened in the middle of dragging. if (m_pRubberBand) { m_pRubberBand->hide(); m_pRubberBand->deleteLater(); m_pRubberBand = NULL; } else { //although it will be set to NULL in mouseReleaseEvent but just to be sure setting it to NULL again m_pRubberBand = NULL; } //if we cannot drag select then continue with Qt event propagation if (!canDragSelect(evt)) { if (FFlag::StudioMimeDataContainsInstancePath) { QDrag *drag = new QDrag(this); QMimeData *mimeData = new QMimeData; mimeData->setText(("Game." + pTreeWidgetItem->getInstance()->getFullName()).c_str()); drag->setMimeData(mimeData); drag->exec(); } return; } //begin drag select m_initialOffset = QPoint(horizontalOffset(), verticalOffset()); m_rubberBandOrigin = evt->pos(); m_pRubberBand = new QRubberBand(QRubberBand::Rectangle, this); m_pRubberBand->setGeometry(QRect(m_rubberBandOrigin, QSize())); m_pRubberBand->show(); } void RobloxTreeWidget::mouseMoveEvent(QMouseEvent * evt) { if (!m_pRubberBand) { QTreeWidget::mouseMoveEvent(evt); if (m_pInstanceSelectionHandler) { boost::shared_ptr hoveredInstance; RobloxTreeWidgetItem* pTreeWidgetItem = dynamic_cast(itemAt(evt->pos())); if (pTreeWidgetItem) hoveredInstance = pTreeWidgetItem->getInstance(); m_pInstanceSelectionHandler->onInstanceHovered(hoveredInstance); } return; } //--- drag selection is ON (do the required updates) if (verticalScrollBar()) { if (evt->y() <= 1) verticalScrollBar()->setValue(verticalScrollBar()->value() + evt->y() - 1); else if (evt->y() >= (viewport()->height() - 1)) verticalScrollBar()->setValue(verticalScrollBar()->value() + evt->y() - viewport()->height() - 1); } if (horizontalScrollBar()) { if (evt->x() <= 1) horizontalScrollBar()->setValue(horizontalScrollBar()->value() + evt->x() - 1); else if (evt->y() >= (viewport()->width() - 1)) horizontalScrollBar()->setValue(horizontalScrollBar()->value() + evt->x() - viewport()->width() - 1); } QPoint offset = QPoint(horizontalOffset() - m_initialOffset.x(), verticalOffset() - m_initialOffset.y()); QRect rubberBandRect = QRect(m_rubberBandOrigin - offset, evt->pos()).normalized(); m_pRubberBand->setGeometry(rubberBandRect); setSelection(QRect(rubberBandRect.bottomLeft(), rubberBandRect.topLeft()), QItemSelectionModel::ClearAndSelect); } void RobloxTreeWidget::mouseReleaseEvent(QMouseEvent * evt) { if (!m_pRubberBand) { QTreeWidget::mouseReleaseEvent(evt); return; } //clean up rubberband m_pRubberBand->hide(); m_pRubberBand->deleteLater(); m_pRubberBand = NULL; } bool RobloxTreeWidget::canDragSelect(QMouseEvent * evt) { if (evt->buttons() != Qt::LeftButton || evt->modifiers() != Qt::NoModifier || m_pInstanceSelectionHandler) return false; QPoint mousePos = evt->pos(); QTreeWidgetItem *pTreeWidgetItem = itemAt(mousePos); if (pTreeWidgetItem) { // - make sure if the mouse is clicked outside the item label we must be able to drag select // - we must not activate drag selection if mouse click is on the left of the label (to ensure the expand keys work fine) // - item drag/drop will work when user clicks on the item label QRect itemRect = visualItemRect(pTreeWidgetItem); if ( !itemRect.contains(mousePos) || (itemRect.contains(mousePos) && (mousePos.x() < (itemRect.bottomLeft().x() + 28 + fontMetrics().width(pTreeWidgetItem->text(0))))) ) return false; } //-- we can do a drag select -- // item under mouse is already selected and the only selected item, we don't proceed further if (pTreeWidgetItem && pTreeWidgetItem->isSelected() && (selectedItems().size() == 1)) return true; //clear previously selected items clearSelection(); //if we have an item then select it if (pTreeWidgetItem) pTreeWidgetItem->setSelected(true); return true; } void RobloxTreeWidget::dragEnterEvent(QDragEnterEvent *evt) { const QMimeData *mime = evt->mimeData(); QString data = mime->text(); QTreeWidget::dragEnterEvent(evt); } void RobloxTreeWidget::dragMoveEvent(QDragMoveEvent * evt) { // call base class to handle auto scrolling QTreeWidget::dragMoveEvent(evt); evt->ignore(); if (RobloxTreeWidgetItem *pTreeWidgetItem = dynamic_cast(itemAt(evt->pos()))) { if (shared_ptr pInstance = pTreeWidgetItem->getInstance()) { RBX::Selection* selection = getSelection(); for (RBX::Instances::const_iterator iter = selection->begin(); iter != selection->end(); ++iter) if (pInstance == *iter || (*iter)->isAncestorOf2(pInstance) || (*iter)->getIsParentLocked()) return; evt->acceptProposedAction(); } } } static void setChildren(RBX::Instance* pParent, shared_ptr pChild); void RobloxTreeWidget::dropEvent(QDropEvent * evt) { try { RobloxTreeWidgetItem *pTreeWidgetItem = dynamic_cast(itemAt(evt->pos())); if (pTreeWidgetItem) { boost::shared_ptr pInstance = pTreeWidgetItem->getInstance(); if (pInstance) { RBX::DataModel::LegacyLock lock(m_pDataModel, RBX::DataModelJob::Write); pTreeWidgetItem->requestItemExpand(); std::for_each(getSelection()->begin(), getSelection()->end(), boost::bind(&setChildren, pInstance.get(), _1)); //add in history for undo/redo RBX::ChangeHistoryService::requestWaypoint("Drop", m_pDataModel.get()); m_pDataModel->setDirty(true); } } } catch (std::exception& e) { QtUtilities::RBXMessageBox msgBox; msgBox.setText(e.what()); msgBox.exec(); } evt->setDropAction(Qt::IgnoreAction); setState(NoState); } static void setChildren(RBX::Instance* pParent, shared_ptr pChild) { //self drag case (ideally qt should have already taken care of this) if (!pChild || pChild->getParent() == pParent) return; pChild->setParent(pParent); } static int nearestWidgetItemIndex(RobloxTreeWidgetItem *pParentWidgetItem, int first, int last, int itemTypeToAdd) { while (first <= last) { int mid = (first + last) / 2; RobloxTreeWidgetItem *pCurrentItem = static_cast(pParentWidgetItem->child(mid)); RBXASSERT(pCurrentItem != NULL); if (itemTypeToAdd == pCurrentItem->itemType()) return mid; else if (itemTypeToAdd > pCurrentItem->itemType()) first = mid+1; else last = mid-1; } return first; } static QItemSelection mergeModelIndexes(const QList &indexes) { QItemSelection colSpans; // merge columns int i = 0; while (i < indexes.count()) { QModelIndex tl = indexes.at(i); QModelIndex br = tl; while (++i < indexes.count()) { QModelIndex next = indexes.at(i); if ((next.parent() == br.parent()) && (next.row() == br.row()) && (next.column() == br.column() + 1)) br = next; else break; } colSpans.append(QItemSelectionRange(tl, br)); } // merge rows QItemSelection rowSpans; i = 0; while (i < colSpans.count()) { QModelIndex tl = colSpans.at(i).topLeft(); QModelIndex br = colSpans.at(i).bottomRight(); QModelIndex prevTl = tl; while (++i < colSpans.count()) { QModelIndex nextTl = colSpans.at(i).topLeft(); QModelIndex nextBr = colSpans.at(i).bottomRight(); if (nextTl.parent() != tl.parent()) break; if ((nextTl.column() == prevTl.column()) && (nextBr.column() == br.column()) && (nextTl.row() == prevTl.row() + 1) && (nextBr.row() == br.row() + 1)) { br = nextBr; prevTl = nextTl; } else { break; } } rowSpans.append(QItemSelectionRange(tl, br)); } return rowSpans; } void RobloxTreeWidget::setInstanceSelectionHandler(InstanceSelectionHandler* handler) { setMouseTracking(handler); m_pInstanceSelectionHandler = handler; } void RobloxTreeWidget::markLocationToSuppressScrolling() { if (m_savedMarkerItem == NULL) { m_savedMarkerItem = getMarkerItem(); } } void RobloxTreeWidget::scrollBackToLastMarkedLocation() { if (m_savedMarkerItem) { QTreeWidgetItem* currenMarkerItem = getMarkerItem(); if (currenMarkerItem != m_savedMarkerItem) { int savedRow = indexFromItem(m_savedMarkerItem).row(); int currentRow = indexFromItem(currenMarkerItem).row(); verticalScrollBar()->setValue(verticalScrollBar()->value() + (savedRow - currentRow) * verticalScrollBar()->singleStep()); } m_savedMarkerItem = NULL; } } QTreeWidgetItem* RobloxTreeWidget::getMarkerItem() { QList selection = selectedItems(); // if we've a single selection and the selected item is not hidden, the use it as benchmark if (selection.count() == 1 && !selection.at(0)->isHidden()) return selection.at(0); // or else use the top item as benchmark return itemAt(rect().topLeft()); } void RobloxTreeWidget::setPlayersDataManager(boost::shared_ptr playersDataManager) { if (m_pPlayersDataManager == playersDataManager) return; if (m_pPlayersDataManager) disconnect(m_pPlayersDataManager.get(), SIGNAL(cloudEditSelectionChanged(int)), this, SLOT(onCloudEditSelectionChanged(int))); m_TreeWidgetDelegate.reset(); m_pPlayersDataManager = playersDataManager; if (m_pPlayersDataManager) { m_TreeWidgetDelegate.reset(new TreeWidgetDelegate(this)); setItemDelegate(m_TreeWidgetDelegate.get()); connect(m_pPlayersDataManager.get(), SIGNAL(cloudEditSelectionChanged(int)), this, SLOT(onCloudEditSelectionChanged(int))); } } // TODO: Merge this with eraseLastSelectedItem void RobloxTreeWidget::itemDeletionRequested(RobloxTreeWidgetItem* pItem) { if (pItem == m_savedMarkerItem) m_savedMarkerItem = NULL; } RobloxExplorerWidget::RobloxExplorerWidget(QWidget *pParent) : QWidget(pParent) , m_robloxTreeWidget(NULL) , m_sendingCounter(false) { m_lineEdit = new QLineEdit(); m_lineEdit->setPlaceholderText(QString("Filter workspace (%1)").arg(UpdateUIManager::Instance().getMainWindow().explorerFilterAction->shortcut().toString())); m_lineEdit->hide(); m_loadingMovie = new QMovie(":/images/loading.gif"); m_loadingLabel = new QLabel(m_lineEdit); m_loadingLabel->setMovie(m_loadingMovie); QHBoxLayout* layout = new QHBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); layout->addStretch(); layout->addWidget(m_loadingLabel); layout->addSpacing(5); m_lineEdit->setLayout(layout); QVBoxLayout *mainLayout = new QVBoxLayout(); mainLayout->setSpacing(4); mainLayout->setContentsMargins(0, 0, 0, 0); mainLayout->addWidget(m_lineEdit); setLayout(mainLayout); setFocusProxy(m_lineEdit); } void RobloxExplorerWidget::setCurrentWidget(RobloxTreeWidget* treeWidget) { if (m_robloxTreeWidget == treeWidget) return; QLayout* mainLayout = layout(); if (m_robloxTreeWidget) { m_lineEdit->hide(); onProcessingFinished(); m_robloxTreeWidget->hide(); mainLayout->removeWidget(m_robloxTreeWidget); disconnect(m_robloxTreeWidget, SIGNAL(finishedProcessing()), this, SLOT(onProcessingFinished())); disconnect(m_robloxTreeWidget, SIGNAL(startedProcessing()), this, SLOT(onProcessingStarted())); disconnect(m_robloxTreeWidget, SIGNAL(focusGained()), this, SIGNAL(focusGained())); disconnect(m_lineEdit, SIGNAL(textChanged(QString)), this, SLOT(filterTreeWidget(QString))); } m_robloxTreeWidget = treeWidget; if (m_robloxTreeWidget) { mainLayout->addWidget(m_robloxTreeWidget); m_robloxTreeWidget->show(); m_lineEdit->show(); m_lineEdit->setText(m_robloxTreeWidget->currentFilter()); connect(m_robloxTreeWidget, SIGNAL(finishedProcessing()), this, SLOT(onProcessingFinished())); connect(m_robloxTreeWidget, SIGNAL(startedProcessing()), this, SLOT(onProcessingStarted())); connect(m_robloxTreeWidget, SIGNAL(focusGained()), this, SIGNAL(focusGained())); connect(m_lineEdit, SIGNAL(textChanged(QString)), this, SLOT(filterTreeWidget(QString))); if (!m_robloxTreeWidget->isFilterEmpty()) { onProcessingStarted(); m_robloxTreeWidget->filterWidget(); } } } void RobloxExplorerWidget::onProcessingStarted() { m_loadingMovie->start(); m_loadingLabel->show(); } void RobloxExplorerWidget::onProcessingFinished() { m_loadingLabel->hide(); m_loadingMovie->stop(); } void RobloxExplorerWidget::filterTreeWidget(const QString& text) { if (m_robloxTreeWidget) { if (!m_sendingCounter) { m_sendingCounter = true; QTimer::singleShot(1000, this, SLOT(sendWidgetCounter())); } m_robloxTreeWidget->filterWidget(text); } } void RobloxExplorerWidget::sendWidgetCounter() { m_sendingCounter = false; RobloxSettings settings; bool previouslyFiltered = settings.value("ExplorerWidgetFilterUsed", false).toBool(); settings.setValue("ExplorerWidgetFilterUsed", true); RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_STUDIO, previouslyFiltered ? "ExplorerWidgetFilterUsed" : "ExplorerWidgetFilterUsed_FTU"); }