/** * FindWidget.cpp * Copyright (c) 2014 ROBLOX Corp. All Rights Reserved. */ #include "stdafx.h" #include #include #include #include #include #include #include #include "V8DataModel/PlayerGui.h" #include "V8DataModel/DataModel.h" #include "V8DataModel/DataModelJob.h" #include "V8Tree/Instance.h" #include "script/CoreScript.h" #include "Script/Script.h" #include "RobloxFindWidget.h" #include "QtUtilities.h" #include "UpdateUIManager.h" #include "RobloxMainWindow.h" #include "RobloxDocManager.h" #include "RobloxGameExplorer.h" #include "RobloxIDEDoc.h" #include "RobloxScriptDoc.h" #include "IRobloxDoc.h" #include "ScriptTextEditor.h" RobloxFindWidget::RobloxFindWidget(QWidget *pParent) : QWidget(pParent) , m_pListView(NULL) , m_pFindListModel(NULL) , m_searchedScripts(0) , m_matchingScripts(0) , m_matchesFound(0) , m_totalAttempted(0) , m_stopPopulating(false) { initWidget(); } RobloxFindWidget::~RobloxFindWidget() { } RobloxFindWidget& RobloxFindWidget::singleton() { static RobloxFindWidget* findWidget = new RobloxFindWidget(); return *findWidget; } void RobloxFindWidget::initWidget() { m_pFindInAllScriptsButton = new QToolButton(this); m_pFindInAllScriptsButton->setIcon(QIcon(":/16x16/images/Studio 2.0 icons/16x16/find.png")); m_pFindInAllScriptsButton->setToolTip("Find in All Scripts"); m_pFindInAllScriptsButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_pGotoLocationButton = new QToolButton(this); m_pGotoLocationButton->setIcon(QIcon(":/16x16/images/Studio 2.0 icons/16x16/undo_square_16.png")); m_pGotoLocationButton->setToolTip("Goto"); m_pGotoLocationButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_pPreviousButton = new QToolButton(this); m_pPreviousButton->setIcon(QIcon(":/16x16/images/Studio 2.0 icons/16x16/arrowleft_green_16.png")); m_pPreviousButton->setToolTip("Previous"); m_pPreviousButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_pNextButton = new QToolButton(this); m_pNextButton->setIcon(QIcon(":/16x16/images/Studio 2.0 icons/16x16/arrowright_green_16.png")); m_pNextButton->setToolTip("Next"); m_pNextButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_pClearButton = new QToolButton(this); m_pClearButton->setIcon(QIcon(":/16x16/images/Studio 2.0 icons/16x16/delete_x_16_h.png")); m_pClearButton->setToolTip("Clear"); m_pClearButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_pStopButton = new QToolButton(this); m_pStopButton->setIcon(QIcon(":/16x16/images/Studio 2.0 icons/16x16/stop_16.png")); m_pStopButton->setToolTip("Stop"); m_pStopButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_pStopButton->setDisabled(true); QSpacerItem *pHorizontalSpacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); QFrame *seperator1 = new QFrame(this); seperator1->setFrameShape(QFrame::VLine); seperator1->setFrameShadow(QFrame::Sunken); QFrame *seperator2 = new QFrame(this); seperator2->setFrameShape(QFrame::VLine); seperator2->setFrameShadow(QFrame::Sunken); QFrame *seperator3 = new QFrame(this); seperator3->setFrameShape(QFrame::VLine); seperator3->setFrameShadow(QFrame::Sunken); QFrame *seperator4 = new QFrame(this); seperator4->setFrameShape(QFrame::VLine); seperator4->setFrameShadow(QFrame::Sunken); QHBoxLayout *buttonLayout = new QHBoxLayout(); buttonLayout->setSpacing(4); buttonLayout->addSpacing(4); buttonLayout->addWidget(m_pFindInAllScriptsButton); buttonLayout->addWidget(seperator1); buttonLayout->addWidget(m_pGotoLocationButton); buttonLayout->addWidget(seperator2); buttonLayout->addWidget(m_pPreviousButton); buttonLayout->addWidget(m_pNextButton); buttonLayout->addWidget(seperator3); buttonLayout->addWidget(m_pClearButton); buttonLayout->addWidget(seperator4); buttonLayout->addWidget(m_pStopButton); buttonLayout->addItem(pHorizontalSpacer); m_pListView = new QListView(this); m_pFindListModel = new FindListModel(); m_pListView->setModel(m_pFindListModel); QGridLayout *mainLayout = new QGridLayout(this); mainLayout->setVerticalSpacing(4); mainLayout->setContentsMargins(0, 0, 0, 0); mainLayout->addLayout(buttonLayout, 0, 0); mainLayout->addWidget(m_pListView, 1, 0); setLayout(mainLayout); connect(m_pListView, SIGNAL(clicked(QModelIndex)), this, SLOT(openItem(QModelIndex))); connect(m_pFindInAllScriptsButton, SIGNAL(clicked()), &UpdateUIManager::Instance().getMainWindow(), SLOT(showFindAllDialog())); connect(m_pGotoLocationButton, SIGNAL(clicked()), this, SLOT(openCurrentItem())); connect(m_pPreviousButton, SIGNAL(clicked()), this, SLOT(previousItem())); connect(m_pNextButton, SIGNAL(clicked()), this, SLOT(nextItem())); connect(m_pClearButton, SIGNAL(clicked()), this, SLOT(clearItems())); connect(m_pStopButton, SIGNAL(clicked()), this, SLOT(stopPopulatingItems())); } void RobloxFindWidget::findAll(const QString& findStr, bool allScripts, eFindFlags flags) { if (!UpdateUIManager::Instance().getMainWindow().viewFindResultsWindowAction->isChecked()) { UpdateUIManager::Instance().getMainWindow().viewFindResultsWindowAction->trigger(); show(); } resetItems(); FindReplaceProvider::instance().getFindAllDialog()->setFindAllRunning(true); addItem(FindData(QString("Find all \"%1\", %2").arg( findStr, allScripts ? "All Scripts " : "Current Script"))); if (RobloxIDEDoc* ideDoc = RobloxDocManager::Instance().getPlayDoc()) if (RBX::DataModel* dm = ideDoc->getDataModel()) { dm->submitTask(boost::bind(&RobloxFindWidget::populateWidget, this, shared_from(dm), findStr, flags, allScripts), RBX::DataModelJob::Read); return; } stopPopulatingItems(); } void RobloxFindWidget::dataModelClosing() { stopPopulatingItems(); resetItems(); } bool RobloxFindWidget::event(QEvent* e) { bool isHandled = QWidget::event(e); if ( e->type() == QEvent::KeyPress ) { QKeyEvent* keyEvent = static_cast(e); QChar key = keyEvent->key(); if (key == Qt::Key_Up) { previousItem(); isHandled = true; } else if (key == Qt::Key_Down) { nextItem(); isHandled = true; } } return isHandled; } static void appendIfScript(shared_ptr instance, QList& scriptList, RBX::CoreGuiService* coreGuiService) { shared_ptr script = RBX::Instance::fastSharedDynamicCast(instance); shared_ptr moduleScript = RBX::Instance::fastSharedDynamicCast(instance); shared_ptr lsc = RBX::Instance::fastSharedDynamicCast(instance); if ((script || moduleScript) && (!coreGuiService || !instance->isDescendantOf(coreGuiService)) && lsc->getScriptId().isNull()) scriptList.append(LuaSourceBuffer::fromInstance(instance)); } void RobloxFindWidget::populateScriptList(bool allScripts) { m_scriptList.clear(); if (allScripts) { RobloxGameExplorer& rge = UpdateUIManager::Instance().getViewWidget(eDW_GAME_EXPLORER); std::vector scriptNames; rge.getListOfScripts(&scriptNames); for (std::vector::const_iterator itr = scriptNames.begin(); itr != scriptNames.end(); ++itr) { m_scriptList.append(LuaSourceBuffer::fromContentId( RBX::ContentId::fromGameAssetName(itr->toStdString()))); } if (RobloxIDEDoc* ideDoc = RobloxDocManager::Instance().getPlayDoc()) if (RBX::DataModel* dm = ideDoc->getDataModel()) dm->visitDescendants(boost::bind(&appendIfScript, _1, boost::ref(m_scriptList), dm->find())); } else { IRobloxDoc* doc = RobloxDocManager::Instance().getCurrentDoc(); if (!doc) return; RobloxScriptDoc* scriptDoc = dynamic_cast(doc); if (!scriptDoc) { RBX::StandardOut::singleton()->print(RBX::MESSAGE_WARNING, "No script currently available."); return; } m_scriptList.append(scriptDoc->getCurrentScript()); } } void RobloxFindWidget::addItem(FindData item) { m_pFindListModel->addItem(item); } void RobloxFindWidget::populateWidget(shared_ptr dm, const QString& findStr, eFindFlags flags, bool allScripts) { populateScriptList(allScripts); if (m_scriptList.size() <= 0) { stopPopulatingItems(); return; } getNextScript(0, dm, findStr, flags); } void RobloxFindWidget::getNextScript(int index, shared_ptr dm, const QString& findStr, eFindFlags flags) { if (m_stopPopulating) { stopPopulatingItems(); return; } LuaSourceBuffer buffer = m_scriptList.at(index); try { QString sourceText = buffer.getScriptText().c_str(); QString hierarchy = QString::fromStdString(buffer.getFullName()); dm->submitTask(boost::bind(&RobloxFindWidget::searchScriptChain, this, index, sourceText, dm, findStr, flags, hierarchy), RBX::DataModelJob::None); } catch (const RBX::base_exception&) { if (index + 1 < m_scriptList.count()) dm->submitTask(boost::bind(&RobloxFindWidget::getNextScript, this, index + 1, dm, findStr, flags), RBX::DataModelJob::Read); else stopPopulatingItems(); } } void RobloxFindWidget::searchScriptChain(int index, const QString& sourceText, shared_ptr dm, const QString& findStr, eFindFlags flags, const QString& heirarchy) { if (m_stopPopulating) { stopPopulatingItems(); return; } QTextDocument sourceDoc(sourceText); m_searchedScripts++; QTextDocument::FindFlags findFlags = 0; if (flags & MATCH_CASE) findFlags |= QTextDocument::FindCaseSensitively; if (flags & MATCH_WORD) findFlags |= QTextDocument::FindWholeWords; bool foundMatch = false; QTextCursor cursor; cursor = findNextInScript(sourceDoc, findStr, cursor, findFlags, flags & REG_EXP); while (!cursor.isNull() && !m_stopPopulating) { foundMatch = true; m_matchesFound++; LuaSourceBuffer buffer = m_scriptList.at(index); FindData findData(buffer, cursor.blockNumber() + 1, cursor.block().text(), heirarchy); findData.setPlaceData(cursor.selectionStart() - cursor.block().position(), cursor.selectionEnd() - cursor.selectionStart()); addItem(findData); if (cursor.atEnd()) break; cursor.movePosition(QTextCursor::NextBlock); cursor = findNextInScript(sourceDoc, findStr, cursor, findFlags, flags & REG_EXP); } if (foundMatch) m_matchingScripts++; if (index + 1 < m_scriptList.count() && !m_stopPopulating) dm->submitTask(boost::bind(&RobloxFindWidget::getNextScript, this, index + 1, dm, findStr, flags), RBX::DataModelJob::Read); //Need read lock else stopPopulatingItems(); } QTextCursor RobloxFindWidget::findNextInScript(const QTextDocument& doc, const QString& findStr, const QTextCursor& cursor, QTextDocument::FindFlags flags, bool regExp) { if (regExp) return doc.find(QRegExp(findStr), cursor, flags); else return doc.find(findStr, cursor, flags); } void RobloxFindWidget::previousItem() { if (m_pFindListModel->rowCount() <= 2) return; int previousPosition = m_pListView->currentIndex().row() - 1; if (previousPosition >= m_pFindListModel->rowCount() - 1 || previousPosition <= 0) previousPosition = m_pFindListModel->rowCount() - 2; m_pListView->setCurrentIndex(m_pFindListModel->index(previousPosition)); openCurrentItem(); } void RobloxFindWidget::nextItem() { if (m_pFindListModel->rowCount() <= 2) return; int nextPosition = m_pListView->currentIndex().row() + 1; if (nextPosition >= m_pFindListModel->rowCount() - 1 || nextPosition <= 0) nextPosition = 1; m_pListView->setCurrentIndex(m_pFindListModel->index(nextPosition)); openCurrentItem(); } void RobloxFindWidget::clearItems() { m_pFindListModel->clear(); m_pListView->reset(); m_pListView->setCurrentIndex(QModelIndex()); if (QDockWidget* dockWidget = dynamic_cast(parent())) dockWidget->setWindowTitle("Find Results"); } void RobloxFindWidget::resetItems() { m_stopPopulating = false; m_pStopButton->setDisabled(false); m_matchesFound = 0; m_matchingScripts = 0; m_searchedScripts = 0; m_totalAttempted = 0; m_pListView->setCurrentIndex(QModelIndex()); m_pListView->clearSelection(); m_pFindListModel->clear(); if (QDockWidget* dockWidget = dynamic_cast(parent())) dockWidget->setWindowTitle("Find Results"); } void RobloxFindWidget::stopPopulatingItems() { if (!m_stopPopulating) { m_stopPopulating = true; addItem(FindData(QString("Matching lines: %1 Matching scripts: %2 Total scripts searched: %3").arg( QString::number(m_matchesFound), QString::number(m_matchingScripts), QString::number(m_searchedScripts)))); if (QDockWidget* dockWidget = dynamic_cast(parent())) dockWidget->setWindowTitle(QString("Find Results - %1 Matching lines").arg(QString::number(m_matchesFound))); } m_pStopButton->setDisabled(true); m_pFindListModel->updateEntireView(); FindReplaceProvider::instance().getFindAllDialog()->setFindAllRunning(false); } void RobloxFindWidget::openCurrentItem() { openItem(m_pListView->currentIndex()); } void RobloxFindWidget::openItem(const QModelIndex & index) { m_pFindListModel->openScript(index.row()); setFocus(Qt::OtherFocusReason); } QString RobloxFindWidget::getInstanceHierarchy(shared_ptr instance) { if (!instance) return ""; QString path(instance->getName().c_str()); const RBX::Instance* pParent = instance->getParent(); const RBX::Instance* pChild = instance.get(); while (pParent) { path.prepend("."); if (pParent->getParent()) path.prepend(pParent->getName().c_str()); else path.prepend("game"); pChild = pParent; pParent = pChild->getParent(); } return path; } FindListModel::FindListModel() : m_previousCount(0) , m_runningUpdater(false) , m_stopUpdater(false) , m_loadedIndex(0) { } void FindListModel::updateEntireView() { m_loadedIndex = m_previousCount - 1; m_stopUpdater = true; Q_EMIT(dataChanged(index(0), index(m_loadedIndex))); } void FindListModel::updateView() { if (m_loadedIndex >= m_previousCount - 1 || m_stopUpdater) { m_runningUpdater = false; m_stopUpdater = false; return; } m_loadedIndex = qMin(m_loadedIndex + 20, m_previousCount); if (rowCount() > 0) Q_EMIT(dataChanged(index(m_loadedIndex - 1), index(m_loadedIndex))); QTimer::singleShot(100, this, SLOT(updateView())); } void FindListModel::addItem(FindData item) { QMutexLocker locker(&m_listMutex); m_FindList.append(item); m_previousCount = m_FindList.count(); if (!m_runningUpdater) { m_runningUpdater = true; m_stopUpdater = false; QTimer::singleShot(0, this, SLOT(updateView())); } } void FindListModel::clear() { QMutexLocker locker(&m_listMutex); m_FindList.clear(); m_previousCount = 0; m_runningUpdater = false; m_stopUpdater = true; m_loadedIndex = 0; } int FindListModel::rowCount(const QModelIndex& parent) const { return qMin(m_previousCount, m_loadedIndex + 1); } QVariant FindListModel::data(const QModelIndex& index, int role) const { int row = index.row(); if (row >= 0 && row < m_previousCount && role == Qt::DisplayRole) { QMutexLocker locker(&m_listMutex); return m_FindList[row].toString(); } return QVariant(QVariant::Invalid); } Qt::ItemFlags FindListModel::flags(const QModelIndex& index) const { QMutexLocker locker(&m_listMutex); return m_FindList[index.row()].flags(); } int FindListModel::openScript(int row) { if (row >= m_previousCount - 1 || row <= 0) return -1; FindData currentRow; { QMutexLocker locker(&m_listMutex); currentRow = m_FindList[row]; } if (currentRow.getLine() == 0) return -1; LuaSourceBuffer buffer = currentRow.getInstance(); if (shared_ptr lsc = buffer.toInstance()) { // decline to open script instance results if they are now linked if (!lsc->getScriptId().isNull() || !lsc->getParent()) { return -1; } } RobloxScriptDoc* scriptDoc = RobloxDocManager::Instance().openDoc(buffer); if (!scriptDoc) return -1; RBX::BaldPtr editor = scriptDoc->getTextEditor(); if (!editor) return -1; int pos = currentRow.getPosition() + editor->document()->findBlockByNumber(currentRow.getLine() - 1).position(); int length = currentRow.getLength(); editor->selectText(pos, pos + length); return row; } Qt::ItemFlags FindData::flags() const { if (m_line <= 0) return 0; return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } QString FindData::toString() const { if (m_line <= 0) return m_lineText; return QString(" %1(%2): %3").arg(m_hierarchy, QString::number(m_line), m_lineText); }