#include "pluginmanager.hpp" #include "../filesystemlistener.hpp" #include #include #include #include using json = nlohmann::json; namespace ecode { PluginManager::PluginManager( const std::string& resourcesPath, const std::string& pluginsPath, std::shared_ptr pool, const OnLoadFileCb& loadFileCb ) : mResourcesPath( resourcesPath ), mPluginsPath( pluginsPath ), mThreadPool( pool ), mLoadFileFn( loadFileCb ) {} PluginManager::~PluginManager() { mClosing = true; for ( auto& plugin : mPlugins ) { Log::debug( "PluginManager: unloading plugin %s", plugin.second->getTitle().c_str() ); eeDelete( plugin.second ); } unsubscribeFileSystemListener(); } void PluginManager::registerPlugin( const PluginDefinition& def ) { mDefinitions[def.id] = def; } UICodeEditorPlugin* ecode::PluginManager::get( const std::string& id ) { auto findIt = mPlugins.find( id ); if ( findIt != mPlugins.end() ) return findIt->second; return nullptr; } bool PluginManager::setEnabled( const std::string& id, bool enable, bool sync ) { mPluginsEnabled[id] = enable; UICodeEditorPlugin* plugin = get( id ); if ( enable && plugin == nullptr && hasDefinition( id ) ) { Log::debug( "PluginManager: loading plugin %s", mDefinitions[id].name.c_str() ); UICodeEditorPlugin* newPlugin = sync && mDefinitions[id].creatorSyncFn ? mDefinitions[id].creatorSyncFn( this ) : mDefinitions[id].creatorFn( this ); mPlugins.insert( std::pair( id, newPlugin ) ); if ( onPluginEnabled ) onPluginEnabled( newPlugin ); return true; } if ( !enable && plugin != nullptr ) { Log::debug( "PluginManager: unloading plugin %s", mDefinitions[id].name.c_str() ); mThreadPool->run( [plugin]() { eeDelete( plugin ); } ); { Lock l( mSubscribedPluginsMutex ); mSubscribedPlugins.erase( id ); } mPlugins.erase( id ); } return false; } bool PluginManager::isEnabled( const std::string& id ) const { return mPluginsEnabled.find( id ) != mPluginsEnabled.end() ? mPluginsEnabled.at( id ) : false; } bool PluginManager::reload( const std::string& id ) { if ( !isPluginReloadEnabled() ) { Log::warning( "PluginManager: tried to reload a plugin but plugin reload is not enabled." ); return false; } if ( isEnabled( id ) ) { Log::warning( "PluginManager: reloading plugin %s from process %u", id.c_str(), Sys::getProcessID() ); setEnabled( id, false ); setEnabled( id, true ); return true; } Log::warning( "PluginManager: tried to reload a plugin but plugin is not enabled." ); return false; } const std::string& PluginManager::getResourcesPath() const { return mResourcesPath; } const std::string& PluginManager::getPluginsPath() const { return mPluginsPath; } const std::map& PluginManager::getPluginsEnabled() const { return mPluginsEnabled; } void PluginManager::onNewEditor( UICodeEditor* editor ) { for ( auto& plugin : mPlugins ) editor->registerPlugin( plugin.second ); } void PluginManager::setPluginsEnabled( const std::map& pluginsEnabled, bool sync ) { mPluginsEnabled = pluginsEnabled; for ( const auto& plugin : pluginsEnabled ) { if ( plugin.second && get( plugin.first ) == nullptr ) setEnabled( plugin.first, true, sync ); } } const std::shared_ptr& PluginManager::getThreadPool() const { return mThreadPool; } const std::map& PluginManager::getDefinitions() const { return mDefinitions; } const PluginDefinition* PluginManager::getDefinitionIndex( const Int64& index ) const { const PluginDefinition* def = nullptr; Int64 i = 0; for ( const auto& curDef : mDefinitions ) { if ( index == i ) def = &curDef.second; ++i; } return def; } UICodeEditorSplitter* PluginManager::getSplitter() const { return mSplitter; } UISceneNode* PluginManager::getUISceneNode() const { return mSplitter ? mSplitter->getUISceneNode() : nullptr; } const std::string& PluginManager::getWorkspaceFolder() const { return mWorkspaceFolder; } void PluginManager::setWorkspaceFolder( const std::string& workspaceFolder ) { mWorkspaceFolder = workspaceFolder; json data{ { "folder", mWorkspaceFolder } }; sendBroadcast( PluginMessageType::WorkspaceFolderChanged, PluginMessageFormat::JSON, &data ); } PluginRequestHandle PluginManager::sendRequest( PluginMessageType type, PluginMessageFormat format, const void* data ) { if ( mClosing ) return PluginRequestHandle::empty(); SubscribedPlugins subscribedPlugins; { Lock l( mSubscribedPluginsMutex ); subscribedPlugins = mSubscribedPlugins; } for ( const auto& plugin : subscribedPlugins ) { auto handle = plugin.second( { type, format, data } ); if ( !handle.isEmpty() ) return handle; } return PluginRequestHandle::empty(); } PluginRequestHandle PluginManager::sendRequest( UICodeEditorPlugin* pluginWho, PluginMessageType type, PluginMessageFormat format, const void* data ) { if ( mClosing ) return PluginRequestHandle::empty(); SubscribedPlugins subscribedPlugins; { Lock l( mSubscribedPluginsMutex ); subscribedPlugins = mSubscribedPlugins; } for ( const auto& plugin : subscribedPlugins ) { if ( pluginWho->getId() != plugin.first ) { auto handle = plugin.second( { type, format, data } ); if ( !handle.isEmpty() ) return handle; } } return PluginRequestHandle::empty(); } void PluginManager::sendResponse( UICodeEditorPlugin* pluginWho, PluginMessageType type, PluginMessageFormat format, const void* data, const PluginIDType& responseID ) { if ( mClosing ) return; SubscribedPlugins subscribedPlugins; { Lock l( mSubscribedPluginsMutex ); subscribedPlugins = mSubscribedPlugins; } for ( const auto& plugin : subscribedPlugins ) if ( pluginWho->getId() != plugin.first ) plugin.second( { type, format, data, responseID } ); } void PluginManager::sendBroadcast( UICodeEditorPlugin* pluginWho, PluginMessageType type, PluginMessageFormat format, const void* data ) { if ( mClosing ) return; SubscribedPlugins subscribedPlugins; { Lock l( mSubscribedPluginsMutex ); subscribedPlugins = mSubscribedPlugins; } for ( const auto& plugin : subscribedPlugins ) if ( pluginWho->getId() != plugin.first ) plugin.second( { type, format, data, -1 } ); } void PluginManager::subscribeMessages( const std::string& uniqueComponentId, std::function cb ) { { Lock l( mSubscribedPluginsMutex ); mSubscribedPlugins[uniqueComponentId] = cb; } if ( !mWorkspaceFolder.empty() ) { json data{ { "folder", mWorkspaceFolder } }; cb( { PluginMessageType::WorkspaceFolderChanged, PluginMessageFormat::JSON, &data } ); } } void PluginManager::unsubscribeMessages( const std::string& uniqueComponentId ) { if ( !mClosing ) { Lock l( mSubscribedPluginsMutex ); mSubscribedPlugins.erase( uniqueComponentId ); } } const PluginManager::OnLoadFileCb& PluginManager::getLoadFileFn() const { return mLoadFileFn; } bool PluginManager::isPluginReloadEnabled() const { return mPluginReloadEnabled; } void PluginManager::setPluginReloadEnabled( bool pluginReloadEnabled ) { mPluginReloadEnabled = pluginReloadEnabled; } void PluginManager::subscribeMessages( UICodeEditorPlugin* plugin, std::function cb ) { subscribeMessages( plugin->getId(), cb ); } void PluginManager::unsubscribeMessages( UICodeEditorPlugin* plugin ) { unsubscribeMessages( plugin->getId() ); } void PluginManager::setSplitter( UICodeEditorSplitter* splitter ) { mSplitter = splitter; } void PluginManager::setFileSystemListener( FileSystemListener* listener ) { if ( listener == mFileSystemListener ) return; mFileSystemListener = listener; sendBroadcast( PluginMessageType::FileSystemListenerReady, PluginMessageFormat::Empty, nullptr ); subscribeFileSystemListener(); } void PluginManager::subscribeFileSystemListener( Plugin* plugin ) { mPluginsFSSubs.insert( plugin ); } void PluginManager::unsubscribeFileSystemListener( Plugin* plugin ) { mPluginsFSSubs.erase( plugin ); } void PluginManager::subscribeFileSystemListener() { if ( mFileSystemListenerCb != 0 || mFileSystemListener == nullptr ) return; mFileSystemListenerCb = mFileSystemListener->addListener( [this]( const FileEvent& ev, const FileInfo& file ) { for ( Plugin* plugin : mPluginsFSSubs ) plugin->onFileSystemEvent( ev, file ); } ); } void PluginManager::unsubscribeFileSystemListener() { if ( mFileSystemListenerCb != 0 && mFileSystemListener ) mFileSystemListener->removeListener( mFileSystemListenerCb ); } void PluginManager::sendBroadcast( const PluginMessageType& notification, const PluginMessageFormat& format, void* data ) { if ( mClosing ) return; SubscribedPlugins subscribedPlugins; { Lock l( mSubscribedPluginsMutex ); subscribedPlugins = mSubscribedPlugins; } for ( const auto& plugin : subscribedPlugins ) plugin.second( { notification, format, data, -1 } ); } bool PluginManager::hasDefinition( const std::string& id ) { return mDefinitions.find( id ) != mDefinitions.end(); } std::shared_ptr PluginsModel::New( PluginManager* manager ) { return std::make_shared( manager ); } size_t PluginsModel::rowCount( const ModelIndex& ) const { return mManager->getDefinitions().size(); } std::string PluginsModel::columnName( const size_t& col ) const { eeASSERT( col < mColumnNames.size() ); return mColumnNames[col]; } Variant PluginsModel::data( const ModelIndex& index, ModelRole role ) const { if ( role == ModelRole::Display ) { const PluginDefinition* def = mManager->getDefinitionIndex( index.row() ); if ( def == nullptr ) return {}; switch ( index.column() ) { case Columns::Version: return Variant( def->version.getVersionString().c_str() ); case Columns::Description: return Variant( def->description.c_str() ); case Columns::Title: return Variant( def->name.c_str() ); case Columns::Enabled: return Variant( mManager->isEnabled( def->id ) ); case Columns::Id: return Variant( def->id.c_str() ); } } return {}; } PluginManager* PluginsModel::getManager() const { return mManager; } class UIPluginManagerTable : public UITableView { public: std::map readyCbs; UIPluginManagerTable() : UITableView() {} std::function onModelEnabledChange; std::function getCheckBoxFn( const ModelIndex& index, const PluginsModel* model ) { return [index, model, this]( UIPushButton* but ) -> UITextView* { UICheckBox* chk = UICheckBox::New(); chk->setChecked( model->data( model->index( index.row(), PluginsModel::Enabled ) ).asBool() ); but->addEventListener( Event::MouseClick, [&, index, model, chk]( const Event* event ) { if ( !( event->asMouseEvent()->getFlags() & EE_BUTTON_LMASK ) ) return 1; UIWidget* chkBut = chk->getCurrentButton(); auto mousePos = chkBut->convertToNodeSpace( event->asMouseEvent()->getPosition().asFloat() ); if ( chkBut->getLocalBounds().contains( mousePos ) ) { bool checked = !chk->isChecked(); chk->setChecked( checked ); std::string id( model->data( model->index( index.row(), PluginsModel::Id ) ).asCStr() ); model->getManager()->setEnabled( id, checked ); if ( onModelEnabledChange ) onModelEnabledChange( id, checked ); } return 1; } ); return chk; }; } UIWidget* createCell( UIWidget* rowWidget, const ModelIndex& index ) { if ( index.column() == PluginsModel::Title ) { UITableCell* widget = UITableCell::NewWithOpt( mTag + "::cell", getCheckBoxFn( index, (const PluginsModel*)getModel() ) ); return setupCell( widget, rowWidget, index ); } return UITableView::createCell( rowWidget, index ); } }; UIWindow* UIPluginManager::New( UISceneNode* sceneNode, PluginManager* manager, std::function loadFileCb ) { if ( !UIWidgetCreator::isWidgetRegistered( "UIPluginManagerTable" ) ) UIWidgetCreator::registerWidget( "UIPluginManagerTable", [] { return eeNew( UIPluginManagerTable, () ); } ); UIWindow* win = sceneNode ->loadLayoutFromString( R"xml( )xml" ) ->asType(); UIWidget* cont = win->getContainer(); UIPushButton* close = cont->find( "plugin-manager-close" ); UIPushButton* prefs = cont->find( "plugin-manager-preferences" ); UIPluginManagerTable* tv = win->getContainer()->find( "plugin-manager-table" ); close->addEventListener( Event::MouseClick, [win]( const Event* event ) { if ( event->asMouseEvent()->getFlags() & EE_BUTTON_LMASK ) win->closeWindow(); } ); tv->setModel( PluginsModel::New( manager ) ); tv->setColumnsVisible( { PluginsModel::Title, PluginsModel::Description, PluginsModel::Version } ); tv->setAutoColumnsWidth( true ); tv->setFitAllColumnsToWidget( true ); tv->setMainColumn( PluginsModel::Description ); prefs->addEventListener( Event::MouseClick, [tv, manager, loadFileCb]( const Event* event ) { if ( event->asMouseEvent()->getFlags() & EE_BUTTON_LMASK && !tv->getSelection().isEmpty() ) { const PluginDefinition* def = manager->getDefinitionIndex( tv->getSelection().first().row() ); if ( def == nullptr || !manager->isEnabled( def->id ) ) return; auto* plugin = manager->get( def->id ); if ( !plugin->hasFileConfig() ) return; if ( FileSystem::fileExists( plugin->getFileConfigPath() ) ) loadFileCb( plugin->getFileConfigPath() ); } } ); tv->setOnSelection( [&, prefs, manager]( const ModelIndex& index ) { const PluginDefinition* def = manager->getDefinitionIndex( index.row() ); if ( def == nullptr ) return; prefs->setEnabled( manager->isEnabled( def->id ) && manager->get( def->id )->hasFileConfig() ); } ); tv->onModelEnabledChange = [&, prefs, manager, tv]( const std::string& id, bool enabled ) { auto* plugin = manager->get( id ); if ( enabled && !plugin->isReady() ) { tv->readyCbs[id] = plugin->addOnReadyCallback( [&, manager, prefs, tv]( UICodeEditorPlugin* plugin, const Uint32& cbId ) { prefs->runOnMainThread( [prefs, manager, plugin]() { prefs->setEnabled( manager->isEnabled( plugin->getId() ) && plugin->hasFileConfig() ); } ); tv->readyCbs.erase( plugin->getId() ); plugin->removeReadyCallback( cbId ); } ); } else { prefs->setEnabled( enabled && plugin->hasFileConfig() ); } }; tv->addEventListener( Event::OnClose, [&, manager, tv]( const Event* ) { if ( tv->readyCbs.empty() ) return; for ( const auto& cb : tv->readyCbs ) { auto* plugin = manager->get( cb.first ); if ( plugin ) plugin->removeReadyCallback( cb.second ); } } ); win->center(); return win; } Plugin::Plugin( PluginManager* manager ) : mManager( manager ), mThreadPool( manager->getThreadPool() ), mReady( false ), // All plugins will start as not ready until proved the contrary mLoading( true ) // All plugins will start as loading until the load is complete, this is to // avoid concurrency issues {} bool Plugin::isReady() const { return mReady; } bool Plugin::isLoading() const { return mLoading; } bool Plugin::isShuttingDown() const { return mShuttingDown; } bool Plugin::hasFileConfig() { return !mConfigPath.empty(); } void Plugin::subscribeFileSystemListener() { mConfigFileInfo = FileInfo( mConfigPath ); mManager->subscribeFileSystemListener( this ); } void Plugin::unsubscribeFileSystemListener() { mManager->unsubscribeFileSystemListener( this ); } std::string Plugin::getFileConfigPath() { return mConfigPath; } PluginManager* Plugin::getManager() const { return mManager; } void Plugin::onFileSystemEvent( const FileEvent& ev, const FileInfo& file ) { if ( ev.type != FileSystemEventType::Modified || mShuttingDown || isLoading() ) return; if ( file.getFilepath() != mConfigPath || file.getModificationTime() == mConfigFileInfo.getModificationTime() ) return; std::string fileContents; FileSystem::fileGet( file.getFilepath(), fileContents ); if ( getConfigFileHash() != String::hash( fileContents ) ) { if ( mManager->isPluginReloadEnabled() && !isLoading() && isReady() ) { mConfigFileInfo = file; unsubscribeFileSystemListener(); mManager->reload( getId() ); } else { Log::debug( "Plugin %s: Configuration file has been modified: %s. But " "plugin reload is not enabled.", getTitle().c_str(), mConfigPath.c_str() ); } } else { Log::debug( "Plugin %s: Configuration file has been modified: %s. But contents " "are the same.", getTitle().c_str(), mConfigPath.c_str() ); } } void Plugin::setReady() { if ( mReady ) { Log::info( "Plugin: %s loaded and ready from process %u", getTitle().c_str(), Sys::getProcessID() ); } } PluginBase::~PluginBase() { mShuttingDown = true; unsubscribeFileSystemListener(); for ( auto editor : mEditors ) { onBeforeUnregister( editor.first ); for ( auto listener : editor.second ) editor.first->removeEventListener( listener ); editor.first->unregisterPlugin( this ); } } void PluginBase::onRegister( UICodeEditor* editor ) { Lock l( mMutex ); std::vector listeners; listeners.push_back( editor->addEventListener( Event::OnDocumentLoaded, [this]( const Event* event ) { Lock l( mMutex ); const DocEvent* docEvent = static_cast( event ); onDocumentLoaded( docEvent->getDoc() ); } ) ); listeners.push_back( editor->addEventListener( Event::OnDocumentClosed, [this]( const Event* event ) { { Lock l( mMutex ); const DocEvent* docEvent = static_cast( event ); TextDocument* doc = docEvent->getDoc(); onDocumentClosed( doc ); onUnregisterDocument( doc ); mDocs.erase( doc ); } } ) ); listeners.push_back( editor->addEventListener( Event::OnDocumentChanged, [&, editor]( const Event* ) { TextDocument* oldDoc = mEditorDocs[editor]; TextDocument* newDoc = editor->getDocumentRef().get(); Lock l( mMutex ); mDocs.erase( oldDoc ); mEditorDocs[editor] = newDoc; onDocumentChanged( editor, oldDoc ); } ) ); onRegisterListeners( editor, listeners ); mEditors.insert( { editor, listeners } ); if ( mDocs.count( editor->getDocumentRef().get() ) == 0 ) { mDocs.insert( editor->getDocumentRef().get() ); onRegisterDocument( editor->getDocumentRef().get() ); } mEditorDocs[editor] = editor->getDocumentRef().get(); } void PluginBase::onUnregister( UICodeEditor* editor ) { onBeforeUnregister( editor ); if ( mShuttingDown ) return; Lock l( mMutex ); TextDocument* doc = mEditorDocs[editor]; auto cbs = mEditors[editor]; for ( auto listener : cbs ) editor->removeEventListener( listener ); onUnregisterEditor( editor ); mEditors.erase( editor ); mEditorDocs.erase( editor ); for ( auto editorIt : mEditorDocs ) if ( editorIt.second == doc ) return; onUnregisterDocument( doc ); mDocs.erase( doc ); } } // namespace ecode