Files
watrbx-game-engine/RobloxStudio/RobloxPropertyWidget.cpp
T
2025-09-18 17:55:52 -04:00

1137 lines
31 KiB
C++

/**
* RobloxPropertyWidget.cpp
* Copyright (c) 2013 ROBLOX Corp. All Rights Reserved.
*/
#include "stdafx.h"
#include "RobloxPropertyWidget.h"
#undef check
// Qt Headers
#include <QApplication>
#include <QPainter>
#include <QGridLayout>
#include <QLineEdit>
#include <QLabel>
#include <QMouseEvent>
#include <QStyledItemDelegate>
#include <QHeaderView>
#include <QSettings>
#include <QDockWidget>
#include <QObject>
// Roblox Headers
#include "v8datamodel/DataModel.h"
#include "v8datamodel/Selection.h"
#include "rbx/CEvent.h"
#include "ReflectionMetaData.h"
#include "util/RobloxGoogleAnalytics.h"
// Roblox Studio Headers
#include "RobloxContextualHelp.h"
#include "RobloxCustomWidgets.h"
#include "RobloxSettings.h"
#include "UpdateUIManager.h"
#include "RobloxMainWindow.h"
FASTFLAG(StudioNewWiki)
FASTFLAGVARIABLE(StudioPropertySearchDisabled, false)
FASTFLAGVARIABLE(StudioPropertyNameBarDisabled, false)
FASTFLAGVARIABLE(StudioPropertyCompareOriginalVals, true)
//utility function to create icons
static QIcon getIndicatorIcon(const QPalette &palette, QStyle *style);
//--------------------------------------------------------------------------------------------
// CategoryItem -- for showing property items in a category
//--------------------------------------------------------------------------------------------
CategoryItem::CategoryItem(const QString &name)
{
setHidden(true);
setFlags(Qt::ItemIsEnabled);
QFont boldFont(font(0));
boldFont.setBold(true);
setFont(0, boldFont);
setText(0, name);
}
void CategoryItem::update(bool storeCollapsedState)
{
int index = 0, numChildModified = 0, numChild = childCount();
PropertyItem *pPropertyItem = NULL;
while (index < numChild)
{
pPropertyItem = static_cast<PropertyItem *>(child(index));
if (pPropertyItem && pPropertyItem->update())
++numChildModified;
++index;
}
setHidden(!numChildModified);
if (storeCollapsedState)
{
if (!isHidden() && isExpanded())
setExpanded(numChildModified);
}
else
{
setExpanded(numChildModified);
}
}
void CategoryItem::applyFilter(const QString &filterString)
{
int index = 0, numChildModified = 0, numChild = childCount();
PropertyItem *pPropertyItem = NULL;
while (index < numChild)
{
pPropertyItem = static_cast<PropertyItem *>(child(index));
if (pPropertyItem && pPropertyItem->applyFilter(filterString))
++numChildModified;
++index;
}
setHidden(!numChildModified);
}
//--------------------------------------------------------------------------------------------
// PropertyItemDelegate -- for editing/customizing property items
//--------------------------------------------------------------------------------------------
class PropertyItemDelegate: public QStyledItemDelegate
{
private:
PropertyTreeWidget *m_pTreeWidget;
public:
PropertyItemDelegate(PropertyTreeWidget* parent)
: QStyledItemDelegate(parent)
, m_pTreeWidget(parent)
{}
QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (index.column() == 1)
{
PropertyItem *pItem = m_pTreeWidget->propertyItemFromIndex(index);
if (pItem)
return pItem->createEditor(parent, option);
}
return NULL;
}
void setEditorData(QWidget * editor, const QModelIndex & index) const
{
if (!editor)
return;
//set value in editor
PropertyItem *pItem = m_pTreeWidget->propertyItemFromIndex(index);
if (pItem)
pItem->setEditorData(editor);
}
void setModelData(QWidget *editor, QAbstractItemModel*, const QModelIndex &index) const
{
if (!editor)
return;
//set value from editor
PropertyItem *pItem = m_pTreeWidget->propertyItemFromIndex(index);
if (pItem)
pItem->setModelData(editor);
}
bool editorEvent(QEvent * evt, QAbstractItemModel*, const QStyleOptionViewItem&, const QModelIndex & index )
{
if (index.column() == 1)
{
//set value from editor
PropertyItem *pItem = m_pTreeWidget->propertyItemFromIndex(index);
if (pItem)
return pItem->editorEvent(evt);
}
return false;
}
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &) const
{ editor->setGeometry(option.rect); }
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{ return QStyledItemDelegate::sizeHint(option, index) + QSize(3, 4); }
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyleOptionViewItemV4 opt = option;
bool hasError = false;
if (PropertyItem* pItem = m_pTreeWidget->propertyItemFromIndex(index))
{
opt.textElideMode = pItem->getTextElideMode();
hasError = pItem->hasError();
}
QStyledItemDelegate::paint(painter, opt, index);
opt.palette.setCurrentColorGroup(QPalette::Active);
QColor color = static_cast<QRgb>(QApplication::style()->styleHint(QStyle::SH_Table_GridLineColor, &opt));
painter->save();
{
if (hasError && index.column() == 1)
{
painter->setPen(QPen(QColor("red")));
painter->drawRect(opt.rect.adjusted(2, 2, -2, -2));
}
painter->setPen(QPen(color));
painter->drawLine(opt.rect.right(), opt.rect.y(), opt.rect.right(), opt.rect.bottom());
}
painter->restore();
}
};
//--------------------------------------------------------------------------------------------
// PropertyItemUpdateJob -- for updating properties of selected items
//--------------------------------------------------------------------------------------------
class PropertyItemUpdateJob : public RBX::DataModelJob
{
public:
typedef enum { SentUpdateMessage, Updating, Waiting } UpdateState;
private:
PropertyTreeWidget *m_pPropertyWidget;
volatile bool m_bIsGridInvalid;
volatile UpdateState m_state;
double m_waitingPhaseStep;
double m_timeSinceLastRender;
public:
RBX::CEvent update; // Set when the UI thread may update
RBX::CEvent updated; // Set when the UI thread has updated
PropertyItemUpdateJob(PropertyTreeWidget *pPropertyWidget, shared_ptr<RBX::DataModel> dataModel)
: RBX::DataModelJob("PropGrid", RBX::DataModelJob::Read, false, dataModel, RBX::Time::Interval(0))
, m_pPropertyWidget(pPropertyWidget)
, m_bIsGridInvalid(true)
, m_state(PropertyItemUpdateJob::Waiting)
, update(false)
, updated(false)
{}
void requestUpdate()
{
if (!m_bIsGridInvalid)
{
m_bIsGridInvalid = true;
RBX::TaskScheduler::singleton().reschedule(shared_from_this());
}
}
void setState(PropertyItemUpdateJob::UpdateState state)
{ m_state = state; }
bool isGridInvalid() { return m_bIsGridInvalid; }
virtual RBX::Time::Interval sleepTime(const Stats&)
{
switch (m_state)
{
default:
case Waiting:
{
if (m_bIsGridInvalid)
return RBX::Time::Interval::zero();
else
return RBX::Time::Interval::max();
}
case Updating:
return RBX::Time::Interval::zero();
case SentUpdateMessage:
return RBX::Time::Interval::max();
}
}
virtual Job::Error error(const Stats& stats)
{
switch (m_state)
{
default:
case Waiting:
{
m_waitingPhaseStep = stats.timespanSinceLastStep.seconds();
Job::Error result;
result.error = m_waitingPhaseStep;
return result;
}
case Updating:
{
Job::Error result;
// The error is the sum of the previous phase plus this phase
result.error = m_waitingPhaseStep + stats.timespanSinceLastStep.seconds();
result.urgent = true;
return result;
}
case SentUpdateMessage:
RBXASSERT(false);
return Job::Error();
}
}
virtual RBX::TaskScheduler::StepResult stepDataModelJob(const Stats& stats)
{
m_timeSinceLastRender = m_waitingPhaseStep + stats.timespanOfLastStep.seconds();
switch (m_state)
{
case Waiting:
m_state = SentUpdateMessage;
m_pPropertyWidget->requestUpdate();
break;
case SentUpdateMessage:
//RBXASSERT(false);
break;
case Updating:
update.Set();
updated.Wait();
m_bIsGridInvalid = false;
m_state = Waiting;
break;
}
return RBX::TaskScheduler::Stepped;
}
};
//--------------------------------------------------------------------------------------------
// PropertyTreeWidget -- collection of property items
//--------------------------------------------------------------------------------------------
PropertyTreeWidget::PropertyTreeWidget(QWidget *pParent)
: QTreeWidget(pParent)
, m_bIsPropertyChanged(false)
, m_bIgnorePropertyChanged(false)
, m_bUpdateRequested(false)
, m_bStoreCollapasedState(false)
, m_bSameOriginalValues(true)
, m_sendingCounter(false)
{
setUniformRowHeights(true);
setAlternatingRowColors(true);
setRootIsDecorated(false);
setColumnCount(2);
setSelectionBehavior(QAbstractItemView::SelectRows);
// set up headers
QStringList headerLabels;
headerLabels << "Property" << "Value";
setHeaderLabels(headerLabels);
setHeaderHidden(true);
header()->setStretchLastSection(true);
header()->setResizeMode(0,QHeaderView::ResizeToContents);
m_expandIcon = getIndicatorIcon(palette(), style());
setEditTriggers(QAbstractItemView::NoEditTriggers);
setItemDelegate(new PropertyItemDelegate(this));
setUpdatesEnabled(false);
//sort items
CategoryItems::const_iterator curIter = m_categoryItems.begin();
while (curIter != m_categoryItems.end())
{
if (curIter->second)
curIter->second->sortChildren(0, Qt::AscendingOrder);
++curIter;
}
setUpdatesEnabled(true);
connect(this, SIGNAL(itemExpanded(QTreeWidgetItem *)), this, SLOT(onItemExpanded(QTreeWidgetItem *)));
connect(this, SIGNAL(itemCollapsed(QTreeWidgetItem *)), this, SLOT(onItemCollapsed(QTreeWidgetItem *)));
connect(this, SIGNAL(helpTopicChanged(const QString&)),
&RobloxContextualHelpService::singleton(), SLOT(onHelpTopicChanged(const QString&)));
}
PropertyTreeWidget::~PropertyTreeWidget()
{
}
void PropertyTreeWidget::reinitialize()
{
for (Connections::iterator iter = m_cPropertyChanged.begin(); iter!=m_cPropertyChanged.end(); ++iter)
iter->second.disconnect();
m_cPropertyChanged.clear();
m_invalidItems.clear();
m_selectionData.clear();
}
void PropertyTreeWidget::setDataModel(boost::shared_ptr<RBX::DataModel> pDataModel)
{
if ( m_pDataModel == pDataModel )
return;
removeUpdateJob();
//clear selection data and update tree
m_selectionData.clear();
m_invalidItems.clear();
m_bIsPropertyChanged = false;
m_bIgnorePropertyChanged = false;
QCoreApplication::removePostedEvents(this);
m_pDataModel = pDataModel;
createUpdateJob();
//make sure if the document is deleted then we don't keep showing it's properties
//WARNING: do not post event as it leads to a thread lock in case user is switching documents
//calling update should be safe as setDataModel will be called from main thread only
if (!m_pUpdateItemsJob)
update();
}
void PropertyTreeWidget::createUpdateJob()
{
if (!m_pDataModel)
return;
m_pUpdateItemsJob.reset(new PropertyItemUpdateJob(this,m_pDataModel));
RBX::TaskScheduler::singleton().add(m_pUpdateItemsJob);
}
void PropertyTreeWidget::removeUpdateJob()
{
if (m_pUpdateItemsJob)
{
for (Connections::iterator iter = m_cPropertyChanged.begin(); iter!=m_cPropertyChanged.end(); ++iter)
iter->second.disconnect();
m_cPropertyChanged.clear();
m_pUpdateItemsJob->updated.Set();
RBX::TaskScheduler::singleton().removeBlocking(m_pUpdateItemsJob);
m_pUpdateItemsJob.reset();
}
}
void PropertyTreeWidget::restoreOriginalValues(PropertyItem* item)
{
if (!item || !item->propertyDescriptor() )
return;
RBX::DataModel::LegacyLock lock(getDataModel(), RBX::DataModelJob::Write);
for (boost::unordered_map<shared_ptr<RBX::Instance>, RBX::Reflection::Variant>::const_iterator iter = m_originalValues.begin(); iter != m_originalValues.end(); ++iter)
if (iter->first && item->propertyDescriptor()->isMemberOf(iter->first.get()))
item->propertyDescriptor()->setVariant(iter->first.get(), iter->second);
}
void PropertyTreeWidget::storeOriginalValues(PropertyItem* item)
{
if (!item || !item->propertyDescriptor() )
return;
m_originalValues.clear();
m_bSameOriginalValues = true;
if (RBX::Selection* selection = RBX::ServiceProvider::create<RBX::Selection>(getDataModel().get()))
{
RBX::Instance* prevInstance = NULL;
for (RBX::Instances::const_iterator iter = selection->begin(); iter != selection->end(); ++iter)
{
if (item->propertyDescriptor()->isMemberOf((*iter).get()))
{
RBX::Reflection::Variant value;
item->propertyDescriptor()->getVariant((*iter).get(), value);
m_originalValues.insert(std::make_pair(*iter, value));
if (FFlag::StudioPropertyCompareOriginalVals)
{
if (prevInstance && m_bSameOriginalValues)
m_bSameOriginalValues = item->propertyDescriptor()->equalValues(prevInstance, (*iter).get());
prevInstance = (*iter).get();
}
}
}
}
}
void PropertyTreeWidget::addInstance(RBX::Instance *pInstance)
{
//first remove (to be sure we don't put in duplicate values)
m_selectionData[&pInstance->getDescriptor()].remove(pInstance);
//now add
m_selectionData[&pInstance->getDescriptor()].push_back(pInstance);
m_cPropertyChanged[pInstance] = pInstance->propertyChangedSignal.connect(boost::bind(&PropertyTreeWidget::onPropertyChanged, this, _1));
m_bIgnorePropertyChanged = true;
//update property view
if (m_pUpdateItemsJob)
m_pUpdateItemsJob->requestUpdate();
else
requestUpdate();
}
void PropertyTreeWidget::removeInstance(RBX::Instance *pInstance)
{
//remove connection
Connections::iterator conIter = m_cPropertyChanged.find(pInstance);
if (conIter != m_cPropertyChanged.end())
{
conIter->second.disconnect();
m_cPropertyChanged.erase(conIter);
}
//remove instance
ClassDescriptorInstanceMap::iterator iter = m_selectionData.find(&pInstance->getDescriptor());
if (iter != m_selectionData.end())
{
iter->second.remove(pInstance);
if (iter->second.empty())
m_selectionData.erase(iter);
}
m_bIgnorePropertyChanged = true;
//update property view
if (m_pUpdateItemsJob)
m_pUpdateItemsJob->requestUpdate();
else
requestUpdate();
}
void PropertyTreeWidget::onPropertyChanged(const RBX::Reflection::PropertyDescriptor* pPropertyDescriptor)
{
QMutexLocker lock(&m_propModifyMutex);
if (m_bIgnorePropertyChanged)
return;
PropertyItemMap::const_iterator iter = m_itemMap.find(pPropertyDescriptor);
if (iter != m_itemMap.end())
{
m_invalidItems.insert(iter->second);
m_bIsPropertyChanged = true;
//update property view
if (m_pUpdateItemsJob)
{
//data model job will take care of avoiding sending duplicate events (so no need to check for update requested)
m_pUpdateItemsJob->requestUpdate();
}
else
{
QMutexLocker lock(&m_propUpdateMutex);
if (!m_bUpdateRequested)
{
requestUpdate();
m_bUpdateRequested = true;
}
}
}
}
void PropertyTreeWidget::requestUpdate()
{
//qt will take ownership of the event so no need to delete
QApplication::postEvent(this, new RobloxCustomEvent(PROPERTY_WIDGET_UPDATE));
}
bool PropertyTreeWidget::event(QEvent* evt)
{
bool retVal = false;
if ( evt->type() == PROPERTY_WIDGET_UPDATE )
{
update();
QMutexLocker lock(&m_propUpdateMutex);
m_bUpdateRequested = false;
retVal = true;
}
else if ( evt->type() == QEvent::ShortcutOverride )
{
retVal = QTreeWidget::event(evt);
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(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 PropertyTreeWidget::setPropertyCallback(const QObject* sender, const char* signal, boost::function<void(QColor)> callback)
{
m_callback = callback;
connect(sender, signal, this, SLOT(updatePropertySlot(QColor)));
}
void PropertyTreeWidget::resetPropertyCallback()
{
disconnect(this, SLOT(updatePropertySlot(QColor)));
m_callback.clear();
}
void PropertyTreeWidget::updatePropertySlot(QColor objectValue)
{
m_callback(objectValue);
}
void PropertyTreeWidget::update()
{
if (m_pUpdateItemsJob)
{
if (!m_pUpdateItemsJob->isGridInvalid())
return;
m_pUpdateItemsJob->setState(PropertyItemUpdateJob::Updating);
RBX::TaskScheduler::singleton().reschedule(m_pUpdateItemsJob);
m_pUpdateItemsJob->update.Wait();
}
setUpdatesEnabled(false);
if (m_itemMap.empty())
{
visitClassProperties(&RBX::Reflection::ClassDescriptor::rootDescriptor());
// create categories for items (if we don't precreate the loop below has O(N^2) layout?)
for (auto& item: m_items)
getCategoryItem(item);
// add items
for (auto& item: m_itemMap)
{
CategoryItem* pCategoryItem = getCategoryItem(item.first);
if (pCategoryItem)
pCategoryItem->addChild(item.second);
}
// sort items
CategoryItems::const_iterator curIter = m_categoryItems.begin();
while (curIter != m_categoryItems.end())
{
if (curIter->second)
curIter->second->sortChildren(0, Qt::AscendingOrder);
++curIter;
}
}
if (m_bIsPropertyChanged && !m_bIgnorePropertyChanged)
{
std::set<PropertyItem*> tempInvalidItems = m_invalidItems;
m_invalidItems.clear();
std::for_each(tempInvalidItems.begin(),
tempInvalidItems.end(),
boost::bind(&PropertyItem::update, _1));
m_bIsPropertyChanged = false;
}
else
{
CategoryItems::const_iterator curIter = m_categoryItems.begin();
CategoryItems::const_iterator endIter = m_categoryItems.end();
while (curIter != endIter)
{
curIter->second->update(m_bStoreCollapasedState);
++curIter;
}
m_bIgnorePropertyChanged = false;
}
setUpdatesEnabled(true);
if (m_pUpdateItemsJob)
m_pUpdateItemsJob->updated.Set();
}
void PropertyTreeWidget::visitClassProperties(const RBX::Reflection::ClassDescriptor* pClassDescriptor)
{
//visit classes
std::for_each(pClassDescriptor->derivedClasses_begin(),
pClassDescriptor->derivedClasses_end(),
boost::bind(&PropertyTreeWidget::visitClassProperties, this, _1));
//initialize properties
std::for_each(pClassDescriptor->begin<RBX::Reflection::PropertyDescriptor>(),
pClassDescriptor->end<RBX::Reflection::PropertyDescriptor>(),
boost::bind(&PropertyTreeWidget::initProperty, this, pClassDescriptor, _1));
}
void PropertyTreeWidget::initProperty(const RBX::Reflection::ClassDescriptor* pClassDescriptor,
const RBX::Reflection::PropertyDescriptor* pPropertyDescriptor)
{
if (!pPropertyDescriptor || !pClassDescriptor)
return;
// Ignore inherited properties
if (pPropertyDescriptor->owner!=*pClassDescriptor)
return;
RBX::Reflection::Metadata::Member* pMember = RBX::Reflection::Metadata::Reflection::singleton()->get(*pPropertyDescriptor);
// only show public, editable properties
if ( !pPropertyDescriptor->isPublic() ||
!pPropertyDescriptor->isEditable() ||
(pPropertyDescriptor->security != RBX::Security::None &&
!(RBX::Reflection::Metadata::Item::isBrowsable(pMember, *pPropertyDescriptor))
)
)
{
return;
}
if(RBX::Reflection::Metadata::Item::isDeprecated(pMember, *pPropertyDescriptor))
return;
PropertyItem *pItem = PropertyItem::createItem(pPropertyDescriptor);
if (!pItem)
return;
m_items.push_back(pPropertyDescriptor);
m_itemMap[pPropertyDescriptor] = pItem;
}
void PropertyTreeWidget::closeEditor(QWidget * editor, QAbstractItemDelegate::EndEditHint hint)
{ QTreeWidget::closeEditor(editor, hint); }
PropertyItem* PropertyTreeWidget::propertyItemFromIndex(const QModelIndex &index) const
{ return dynamic_cast<PropertyItem *>(itemFromIndex(index)); }
CategoryItem* PropertyTreeWidget::getCategoryItem(const RBX::Reflection::PropertyDescriptor* pPropertyDescriptor)
{
QString categoryName = pPropertyDescriptor->category.toString().c_str();
CategoryItems::const_iterator iter = m_categoryItems.find(categoryName);
if (iter != m_categoryItems.end())
return iter->second;
CategoryItem *pItem = new CategoryItem(categoryName);
addTopLevelItem(pItem);
setFirstItemColumnSpanned(pItem, true);
pItem->setIcon(0, m_expandIcon);
static QString collapsedCategoryItems("Surface Inputs"); //this will collapse both Surface and Surface Inputs
pItem->setExpanded(m_bStoreCollapasedState ? (!collapsedCategoryItems.contains(categoryName) && RobloxSettings().value(getRegistryKey(pItem), true).toBool()) : true);
m_categoryItems[categoryName] = pItem;
return pItem;
}
void PropertyTreeWidget::sendPropertySearchCounter()
{
m_sendingCounter = false;
RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION, "StudioUserAction", "PropertySearchUsed");
}
void PropertyTreeWidget::setFilter(QString filter)
{
setUpdatesEnabled(false);
CategoryItems::const_iterator curIter = m_categoryItems.begin();
CategoryItems::const_iterator endIter = m_categoryItems.end();
while (curIter != endIter)
{
if (curIter->second)
curIter->second->applyFilter(filter);
++curIter;
}
setUpdatesEnabled(true);
if (!m_sendingCounter)
{
m_sendingCounter = true;
QTimer::singleShot(1000, this, SLOT(sendPropertySearchCounter()));
}
}
void PropertyTreeWidget::mousePressEvent(QMouseEvent *event)
{
QTreeWidget::mousePressEvent(event);
QTreeWidgetItem* treeWidgetItem = itemAt(event->pos());
QTreeWidgetItem* categoryItem = dynamic_cast<CategoryItem *>(treeWidgetItem);
if ( categoryItem )
{
if ( event->pos().x() + header()->offset() < 20 )
categoryItem->setExpanded(!categoryItem->isExpanded());
}
else
{
if (treeWidgetItem)
{
if(FFlag::StudioNewWiki)
{
QTreeWidgetItem* curr = treeWidgetItem;
QString wikiQString;
bool done = false;
while (!done) {
if(curr == invisibleRootItem()) {
done = true;
}
PropertyItem* pi = dynamic_cast<PropertyItem*>(curr);
const RBX::Reflection::PropertyDescriptor* desc = pi->propertyDescriptor();
if(desc) {
done = true;
std::string wikiString = "API:Class/";
wikiString += desc->owner.name.str + "/";
wikiQString = pi->text(0).prepend(wikiString.c_str());
} else {
curr = curr->parent();
}
}
if (!wikiQString.isEmpty()) {
Q_EMIT(helpTopicChanged(wikiQString));
}
}
else
{
Q_EMIT(helpTopicChanged(treeWidgetItem->text(0) + "_(Property)"));
}
}
bool handled = false;
if (PropertyItem* propertyItem = dynamic_cast<PropertyItem*>(treeWidgetItem))
{
handled = propertyItem->customLaunchEditorHook(event);
}
if (!handled)
{
setCurrentIndex(indexFromItem(treeWidgetItem, 1));
editItem(treeWidgetItem,1);
}
}
}
void PropertyTreeWidget::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyleOptionViewItemV4 opt = option;
PropertyItem *pItem = propertyItemFromIndex(index);
if (!pItem)
{
const QColor c = QColor(233, 233, 233);
painter->fillRect(option.rect, c);
opt.palette.setColor(QPalette::AlternateBase, c);
}
else if (!(pItem->flags() & Qt::ItemIsEditable))
{
opt.palette.setColor(QPalette::Text, QColor("gray"));
}
QTreeWidget::drawRow(painter, opt, index);
QColor color = static_cast<QRgb>(QApplication::style()->styleHint(QStyle::SH_Table_GridLineColor, &opt));
painter->save();
{
painter->setPen(QPen(color));
painter->drawLine(opt.rect.x(), opt.rect.bottom(), opt.rect.right(), opt.rect.bottom());
}
painter->restore();
}
void PropertyTreeWidget::onItemExpanded(QTreeWidgetItem * pItem)
{
if (m_bStoreCollapasedState)
{
//don't store unnecessary entires, on expansion remove since it's the default value
CategoryItem *pCategoryItem = dynamic_cast<CategoryItem *>(pItem);
if (pCategoryItem)
RobloxSettings().remove(getRegistryKey(pCategoryItem));
}
}
void PropertyTreeWidget::onItemCollapsed(QTreeWidgetItem * pItem)
{
if (m_bStoreCollapasedState)
{
CategoryItem *pCategoryItem = dynamic_cast<CategoryItem *>(pItem);
if (pCategoryItem)
RobloxSettings().setValue(getRegistryKey(pCategoryItem), false);
}
}
QString PropertyTreeWidget::getRegistryKey(CategoryItem* pCategoryItem)
{
if (pCategoryItem)
return parent()->objectName() + "_" + pCategoryItem->text(0);
return QString("");
}
//--------------------------------------------------------------------------------------------
// RobloxPropertyWidget -- main widget for showing properties of selected items
//--------------------------------------------------------------------------------------------
RobloxPropertyWidget::RobloxPropertyWidget(QWidget *pParent)
: QWidget(pParent)
, m_bUpdateRequested(false)
{
setObjectName("RobloxPropertyWidget");
QGridLayout *pMainGridLayout = new QGridLayout(this);
pMainGridLayout->setVerticalSpacing(4);
pMainGridLayout->setHorizontalSpacing(0);
pMainGridLayout->setContentsMargins(0, 0, 0, 0);
QFrame *pFrame = new QFrame(this);
pFrame->setFrameShape(QFrame::Box);
pFrame->setFrameShadow(QFrame::Raised);
pFrame->setVisible(!FFlag::StudioPropertyNameBarDisabled);
m_pSelectionInfoLabel = new QLabel(pFrame);
m_pSelectionInfoLabel->setTextFormat(Qt::PlainText);
QSpacerItem *pHorizontalSpacer = new QSpacerItem(20, 18, QSizePolicy::Expanding, QSizePolicy::Minimum);
QGridLayout *pFrameGridLayout = new QGridLayout(pFrame);
QMargins margins = pFrameGridLayout->contentsMargins();
margins.setTop(0);
margins.setBottom(0);
pFrameGridLayout->setContentsMargins(margins);
pFrameGridLayout->setHorizontalSpacing(4);
pFrameGridLayout->addWidget(m_pSelectionInfoLabel, 0, 0, 1, 1);
pFrameGridLayout->addItem(pHorizontalSpacer, 0, 1, 1, 1);
pMainGridLayout->addWidget(pFrame, 0, 0, 1, 3);
/*
m_pSortItems = new QPushButton(this);
m_pSortItems->setText("Sort");
m_pSortItems->setCheckable(true);
*/
m_pFilterEdit = new QLineEdit(this);
m_pFilterEdit->setPlaceholderText("Search Properties");
m_pFilterEdit->setVisible(!FFlag::StudioPropertySearchDisabled);
pMainGridLayout->addWidget(m_pFilterEdit, 1, 0, 1, 1);
m_pTreeWidget = new PropertyTreeWidget(this);
m_pTreeWidget->setStoreCollapsedState(true);
pMainGridLayout->addWidget(m_pTreeWidget, 2, 0, 1, 3);
connect(m_pFilterEdit, SIGNAL(textChanged(QString)), m_pTreeWidget, SLOT(setFilter(QString)));
}
RobloxPropertyWidget::~RobloxPropertyWidget()
{
}
QSize RobloxPropertyWidget::sizeHint() const
{
return QSize(295, UpdateUIManager::Instance().getMainWindow().size().height() / 2);
}
void RobloxPropertyWidget::setDataModel(boost::shared_ptr<RBX::DataModel> pDataModel)
{
if ( m_pDataModel == pDataModel )
return;
//first disconnect the connection
m_cInstanceSelectionChanged.disconnect();
m_selectedInstaces.clear();
m_pDataModel = pDataModel;
m_pTreeWidget->setDataModel(m_pDataModel);
if (m_pDataModel)
{
//check if we need to connect again
RBX::DataModel::LegacyLock lock(m_pDataModel, RBX::DataModelJob::Write);
RBX::Selection* pSelection = m_pDataModel->create<RBX::Selection>();
if (pSelection)
{
std::for_each(pSelection->begin(), pSelection->end(), boost::bind(&RobloxPropertyWidget::addInstance, this, _1));
m_cInstanceSelectionChanged = pSelection->selectionChanged.connect(boost::bind(&RobloxPropertyWidget::onInstanceSelectionChanged, this, _1));
}
}
requestInfoLabelUpdate();
}
void RobloxPropertyWidget::addInstance(boost::shared_ptr<RBX::Instance> pInstance)
{
if (!pInstance)
return;
//RBX::readwrite_concurrency_catcher::scoped_write_request lock(m_selectedInstancesGuard);
m_selectedInstaces.push_back(pInstance.get());
m_pTreeWidget->addInstance(pInstance.get());
}
void RobloxPropertyWidget::removeInstance(boost::shared_ptr<RBX::Instance> pInstance)
{
if (!pInstance)
return;
//RBX::readwrite_concurrency_catcher::scoped_write_request lock(m_selectedInstancesGuard);
InstanceList::iterator iter = std::find(m_selectedInstaces.begin(), m_selectedInstaces.end(), pInstance.get());
if (iter != m_selectedInstaces.end())
m_selectedInstaces.erase(iter);
m_pTreeWidget->removeInstance(pInstance.get());
}
void RobloxPropertyWidget::onInstanceSelectionChanged(const RBX::SelectionChanged& evt)
{
QMutexLocker lock(&m_propertyWidgetMutex);
if (evt.addedItem)
{
addInstance(evt.addedItem);
}
if (evt.removedItem)
{
removeInstance(evt.removedItem);
}
//remove the filter on selection change
if (!m_pFilterEdit->text().isEmpty())
m_pFilterEdit->clear();
requestInfoLabelUpdate();
}
void RobloxPropertyWidget::requestInfoLabelUpdate()
{
if (m_bUpdateRequested)
return;
//qt will take ownership of the event so no need to delete
QApplication::postEvent(this, new RobloxCustomEvent(PROPERTY_LABEL_UPDATE));
m_bUpdateRequested = true;
}
bool RobloxPropertyWidget::event(QEvent * evt)
{
if (evt->type() != PROPERTY_LABEL_UPDATE)
return QWidget::event(evt);
updateSelectionInfoLabel();
m_bUpdateRequested = false;
return true;
}
void RobloxPropertyWidget::updateSelectionInfoLabel()
{
QMutexLocker mutexLock(&m_propertyWidgetMutex);
//if no selection just clear the label
if (m_selectedInstaces.empty())
{
if (FFlag::StudioPropertyNameBarDisabled)
dynamic_cast<QDockWidget *>(this->parent())->setWindowTitle("Properties");
m_pSelectionInfoLabel->setText("");
return;
}
int numInstances = m_selectedInstaces.size();
InstanceList::const_iterator curIter = m_selectedInstaces.begin();
InstanceList::const_iterator endIter = m_selectedInstaces.end();
//RBX::DataModel::LegacyLock lock(m_pDataModel, RBX::DataModelJob::Read);
std::string instanceName = (*curIter)->getName();
const RBX::Name* className = &(*curIter)->getClassName();
QString labelText;
if (numInstances == 1)
{
labelText = QString("%1 \"%2\"").arg(className ? className->toString().c_str() : "").arg(instanceName.c_str());
}
else
{
while (++curIter != endIter)
{
if ((*curIter)->getName() != instanceName)
{
instanceName.clear();
if (className==NULL)
break;
}
if ((*curIter)->getClassName() != *className)
{
className = NULL;
if (instanceName.empty())
break;
}
}
//modify label appropriately
if (className != NULL && !instanceName.empty())
labelText = QString("%1 \"%2\" - %3 items").arg(className->toString().c_str()).arg(instanceName.c_str()).arg(numInstances);
else if (className != NULL)
labelText = QString("%1 - %2 items").arg(className->toString().c_str()).arg(numInstances);
else
labelText = QString("%1 items").arg(numInstances);
}
//elide text if required
QFontMetrics fm = fontMetrics();
m_pSelectionInfoLabel->setText( fm.elidedText(labelText, Qt::ElideRight, width()-24) );
if (FFlag::StudioPropertyNameBarDisabled)
dynamic_cast<QDockWidget *>(this->parent())->setWindowTitle(QString("Properties - %1").arg(labelText));
}
// Draw an icon indicating open/closed branches
static QIcon getIndicatorIcon(const QPalette &palette, QStyle *style)
{
QPixmap pix(14, 14);
pix.fill(Qt::transparent);
QStyleOptionViewItemV4 branchOption;
QRect r(QPoint(0, 0), pix.size());
branchOption.rect = QRect(2, 2, 9, 9);
branchOption.palette = palette;
branchOption.state = QStyle::State_Children;
QPainter p;
// closed state
p.begin(&pix);
style->drawPrimitive(QStyle::PE_IndicatorBranch, &branchOption, &p);
p.end();
QIcon rc = pix;
rc.addPixmap(pix, QIcon::Selected, QIcon::Off);
// open state
branchOption.state |= QStyle::State_Open;
pix.fill(Qt::transparent);
p.begin(&pix);
style->drawPrimitive(QStyle::PE_IndicatorBranch, &branchOption, &p);
p.end();
rc.addPixmap(pix, QIcon::Normal, QIcon::On);
rc.addPixmap(pix, QIcon::Selected, QIcon::On);
return rc;
}