mirror of
https://github.com/copyrighttxt/watrbx-game-engine.git
synced 2026-09-04 20:57:49 +00:00
1422 lines
43 KiB
C++
1422 lines
43 KiB
C++
/**
|
|
* RobloxPluginHost.cpp
|
|
* Copyright (c) 2013 ROBLOX Corp. All rights reserved.
|
|
*/
|
|
|
|
#include "stdafx.h"
|
|
#include "RobloxPluginHost.h"
|
|
|
|
// Qt Headers
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
#include <QStringList>
|
|
#include <QTextStream>
|
|
#include <QThread>
|
|
#include <QToolBar>
|
|
#include <QIODevice>
|
|
|
|
// Roblox Headers
|
|
#include "GfxBase/ViewBase.h"
|
|
#include "Script/ScriptContext.h"
|
|
#include "Util/ProtectedString.h"
|
|
#include "V8DataModel/DataModel.h"
|
|
#include "V8DataModel/PluginManager.h"
|
|
#include "V8DataModel/Stats.h"
|
|
#include "V8DataModel/Workspace.h"
|
|
#include "v8datamodel/ContentProvider.h"
|
|
#include "V8Xml/Serializer.h"
|
|
#include "V8Xml/WebParser.h"
|
|
#include "V8Xml/XmlSerializer.h"
|
|
|
|
// Roblox Studio Headers
|
|
#include "AuthoringSettings.h"
|
|
#include "RobloxCustomWidgets.h"
|
|
#include "RobloxDocManager.h"
|
|
#include "RobloxIDEDoc.h"
|
|
#include "RobloxMainWindow.h"
|
|
#include "RobloxSettings.h"
|
|
#include "RobloxUser.h"
|
|
#include "UpdateUIManager.h"
|
|
#include "RobloxQuickAccessConfig.h"
|
|
#include "RobloxContextualHelp.h"
|
|
#include "IRobloxDoc.h"
|
|
#include "QtUtilities.h"
|
|
|
|
#include "Util/RobloxGoogleAnalytics.h"
|
|
#include "PluginAction.h"
|
|
|
|
#include "CSGOperations.h"
|
|
#include "v8datamodel/PartOperation.h"
|
|
#include "V8DataModel/NonReplicatedCSGDictionaryService.h"
|
|
#include "V8DataModel/ChangeHistory.h"
|
|
#include "V8Xml/SerializerBinary.h"
|
|
#include "V8Tree/Service.h"
|
|
|
|
#include "rbx/make_shared.h"
|
|
|
|
Q_DECLARE_METATYPE(QList<int>);
|
|
|
|
FASTFLAG(StudioEnableGameAnimationsTab)
|
|
|
|
FASTFLAGVARIABLE(EnableCSGScripting, true)
|
|
FASTFLAGVARIABLE(StudioDisableToolbarUpdateOnButtonActive, false)
|
|
|
|
static const QString PluginToolbarName = "Plugins";
|
|
static const QString TerrainToolbarName = "Terrain";
|
|
static const QString TransformToolbarName = "Transform";
|
|
static const QString SmoothTerrainToolbarName = "Smooth Terrain";
|
|
|
|
int RobloxPluginHost::m_NextID = 1;
|
|
static const char* kFailedToLoadPluginIconIcon = "/textures/ui/CloseButton_dn.png";
|
|
|
|
FASTFLAG(GoogleAnalyticsTrackingEnabled)
|
|
|
|
static const char* kPluginFileName = "Plugin.rbxm";
|
|
static const char* kAssetVersionJsonKey = "AssetVersion";
|
|
static const char* kEnabledJsonKey = "Enabled";
|
|
static const char* kPluginSettingsFileName = "settings.json";
|
|
|
|
static const char* kIsChecked = "isChecked";
|
|
static const char* kIsVisible = "isVisible";
|
|
|
|
RobloxPluginHost::RobloxPluginHost(QObject* pParent)
|
|
: QObject(pParent),
|
|
m_PluginsToolbar(NULL),
|
|
m_TerrainToolbar(NULL),
|
|
m_ConvertToSmoothID(0)
|
|
{
|
|
RBX::PluginManager::singleton()->setStudioHost(this);
|
|
|
|
installEventFilter(this);
|
|
}
|
|
|
|
RobloxPluginHost::~RobloxPluginHost()
|
|
{
|
|
}
|
|
|
|
void RobloxPluginHost::setNotifier(RBX::IHostNotifier *notifier)
|
|
{
|
|
m_pNotifier = notifier;
|
|
}
|
|
|
|
void* RobloxPluginHost::createToolbar(const std::string& name)
|
|
{
|
|
RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow();
|
|
|
|
if (mainWindow.isRibbonStyle())
|
|
{
|
|
if (name == TerrainToolbarName.toStdString())
|
|
return (void*)m_TerrainToolbarID;
|
|
else if (name == TransformToolbarName.toStdString())
|
|
return (void*)m_TransformToolbarID;
|
|
}
|
|
|
|
int toolbarID = m_NextID++;
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
"onCreateToolbar",
|
|
Qt::QueuedConnection,
|
|
Q_ARG(int,toolbarID),
|
|
Q_ARG(QString,name.c_str()) );
|
|
|
|
return (void*)toolbarID;
|
|
}
|
|
|
|
void* RobloxPluginHost::createButton(
|
|
void* tbId,
|
|
const std::string& text,
|
|
const std::string& tooltip,
|
|
const std::string& iconFilePath )
|
|
{
|
|
int toolbarID = (int)tbId;
|
|
|
|
RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow();
|
|
|
|
if (mainWindow.isRibbonStyle() && m_TextActionMap.contains(text.c_str()))
|
|
{
|
|
if (toolbarID == m_TerrainToolbarID)
|
|
return (void*)m_TextActionMap[text.c_str()];
|
|
else if(toolbarID == m_TransformToolbarID)
|
|
return (void*)m_TextActionMap[text.c_str()];
|
|
}
|
|
|
|
int actionID = m_NextID++;
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
"onCreateButton",
|
|
Qt::QueuedConnection,
|
|
Q_ARG(int,toolbarID),
|
|
Q_ARG(int,actionID),
|
|
Q_ARG(QString,text.c_str()),
|
|
Q_ARG(QString,tooltip.c_str()),
|
|
Q_ARG(QString,QString::fromUtf8(iconFilePath.c_str())) );
|
|
|
|
return (void*)actionID;
|
|
}
|
|
|
|
void RobloxPluginHost::setButtonActive(void *bId, bool active)
|
|
{
|
|
QMetaObject::invokeMethod(this, "onSetButtonActive", Qt::QueuedConnection,
|
|
Q_ARG(int, (int)(bId)), Q_ARG(bool, active));
|
|
}
|
|
|
|
void RobloxPluginHost::setButtonIcon_deprecated(void* buttonId, const std::string& iconFilePath)
|
|
{
|
|
QMetaObject::invokeMethod(this, "setButtonIconInternal_deprecated", Qt::QueuedConnection,
|
|
Q_ARG(int, (int)(buttonId)), Q_ARG(QString, QString(iconFilePath.c_str())));
|
|
}
|
|
|
|
void RobloxPluginHost::setButtonIcon(void* buttonId, const std::string& imageContent)
|
|
{
|
|
QImage image;
|
|
image.loadFromData((const uchar*)imageContent.c_str(), imageContent.size());
|
|
if (image.isNull())
|
|
{
|
|
RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR,
|
|
"Unable to load plugin icon. Image may have an invalid or unknown format.");
|
|
buttonIconFailedToLoad(buttonId);
|
|
}
|
|
else
|
|
{
|
|
QMetaObject::invokeMethod(this, "setButtonIconInternal", Qt::QueuedConnection,
|
|
Q_ARG(int, (int)(buttonId)), Q_ARG(QImage, image));
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::buttonIconFailedToLoad(void* buttonId)
|
|
{
|
|
QImage image;
|
|
image.load(QString(AppSettings::instance().contentFolder() + kFailedToLoadPluginIconIcon));
|
|
QMetaObject::invokeMethod(this, "setButtonIconInternal", Qt::QueuedConnection,
|
|
Q_ARG(int, (int)(buttonId)), Q_ARG(QImage, image));
|
|
}
|
|
|
|
void RobloxPluginHost::setButtonIconInternal_deprecated(int actionId, QString iconFilePath)
|
|
{
|
|
if (!m_ActionMap.contains(actionId))
|
|
{
|
|
// This can happen if it takes a long time to load an icon
|
|
return;
|
|
}
|
|
|
|
QPixmap iconPixmap;
|
|
if (!iconPixmap.load(iconFilePath))
|
|
{
|
|
RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR,
|
|
"Unable to load plugin icon. Image may have an invalid or unknown format.");
|
|
iconPixmap.load(AppSettings::instance().contentFolder() + kFailedToLoadPluginIconIcon);
|
|
}
|
|
|
|
QAction* action = m_ActionMap[actionId];
|
|
|
|
QSize scale(16, 16); // default size of icons for SystemMenu UI
|
|
|
|
RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow();
|
|
if ( mainWindow.isRibbonStyle() )
|
|
{
|
|
// max size ribbongroup can support is 32 x 32 so scale accordingly
|
|
if (iconPixmap.width() >= 32 || iconPixmap.height() >= 32)
|
|
{
|
|
scale = QSize(32, 32);
|
|
}
|
|
else
|
|
{
|
|
int min = qMin(iconPixmap.width(), iconPixmap.height());
|
|
scale = QSize(min, min);
|
|
}
|
|
}
|
|
|
|
QIcon icon(iconPixmap.scaled(scale));
|
|
action->setIcon(icon);
|
|
}
|
|
|
|
void RobloxPluginHost::setButtonIconInternal(int actionId, QImage image)
|
|
{
|
|
if (!m_ActionMap.contains(actionId))
|
|
{
|
|
// This can happen if it takes a long time to load an icon
|
|
return;
|
|
}
|
|
|
|
QAction* action = m_ActionMap[actionId];
|
|
|
|
QSize scale(16, 16); // default size of icons for SystemMenu UI
|
|
|
|
RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow();
|
|
if ( mainWindow.isRibbonStyle() )
|
|
{
|
|
// max size ribbongroup can support is 32 x 32 so scale accordingly
|
|
if (image.width() >= 32 || image.height() >= 32)
|
|
{
|
|
scale = QSize(32, 32);
|
|
}
|
|
else
|
|
{
|
|
int min = qMin(image.width(), image.height());
|
|
scale = QSize(min, min);
|
|
}
|
|
}
|
|
|
|
QIcon icon(QPixmap::fromImage(image).scaled(scale));
|
|
action->setIcon(icon);
|
|
}
|
|
|
|
void RobloxPluginHost::hideToolbars(const std::vector<void*>& toolbars, bool hide)
|
|
{
|
|
RobloxMainWindow& main_window = UpdateUIManager::Instance().getMainWindow();
|
|
if ( !hide && main_window.getBuildMode() == BM_BASIC )
|
|
return;
|
|
|
|
if (toolbars.empty())
|
|
return;
|
|
|
|
QList<int> toolbarIds;
|
|
for ( size_t i = 0 ; i < toolbars.size( ); ++i )
|
|
toolbarIds.push_back((int)toolbars[i]);
|
|
|
|
QVariant toolbarsVariant;
|
|
toolbarsVariant.setValue(toolbarIds);
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
"onShowToolbars",
|
|
Qt::QueuedConnection,
|
|
Q_ARG(QVariant,toolbarsVariant),
|
|
Q_ARG(bool,!hide) );
|
|
}
|
|
|
|
void RobloxPluginHost::disableToolbars(const std::vector<void*>& toolbars, bool disable)
|
|
{
|
|
RobloxMainWindow& main_window = UpdateUIManager::Instance().getMainWindow();
|
|
if ( !disable && main_window.getBuildMode() == BM_BASIC )
|
|
return;
|
|
|
|
for ( size_t i = 0 ; i < toolbars.size( ); ++i )
|
|
{
|
|
int toolbarID = (int)toolbars[i];
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
"onEnableToolbar",
|
|
Qt::QueuedConnection,
|
|
Q_ARG(int,toolbarID),
|
|
Q_ARG(bool,!disable) );
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::onShowToolbar(int toolbarID, bool show)
|
|
{
|
|
if ( !m_ToolBarMap.contains(toolbarID) && !m_RibbonGroupMap.contains(toolbarID) )
|
|
{
|
|
// only output an error if we're showing a toolbar that is supposed to exist
|
|
if ( show )
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"Plugin attempting to show toolbar that does not exist - %i",toolbarID);
|
|
return;
|
|
}
|
|
|
|
RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow();
|
|
if ( mainWindow.isRibbonStyle() )
|
|
{
|
|
RibbonGroup* group = m_RibbonGroupMap[toolbarID];
|
|
|
|
group->setVisible(show);
|
|
// make sure we set the property so it can be used to correctly update the status in ribbon bar
|
|
QList<QAction*> actions = group->actions();
|
|
for (QList<QAction*>::iterator iter = actions.begin(); iter != actions.end(); iter++)
|
|
(*iter)->setProperty(kIsVisible, show);
|
|
}
|
|
else
|
|
{
|
|
QToolBar* toolbar = m_ToolBarMap[toolbarID];
|
|
toolbar->setVisible(show);
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::onShowToolbars(QVariant toolbarsVariant, bool show)
|
|
{
|
|
QList<int> toolbars = toolbarsVariant.value<QList<int> >();
|
|
|
|
QVector<bool> shows(toolbars.size(), show);
|
|
|
|
if (show)
|
|
{
|
|
bool hasSmoothTerrainToolbar = false;
|
|
|
|
for (int i = 0; i < toolbars.size(); ++i)
|
|
if (getToolbarName(toolbars[i]) == SmoothTerrainToolbarName)
|
|
hasSmoothTerrainToolbar = true;
|
|
|
|
for (int i = 0; i < toolbars.size(); ++i)
|
|
if (hasSmoothTerrainToolbar && getToolbarName(toolbars[i]) == TerrainToolbarName)
|
|
shows[i] = false;
|
|
}
|
|
|
|
for (int i = 0; i < toolbars.size(); ++i)
|
|
{
|
|
onShowToolbar(toolbars[i], shows[i]);
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::onEnableToolbar(int toolbarID, bool enable)
|
|
{
|
|
if ( !m_ToolBarMap.contains(toolbarID) && !m_RibbonGroupMap.contains(toolbarID) )
|
|
{
|
|
// only output an error if we're showing a toolbar that is supposed to exist
|
|
if ( enable )
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"Plugin attempting to disable/enable toolbar that does not exist - %i",toolbarID);
|
|
return;
|
|
}
|
|
|
|
RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow();
|
|
if ( mainWindow.isRibbonStyle() )
|
|
{
|
|
RibbonGroup* group = m_RibbonGroupMap[toolbarID];
|
|
group->setEnabled(enable);
|
|
// make sure we set the property so it can be used to correctly update the status in ribbon bar
|
|
QList<QAction*> actions = group->actions();
|
|
for (QList<QAction*>::iterator iter = actions.begin(); iter != actions.end(); iter++)
|
|
(*iter)->setEnabled(enable);
|
|
}
|
|
else
|
|
{
|
|
QToolBar* toolbar = m_ToolBarMap[toolbarID];
|
|
toolbar->setEnabled(enable);
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::deleteToolbars(const std::vector<void*>& toolbars)
|
|
{
|
|
RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow();
|
|
|
|
for ( size_t i = 0 ; i < toolbars.size() ; ++i )
|
|
{
|
|
int toolbarID = (int)toolbars[i];
|
|
if ( mainWindow.isRibbonStyle() && toolbarID == m_TerrainToolbarID )
|
|
{
|
|
handleTerrainRibbonGroupDeletion();
|
|
continue;
|
|
}
|
|
|
|
QObject* parent = NULL;
|
|
|
|
if ( mainWindow.isRibbonStyle() )
|
|
{
|
|
if ( !m_RibbonGroupMap.contains(toolbarID) )
|
|
{
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"Plugin attempting to delete toolbar that does not exist - %i",toolbarID);
|
|
continue;
|
|
}
|
|
|
|
RibbonGroup* group = m_RibbonGroupMap[toolbarID];
|
|
parent = group;
|
|
}
|
|
else
|
|
{
|
|
if ( !m_ToolBarMap.contains(toolbarID) )
|
|
{
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"Plugin attempting to delete toolbar that does not exist - %i",toolbarID);
|
|
continue;
|
|
}
|
|
|
|
QToolBar* toolbar = m_ToolBarMap[toolbarID];
|
|
parent = toolbar;
|
|
}
|
|
|
|
// delete all actions in the map that are parented to this toolbar
|
|
removeActionsFromMap(parent);
|
|
|
|
if ( mainWindow.isRibbonStyle() )
|
|
{
|
|
if (toolbarID == m_TransformToolbarID)
|
|
continue;
|
|
RibbonGroup* group = m_RibbonGroupMap[toolbarID];
|
|
m_RibbonGroupMap.remove(toolbarID);
|
|
|
|
qobject_cast<Qtitan::RibbonPage*>(group->parentWidget())->removeGroup(group);
|
|
}
|
|
else
|
|
{
|
|
QToolBar* toolbar = m_ToolBarMap[toolbarID];
|
|
m_ToolBarMap.remove(toolbarID);
|
|
QMetaObject::invokeMethod(toolbar, "deleteLater", Qt::QueuedConnection);
|
|
}
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::setSetting(int assetId, const std::string& key,
|
|
const RBX::Reflection::Variant& value)
|
|
{
|
|
shared_ptr<RBX::Reflection::ValueTable> values(new RBX::Reflection::ValueTable());
|
|
readPluginSettings(assetId, values);
|
|
(*values)[key] = value;
|
|
writePluginSettings(assetId, values);
|
|
}
|
|
|
|
void RobloxPluginHost::getSetting(int assetId, const std::string& key,
|
|
RBX::Reflection::Variant* result)
|
|
{
|
|
shared_ptr<RBX::Reflection::ValueTable> values(new RBX::Reflection::ValueTable());
|
|
readPluginSettings(assetId, values);
|
|
(*result) = (*values)[key];
|
|
}
|
|
|
|
bool RobloxPluginHost::getLoggedInUserId(int* userIdOut)
|
|
{
|
|
int userId = RobloxUser::singleton().getUserId();
|
|
if (userId > 0)
|
|
{
|
|
*userIdOut = userId;
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::handlePluginAction()
|
|
{
|
|
QAction* pAction = qobject_cast<QAction*>(sender());
|
|
if ( !pAction )
|
|
return;
|
|
|
|
if (RobloxIDEDoc::getIsCloudEditSession() && pAction->data().toInt() == m_ConvertToSmoothID)
|
|
{
|
|
QtUtilities::RBXMessageBox warning;
|
|
warning.setText("Cannot conver to smooth during Cloud Edit session");
|
|
warning.setStandardButtons(QMessageBox::Ok);
|
|
warning.exec();
|
|
return;
|
|
}
|
|
|
|
// revert the Qt's checked status (it will be taken care by the script code by calling "setButtonActive"
|
|
pAction->setChecked(!pAction->isChecked());
|
|
|
|
// action to be handled by the relevant document
|
|
if ( RobloxDocManager::Instance().getCurrentDoc() )
|
|
{
|
|
int actionID = pAction->data().toInt();
|
|
RobloxDocManager::Instance().getCurrentDoc()->handlePluginAction(m_pNotifier,(void*)(actionID));
|
|
}
|
|
|
|
// update toolbar status
|
|
UpdateUIManager::Instance().updateToolBars();
|
|
}
|
|
|
|
void RobloxPluginHost::onCreateToolbar(int toolbarID,QString name)
|
|
{
|
|
RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow();
|
|
|
|
if ( mainWindow.isRibbonStyle() )
|
|
{
|
|
RBXASSERT(!m_RibbonGroupMap.contains(toolbarID));
|
|
|
|
RibbonPage* page = (name == SmoothTerrainToolbarName) ? m_TerrainToolbar : m_PluginsToolbar;
|
|
|
|
RibbonGroup* group = page->addGroup(name);
|
|
|
|
group->setObjectName(name);
|
|
group->setTitle(name);
|
|
|
|
group->setVisible(false);
|
|
m_RibbonGroupMap[toolbarID] = group;
|
|
}
|
|
else
|
|
{
|
|
RBXASSERT(!m_ToolBarMap.contains(toolbarID));
|
|
|
|
RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow();
|
|
QToolBar* pToolbar = mainWindow.addToolBar(name);
|
|
RBXASSERT(pToolbar);
|
|
|
|
pToolbar->setObjectName(name);
|
|
|
|
pToolbar->setIconSize(QSize(16,16));
|
|
pToolbar->setVisible(false);
|
|
m_ToolBarMap[toolbarID] = pToolbar;
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::uiAction(std::string uiActionName)
|
|
{
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
"onUIAction",
|
|
Qt::QueuedConnection,
|
|
Q_ARG(QString,uiActionName.c_str()) );
|
|
}
|
|
|
|
void RobloxPluginHost::onUIAction(QString uiActionName)
|
|
{
|
|
QObjectList objects = UpdateUIManager::Instance().getMainWindow().children();
|
|
for (QObjectList::iterator iter = objects.begin();
|
|
iter != objects.end();
|
|
++iter)
|
|
{
|
|
RBX::BaldPtr<QAction> action = dynamic_cast<QAction*>(*iter);
|
|
if (action &&
|
|
action->objectName() == uiActionName)
|
|
{
|
|
action->trigger();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::onCreateButton(int toolbarID,int actionID,QString text,QString tooltip,QString iconName)
|
|
{
|
|
if ( !m_RibbonGroupMap.contains(toolbarID) && !m_ToolBarMap.contains(toolbarID) )
|
|
{
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"Plugin attempting to add button to toolbar that does not exist - %i, %i",toolbarID,actionID);
|
|
return;
|
|
}
|
|
|
|
QAction* action = NULL;
|
|
|
|
if ( text.isEmpty() )
|
|
text = tooltip;
|
|
|
|
RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow();
|
|
if ( mainWindow.isRibbonStyle() )
|
|
{
|
|
Qtitan::RibbonGroup* pGroup = m_RibbonGroupMap[toolbarID];
|
|
action = new PluginAction(text,pGroup);
|
|
if (mainWindow.ribbonBar())
|
|
action->setFont(mainWindow.ribbonBar()->font());
|
|
// by default an action is visible (unless it is explicitly set to invisible)
|
|
action->setProperty(kIsVisible, true);
|
|
pGroup->addAction(action,Qt::ToolButtonTextUnderIcon);
|
|
}
|
|
else
|
|
{
|
|
QToolBar* pToolbar = m_ToolBarMap[toolbarID];
|
|
action = new PluginAction(text,pToolbar);
|
|
pToolbar->addAction(action);
|
|
}
|
|
|
|
action->setObjectName(text);
|
|
connect(action,SIGNAL(triggered()),this,SLOT(handlePluginAction()));
|
|
|
|
if ( text.isEmpty() )
|
|
action->setPriority(QAction::LowPriority);
|
|
else
|
|
action->setText(text);
|
|
|
|
action->setToolTip(tooltip);
|
|
action->setCheckable(true);
|
|
action->setData(actionID);
|
|
|
|
m_ActionMap[actionID] = action;
|
|
|
|
if ( !iconName.isEmpty() )
|
|
{
|
|
#ifdef Q_WS_MAC
|
|
iconName.replace('\\',QDir::separator());
|
|
#endif
|
|
// set icon for action (safe to call this method as we should be in MainThread)
|
|
setButtonIconInternal_deprecated(actionID, iconName);
|
|
}
|
|
|
|
// make sure we enable plugin actions for shortcut customization
|
|
mainWindow.addAction(action);
|
|
// for ribbon bar mode, check if we need to add created action in quick access bar
|
|
if (mainWindow.isRibbonStyle())
|
|
{
|
|
RobloxQuickAccessConfig& quickAccessConfig = RobloxQuickAccessConfig::singleton();
|
|
if (QAction* mappedAction = quickAccessConfig.mapProxyAction(action))
|
|
quickAccessConfig.addToQuickAccessBar(mappedAction, quickAccessConfig.visibleInQuickAccessBar(action));
|
|
|
|
// QTitanRibbon doesn't recalculate group width, make sure we force the width recalculation
|
|
Qtitan::RibbonGroup* pGroup = m_RibbonGroupMap[toolbarID];
|
|
if (pGroup)
|
|
{
|
|
pGroup->recalcWidths();
|
|
QEvent evt(QEvent::LayoutRequest);
|
|
QApplication::sendEvent(pGroup->parentWidget(), &evt);
|
|
}
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::onSetButtonActive(int buttonID, bool active)
|
|
{
|
|
if ( !m_ActionMap.contains(buttonID) )
|
|
{
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,
|
|
"Plugin attempting to set active state for missing button ID - %i", buttonID);
|
|
return;
|
|
}
|
|
|
|
QAction *pAction = m_ActionMap[buttonID];
|
|
pAction->setChecked(active);
|
|
//make sure we set the property so it can be used to correctly status in ribbon bar
|
|
pAction->setProperty(kIsChecked, active);
|
|
|
|
if (!FFlag::StudioDisableToolbarUpdateOnButtonActive)
|
|
QTimer::singleShot(1, &UpdateUIManager::Instance(), SLOT(updateToolBars()));
|
|
}
|
|
|
|
bool RobloxPluginHost::event(QEvent* evt)
|
|
{
|
|
if ( evt->type() != PLUGIN_LOAD )
|
|
return QObject::event(evt);
|
|
|
|
RobloxCustomEventWithArg *pCustomEvent = dynamic_cast<RobloxCustomEventWithArg*>(evt);
|
|
if ( pCustomEvent )
|
|
{
|
|
boost::function<void()>* pFunctionObj = pCustomEvent->m_pEventArg;
|
|
if ( pFunctionObj )
|
|
(*pFunctionObj)();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void RobloxPluginHost::loadPlugins(RBX::DataModel* dataModel)
|
|
{
|
|
if ( !dataModel )
|
|
return;
|
|
|
|
// Old style plugins
|
|
loadPluginsFromPath(dataModel,RobloxPluginHost::builtInPluginPath());
|
|
loadPluginsFromPath(dataModel,RobloxPluginHost::userPluginPath());
|
|
|
|
findAndLoadModelPlugins(dataModel, builtInPluginPath());
|
|
loadInstalledModelPlugins(dataModel);
|
|
findAndLoadModelPlugins(dataModel, userPluginPath());
|
|
|
|
RBX::PluginManager::singleton()->startModelPluginScripts(dataModel);
|
|
}
|
|
|
|
void RobloxPluginHost::loadPluginsFromPath(RBX::DataModel* dataModel,const QString &srcPath)
|
|
{
|
|
if ( !dataModel || srcPath.isEmpty() )
|
|
return;
|
|
UpdateUIManager::Instance().getMainWindow().pluginHost().loadPluginsInternal(dataModel,srcPath);
|
|
}
|
|
|
|
void RobloxPluginHost::loadPluginsInternal(RBX::DataModel* dataModel,const QString& srcPath)
|
|
{
|
|
QDir srcDir(srcPath);
|
|
QFileInfoList fileList = srcDir.entryInfoList(QStringList("*.lua"),QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot);
|
|
|
|
QFileInfo fileInfo;
|
|
for ( int ii = 0 ; ii < fileList.size() ; ++ii )
|
|
{
|
|
fileInfo = fileList.at(ii);
|
|
|
|
if ( fileInfo.isSymLink() )
|
|
continue;
|
|
|
|
if ( fileInfo.isDir() )
|
|
loadPluginsInternal(dataModel, fileInfo.absoluteFilePath());
|
|
else
|
|
QApplication::postEvent(this, new RobloxCustomEventWithArg(PLUGIN_LOAD,boost::bind(&RobloxPluginHost::doLoadPlugin,dataModel,fileInfo.absoluteFilePath())));
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::doLoadPlugin(RBX::DataModel* dataModel, QString filename)
|
|
{
|
|
boost::mutex::scoped_lock lock(RBX::PluginManager::singleton()->mutex);
|
|
|
|
QFile file(filename);
|
|
if ( !file.open(QIODevice::ReadOnly | QIODevice::Text) )
|
|
return;
|
|
|
|
QTextStream inStream(&file);
|
|
QString toExecute = inStream.readAll();
|
|
if ( toExecute.isEmpty() )
|
|
return;
|
|
|
|
QByteArray baToExe = toExecute.toAscii();
|
|
const char* c_strToExe = baToExe.data();
|
|
|
|
QByteArray baFileName = filename.toAscii();
|
|
const char* c_strFileName = baFileName.data();
|
|
|
|
RBX::PluginManager::singleton()->setLastPath(QFileInfo(filename).absoluteDir().absolutePath().toStdString());
|
|
|
|
try
|
|
{
|
|
RBX::DataModel::LegacyLock legacylock(dataModel, RBX::DataModelJob::Write);
|
|
boost::shared_ptr<RBX::ScriptContext> pContext = RBX::shared_from(dataModel->create<RBX::ScriptContext>());
|
|
if (pContext)
|
|
pContext->executeInNewThread(RBX::Security::StudioPlugin,
|
|
RBX::ProtectedString::fromTrustedSource(c_strToExe), c_strFileName);
|
|
}
|
|
catch (std::exception &e)
|
|
{
|
|
RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR,e.what());
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::findAndLoadModelPlugins(RBX::DataModel* dataModel, const QString& srcPath)
|
|
{
|
|
QStringList extensions;
|
|
extensions += "*.rbxm";
|
|
extensions += "*.rbxmx";
|
|
|
|
QDir srcDir(srcPath);
|
|
QFileInfoList fileList = srcDir.entryInfoList(extensions, QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks);
|
|
|
|
QFileInfo fileInfo;
|
|
for (int ii = 0 ; ii < fileList.size() ; ++ii)
|
|
{
|
|
fileInfo = fileList.at(ii);
|
|
if (!fileInfo.isDir())
|
|
{
|
|
doLoadModelPlugin(dataModel, fileInfo.absoluteFilePath(), -1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::loadInstalledModelPlugins(RBX::DataModel* dataModel)
|
|
{
|
|
shared_ptr<const RBX::Reflection::ValueTable> values(new RBX::Reflection::ValueTable);
|
|
QString pluginMetadataString = getPluginMetadataJson();
|
|
std::stringstream jsonStream(pluginMetadataString.toUtf8().constData());
|
|
if (!pluginMetadataString.isEmpty() && !RBX::WebParser::parseJSONTable(pluginMetadataString.toUtf8().constData(), values))
|
|
{
|
|
RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR,
|
|
qPrintable(tr("Unable to load metadata for installed plugins. "
|
|
"Installed plugins will not be loaded.")));
|
|
return;
|
|
}
|
|
|
|
QDir basePluginFolder(modelPluginsPath());
|
|
for (RBX::Reflection::ValueTable::const_iterator itr = values->begin();
|
|
itr != values->end(); ++itr)
|
|
{
|
|
// the key in the map should be asset id
|
|
QString assetId = QString::fromStdString(itr->first);
|
|
bool intConversionWorked = true;
|
|
int intAssetId = assetId.toInt(&intConversionWorked);
|
|
|
|
if (!intConversionWorked)
|
|
{
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_WARNING,
|
|
qPrintable(tr("Error parsing plugin asset id for plugin: %s")),
|
|
qPrintable(assetId));
|
|
continue;
|
|
}
|
|
|
|
// skip the plugin if it marked as not enabled
|
|
shared_ptr<const RBX::Reflection::ValueTable> valueTable =
|
|
itr->second.cast<shared_ptr<const RBX::Reflection::ValueTable> >();
|
|
RBX::Reflection::ValueTable::const_iterator enabledData = valueTable->find(kEnabledJsonKey);
|
|
if (enabledData != valueTable->end() &&
|
|
enabledData->second.cast<bool>() == false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
QDir subFolder = basePluginFolder;
|
|
QString assetFile = QString(kPluginFileName);
|
|
if (!subFolder.cd(assetId) || !subFolder.exists(assetFile))
|
|
{
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_WARNING,
|
|
qPrintable(tr("Unable to find plugin with the following id: %s")),
|
|
qPrintable(assetId));
|
|
continue;
|
|
}
|
|
|
|
// everything is good! load the plugin
|
|
doLoadModelPlugin(dataModel, subFolder.absoluteFilePath(assetFile),
|
|
intAssetId);
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::doLoadModelPlugin(RBX::DataModel* dataModel,
|
|
const QString& filename, int assetId)
|
|
{
|
|
std::ifstream stream(qPrintable(filename), std::ios_base::in | std::ios_base::binary);
|
|
|
|
if (!stream.is_open())
|
|
{
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,
|
|
qPrintable(tr("Error while trying to open plugin file: %s")),
|
|
qPrintable(filename));
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Read the items
|
|
RBX::Instances instances;
|
|
Serializer().loadInstances(stream, instances);
|
|
if (RBX::ContentProvider* cp = dataModel->create<RBX::ContentProvider>())
|
|
{
|
|
RBX::LuaSourceContainer::blockingLoadLinkedScriptsForInstances(cp, instances);
|
|
}
|
|
|
|
RBX::PluginManager::singleton()->addModelPlugin(dataModel, instances, assetId);
|
|
}
|
|
catch (const std::runtime_error&) // xml parse can throw
|
|
{
|
|
RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR,
|
|
qPrintable(tr("Unable to open plugin file (bad file format): ")));
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,
|
|
"%s", qPrintable(filename));
|
|
}
|
|
}
|
|
|
|
QString RobloxPluginHost::builtInPluginPath()
|
|
{
|
|
return RobloxSettings::getBuiltInPluginsFolder();
|
|
}
|
|
|
|
QString RobloxPluginHost::userPluginPath()
|
|
{
|
|
return AuthoringSettings::singleton().pluginsDir.absolutePath();
|
|
}
|
|
|
|
QString RobloxPluginHost::modelPluginsPath()
|
|
{
|
|
return AuthoringSettings::singleton().modelPluginsDir.absolutePath();
|
|
}
|
|
|
|
void RobloxPluginHost::processInstallPluginFromWebsite(int assetId, int assetVersion,
|
|
std::string* serialized, std::exception* error, boost::function<void(bool)> resultCallback)
|
|
{
|
|
QString assetIdString = QString("%1").arg(assetId);
|
|
// Track an event for each attempt to install a plugin
|
|
if (FFlag::GoogleAnalyticsTrackingEnabled)
|
|
{
|
|
RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION, "PluginInstall", assetIdString.toStdString().c_str());
|
|
}
|
|
|
|
if (error || !serialized || serialized->empty() || userPluginPath().isEmpty())
|
|
{
|
|
RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR,
|
|
qPrintable(tr("Unable to download plugin at this time. Try again later.")));
|
|
if (FFlag::GoogleAnalyticsTrackingEnabled)
|
|
{
|
|
RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ERROR, "PluginInstallFailed", assetIdString.toStdString().c_str());
|
|
}
|
|
resultCallback(false);
|
|
return;
|
|
}
|
|
|
|
QDir pluginFolder = getSpecificPluginFolder(assetId);
|
|
QFile pluginFile(pluginFolder.absoluteFilePath(kPluginFileName));
|
|
if (!pluginFile.open(QIODevice::Truncate | QIODevice::WriteOnly) ||
|
|
!pluginFile.write(QByteArray(serialized->c_str(), serialized->size())))
|
|
{
|
|
RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR,
|
|
qPrintable(tr("Unable to create plugin file.")));
|
|
if (FFlag::GoogleAnalyticsTrackingEnabled)
|
|
{
|
|
RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ERROR, "PluginInstallFailed", assetIdString.toStdString().c_str());
|
|
}
|
|
resultCallback(false);
|
|
return;
|
|
}
|
|
pluginFile.flush();
|
|
|
|
// Update Metadata
|
|
{
|
|
shared_ptr<RBX::Reflection::ValueTable> metadata(new RBX::Reflection::ValueTable);
|
|
if (readPluginMetadata(metadata))
|
|
{
|
|
std::string assetIdStdString(qPrintable(assetIdString));
|
|
|
|
RBX::Reflection::ValueTable::iterator pluginData = metadata->find(assetIdStdString);
|
|
shared_ptr<RBX::Reflection::ValueTable> newData;
|
|
if (pluginData == metadata->end())
|
|
{
|
|
newData.reset(new RBX::Reflection::ValueTable);
|
|
(*newData)[kEnabledJsonKey] = true;
|
|
}
|
|
else
|
|
{
|
|
if (FFlag::GoogleAnalyticsTrackingEnabled)
|
|
{
|
|
RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION, "PluginReInstall",
|
|
assetIdString.toStdString().c_str());
|
|
}
|
|
|
|
newData.reset(new RBX::Reflection::ValueTable(
|
|
*(pluginData->second.cast<shared_ptr<const RBX::Reflection::ValueTable> >())));
|
|
}
|
|
(*newData)[kAssetVersionJsonKey] = assetVersion;
|
|
(*metadata)[assetIdStdString] = shared_ptr<const RBX::Reflection::ValueTable>(newData);
|
|
|
|
writePluginMetadata(metadata);
|
|
}
|
|
}
|
|
|
|
RBX::StandardOut::singleton()->print(RBX::MESSAGE_INFO,
|
|
qPrintable(tr("Finished installing plugin. Open a new window to start using it.")));
|
|
resultCallback(true);
|
|
}
|
|
|
|
QString RobloxPluginHost::getPluginMetadataJson()
|
|
{
|
|
QFile pluginMetadataFile(getPluginMetadataFilePath());
|
|
|
|
if (!pluginMetadataFile.open(QIODevice::ReadOnly | QIODevice::Text))
|
|
{
|
|
return QString();
|
|
}
|
|
|
|
return QTextStream(&pluginMetadataFile).readAll();
|
|
}
|
|
|
|
void RobloxPluginHost::setPluginEnabled(int assetId, bool enabled)
|
|
{
|
|
std::string assetIdString(qPrintable(QString("%1").arg(assetId)));
|
|
|
|
if (FFlag::GoogleAnalyticsTrackingEnabled)
|
|
{
|
|
RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION,
|
|
enabled ? "EnablePlugin" : "DisablePlugin", QString::fromUtf8(assetIdString.c_str()).toStdString().c_str());
|
|
}
|
|
|
|
shared_ptr<RBX::Reflection::ValueTable> metadata(new RBX::Reflection::ValueTable);
|
|
if (readPluginMetadata(metadata))
|
|
{
|
|
RBX::Reflection::ValueTable::iterator itr = metadata->find(assetIdString);
|
|
if (itr == metadata->end())
|
|
{
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,
|
|
qPrintable(tr("Unable to set enabled/disabled state for plugin: %d")),
|
|
assetId);
|
|
return;
|
|
}
|
|
|
|
shared_ptr<RBX::Reflection::ValueTable> newData(
|
|
new RBX::Reflection::ValueTable(
|
|
*(itr->second.cast<shared_ptr<const RBX::Reflection::ValueTable> >())));
|
|
(*newData)[kEnabledJsonKey] = enabled;
|
|
(*metadata)[assetIdString] = shared_ptr<const RBX::Reflection::ValueTable>(newData);
|
|
|
|
writePluginMetadata(metadata);
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::uninstallPlugin(int assetId)
|
|
{
|
|
if (modelPluginsPath().isEmpty())
|
|
{
|
|
// If we don't know where the model plugins should be, we can't delete.
|
|
return;
|
|
}
|
|
|
|
QString assetIdString = QString("%1").arg(assetId);
|
|
std::string assetIdStdString(qPrintable(assetIdString));
|
|
|
|
if (FFlag::GoogleAnalyticsTrackingEnabled)
|
|
{
|
|
RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION, "UninstallPlugin", assetIdString.toStdString().c_str());
|
|
}
|
|
|
|
{
|
|
shared_ptr<RBX::Reflection::ValueTable> metadata(new RBX::Reflection::ValueTable);
|
|
if (readPluginMetadata(metadata))
|
|
{
|
|
metadata->erase(assetIdStdString);
|
|
writePluginMetadata(metadata);
|
|
}
|
|
}
|
|
|
|
QDir removingPluginPath(getSpecificPluginFolder(assetId));
|
|
|
|
bool cleanPluginFileSucceeded =
|
|
!removingPluginPath.exists(kPluginFileName) ||
|
|
removingPluginPath.remove(kPluginFileName);
|
|
bool cleanPluginSettingsSucceeded =
|
|
!removingPluginPath.exists(kPluginSettingsFileName) ||
|
|
removingPluginPath.remove(kPluginSettingsFileName);
|
|
bool removeDirectorySucceeded = false;
|
|
removeDirectorySucceeded =
|
|
!removingPluginPath.exists() ||
|
|
(removingPluginPath.cdUp() && removingPluginPath.rmdir(assetIdString));
|
|
|
|
if (!cleanPluginFileSucceeded || !cleanPluginSettingsSucceeded || !removeDirectorySucceeded)
|
|
{
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,
|
|
qPrintable(tr("Unable to remove all of the plugin files in directory %s. "
|
|
"Please clean this location manually")),
|
|
qPrintable(removingPluginPath.absolutePath()));
|
|
}
|
|
}
|
|
|
|
QDir RobloxPluginHost::getSpecificPluginFolder(int assetId)
|
|
{
|
|
QString assetIdString = QString("%1").arg(assetId);
|
|
QDir result(modelPluginsPath());
|
|
result.mkpath(QString("%1").arg(assetIdString));
|
|
result.cd(assetIdString);
|
|
return result;
|
|
}
|
|
|
|
QString RobloxPluginHost::getPluginMetadataFilePath()
|
|
{
|
|
static const char* kPluginMetadataFilename = "PluginMetadata.json";
|
|
return QDir(modelPluginsPath()).absoluteFilePath(kPluginMetadataFilename);
|
|
}
|
|
|
|
bool RobloxPluginHost::readPluginMetadata(shared_ptr<RBX::Reflection::ValueTable>& values)
|
|
{
|
|
if (userPluginPath().isEmpty())
|
|
{
|
|
RBX::StandardOut::singleton()->print(RBX::MESSAGE_WARNING,
|
|
qPrintable(tr("Warning: unable to add new plugin to metadata file.")));
|
|
return false;
|
|
}
|
|
|
|
QString pluginMetadataString = getPluginMetadataJson();
|
|
std::stringstream jsonStream(pluginMetadataString.toUtf8().constData());
|
|
if (!pluginMetadataString.isEmpty())
|
|
{
|
|
shared_ptr<const RBX::Reflection::ValueTable> readValues;
|
|
if (!RBX::WebParser::parseJSONTable(pluginMetadataString.toUtf8().constData(), readValues) )
|
|
{
|
|
RBX::StandardOut::singleton()->print(RBX::MESSAGE_WARNING,
|
|
qPrintable(tr("Plugin metadata file is corrupted. "
|
|
"Version information for your plugins may have been lost.")));
|
|
|
|
return false;
|
|
}
|
|
|
|
*values = *readValues;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void RobloxPluginHost::writePluginMetadata(const shared_ptr<RBX::Reflection::ValueTable>& values)
|
|
{
|
|
std::stringstream outputBuffer;
|
|
bool needComma = false;
|
|
outputBuffer << "{";
|
|
RBX::Stats::JsonWriter writer(outputBuffer, needComma);
|
|
writer.writeTableEntries(*values);
|
|
outputBuffer << "}";
|
|
|
|
QFile pluginMetadataFile(getPluginMetadataFilePath());
|
|
|
|
if (!pluginMetadataFile.open(QIODevice::Truncate | QIODevice::WriteOnly | QIODevice::Text)
|
|
|| !pluginMetadataFile.write(outputBuffer.str().c_str()))
|
|
{
|
|
RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR,
|
|
qPrintable(tr("Unable to write plugin metadata. "
|
|
"Version information for your plugins may be lost.")));
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::readPluginSettings(int assetId,
|
|
boost::shared_ptr<RBX::Reflection::ValueTable>& settings)
|
|
{
|
|
QDir pluginFolder = getSpecificPluginFolder(assetId);
|
|
QString settingsFileContents;
|
|
{
|
|
QFile settingsFile(pluginFolder.absoluteFilePath(kPluginSettingsFileName));
|
|
if (settingsFile.open(QIODevice::ReadOnly | QIODevice::Text))
|
|
{
|
|
settingsFileContents = settingsFile.readAll();
|
|
}
|
|
}
|
|
|
|
// initialize readValues to an empty table, in case the settings cannot be read from file.
|
|
shared_ptr<const RBX::Reflection::ValueTable> readValues =
|
|
rbx::make_shared<const RBX::Reflection::ValueTable>();
|
|
|
|
if (!settingsFileContents.isEmpty())
|
|
{
|
|
std::stringstream jsonStream(settingsFileContents.toStdString());
|
|
if ( RBX::WebParser::parseJSONTable(settingsFileContents.toStdString(), readValues) )
|
|
{
|
|
*settings = *readValues;
|
|
}
|
|
else
|
|
{
|
|
RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR,
|
|
qPrintable(tr("Unable to parse settings file for plugin")));
|
|
if (FFlag::GoogleAnalyticsTrackingEnabled)
|
|
{
|
|
RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ERROR,
|
|
"ParsePluginSettingsFailed", QString("%1").arg(assetId).toStdString().c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::writePluginSettings(int assetId,
|
|
const boost::shared_ptr<RBX::Reflection::ValueTable>& settings)
|
|
{
|
|
std::stringstream stream;
|
|
bool needComma = false;
|
|
stream << "{";
|
|
RBX::Stats::JsonWriter writer(stream, needComma);
|
|
writer.writeTableEntries(*settings.get());
|
|
stream << "}";
|
|
|
|
QDir pluginFolder = getSpecificPluginFolder(assetId);
|
|
QFile settingsFile(pluginFolder.absoluteFilePath(kPluginSettingsFileName));
|
|
|
|
if (settingsFile.open(QIODevice::Truncate | QIODevice::WriteOnly | QIODevice::Text))
|
|
{
|
|
settingsFile.write(stream.str().c_str());
|
|
}
|
|
}
|
|
void RobloxPluginHost::init()
|
|
{
|
|
// All must be done on the main thread
|
|
// Create our Add-ons and Terrain special pages for Ribbon
|
|
RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow();
|
|
if ( mainWindow.isRibbonStyle() )
|
|
{
|
|
RobloxQuickAccessConfig::singleton().setPluginHostInitialized(true);
|
|
|
|
if ( !m_PluginsToolbar )
|
|
m_PluginsToolbar = mainWindow.ribbonBar()->findChild<RibbonPage*>(PluginToolbarName);
|
|
|
|
if ( !m_TerrainToolbar )
|
|
{
|
|
const int pagesCount = mainWindow.ribbonBar()->getPageCount();
|
|
for ( int i = 0 ; i < pagesCount ; ++i )
|
|
{
|
|
RibbonPage* page = mainWindow.ribbonBar()->getPage(i);
|
|
if ( page->objectName() != TerrainToolbarName )
|
|
continue;
|
|
|
|
m_TerrainToolbar = page;
|
|
|
|
const QList<RibbonGroup*> ribbonGroups = mainWindow.ribbonBar()->getPage(i)->findChildren<RibbonGroup*>();
|
|
for (int j = 0; j < ribbonGroups.count(); j++)
|
|
{
|
|
RibbonGroup* group = ribbonGroups[j];
|
|
if ( group->objectName() != TerrainToolbarName )
|
|
continue;
|
|
|
|
m_TerrainToolbarID = m_NextID++;
|
|
m_RibbonGroupMap[m_TerrainToolbarID] = group;
|
|
|
|
const QList<QAction*>& actions = group->actions();
|
|
for ( int k = 0 ; k < actions.size() ; ++k )
|
|
{
|
|
QAction* action = actions[k];
|
|
const int actionID = m_NextID++;
|
|
|
|
m_ActionMap[actionID] = action;
|
|
m_TextActionMap[action->text()] = actionID;
|
|
|
|
action->setCheckable(true);
|
|
action->setData(actionID);
|
|
|
|
if (action->text() == "Convert To Smooth")
|
|
{
|
|
m_ConvertToSmoothID = actionID;
|
|
}
|
|
|
|
connect(action,SIGNAL(triggered()),this,SLOT(handlePluginAction()));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (RibbonPage* page = mainWindow.ribbonBar()->findChild<RibbonPage*>("Model"))
|
|
{
|
|
if (RibbonGroup* group = page->findChild<RibbonGroup*>("Tools"))
|
|
{
|
|
m_TransformToolbarID = m_NextID++;
|
|
m_RibbonGroupMap[m_TransformToolbarID] = group;
|
|
|
|
const QList<QAction*>& actions = group->actions();
|
|
for (int k = 0; k < actions.size(); ++k)
|
|
{
|
|
QAction* action = actions[k];
|
|
if (action->objectName() != TransformToolbarName)
|
|
continue;
|
|
|
|
const int actionID = m_NextID++;
|
|
|
|
m_ActionMap[actionID] = action;
|
|
m_TextActionMap[action->text()] = actionID;
|
|
|
|
action->setCheckable(true);
|
|
action->setData(actionID);
|
|
|
|
connect(action, SIGNAL(triggered()),this,SLOT(handlePluginAction()));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::removeActionsFromMap(QObject* parent)
|
|
{
|
|
QMap<int,QAction*>::iterator iter = m_ActionMap.begin();
|
|
while ( iter != m_ActionMap.end() )
|
|
{
|
|
if ( iter.value()->parent() == parent )
|
|
iter = m_ActionMap.erase(iter);
|
|
else
|
|
++iter;
|
|
}
|
|
}
|
|
|
|
void RobloxPluginHost::handleTerrainRibbonGroupDeletion()
|
|
{
|
|
RibbonGroup* group = m_RibbonGroupMap[m_TerrainToolbarID];
|
|
if (!group)
|
|
return;
|
|
|
|
const QList<QAction*>& actions = group->actions();
|
|
for ( int k = 0 ; k < actions.size() ; ++k )
|
|
{
|
|
if (!m_TextActionMap.contains(actions.at(k)->text()))
|
|
group->removeAction(actions.at(k));
|
|
}
|
|
|
|
removeActionsFromMap(group);
|
|
|
|
group->setVisible(true);
|
|
}
|
|
|
|
QString RobloxPluginHost::getToolbarName(int id) const
|
|
{
|
|
RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow();
|
|
|
|
if (mainWindow.isRibbonStyle())
|
|
{
|
|
RibbonGroup* group = m_RibbonGroupMap[id];
|
|
|
|
if (group)
|
|
return group->objectName();
|
|
}
|
|
else
|
|
{
|
|
QToolBar* toolbar = m_ToolBarMap[id];
|
|
|
|
if (toolbar)
|
|
return toolbar->objectName();
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
bool RobloxPluginHost::actionState(const QString &actionID, bool &enableState, bool &checkedState)
|
|
{
|
|
QAction* pAction = NULL;
|
|
QMapIterator<int,QAction*> iter(m_ActionMap);
|
|
|
|
while (iter.hasNext())
|
|
{
|
|
iter.next();
|
|
|
|
pAction = iter.value();
|
|
if (pAction && ((pAction->objectName() == actionID) || (pAction->objectName() + "_proxyAct" == actionID)) && pAction->property(kIsVisible).toBool())
|
|
{
|
|
enableState = true;
|
|
checkedState = pAction->property(kIsChecked).toBool();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void RobloxPluginHost::openScriptDoc(shared_ptr<RBX::Instance> instance, int lineNumber)
|
|
{
|
|
if (RBX::Instance::fastSharedDynamicCast<RBX::Script>(instance) ||
|
|
RBX::Instance::fastSharedDynamicCast<RBX::ModuleScript>(instance))
|
|
QMetaObject::invokeMethod(&RobloxDocManager::Instance(), "openDoc", Qt::QueuedConnection, Q_ARG(shared_ptr<RBX::Instance>, instance), Q_ARG(int, lineNumber));
|
|
}
|
|
|
|
void RobloxPluginHost::openWikiPage(const std::string& url)
|
|
{
|
|
UpdateUIManager::Instance().setDockVisibility(eDW_CONTEXTUAL_HELP, true);
|
|
|
|
QMetaObject::invokeMethod(&UpdateUIManager::Instance().getViewWidget<RobloxContextualHelp>(eDW_CONTEXTUAL_HELP), "onHelpTopicChanged", Q_ARG(QString, QString::fromStdString(url)));
|
|
}
|
|
|
|
void RobloxPluginHost::exportPlace(std::string filePath, RBX::ExporterSaveType exportType)
|
|
{
|
|
RobloxIDEDoc* playDoc = RobloxDocManager::Instance().getPlayDoc();
|
|
if (!playDoc)
|
|
return;
|
|
|
|
playDoc->exportSelection(filePath, exportType);
|
|
}
|
|
|
|
void RobloxPluginHost::promptForExistingAssetId(const std::string& assetType, boost::function<void(int)> resumeFunction, boost::function<void(std::string)> errorFunction)
|
|
{
|
|
if (FFlag::StudioEnableGameAnimationsTab)
|
|
{
|
|
RobloxIDEDoc* playDoc = RobloxDocManager::Instance().getPlayDoc();
|
|
if (playDoc)
|
|
playDoc->promptForExistingAssetId(assetType, resumeFunction, errorFunction);
|
|
}
|
|
else
|
|
{
|
|
errorFunction("Requested functionality is not available.");
|
|
}
|
|
}
|
|
|
|
void operationFailed(const char* title, const char* msg)
|
|
{
|
|
throw RBX::runtime_error("%s %s", title, msg);
|
|
}
|
|
|
|
shared_ptr<RBX::Instance> RobloxPluginHost::csgUnion(shared_ptr<const RBX::Instances> instances, RBX::DataModel* dataModel)
|
|
{
|
|
if (!FFlag::EnableCSGScripting)
|
|
throw std::runtime_error("Scripted CSG operations are disabled");
|
|
|
|
if (instances->size() == 0)
|
|
throw std::runtime_error("Union function requires instances; no instances passed.");
|
|
|
|
RBX::CSGOperations csgOps( dataModel, operationFailed );
|
|
shared_ptr<RBX::PartOperation> partOperation;
|
|
csgOps.doUnion( instances->begin(), instances->end(), partOperation );
|
|
return partOperation;
|
|
}
|
|
|
|
shared_ptr<const RBX::Instances> RobloxPluginHost::csgNegate(shared_ptr<const RBX::Instances> instances, RBX::DataModel* dataModel)
|
|
{
|
|
if (!FFlag::EnableCSGScripting)
|
|
throw std::runtime_error("Scripted CSG operations are disabled");
|
|
|
|
if (instances->size() == 0)
|
|
throw std::runtime_error("Negate function requires instances; no instances passed.");
|
|
|
|
RBX::CSGOperations csgOps( dataModel, operationFailed );
|
|
|
|
std::vector<shared_ptr<RBX::Instance> > toSelect;
|
|
csgOps.doNegate( instances->begin(), instances->end(), toSelect );
|
|
shared_ptr<RBX::Instances> result = rbx::make_shared<RBX::Instances>(toSelect.size());
|
|
int i = 0;
|
|
for ( std::vector<shared_ptr<RBX::Instance> >::const_iterator iter = toSelect.begin(); iter != toSelect.end(); iter++, i++ )
|
|
(*result)[i] = (*iter);
|
|
return result;
|
|
}
|
|
|
|
shared_ptr<const RBX::Instances> RobloxPluginHost::csgSeparate(shared_ptr<const RBX::Instances> instances, RBX::DataModel* dataModel)
|
|
{
|
|
if (!FFlag::EnableCSGScripting)
|
|
throw std::runtime_error("Scripted CSG operations are disabled");
|
|
|
|
if (instances->size() == 0)
|
|
throw std::runtime_error("Separate function requires instances; no instances passed.");
|
|
|
|
RBX::CSGOperations csgOps( dataModel, operationFailed );
|
|
|
|
std::vector<shared_ptr<RBX::Instance> > ungroupedItems;
|
|
csgOps.doSeparate( instances->begin(), instances->end(), ungroupedItems );
|
|
shared_ptr<RBX::Instances> result = rbx::make_shared<RBX::Instances>(ungroupedItems.size());
|
|
int i = 0;
|
|
for ( std::vector<shared_ptr<RBX::Instance> >::const_iterator iter = ungroupedItems.begin(); iter != ungroupedItems.end(); iter++, i++ )
|
|
(*result)[i] = (*iter);
|
|
return result;
|
|
}
|
|
|
|
void RobloxPluginHost::handleDragEnterEvent(RBX::DataModel* dataModel, shared_ptr<const RBX::Instances> instances, RBX::Vector2 location)
|
|
{
|
|
if (!m_pNotifier)
|
|
return;
|
|
m_pNotifier->fireDragEnterEvent(dataModel, instances, location);
|
|
}
|