/** * DebuggerClient.cpp * Copyright (c) 2013 ROBLOX Corp. All Rights Reserved. */ #include "stdafx.h" #include "DebuggerClient.h" // Roblox Headers #include "util/StandardOut.h" #include "util/ScopedAssign.h" #include "v8datamodel/datamodel.h" #include "v8datamodel/changehistory.h" #include "reflection/Property.h" #include "Script/Script.h" // Qt Headers #include #include #include // Roblox Studio Headers #include "RobloxScriptDoc.h" #include "RobloxMainWindow.h" #include "DebuggerWidgets.h" #include "UpdateUIManager.h" #include "ScriptTextEditor.h" #include "RobloxDocManager.h" #include "QtUtilities.h" #include "RobloxSettings.h" #define STUDIO_EMIT_SIGNAL //dummy define to know where we are emitting a signal FASTFLAG(LuaDebugger) FASTFLAG(LuaDebuggerBreakOnError) FASTFLAGVARIABLE(StudioRemoveDebuggerResumeLock, false) static const char* kBreakOnErrorModeSetting = "BreakOnErrorMode"; static const char* kBreakOnErrorDialogDisabled = "BreakOnErrorDialogDisabled"; DebuggerClientManager& DebuggerClientManager::Instance() { static DebuggerClientManager instance; return instance; } DebuggerClientManager::DebuggerClientManager() : m_pCurrentDebuggerClient(NULL) , m_pActiveDebuggerClient(NULL) , m_pSeparatorAction(NULL) , m_bIgnorePauseExecution(false) , m_bDebuggerListUpdateRequested(false) { if (!FFlag::LuaDebugger) return; RobloxMainWindow& rbxMainWindow = UpdateUIManager::Instance().getMainWindow(); //connect with debugging commands rbxMainWindow.stepIntoAction->setData(STEP_INTO); rbxMainWindow.stepOutAction->setData(STEP_OUT); rbxMainWindow.stepOverAction->setData(STEP_OVER); //TODO: modify this code to use "actionState" paradigm connect(rbxMainWindow.stepIntoAction, SIGNAL(triggered()), this, SLOT(onStepAction())); connect(rbxMainWindow.stepOutAction, SIGNAL(triggered()), this, SLOT(onStepAction())); connect(rbxMainWindow.stepOverAction, SIGNAL(triggered()), this, SLOT(onStepAction())); //add shortcuts to action help QtUtilities::setActionShortcuts(*(rbxMainWindow.stepIntoAction), rbxMainWindow.stepIntoAction->shortcuts()); QtUtilities::setActionShortcuts(*(rbxMainWindow.stepOverAction), rbxMainWindow.stepOverAction->shortcuts()); QtUtilities::setActionShortcuts(*(rbxMainWindow.stepOutAction), rbxMainWindow.stepOutAction->shortcuts()); } void DebuggerClientManager::setDataModel(shared_ptr spDataModel) { if (m_spDataModel == spDataModel) return; if (m_spDataModel) { m_pCurrentDebuggerClient = NULL; m_pActiveDebuggerClient = NULL; m_cDebuggerAddedConnection.disconnect(); m_cDebuggerRemovedConnection.disconnect(); m_cRunTransitionConnection.disconnect(); updateDebugActions(false); std::vector copyDebuggerClients(debuggerClients); debuggerClients.clear(); for (size_t i = 0; i < copyDebuggerClients.size(); ++i) delete copyDebuggerClients[i]; STUDIO_EMIT_SIGNAL clearAll(); } m_spDataModel = spDataModel; if (m_spDataModel) { //if DebuggerManager is already present then add saved breakpoints RBX::Scripting::DebuggerManager& debuggerManager = RBX::Scripting::DebuggerManager::singleton(); m_cDebuggerAddedConnection = debuggerManager.debuggerAdded.connect(boost::bind(&DebuggerClientManager::onDebuggerAdded, this, _1)); m_cDebuggerRemovedConnection = debuggerManager.debuggerRemoved.connect(boost::bind(&DebuggerClientManager::onDebuggerRemoved, this, _1)); // also enable debugging if required if (!debuggerManager.getEnabled()) debuggerManager.enableDebugging(); if (FFlag::LuaDebuggerBreakOnError) { RobloxSettings settings; debuggerManager.setBreakOnErrorMode((RBX::Scripting::BreakOnErrorMode)settings.value(kBreakOnErrorModeSetting, (int)RBX::Scripting::BreakOnErrorMode_Never).toInt()); } RBX::RunService* pRunService = m_spDataModel->create(); if (pRunService) m_cRunTransitionConnection = pRunService->runTransitionSignal.connect(boost::bind(&DebuggerClientManager::onRunTransition, this, _1)); } } shared_ptr DebuggerClientManager::getDataModel() { return m_spDataModel; } bool DebuggerClientManager::actionState(const QString &actionID, bool &enableState, bool &checkedState) { if (FFlag::LuaDebuggerBreakOnError) { if (actionID == "neverBreakOnScriptErrorsAction") { checkedState = RBX::Scripting::DebuggerManager::singleton().getBreakOnErrorMode() == RBX::Scripting::BreakOnErrorMode_Never; enableState = true; return true; } if (actionID == "breakOnAllScriptErrorsAction") { checkedState = RBX::Scripting::DebuggerManager::singleton().getBreakOnErrorMode() == RBX::Scripting::BreakOnErrorMode_AllExceptions; enableState = true; return true; } if (actionID == "breakOnUnhandledScriptErrorsAction") { checkedState = RBX::Scripting::DebuggerManager::singleton().getBreakOnErrorMode() == RBX::Scripting::BreakOnErrorMode_UnhandledExceptions; enableState = true; return true; } } return false; } bool DebuggerClientManager::handleAction(const QString &actionID, bool checkedState) { if (FFlag::LuaDebuggerBreakOnError) { if (actionID == "neverBreakOnScriptErrorsAction") { RBX::Scripting::DebuggerManager::singleton().setBreakOnErrorMode(RBX::Scripting::BreakOnErrorMode_Never); RobloxSettings().setValue(kBreakOnErrorModeSetting, (int)RBX::Scripting::BreakOnErrorMode_Never); return true; } if (actionID == "breakOnAllScriptErrorsAction") { RBX::Scripting::DebuggerManager::singleton().setBreakOnErrorMode(RBX::Scripting::BreakOnErrorMode_AllExceptions); RobloxSettings().setValue(kBreakOnErrorModeSetting, (int)RBX::Scripting::BreakOnErrorMode_AllExceptions); return true; } if (actionID == "breakOnUnhandledScriptErrorsAction") { RBX::Scripting::DebuggerManager::singleton().setBreakOnErrorMode(RBX::Scripting::BreakOnErrorMode_UnhandledExceptions); RobloxSettings().setValue(kBreakOnErrorModeSetting, (int)RBX::Scripting::BreakOnErrorMode_UnhandledExceptions); return true; } } return false; } void DebuggerClientManager::addDebuggerClient(DebuggerClient* pDebuggerClient) { if (!m_spDataModel) return; QMutexLocker lock(&m_debuggerClientMutex); std::vector::iterator iter = debuggerClients.begin(); while (iter != debuggerClients.end()) { if (*iter == pDebuggerClient) return; ++iter; } debuggerClients.push_back(pDebuggerClient); } void DebuggerClientManager::removeDebuggerClient(DebuggerClient* pDebuggerClient) { if (!pDebuggerClient) return; QMutexLocker lock(&m_debuggerClientMutex); std::vector::iterator iter = debuggerClients.begin(); while (iter != debuggerClients.end()) { if (*iter == pDebuggerClient) { if (pDebuggerClient == m_pCurrentDebuggerClient) m_pCurrentDebuggerClient = NULL; if (pDebuggerClient == m_pActiveDebuggerClient) m_pActiveDebuggerClient = NULL; debuggerClients.erase(iter); break; } ++iter; } } void DebuggerClientManager::setActiveDebuggerClient(DebuggerClient* pDebuggerClient) { m_pActiveDebuggerClient = pDebuggerClient; if (m_pActiveDebuggerClient) STUDIO_EMIT_SIGNAL clientActivated(m_pActiveDebuggerClient->getCurrentLine(), m_pActiveDebuggerClient->getCurrentCallStack()); else STUDIO_EMIT_SIGNAL clientDeactivated(); } RBX::Reflection::Variant DebuggerClientManager::getWatchValue(boost::shared_ptr spWatch) { if (m_pActiveDebuggerClient) return m_pActiveDebuggerClient->getWatchValue(spWatch); return RBX::Reflection::Variant(); } void DebuggerClientManager::addWatch(const QString& watchKey) { if (m_pActiveDebuggerClient) m_pActiveDebuggerClient->addWatch(watchKey); } void DebuggerClientManager::syncBreakpointState(shared_ptr spBreakpoint) { if (!spBreakpoint || !spBreakpoint->getParent()) return; RBX::Scripting::ScriptDebugger* pDebugger = RBX::Instance::fastDynamicCast(spBreakpoint->getParent()); if (pDebugger) { DebuggerClient* pDebuggerClient = getDebuggerClient(shared_from(pDebugger->getScript())); if (pDebuggerClient) pDebuggerClient->syncBreakpointState(spBreakpoint->getLine()); } } DebuggerClient* DebuggerClientManager::getOrCreateDebuggerClient(shared_ptr spScript) { if (!FFlag::LuaDebugger || !m_spDataModel || !m_spDataModel->isAncestorOf(spScript.get())) return NULL; DebuggerClient* pDebuggerClient = getDebuggerClient(spScript); if (!pDebuggerClient) { try { pDebuggerClient = new DebuggerClient(spScript); } catch (std::runtime_error const& exp) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Exception: %s", exp.what()); } } return pDebuggerClient; } DebuggerClient* DebuggerClientManager::getDebuggerClient(shared_ptr spScript) { if (FFlag::LuaDebugger && m_spDataModel) { QMutexLocker lock(&m_debuggerClientMutex); for (size_t ii = 0; ii < debuggerClients.size(); ++ii) { if (debuggerClients[ii]->getScript() == spScript) return debuggerClients[ii]; } } return NULL; } const std::vector& DebuggerClientManager::getDebuggerClients() { return debuggerClients; } void DebuggerClientManager::breakpointEncounter(DebuggerClient *pDebuggerClient) { static QMutex breakpointEncounterMutex; QMutexLocker lock(&breakpointEncounterMutex); //looking for the first breakpoint hit, ignore rest of the hits if (m_pCurrentDebuggerClient) { // this has to be done in Main thread if (!m_bDebuggerListUpdateRequested) { m_bDebuggerListUpdateRequested = true; QMetaObject::invokeMethod(this, "updateDebuggersList", Qt::QueuedConnection); } return; } m_pCurrentDebuggerClient = pDebuggerClient; //open script document where we encountered first breakpoint m_bDebuggerListUpdateRequested = true; QMetaObject::invokeMethod(this, "updateScriptDocument", Qt::QueuedConnection); } void DebuggerClientManager::updateScriptDocument() { //make sure we've a current document (important in case of Play solo) if (!RobloxDocManager::Instance().getCurrentDoc()) { //TODO: how to ensure this doesn't go in an infinite loop? (counter?) QMetaObject::invokeMethod(this, "updateScriptDocument", Qt::QueuedConnection); return; } if (m_pCurrentDebuggerClient) { RBX::RunService* pRunState = m_spDataModel->find(); if(pRunState && pRunState->getRunState() == RBX::RS_RUNNING) pRunState->pause(); m_pCurrentDebuggerClient->setCurrentThread(m_pCurrentDebuggerClient->getCurrentThread()); //Break On Error Dialog if (m_pCurrentDebuggerClient->hasError()) displayBreakOnErrorDialog(); //Update document and debugger widgets updateDocumentData(); STUDIO_EMIT_SIGNAL breakpointEncountered(m_pCurrentDebuggerClient->getCurrentLine(), m_pCurrentDebuggerClient); } } void DebuggerClientManager::updateDebuggersList() { //Update document and debugger widgets //Here, we need to emit signal first and then update document data. signal emit will make sure correct thread is set! STUDIO_EMIT_SIGNAL debuggersListUpdated(m_pCurrentDebuggerClient); updateDocumentData(false, false); } void DebuggerClientManager::updateDocumentData(bool openDoc, bool updateMarkers) { if (m_pCurrentDebuggerClient && m_pCurrentDebuggerClient->updateDocument(openDoc, updateMarkers)) updateDebugActions(true); UpdateUIManager::Instance().updateToolBars(); m_bDebuggerListUpdateRequested = false; } void DebuggerClientManager::displayBreakOnErrorDialog() { bool breakOnErrorSet = RobloxSettings().value(kBreakOnErrorDialogDisabled, false).toBool(); if (!breakOnErrorSet) { // Create Dialog Box RBX::Scripting::DebuggerManager::singleton(); std::string labelMessage = std::string("The Roblox Lua Debugger caught an error and will now take you to the exception in the script.") + "\n\n You can modify this behavior, when a script is open, by going to the Script Menu tab and changing the 'Break On Error' settings. \n"; QtUtilities::RBXConfirmationMessageBox msgBox( QObject::tr(labelMessage.c_str()), QObject::tr("Keep breaking on error"), QObject::tr("Disable") ); msgBox.setMinimumWidth(350); msgBox.setMinimumHeight(90); // Execute Dialog Box UpdateUIManager::Instance().setBusy(true, false); QApplication::setOverrideCursor(Qt::ArrowCursor); msgBox.exec(); QApplication::restoreOverrideCursor(); UpdateUIManager::Instance().setBusy(false, false); // {Process Dialog Response if (msgBox.getClickedYes()) { RBX::Scripting::DebuggerManager::singleton().setBreakOnErrorMode(RBX::Scripting::DebuggerManager::singleton().getBreakOnErrorMode()); RobloxSettings().setValue(kBreakOnErrorModeSetting, (int)RBX::Scripting::DebuggerManager::singleton().getBreakOnErrorMode()); } else { RBX::Scripting::DebuggerManager::singleton().setBreakOnErrorMode(RBX::Scripting::BreakOnErrorMode_Never); RobloxSettings().setValue(kBreakOnErrorModeSetting, (int)RBX::Scripting::BreakOnErrorMode_Never); } if (msgBox.getCheckBoxState()) { RobloxSettings().setValue(kBreakOnErrorDialogDisabled, true); } } } void DebuggerClientManager::startDebugging(RBX::RunTransition evt) { try { if (evt.oldState == RBX::RS_STOPPED) { RBX::Scripting::DebuggerManager::singleton().setScriptAutoResume(false); STUDIO_EMIT_SIGNAL debuggingStarted(); } else if (evt.oldState == RBX::RS_PAUSED) { resumeAllClients(true); } } catch (std::runtime_error const& exp) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Exception: %s", exp.what()); } } void DebuggerClientManager::pauseDebugging(RBX::RunTransition evt) { try { pauseAllClients(true); } catch (std::runtime_error const& exp) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Exception: %s", exp.what()); } } void DebuggerClientManager::stopDebugging(RBX::RunTransition evt) { try { //reset current debugger client m_pCurrentDebuggerClient = NULL; RBX::Scripting::DebuggerManager::singleton().reset(); resetAllClients(); QMetaObject::invokeMethod(this, "updateDebugActions", Qt::QueuedConnection, Q_ARG(bool, false)); STUDIO_EMIT_SIGNAL debuggingStopped(); } catch (std::runtime_error const& exp) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Exception: %s", exp.what()); } } void DebuggerClientManager::onStepAction() { QAction* pAction = qobject_cast(sender()); if (!pAction) return; try { RBX::DataModel::LegacyLock lock(m_spDataModel, RBX::DataModelJob::Write); if (m_pCurrentDebuggerClient) m_pCurrentDebuggerClient->resetExecution(); STUDIO_EMIT_SIGNAL executionDataCleared(); m_pCurrentDebuggerClient = NULL; eStepMode stepMode = (eStepMode)pAction->data().toInt(); switch (stepMode) { case STEP_INTO: RBX::Scripting::DebuggerManager::singleton().stepInto(); break; case STEP_OUT: RBX::Scripting::DebuggerManager::singleton().stepOut(); break; case STEP_OVER: RBX::Scripting::DebuggerManager::singleton().stepOver(); break; } RBX::RunService* pRunState = m_spDataModel->find(); if(pRunState && pRunState->getRunState() == RBX::RS_PAUSED) pRunState->run(); UpdateUIManager::Instance().updateToolBars(); } catch (std::runtime_error const& exp) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Exception: %s", exp.what()); } catch (...) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Exception: Not known"); } } void DebuggerClientManager::onRunTransition(RBX::RunTransition evt) { try { switch (evt.newState) { case RBX::RS_STOPPED: stopDebugging(evt); break; case RBX::RS_RUNNING: startDebugging(evt); break; case RBX::RS_PAUSED: pauseDebugging(evt); break; } } catch (std::runtime_error const& exp) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Exception: %s", exp.what()); } UpdateUIManager::Instance().updateToolBars(); } void DebuggerClientManager::onDebuggerAdded(boost::shared_ptr spInstance) { if (!m_spDataModel) return; shared_ptr spDebugger = RBX::Instance::fastSharedDynamicCast(spInstance); if (spDebugger && spDebugger->getScript()) getOrCreateDebuggerClient(shared_from(spDebugger->getScript())); } void DebuggerClientManager::onDebuggerRemoved(boost::shared_ptr spInstance) { shared_ptr spDebugger = RBX::Instance::fastSharedDynamicCast(spInstance); if (spDebugger && spDebugger->getScript()) { DebuggerClient* pDebuggerClient = getDebuggerClient(shared_from(spDebugger->getScript())); if (pDebuggerClient) { // this has to be done in Main thread if (!m_bDebuggerListUpdateRequested) { if (pDebuggerClient->isPaused() || pDebuggerClient->hasError()) { m_bDebuggerListUpdateRequested = true; QMetaObject::invokeMethod(this, "updateDebuggersList", Qt::QueuedConnection); } } delete pDebuggerClient; } } } void DebuggerClientManager::pauseAllClients(bool forceAll) { static QMutex pauseExecutionMutex; QMutexLocker mlock(&pauseExecutionMutex); STUDIO_EMIT_SIGNAL executionDataCleared(); RBX::Scripting::DebuggerManager::singleton().pause(); } void DebuggerClientManager::resumeAllClients(bool forceAll) { DebuggerClient *pClient = m_pCurrentDebuggerClient; if (FFlag::StudioRemoveDebuggerResumeLock) { if (pClient) pClient->resetExecution(); STUDIO_EMIT_SIGNAL executionDataCleared(); m_pCurrentDebuggerClient = NULL; RBX::Scripting::DebuggerManager::singleton().resume(); } else { RBX::DataModel::LegacyLock lock(m_spDataModel, RBX::DataModelJob::Write); if (pClient) pClient->resetExecution(); STUDIO_EMIT_SIGNAL executionDataCleared(); m_pCurrentDebuggerClient = NULL; RBX::Scripting::DebuggerManager::singleton().resume(); } } void DebuggerClientManager::resetAllClients() { for (size_t ii = 0; ii < debuggerClients.size(); ++ii) debuggerClients[ii]->resetExecution(); STUDIO_EMIT_SIGNAL executionDataCleared(); } void DebuggerClientManager::updateDebugActions(bool add) { RobloxMainWindow& rbxMainWindow = UpdateUIManager::Instance().getMainWindow(); if (rbxMainWindow.isRibbonStyle()) return; if (add) { if (!m_pSeparatorAction) m_pSeparatorAction = rbxMainWindow.runToolBar->insertSeparator(NULL); else rbxMainWindow.runToolBar->insertAction(NULL, m_pSeparatorAction); rbxMainWindow.runToolBar->insertAction(NULL, rbxMainWindow.stepIntoAction); rbxMainWindow.runToolBar->insertAction(NULL, rbxMainWindow.stepOverAction); rbxMainWindow.runToolBar->insertAction(NULL, rbxMainWindow.stepOutAction); } else { rbxMainWindow.runToolBar->removeAction(rbxMainWindow.stepOutAction); rbxMainWindow.runToolBar->removeAction(rbxMainWindow.stepOverAction); rbxMainWindow.runToolBar->removeAction(rbxMainWindow.stepIntoAction); rbxMainWindow.runToolBar->removeAction(m_pSeparatorAction); } } //-------------------------------------------------------------------------------------------- // DebuggerClient //-------------------------------------------------------------------------------------------- DebuggerClient::DebuggerClient(boost::shared_ptr script) : m_spScript(script) , m_pQDebuggerClient(NULL) , m_pExtDebuggerClient(NULL) , m_CurrentFrame(0) , m_bIgnoreBreakpointAddRemove(false) , m_bIsActive(false) , m_currentThreadID(0) { DebuggerClientManager::Instance().addDebuggerClient(this); m_spDataModel = RBX::shared_from(RBX::Instance::fastDynamicCast(m_spScript->getRootAncestor())); // addDebugger can throw try { //'addDebugger' returns the current debugger if it is already added m_spDebugger = RBX::Scripting::DebuggerManager::singleton().addDebugger(m_spScript.get()); } catch (std::runtime_error const&) { // make sure we remove DebuggerClient from DebuggerClientManager and rethrow DebuggerClientManager::Instance().removeDebuggerClient(this); throw; } // add breakpoints const RBX::Scripting::ScriptDebugger::Breakpoints& breakpoints = m_spDebugger->getBreakpoints(); for (RBX::Scripting::ScriptDebugger::Breakpoints::const_iterator iter = breakpoints.begin(); iter != breakpoints.end(); ++iter) STUDIO_EMIT_SIGNAL DebuggerClientManager::Instance().breakpointAdded(shared_from(iter->second)); // add watches const RBX::Scripting::ScriptDebugger::Watches& watches = m_spDebugger->getWatches(); for (RBX::Scripting::ScriptDebugger::Watches::const_iterator iter = watches.begin(); iter != watches.end(); ++iter) STUDIO_EMIT_SIGNAL DebuggerClientManager::Instance().watchAdded(shared_from(*iter)); m_cConnections.push_back(m_spDebugger->resuming.connect(boost::bind(&DebuggerClient::onDebuggerResume, this))); m_cConnections.push_back(m_spDebugger->encounteredBreak.connect(boost::bind(&DebuggerClient::onEncounteredBreakpoint, this, _1))); m_cConnections.push_back(m_spDebugger->watchAdded.connect(boost::bind(&DebuggerClient::onWatchAdded, this, _1))); m_cConnections.push_back(m_spDebugger->breakpointAdded.connect(boost::bind(&DebuggerClient::onBreakpointAdded, this, _1))); m_cConnections.push_back(m_spDebugger->breakpointRemoved.connect(boost::bind(&DebuggerClient::onBreakpointRemoved, this, _1))); m_cConnections.push_back(m_spDebugger->watchRemoved.connect(boost::bind(&DebuggerClient::onWatchRemoved, this, _1))); m_cConnections.push_back(m_spDebugger->scriptErrorDetected.connect(boost::bind(&DebuggerClient::onScriptErrorDetected, this, _1, _2, _3))); if (RobloxScriptDoc* pDoc = RobloxDocManager::Instance().findOpenScriptDoc( LuaSourceBuffer::fromInstance(script))) { setDocument(pDoc); if (RobloxDocManager::Instance().getCurrentDoc() == pDoc) activate(); } } DebuggerClient::~DebuggerClient() { DebuggerClientManager::Instance().removeDebuggerClient(this); for (std::list::iterator iter=m_cConnections.begin(); iter != m_cConnections.end(); ++iter) (*iter).disconnect(); m_cConnections.clear(); if (m_pQDebuggerClient) { // we can have same text editor being used for a different script, so remove all connections as deletion can be delayed m_pQDebuggerClient->deActivate(); // now, request deletion m_pQDebuggerClient->deleteLater(); } } void DebuggerClient::setDocument(IRobloxDoc *pDocument) { if (pDocument && m_pQDebuggerClient) return; if (pDocument) { ScriptTextEditor* pTextEdit = dynamic_cast(pDocument->getViewer()); if (pTextEdit) { m_pQDebuggerClient = new QDebuggerClient(this, pTextEdit); //also mark breakpoints in the text editor RBX::Scripting::ScriptDebugger::Breakpoints breakpoints = m_spDebugger->getBreakpoints(); for (RBX::Scripting::ScriptDebugger::Breakpoints::const_iterator iter = breakpoints.begin(); iter != breakpoints.end(); ++iter) { RBX::Scripting::DebuggerBreakpoint* pBreakpoint = RBX::Instance::fastDynamicCast(iter->second); if(pBreakpoint) m_pQDebuggerClient->updateBreakpointState(pBreakpoint->getLine() - 1, RBX::Scripting::DebuggerBreakpoint::prop_Enabled.getValue(pBreakpoint) ? RBXTextUserData::ENABLED : RBXTextUserData::DISABLED); } if (m_pExtDebuggerClient) m_pQDebuggerClient->setExternalDebuggerClient(m_pExtDebuggerClient); } //clear code lines as now we will be having text editor to read the values from m_ScriptLines.clear(); } else { if (m_pQDebuggerClient) m_pQDebuggerClient->deleteLater(); m_pQDebuggerClient = NULL; } } void DebuggerClient::activate() { if (m_bIsActive) return; if (m_pQDebuggerClient) m_pQDebuggerClient->activate(); DebuggerClientManager::Instance().setActiveDebuggerClient(this); m_bIsActive = true; } void DebuggerClient::deActivate() { if (!m_bIsActive) return; if (m_pQDebuggerClient) m_pQDebuggerClient->deActivate(); DebuggerClientManager::Instance().setActiveDebuggerClient(NULL); m_bIsActive = false; } boost::shared_ptr DebuggerClient::getScript() { return m_spScript; } boost::shared_ptr DebuggerClient::getScript(int frameNo) { if (m_pExtDebuggerClient && (m_pExtDebuggerClient != this)) return m_pExtDebuggerClient->getScript(frameNo); if ((frameNo >= 0) && (frameNo < (int)m_CallStackAtPausedLine.size()) && m_CallStackAtPausedLine[frameNo].script) return m_CallStackAtPausedLine[frameNo].script; return getScript(); } bool DebuggerClient::updateDocument(bool openDoc, bool updateMarkers) { boost::shared_ptr spScript = getScript(getCurrentFrame()); if (!spScript) return false; DebuggerClient* pLuaDebuggerClient(this); if (spScript != getScript()) { //if scripts are different then set the external debugger for querying values pLuaDebuggerClient = DebuggerClientManager::Instance().getDebuggerClient(spScript); //make sure we've a valid debugger client (script can be deleted by now) if (!pLuaDebuggerClient) return false; pLuaDebuggerClient->setExternalDebuggerClient(this); } if (openDoc) RobloxDocManager::Instance().openDoc(LuaSourceBuffer::fromInstance(spScript)); if (updateMarkers) { int textEditLine = getCurrentLine() - 1; pLuaDebuggerClient->setMarker(textEditLine, ":/16x16/images/Studio 2.0 icons/16x16/debugger_arrow.png", true); pLuaDebuggerClient->highlightLine(textEditLine); } return true; } void DebuggerClient::setExternalDebuggerClient(DebuggerClient* pExtDebuggerClient) { RBXASSERT(pExtDebuggerClient != this); // just want to see when does this happen? if (pExtDebuggerClient != this) { m_pExtDebuggerClient = pExtDebuggerClient; if (m_pQDebuggerClient) m_pQDebuggerClient->setExternalDebuggerClient(pExtDebuggerClient); } } RBX::Reflection::Variant DebuggerClient::getWatchValue(boost::shared_ptr spWatch) { // if we have an external client set then get value from the external client if (m_pExtDebuggerClient && (m_pExtDebuggerClient != this)) return m_pExtDebuggerClient->getWatchValue(spWatch); if ((isPaused() || hasError()) && m_spDebugger->isAncestorOf(spWatch.get())) { RBX::DataModel::LegacyLock lock(m_spDataModel, RBX::DataModelJob::Write); RBX::Reflection::Variant var = m_spDebugger->getWatchValue(spWatch.get(), getCurrentFrame()); return var; } return RBX::Reflection::Variant(); } void DebuggerClient::addWatch(const QString& watchKey) { if (watchKey.isEmpty()) return; //first check if watch is already present const RBX::Scripting::ScriptDebugger::Watches& watches = m_spDebugger->getWatches(); for (RBX::Scripting::ScriptDebugger::Watches::const_iterator iter = watches.begin(); iter != watches.end(); ++iter) { if (QString::compare((*iter)->getExpression().c_str(), watchKey, Qt::CaseSensitive) == 0) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Watch expression '%s' is already available", qPrintable(watchKey)); return; } } RBX::DataModel::LegacyLock lock(m_spDataModel, RBX::DataModelJob::Write); boost::shared_ptr spWatch = m_spDebugger->addWatch(watchKey.toStdString()); try { spWatch->checkExpressionSyntax(); } catch (std::runtime_error const& exp) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Exception: %s", exp.what()); } } void DebuggerClient::highlightLine(int textEditLine) { if (m_pQDebuggerClient) m_pQDebuggerClient->highlightLine(textEditLine); } int DebuggerClient::getCurrentLine() { if (m_CallStackAtPausedLine.size() > 0 && m_CurrentFrame < (int)m_CallStackAtPausedLine.size()) return m_CallStackAtPausedLine[m_CurrentFrame].currentline; return (m_spDebugger ? m_spDebugger->getCurrentLine() : -1); } void DebuggerClient::setCurrentFrame(int frame) { LuaSourceBuffer scriptToOpen; if (m_pExtDebuggerClient) { DebuggerClientManager::Instance().setActiveDebuggerClient(m_pExtDebuggerClient); scriptToOpen = LuaSourceBuffer::fromInstance(m_pExtDebuggerClient->getScript(frame)); m_pExtDebuggerClient->setCurrentFrame(frame); } else { m_CurrentFrame = frame; if (m_CallStackAtPausedLine.size() > 0 && m_CurrentFrame < (int)m_CallStackAtPausedLine.size()) { DebuggerClientManager::Instance().setActiveDebuggerClient(this); scriptToOpen = LuaSourceBuffer::fromInstance(m_CallStackAtPausedLine[m_CurrentFrame].script); } } if (scriptToOpen.empty()) scriptToOpen = LuaSourceBuffer::fromInstance(m_spScript); RobloxDocManager::Instance().openDoc(scriptToOpen); } void DebuggerClient::setCurrentLine(int textEditLine) { if (m_pQDebuggerClient) m_pQDebuggerClient->setCurrentLine(textEditLine); } void DebuggerClient::setMarker(int textEditLine, const QString& marker, bool setBlockCurrent) { if (m_pQDebuggerClient) m_pQDebuggerClient->setMarker(textEditLine, marker, setBlockCurrent); } QString DebuggerClient::getSourceCodeAtLine(int breakpointLine) { if (breakpointLine > 0) { //there's an offset of 1 between breakpoint line and text editor block number if (m_pQDebuggerClient) return m_pQDebuggerClient->getSourceCodeAtLine(breakpointLine-1); try { if (m_ScriptLines.isEmpty()) { QString scriptCode = LuaSourceBuffer::fromInstance(m_spScript).getScriptText().c_str(); m_ScriptLines = scriptCode.split("\n"); } if ((breakpointLine - 1) < m_ScriptLines.size()) return m_ScriptLines[breakpointLine-1]; } catch (std::runtime_error const&) { m_ScriptLines.clear(); } } return ""; } void DebuggerClient::syncBreakpointState(int breakpointLine) { if (!m_pQDebuggerClient) return; RBXTextUserData::eBreakpointState state(RBXTextUserData::NO_BREAKPOINT); RBX::Scripting::DebuggerBreakpoint* pBreakpoint = m_spDebugger->findBreakpoint(breakpointLine); if (pBreakpoint) state = pBreakpoint->isEnabled() ? RBXTextUserData::ENABLED : RBXTextUserData::DISABLED; //there's an offset of 1 between breakpoint line and text editor block number m_pQDebuggerClient->updateBreakpointState(breakpointLine - 1, state); } void DebuggerClient::pauseExecution() { m_pExtDebuggerClient = NULL; if (!isPaused()) { m_spDebugger->pause(); if (m_bIsActive && m_pQDebuggerClient) m_pQDebuggerClient->updateEditor(); } } void DebuggerClient::resumeExecution() { m_pExtDebuggerClient = NULL; if (m_spDebugger && m_spDebugger->isDebugging()) { m_spDebugger->resume(); if (m_bIsActive && m_pQDebuggerClient) m_pQDebuggerClient->updateEditor(); } } void DebuggerClient::resetExecution() { if (m_pQDebuggerClient) { m_pQDebuggerClient->clearLineHighlight(); m_pQDebuggerClient->setMarker(m_spDebugger->getCurrentLine() - 1, "", false); } boost::shared_ptr spScript = getScript(m_CurrentFrame); if (spScript != getScript()) { DebuggerClient* pLuaDebuggerClient = DebuggerClientManager::Instance().getDebuggerClient(spScript); if (pLuaDebuggerClient && pLuaDebuggerClient->m_pQDebuggerClient) { pLuaDebuggerClient->m_pQDebuggerClient->clearLineHighlight(); pLuaDebuggerClient->m_pQDebuggerClient->setMarker(pLuaDebuggerClient->m_spDebugger->getCurrentLine() - 1, "", false); } } m_pExtDebuggerClient = NULL; m_CallStackAtPausedLine.clear(); m_CurrentFrame = 0; m_ErrorMessage = ""; m_currentThreadID = 0; } bool DebuggerClient::isDebugging() { if (m_spDebugger) return m_spDebugger->isDebugging(); return false; } bool DebuggerClient::isPaused() { if (m_spDebugger) return m_spDebugger->isPausedThread(m_currentThreadID); return false; } bool DebuggerClient::hasError() { if (m_spDebugger) return m_spDebugger->isErrorThread(m_currentThreadID); return false; } void DebuggerClient::syncScriptWithTextEditor(const BreakpointDetails& textEditBreakpoints) { //RBX::DataModel::LegacyLock lock(m_spDataModel, RBX::DataModelJob::Write); //delete existing breakpoints RBX::Scripting::ScriptDebugger::Breakpoints breakpoints = m_spDebugger->getBreakpoints(); for (RBX::Scripting::ScriptDebugger::Breakpoints::iterator iter = breakpoints.begin(); iter != breakpoints.end(); ++iter) (iter->second)->setParent(NULL); //recreate all the breakpoints again! boost::shared_ptr spBreakpoint; for (BreakpointDetails::const_iterator iter = textEditBreakpoints.begin(); iter != textEditBreakpoints.end(); ++iter) { spBreakpoint = m_spDebugger->setBreakpoint((*iter).line); if (spBreakpoint) { RBX::Scripting::DebuggerBreakpoint::prop_Enabled.setValue(spBreakpoint.get(), (*iter).isEnabled); //RBX::Scripting::DebuggerBreakpoint::prop_Condition.setValue(spBreakpoint.get(), condition); - TODO: add support for condition } } } void DebuggerClient::syncTextEdtiorWithScript() { if (!m_spDebugger) return; RBX::Scripting::ScriptDebugger::Breakpoints breakpoints = m_spDebugger->getBreakpoints(); for (RBX::Scripting::ScriptDebugger::Breakpoints::iterator iter = breakpoints.begin(); iter != breakpoints.end(); ++iter) syncBreakpointState(iter->first); } void DebuggerClient::toggleBreakpoint(int breakpointLine) { if(!isValid()) return; RBX::ScopedAssign ignoreBreakpointAddRemove(m_bIgnoreBreakpointAddRemove, true); bool isBreakpointAdded = false; RBX::DataModel::LegacyLock lock(m_spDataModel, RBX::DataModelJob::Write); RBX::Scripting::DebuggerBreakpoint* pBreakpoint = m_spDebugger->findBreakpoint(breakpointLine); if(!pBreakpoint) { pBreakpoint = m_spDebugger->setBreakpoint(breakpointLine).get(); STUDIO_EMIT_SIGNAL DebuggerClientManager::Instance().breakpointAdded(shared_from(pBreakpoint)); isBreakpointAdded = true; } else { STUDIO_EMIT_SIGNAL DebuggerClientManager::Instance().breakpointRemoved(shared_from(pBreakpoint)); pBreakpoint->setParent(NULL); } //update state in text editor also if (m_pQDebuggerClient) m_pQDebuggerClient->updateBreakpointState(breakpointLine - 1, isBreakpointAdded ? RBXTextUserData::ENABLED : RBXTextUserData::NO_BREAKPOINT); } void DebuggerClient::toggleBreakpointState(int breakpointLine) { RBX::Scripting::DebuggerBreakpoint* pBreakpoint = m_spDebugger->findBreakpoint(breakpointLine); if (pBreakpoint) { RBX::DataModel::LegacyLock lock(m_spDataModel, RBX::DataModelJob::Write); RBX::Scripting::DebuggerBreakpoint::prop_Enabled.setValue(pBreakpoint, !pBreakpoint->isEnabled()); } } bool DebuggerClient::isValid() { return (m_spDataModel && m_spScript && m_spDebugger); } int DebuggerClient::getCurrentThread() { return m_currentThreadID; } void DebuggerClient::setCurrentThread(int threadID) { if (m_pQDebuggerClient && (threadID != m_currentThreadID)) m_pQDebuggerClient->clearLineHighlight(); m_currentThreadID = threadID; m_spDebugger->setCurrentThread(threadID); const RBX::Scripting::ScriptDebugger::PausedThreads& pausedThreads = getPausedThreads(); RBX::Scripting::ScriptDebugger::PausedThreads::const_iterator iter = pausedThreads.find(threadID); if (iter != pausedThreads.cend()) { m_CallStackAtPausedLine = iter->second.callStack; m_ErrorMessage = iter->second.errorMessage; //set current frame if (m_CallStackAtPausedLine.size() > 0) m_CurrentFrame = m_CallStackAtPausedLine[0].frame; } } bool DebuggerClient::isHighlightingRequired() { return ((m_CallStackAtPausedLine.size() > 0) && (m_CallStackAtPausedLine[0].frame == m_CurrentFrame) && (getScript() == getScript(getCurrentFrame()))); } bool DebuggerClient::isTopFrameCurrent() { return ((m_CallStackAtPausedLine.size() > 0) && (m_CallStackAtPausedLine[0].frame == m_CurrentFrame)); } const RBX::Scripting::ScriptDebugger::PausedThreads& DebuggerClient::getPausedThreads() { return m_spDebugger->getPausedThreads(); } void DebuggerClient::onDebuggerResume() { resetExecution(); } void DebuggerClient::onEncounteredBreakpoint(int breakpointLine) { if(breakpointLine <= 0) return; if (!m_currentThreadID) m_currentThreadID = m_spDebugger->getCurrentThread(); //notify manager about the breakpoint DebuggerClientManager::Instance().breakpointEncounter(this); } void DebuggerClient::onScriptErrorDetected(int errorLine, std::string errorMessage, RBX::Scripting::ScriptDebugger::Stack stack) { if (errorLine <= 0) return; //save current stack information and error message m_CallStackAtPausedLine = stack; m_ErrorMessage = errorMessage; if (!m_currentThreadID) m_currentThreadID = m_spDebugger->getCurrentThread(); //set current frame if (m_CallStackAtPausedLine.size() > 0) m_CurrentFrame = m_CallStackAtPausedLine[0].frame; //notify manager about the breakpoint DebuggerClientManager::Instance().breakpointEncounter(this); } void DebuggerClient::onBreakpointAdded(shared_ptr spInstance) { if (m_bIgnoreBreakpointAddRemove) return; shared_ptr spBreakpoint = RBX::Instance::fastSharedDynamicCast(spInstance); if (!spBreakpoint || (spBreakpoint->getLine() < 0)) return; STUDIO_EMIT_SIGNAL DebuggerClientManager::Instance().breakpointAdded(spBreakpoint); if (m_pQDebuggerClient) { m_pQDebuggerClient->updateBreakpointState(spBreakpoint->getLine() - 1, RBX::Scripting::DebuggerBreakpoint::prop_Enabled.getValue(spBreakpoint.get()) ? RBXTextUserData::ENABLED : RBXTextUserData::DISABLED); } } void DebuggerClient::onBreakpointRemoved(shared_ptr spInstance) { if (m_bIgnoreBreakpointAddRemove) return; shared_ptr spBreakpoint = RBX::Instance::fastSharedDynamicCast(spInstance); if (!spBreakpoint || (spBreakpoint->getLine() <= 0)) return; STUDIO_EMIT_SIGNAL DebuggerClientManager::Instance().breakpointRemoved(spBreakpoint); if (m_pQDebuggerClient) m_pQDebuggerClient->updateBreakpointState(spBreakpoint->getLine() - 1); } void DebuggerClient::onWatchAdded(shared_ptr spInstance) { shared_ptr spWatch = RBX::Instance::fastSharedDynamicCast(spInstance); if (spWatch) STUDIO_EMIT_SIGNAL DebuggerClientManager::Instance().watchAdded(spWatch); } void DebuggerClient::onWatchRemoved(shared_ptr spInstance) { shared_ptr spWatch = RBX::Instance::fastSharedDynamicCast(spInstance); if (spWatch) STUDIO_EMIT_SIGNAL DebuggerClientManager::Instance().watchRemoved(spWatch); } void DebuggerClient::onScriptStopped() { if (m_pQDebuggerClient) m_pQDebuggerClient->clearLineHighlight(); m_CallStackAtPausedLine.clear(); m_CurrentFrame = 0; } bool DebuggerClient::getValue(const QString& key, RBX::Reflection::Variant& value, int frame) { bool result = false; try { RBX::DataModel::LegacyLock lock(m_spDataModel, RBX::DataModelJob::Write); value = m_spDebugger->getKeyValue(key.toStdString(), frame > 0 ? frame : m_CurrentFrame); if (!value.isVoid()) result = true; } catch (...) { result = false; } return result; } bool DebuggerClient::isKeyDefined(boost::shared_ptr variables, const QString &key, RBX::Reflection::Variant &value) { if(variables && !variables->empty()) { for(RBX::Reflection::ValueMap::const_iterator iter = variables->begin(); iter != variables->end(); ++iter) { if (QString::compare(iter->first.c_str(), key, Qt::CaseSensitive) == 0) { value = iter->second; return true; } } } return false; } bool DebuggerClient::actionState(const QString &actionID, bool &enableState, bool &checkedState) { static QString defaultActiveActions("insertBreakpointAction deleteBreakpointAction toggleBreakpointStateAction addWatchAction"); if (defaultActiveActions.contains(actionID)) { checkedState = false; enableState = true; return true; } if (DebuggerClientManager::Instance().actionState(actionID, enableState, checkedState)) return true; static QString defaultDebugActions("stepIntoAction stepOverAction stepOutAction"); if (defaultDebugActions.contains(actionID)) { DebuggerClient* pClient = this; if (m_pExtDebuggerClient) pClient = m_pExtDebuggerClient; checkedState = false; enableState = pClient->isPaused() && !pClient->hasError(); return true; } return false; } bool DebuggerClient::handleAction(const QString &actionID, bool checkedState) { if (DebuggerClientManager::Instance().handleAction(actionID, checkedState)) return true; return false; } //-------------------------------------------------------------------------------------------- // QDebuggerClient //-------------------------------------------------------------------------------------------- QDebuggerClient::QDebuggerClient(DebuggerClient* pDebuggerClient, ScriptTextEditor* pTextEdit) : m_pDebuggerClient(pDebuggerClient) , m_pExtDebuggerClient(NULL) , m_pBreakpointMenuAction(NULL) , m_pSeparatorAction1(NULL) , m_pSeparatorAction2(NULL) , m_pTextEdit(pTextEdit) , m_lastBlockCount(pTextEdit->blockCount()) , m_isModifiedByKeyBoard(false) , m_ignoreAutoRepeatEvent(true) { m_pToolTipWidget = new DebuggerToolTipWidget(m_pTextEdit); } QDebuggerClient::~QDebuggerClient() { m_pToolTipWidget->deleteLater(); m_pToolTipWidget = NULL; m_pTextEdit = NULL;//this will get deleted as a part of ScriptDoc } void QDebuggerClient::activate() { connect(m_pTextEdit, SIGNAL(toggleBreakpoint(int)), this, SLOT(onToggleBreakpoint(int))); connect(m_pTextEdit, SIGNAL(showToolTip(const QPoint&, const QString&)), this, SLOT(onShowToolTip(const QPoint&, const QString&))); connect(m_pTextEdit, SIGNAL(updateContextualMenu(QMenu*, QPoint)), this, SLOT(onUpdateContextualMenu(QMenu*, QPoint))); connect(m_pTextEdit->document(), SIGNAL(contentsChanged()), this, SLOT(onContentsChanged())); RobloxMainWindow& rbxMainWindow = UpdateUIManager::Instance().getMainWindow(); connect(rbxMainWindow.insertBreakpointAction, SIGNAL(triggered()), this, SLOT(onInsertBreakpoint())); connect(rbxMainWindow.deleteBreakpointAction, SIGNAL(triggered()), this, SLOT(onDeleteBreakpoint())); connect(rbxMainWindow.toggleBreakpointStateAction, SIGNAL(triggered()), this, SLOT(onToggleBreakpointState())); connect(rbxMainWindow.addWatchAction, SIGNAL(triggered()), this, SLOT(onAddWatch())); if (m_pDebuggerClient->isHighlightingRequired()) highlightLine(m_pDebuggerClient->getCurrentLine() - 1); m_pTextEdit->installEventFilter(this); } void QDebuggerClient::deActivate() { m_pTextEdit->removeEventFilter(this); cleanupContextMenu(); disconnect(m_pTextEdit, SIGNAL(toggleBreakpoint(int)), this, SLOT(onToggleBreakpoint(int))); disconnect(m_pTextEdit, SIGNAL(showToolTip(const QPoint&, const QString&)), this, SLOT(onShowToolTip(const QPoint&, const QString&))); disconnect(m_pTextEdit, SIGNAL(updateContextualMenu(QMenu*, QPoint)), this, SLOT(onUpdateContextualMenu(QMenu*, QPoint))); disconnect(m_pTextEdit->document(), SIGNAL(contentsChanged()), this, SLOT(onContentsChanged())); RobloxMainWindow& rbxMainWindow = UpdateUIManager::Instance().getMainWindow(); disconnect(rbxMainWindow.insertBreakpointAction, SIGNAL(triggered()), this, SLOT(onInsertBreakpoint())); disconnect(rbxMainWindow.deleteBreakpointAction, SIGNAL(triggered()), this, SLOT(onDeleteBreakpoint())); disconnect(rbxMainWindow.toggleBreakpointStateAction, SIGNAL(triggered()), this, SLOT(onToggleBreakpointState())); disconnect(rbxMainWindow.addWatchAction, SIGNAL(triggered()), this, SLOT(onAddWatch())); } void QDebuggerClient::highlightLine(int textEditLine) { QMetaObject::invokeMethod(this, "highlightLine_MT", Qt::AutoConnection, Q_ARG(int, textEditLine)); } void QDebuggerClient::clearLineHighlight() { QMetaObject::invokeMethod(this, "clearLineHighlight_MT", Qt::AutoConnection); } void QDebuggerClient::setCurrentLine(int textEditLine) { QMetaObject::invokeMethod(this, "setCurrentLine_MT", Qt::AutoConnection, Q_ARG(int, textEditLine)); } void QDebuggerClient::setMarker(int textEditLine, const QString& marker, bool setBlockCurrent) { QMetaObject::invokeMethod(this, "setMarker_MT", Qt::AutoConnection, Q_ARG(int, textEditLine), Q_ARG(QString, marker), Q_ARG(bool, setBlockCurrent)); } void QDebuggerClient::updateEditor() { if (m_pTextEdit) m_pTextEdit->update(); } QString QDebuggerClient::getSourceCodeAtLine(int textEditLine) { QString text; QTextBlock currentBlock = m_pTextEdit->document()->findBlockByNumber(textEditLine); if(currentBlock.isValid()) text = currentBlock.text(); return text; } void QDebuggerClient::updateBreakpointState(int textEditLine, RBXTextUserData::eBreakpointState state) { updateTextUserData(textEditLine, state); } void QDebuggerClient::onInsertBreakpoint() { QTextBlock block = m_pTextEdit->textCursor().block(); if (block.isValid()) { m_pDebuggerClient->toggleBreakpoint(block.blockNumber()+1); updateTextUserData(block.blockNumber(), RBXTextUserData::ENABLED); } } void QDebuggerClient::onDeleteBreakpoint() { QTextBlock block = m_pTextEdit->textCursor().block(); if (block.isValid()) { m_pDebuggerClient->toggleBreakpoint(block.blockNumber()+1); updateTextUserData(block.blockNumber(), RBXTextUserData::NO_BREAKPOINT); } } void QDebuggerClient::onToggleBreakpoint(int textEditLine) { m_pDebuggerClient->toggleBreakpoint(textEditLine+1); } void QDebuggerClient::onToggleBreakpointState() { QTextBlock block = m_pTextEdit->textCursor().block(); if (block.isValid()) m_pDebuggerClient->toggleBreakpointState(block.blockNumber()+1); } void QDebuggerClient::onAddWatch() { QString watchExpression(m_pTextEdit->textCursor().selectedText()); // if there's no selected text then get user input if (watchExpression.isEmpty()) watchExpression = QInputDialog::getText(m_pTextEdit, tr("Watch"), tr("Watch Expression:"), QLineEdit::Normal, watchExpression, NULL, Qt::WindowTitleHint | Qt::WindowSystemMenuHint); // if we've got an input, add watch if (!watchExpression.isEmpty()) { m_pDebuggerClient->addWatch(watchExpression); // make sure watch widget is visible DebuggerViewsManager::Instance().showDockWidget(DebuggerViewsManager::eDV_WATCH); } } void QDebuggerClient::highlightLine_MT(int textEditLine) { if(!m_pTextEdit || textEditLine < 0 || textEditLine > m_pTextEdit->blockCount()) return; // do not highlight if there's no breakpoint hit or error DebuggerClient* pDebuggerClient = m_pExtDebuggerClient ? m_pExtDebuggerClient : m_pDebuggerClient; if (!pDebuggerClient || (!pDebuggerClient->isPaused() && !pDebuggerClient->hasError())) return; if (m_pToolTipWidget) m_pToolTipWidget->hideToolTip(); QTextBlock blockToSet = m_pTextEdit->document()->findBlockByNumber(textEditLine); if(!blockToSet.isValid()) return; //make block current setCurrentBlock(blockToSet); //highlight block highlightBlock(blockToSet); } void QDebuggerClient::clearLineHighlight_MT() { // Clear text edit extra selections m_pTextEdit->removeFromExtraSelections(m_extraSelections); m_extraSelections.clear(); } void QDebuggerClient::setCurrentLine_MT(int textEditLine) { if(!m_pTextEdit || textEditLine < 0 || textEditLine > m_pTextEdit->blockCount()) return; QTextBlock block = m_pTextEdit->document()->findBlockByNumber(textEditLine); if(!block.isValid()) return; //make block current setCurrentBlock(block); } void QDebuggerClient::setMarker_MT(int textEditLine, QString marker, bool setBlockCurrent) { if(!m_pTextEdit || textEditLine < 0 || textEditLine > m_pTextEdit->blockCount()) return; QTextBlock block = m_pTextEdit->document()->findBlockByNumber(textEditLine); if(!block.isValid()) return; //set marker RBXTextUserData* pUserData = dynamic_cast(block.userData()); if (pUserData) pUserData->setMarker(marker); //make block current if (setBlockCurrent) setCurrentBlock(block); } void QDebuggerClient::setCurrentBlock(const QTextBlock &blockToSet) { QTextCursor cursor = m_pTextEdit->textCursor(); cursor.setPosition(blockToSet.position()); m_pTextEdit->setTextCursor(cursor); } void QDebuggerClient::highlightBlock(const QTextBlock &blockToHighlight) { QTextEdit::ExtraSelection selection; selection.format.setProperty(QTextFormat::FullWidthSelection, true); QColor backgroundColor(QColor(Qt::yellow).lighter(180)); DebuggerClient* pDebuggerClient = m_pExtDebuggerClient ? m_pExtDebuggerClient : m_pDebuggerClient; if (pDebuggerClient->hasError()) backgroundColor = QColor(Qt::red).lighter(180); selection.format.setBackground(QBrush(backgroundColor)); selection.cursor = QTextCursor(blockToHighlight); selection.cursor.clearSelection(); if (m_pTextEdit->addToExtraSelections(selection, true)) m_extraSelections.append(selection); } bool QDebuggerClient::onShowToolTip(const QPoint& pos, const QString& key) { DebuggerClient* pDebuggerClient = m_pExtDebuggerClient ? m_pExtDebuggerClient : m_pDebuggerClient; if (pDebuggerClient->hasError() && pDebuggerClient->isTopFrameCurrent()) { QTextCursor cursor = m_pTextEdit->cursorForPosition(m_pTextEdit->viewport()->mapFromGlobal(pos)); if (cursor.block().isValid() && (cursor.block().blockNumber() == (pDebuggerClient->getCurrentLine() - 1)) && !pDebuggerClient->getErrorMessage().empty()) { m_pToolTipWidget->showToolTip(pos, "Error", pDebuggerClient->getErrorMessage()); QTreeWidgetItem* pItem = m_pToolTipWidget->topLevelItem(0); if (pItem) pItem->setIcon(0, QIcon(":/16x16/images/Studio 2.0 icons/16x16/stop_16.png")); return true; } } //if empty key or no debugging, hide any visible tooltip // variable name can start with a letter or '_' if (key.isEmpty() || (!key.at(0).isLetter() && key.at(0) != '_') || !pDebuggerClient->isDebugging() || !(pDebuggerClient->isPaused() || pDebuggerClient->hasError())) { m_pToolTipWidget->hideToolTip(); return false; } //if the key hasn't been changed just mouse is move then move the tooltip if (m_pToolTipWidget->isKeyValid(key)) { m_pToolTipWidget->showAt(pos); return true; } //hide if tooptip is already visible m_pToolTipWidget->hideToolTip(); //now get the value corresponding to the key and show tooltip RBX::Reflection::Variant value; if (!pDebuggerClient->getValue(key, value, m_pDebuggerClient->getCurrentFrame())) return false; m_pToolTipWidget->showToolTip(pos, key, value); return true; } void QDebuggerClient::onContentsChanged() { if (m_lastBlockCount != m_pTextEdit->blockCount()) { m_pDebuggerClient->syncScriptWithTextEditor(getBreakpointDetails()); m_lastBlockCount = m_pTextEdit->blockCount(); } else if (!m_isModifiedByKeyBoard) { //update only if contents are not changed by keyboard, it must have been changed by an action (Undo/Redo/Comment Selection etc) removeBreakpointState(); m_pDebuggerClient->syncTextEdtiorWithScript(); } } void QDebuggerClient::updateTextUserData(int textEditLine, RBXTextUserData::eBreakpointState state) { QTextBlock block = m_pTextEdit->document()->findBlockByNumber(textEditLine); if(block.isValid()) { RBXTextUserData* pUserData = dynamic_cast(block.userData()); if(pUserData) { pUserData->setBreakpointState(state); updateEditor(); } } } void QDebuggerClient::modifyContextualMenu(QMenu* pContextualMenu) { if (!pContextualMenu || m_pBreakpointMenuAction) return; RobloxMainWindow& rbxMainWindow = UpdateUIManager::Instance().getMainWindow(); QMenu* pBreakpointMenu = new QMenu(tr("Breakpoint"), pContextualMenu); pBreakpointMenu->setObjectName("BreakpointMenu"); pBreakpointMenu->addAction(rbxMainWindow.insertBreakpointAction); pBreakpointMenu->addAction(rbxMainWindow.deleteBreakpointAction); pBreakpointMenu->addAction(rbxMainWindow.toggleBreakpointStateAction); connect(pBreakpointMenu, SIGNAL(aboutToShow()), &UpdateUIManager::Instance(), SLOT(onMenuShow())); m_pBreakpointMenuAction = pContextualMenu->insertMenu(rbxMainWindow.commentSelectionAction, pBreakpointMenu); m_pSeparatorAction1 = pContextualMenu->insertSeparator(rbxMainWindow.commentSelectionAction); pContextualMenu->insertAction(rbxMainWindow.commentSelectionAction, rbxMainWindow.addWatchAction); m_pSeparatorAction2 = pContextualMenu->insertSeparator(rbxMainWindow.commentSelectionAction); } void QDebuggerClient::cleanupContextMenu() { QMenu* pContextualMenu = m_pTextEdit->getContextualMenu(); if (pContextualMenu && m_pBreakpointMenuAction) { pContextualMenu->removeAction(m_pBreakpointMenuAction); delete m_pBreakpointMenuAction; m_pBreakpointMenuAction = NULL; QMenu* pBreakpointMenu = pContextualMenu->findChild("BreakpointMenu"); if (pBreakpointMenu) { disconnect(pBreakpointMenu, SIGNAL(aboutToShow()), &UpdateUIManager::Instance(), SLOT(onMenuShow())); delete pBreakpointMenu; } pContextualMenu->removeAction(m_pSeparatorAction1); delete m_pSeparatorAction1; m_pSeparatorAction1 = NULL; pContextualMenu->removeAction(m_pSeparatorAction2); delete m_pSeparatorAction2; m_pSeparatorAction2 = NULL; pContextualMenu->removeAction(UpdateUIManager::Instance().getMainWindow().addWatchAction); } } void QDebuggerClient::onUpdateContextualMenu(QMenu* pContextualMenu, QPoint pos) { if (!pContextualMenu) return; // if required, update contextual menu modifyContextualMenu(pContextualMenu); // check if we need to update cursor position QTextCursor clickedCursor = m_pTextEdit->cursorForPosition(pos); QTextCursor selectedTextCursor = m_pTextEdit->textCursor(); int startPos = selectedTextCursor.selectionStart(); int endPos = selectedTextCursor.selectionEnd(); if ( selectedTextCursor.hasSelection() && (clickedCursor.position() >= startPos) && (clickedCursor.position() <= endPos) ) { // if clicked inside the selected text QTextBlock startBlock = m_pTextEdit->document()->findBlock(startPos); QTextBlock endBlock = m_pTextEdit->document()->findBlock(endPos); if (startBlock.blockNumber() != endBlock.blockNumber()) { // if blocks number are different i.e. multiple line selection, then disable breakpoint and watch actions m_pBreakpointMenuAction->setEnabled(false); UpdateUIManager::Instance().getMainWindow().addWatchAction->setEnabled(false); return; } } else { // if clicked outside selected text then set clicked position and remove selection if any m_pTextEdit->setTextCursor(clickedCursor); } // now update relevent actions QTextBlock block = m_pTextEdit->textCursor().block(); if (block.isValid()) { RBXTextUserData* pUserData = dynamic_cast(block.userData()); if(pUserData) { RBXTextUserData::eBreakpointState state = pUserData->getBreakpointState(); RobloxMainWindow& rbxMainWindow = UpdateUIManager::Instance().getMainWindow(); rbxMainWindow.insertBreakpointAction->setVisible(state == RBXTextUserData::NO_BREAKPOINT); rbxMainWindow.deleteBreakpointAction->setVisible(state != RBXTextUserData::NO_BREAKPOINT); rbxMainWindow.toggleBreakpointStateAction->setVisible(state != RBXTextUserData::NO_BREAKPOINT); //update action text appropriately if (state == RBXTextUserData::ENABLED) { rbxMainWindow.toggleBreakpointStateAction->setText(tr("Disable Breakpoint")); rbxMainWindow.toggleBreakpointStateAction->setToolTip(tr("Disable Breakpoint")); } else if (state == RBXTextUserData::DISABLED) { rbxMainWindow.toggleBreakpointStateAction->setText(tr("Enable Breakpoint")); rbxMainWindow.toggleBreakpointStateAction->setToolTip(tr("Enable Breakpoint")); } m_pBreakpointMenuAction->setEnabled(true); rbxMainWindow.addWatchAction->setEnabled(true); } } } bool QDebuggerClient::eventFilter(QObject *obj, QEvent *evt) { if (obj == m_pTextEdit) { // For scenario mentioned in DE7694: to avoid accidental modification of script text keep on ignoring auto repeat events till it stops if (m_ignoreAutoRepeatEvent && ((evt->type() == QEvent::KeyPress) || (evt->type() == QEvent::KeyRelease))) { if (static_cast(evt)->isAutoRepeat()) { evt->accept(); return true; } else { m_ignoreAutoRepeatEvent = false; } } if (evt->type() == QEvent::FocusOut) { m_ignoreAutoRepeatEvent = true; } else if ((evt->type() == QEvent::KeyPress) || (evt->type() == QEvent::ShortcutOverride)) { m_isModifiedByKeyBoard = true; //if either of the modifiers are set, then user might be using a shortcut or else ignore all contents changed by keyboard Qt::KeyboardModifiers modifier = static_cast(evt)->modifiers(); if (modifier & Qt::ShiftModifier || modifier & Qt::ControlModifier || modifier & Qt::AltModifier) m_isModifiedByKeyBoard = false; } else if (evt->type() == QEvent::KeyRelease) { m_isModifiedByKeyBoard = false; } } return false; } DebuggerClient::BreakpointDetails QDebuggerClient::getBreakpointDetails() { DebuggerClient::BreakpointDetails textEditBreakpoints; QTextBlock block = m_pTextEdit->document()->firstBlock(); while (block.isValid()) { RBXTextUserData* pUserData = dynamic_cast(block.userData()); if(pUserData) { RBXTextUserData::eBreakpointState state = pUserData->getBreakpointState(); if (state != RBXTextUserData::NO_BREAKPOINT) textEditBreakpoints.push_back(DebuggerClient::BreakpointDetail(block.blockNumber() + 1, state == RBXTextUserData::ENABLED ? true: false)); } block = block.next(); } return textEditBreakpoints; } void QDebuggerClient::removeBreakpointState() { QTextBlock block = m_pTextEdit->document()->firstBlock(); while (block.isValid()) { RBXTextUserData* pUserData = dynamic_cast(block.userData()); if(pUserData) pUserData->setBreakpointState(RBXTextUserData::NO_BREAKPOINT); block = block.next(); } }