diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 85cffd1e2..af090560d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -696,12 +696,14 @@ if(HAVE_SCRIPTING) scripting/scriptdialogtabwidget.cpp scripting/scriptinterface.cpp scripting/scriptmanager.cpp + scripting/uiinterface.cpp ) list(APPEND HEADERS scripting/scriptdialog.h scripting/scriptdialogtabwidget.h scripting/scriptinterface.h scripting/scriptmanager.h + scripting/uiinterface.h ) list(APPEND UI scripting/scriptdialog.ui diff --git a/src/scripting/languageengine.h b/src/scripting/languageengine.h index 1b9c9c779..da6367f51 100644 --- a/src/scripting/languageengine.h +++ b/src/scripting/languageengine.h @@ -34,6 +34,7 @@ public: virtual Script* CreateScript(const QString& path, const QString& script_file, const QString& id) = 0; + virtual void DestroyScript(Script* script) = 0; private: ScriptManager* manager_; diff --git a/src/scripting/python/clementine.sip b/src/scripting/python/clementine.sip index a0f12c970..95672564f 100644 --- a/src/scripting/python/clementine.sip +++ b/src/scripting/python/clementine.sip @@ -11,3 +11,4 @@ %Include pythonengine.sip %Include scriptinterface.sip %Include song.sip +%Include uiinterface.sip diff --git a/src/scripting/python/pythonengine.cpp b/src/scripting/python/pythonengine.cpp index ed5c8318d..5bc4df50b 100644 --- a/src/scripting/python/pythonengine.cpp +++ b/src/scripting/python/pythonengine.cpp @@ -16,6 +16,7 @@ */ #include +#include #include #include "pythonengine.h" @@ -23,8 +24,10 @@ #include "sipAPIclementine.h" #include +#include const char* PythonEngine::kModulePrefix = "clementinescripts"; +PythonEngine* PythonEngine::sInstance = NULL; extern "C" { void initclementine(); @@ -34,6 +37,12 @@ PythonEngine::PythonEngine(ScriptManager* manager) : LanguageEngine(manager), initialised_(false) { + Q_ASSERT(sInstance == NULL); + sInstance = this; +} + +PythonEngine::~PythonEngine() { + sInstance = NULL; } const sipAPIDef* PythonEngine::GetSIPApi() { @@ -98,6 +107,7 @@ Script* PythonEngine::CreateScript(const QString& path, // Add objects to the module AddObject(manager()->data().player_, sipType_Player, "player"); AddObject(manager()->data().playlists_, sipType_PlaylistManager, "playlists"); + AddObject(manager()->ui(), sipType_UIInterface, "ui"); AddObject(this, sipType_PythonEngine, "pythonengine"); // Create a module for scripts @@ -121,15 +131,21 @@ Script* PythonEngine::CreateScript(const QString& path, } Script* ret = new PythonScript(this, path, script_file, id); + loaded_scripts_[id] = ret; // Used by RegisterNativeObject during startup if (ret->Init()) { return ret; } - ret->Unload(); - delete ret; + DestroyScript(ret); return NULL; } +void PythonEngine::DestroyScript(Script* script) { + loaded_scripts_.remove(script->id()); + script->Unload(); + delete script; +} + void PythonEngine::AddObject(void* object, const _sipTypeDef* type, const char * name) const { PyObject* python_object = sip_api_->api_convert_from_type(object, type, NULL); @@ -139,3 +155,48 @@ void PythonEngine::AddObject(void* object, const _sipTypeDef* type, void PythonEngine::AddLogLine(const QString& message, bool error) { manager()->AddLogLine("Python", message, error); } + +Script* PythonEngine::FindScriptMatchingId(const QString& id) const { + foreach (const QString& script_id, loaded_scripts_.keys()) { + if (script_id == id || id.startsWith(script_id + ".")) { + return loaded_scripts_[script_id]; + } + } + return NULL; +} + +void PythonEngine::RegisterNativeObject(QObject* object) { + // This function is called from Python, we need to figure out which script + // called it, so we look at the __package__ variable in the bottom stack + // frame. + + PyFrameObject* frame = PyEval_GetFrame(); + if (!frame) { + qWarning() << __PRETTY_FUNCTION__ << "unable to get stack frame"; + return; + } + while (frame->f_back) { + frame = frame->f_back; + } + + PyObject* __package__ = PyMapping_GetItemString( + frame->f_globals, const_cast("__package__")); + if (!__package__) { + qWarning() << __PRETTY_FUNCTION__ << "unable to get __package__"; + return; + } + + QString package = PyString_AsString(__package__); + Py_DECREF(__package__); + package.remove(QString(kModulePrefix) + "."); + + Script* script = FindScriptMatchingId(package); + if (!script) { + qWarning() << __PRETTY_FUNCTION__ << "unable to find script for package" << package; + return; + } + + // Finally got the script - tell it about this object so it will get destroyed + // when the script is unloaded. + script->AddNativeObject(object); +} diff --git a/src/scripting/python/pythonengine.h b/src/scripting/python/pythonengine.h index 12cd8223f..ae5513ab0 100644 --- a/src/scripting/python/pythonengine.h +++ b/src/scripting/python/pythonengine.h @@ -27,6 +27,9 @@ struct _sipTypeDef; class PythonEngine : public LanguageEngine { public: PythonEngine(ScriptManager* manager); + ~PythonEngine(); + + static PythonEngine* instance() { return sInstance; } static const char* kModulePrefix; @@ -35,20 +38,32 @@ public: Script* CreateScript(const QString& path, const QString& script_file, const QString& id); + void DestroyScript(Script* script); const _sipAPIDef* sip_api() const { return sip_api_; } void AddLogLine(const QString& message, bool error = false); + void RegisterNativeObject(QObject* object); private: static const _sipAPIDef* GetSIPApi(); void AddObject(void* object, const _sipTypeDef* type, const char* name) const; -private: - bool initialised_; + // Looks for a loaded script whose ID either exactly matches id, or matches + // some string at the start of id followed by a dot. For example, + // FindScriptMatchingId("foo") and FindScriptMatchingId("foo.bar") would both + // match a Script with an ID of foo, but FindScriptMatchingId("foobar") + // would not. + Script* FindScriptMatchingId(const QString& id) const; +private: + static PythonEngine* sInstance; + + bool initialised_; _object* clementine_module_; const _sipAPIDef* sip_api_; + + QMap loaded_scripts_; }; #endif // PYTHONENGINE_H diff --git a/src/scripting/python/pythonscript.cpp b/src/scripting/python/pythonscript.cpp index 3c76e5a7b..43f58adb1 100644 --- a/src/scripting/python/pythonscript.cpp +++ b/src/scripting/python/pythonscript.cpp @@ -111,7 +111,10 @@ bool PythonScript::Unload() { foreach (const QString& key, keys_to_delete) { PyDict_DelItemString(modules, key.toAscii().constData()); } - PyEval_ReleaseLock(); + + // Delete any native objects this script created + qDeleteAll(native_objects_); + return true; } diff --git a/src/scripting/script.cpp b/src/scripting/script.cpp index 73c286f5a..c397c3c8b 100644 --- a/src/scripting/script.cpp +++ b/src/scripting/script.cpp @@ -30,3 +30,7 @@ Script::Script(LanguageEngine* language, const QString& path, Script::~Script() { } + +void Script::AddNativeObject(QObject* object) { + native_objects_ << object; +} diff --git a/src/scripting/script.h b/src/scripting/script.h index c4edb6153..9c5e6fd9b 100644 --- a/src/scripting/script.h +++ b/src/scripting/script.h @@ -18,6 +18,7 @@ #ifndef SCRIPT_H #define SCRIPT_H +#include #include #include @@ -25,6 +26,8 @@ class LanguageEngine; class ScriptInterface; +class QObject; + class Script { public: Script(LanguageEngine* language, const QString& path, @@ -37,15 +40,20 @@ public: const QString& id() const { return id_; } ScriptInterface* interface() const { return interface_.get(); } + // The script can "own" QObjects like QActions that must be deleted (and + // removed from the UI, etc.) when the script is unloaded. + void AddNativeObject(QObject* object); + virtual bool Init() = 0; virtual bool Unload() = 0; protected: - boost::scoped_ptr interface_; + QList native_objects_; private: Q_DISABLE_COPY(Script); + boost::scoped_ptr interface_; LanguageEngine* language_; QString path_; QString script_file_; diff --git a/src/scripting/scriptinterface.cpp b/src/scripting/scriptinterface.cpp index 7abe49cf1..8dd90d6ef 100644 --- a/src/scripting/scriptinterface.cpp +++ b/src/scripting/scriptinterface.cpp @@ -19,6 +19,8 @@ #include "script.h" #include "scriptinterface.h" +#include + ScriptInterface::ScriptInterface(Script* script, QObject* parent) : QObject(parent), script_(script) @@ -28,7 +30,3 @@ ScriptInterface::ScriptInterface(Script* script, QObject* parent) void ScriptInterface::ShowSettingsDialog() { emit SettingsDialogRequested(); } - -void ScriptInterface::AddLogLine(const QString& message, bool error) { - script_->language()->manager()->AddLogLine(script_->id(), message, error); -} diff --git a/src/scripting/scriptinterface.h b/src/scripting/scriptinterface.h index 86fe7342c..b248c0e5f 100644 --- a/src/scripting/scriptinterface.h +++ b/src/scripting/scriptinterface.h @@ -35,9 +35,6 @@ public slots: // Callable by C++ void ShowSettingsDialog(); - // Callable by the script - void AddLogLine(const QString& message, bool error = false); - signals: // Scripts should connect to this and show a settings dialog void SettingsDialogRequested(); diff --git a/src/scripting/scriptmanager.cpp b/src/scripting/scriptmanager.cpp index 2a885f770..71f43e493 100644 --- a/src/scripting/scriptmanager.cpp +++ b/src/scripting/scriptmanager.cpp @@ -20,6 +20,7 @@ #include "script.h" #include "scriptinterface.h" #include "scriptmanager.h" +#include "uiinterface.h" #include "core/utilities.h" #ifdef HAVE_SCRIPTING_PYTHON @@ -36,7 +37,8 @@ const char* ScriptManager::kIniFileName = "script.ini"; const char* ScriptManager::kIniSettingsGroup = "Script"; ScriptManager::ScriptManager(QObject* parent) - : QAbstractListModel(parent) + : QAbstractListModel(parent), + ui_interface_(new UIInterface(this)) { #ifdef HAVE_SCRIPTING_PYTHON engines_ << new PythonEngine(this); @@ -48,8 +50,7 @@ ScriptManager::ScriptManager(QObject* parent) ScriptManager::~ScriptManager() { foreach (const ScriptInfo& info, info_) { if (info.loaded_) { - info.loaded_->Unload(); - delete info.loaded_; + info.loaded_->language()->DestroyScript(info.loaded_); } } @@ -244,8 +245,7 @@ void ScriptManager::Disable(const QModelIndex& index) { if (!info->loaded_) return; - info->loaded_->Unload(); - delete info->loaded_; + info->loaded_->language()->DestroyScript(info->loaded_); info->loaded_ = NULL; enabled_scripts_.remove(info->id_); diff --git a/src/scripting/scriptmanager.h b/src/scripting/scriptmanager.h index 6b8ed852b..e4a9facd4 100644 --- a/src/scripting/scriptmanager.h +++ b/src/scripting/scriptmanager.h @@ -27,6 +27,7 @@ class LanguageEngine; class Player; class PlaylistManager; class Script; +class UIInterface; class ScriptManager : public QAbstractListModel { Q_OBJECT @@ -68,6 +69,7 @@ public: void Init(const GlobalData& data); const GlobalData& data() const { return data_; } + UIInterface* ui() const { return ui_interface_; } void Enable(const QModelIndex& index); void Disable(const QModelIndex& index); @@ -130,6 +132,7 @@ private: // Things available to scripts GlobalData data_; + UIInterface* ui_interface_; }; #endif // SCRIPTMANAGER_H diff --git a/src/scripting/uiinterface.cpp b/src/scripting/uiinterface.cpp new file mode 100644 index 000000000..d350e2ed2 --- /dev/null +++ b/src/scripting/uiinterface.cpp @@ -0,0 +1,95 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + Clementine is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Clementine is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Clementine. If not, see . +*/ + +#include "uiinterface.h" + +#include +#include +#include + +UIInterface::UIInterface(QObject* parent) + : QObject(parent) +{ +} + +void UIInterface::RegisterActionLocation(const QString& id, QMenu* menu, QAction* before) { + if (locations_.contains(id)) { + qDebug() << __PRETTY_FUNCTION__ + << "A location with ID" << id << "was already registered"; + return; + } + + locations_[id] = Location(menu, before); + + connect(menu, SIGNAL(destroyed()), SLOT(MenuDestroyed())); + if (before) { + connect(before, SIGNAL(destroyed()), SLOT(MenuActionDestroyed())); + } + + // Add any actions that were waiting + foreach (const IdAndAction& id_action, pending_actions_) { + if (id_action.first == id) { + DoAddAction(id_action.first, id_action.second); + pending_actions_.removeAll(id_action); + } + } +} + +void UIInterface::AddAction(const QString& id, QAction* action) { + if (locations_.contains(id)) { + DoAddAction(id, action); + } else { + // Maybe that part of the UI hasn't been lazy created yet + pending_actions_ << IdAndAction(id, action); + connect(action, SIGNAL(destroyed()), SLOT(ActionDestroyed())); + } +} + +void UIInterface::DoAddAction(const QString& id, QAction* action) { + const Location& location = locations_[id]; + + if (location.menu_) { + location.menu_->insertAction(location.before_, action); + } +} + +void UIInterface::MenuDestroyed() { + QMenu* menu = qobject_cast(sender()); + foreach (const QString& id, locations_.keys()) { + if (locations_[id].menu_ == menu) { + locations_.remove(id); + } + } +} + +void UIInterface::MenuActionDestroyed() { + QAction* action = qobject_cast(sender()); + foreach (const QString& id, locations_.keys()) { + if (locations_[id].before_ == action) { + locations_.remove(id); + } + } +} + +void UIInterface::ActionDestroyed() { + QAction* action = qobject_cast(sender()); + foreach (const IdAndAction& id_action, pending_actions_) { + if (id_action.second == action) { + pending_actions_.removeAll(id_action); + } + } +} diff --git a/src/scripting/uiinterface.h b/src/scripting/uiinterface.h new file mode 100644 index 000000000..5c2f3e86e --- /dev/null +++ b/src/scripting/uiinterface.h @@ -0,0 +1,64 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + Clementine is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Clementine is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Clementine. If not, see . +*/ + +#ifndef UIINTERFACE_H +#define UIINTERFACE_H + +#include +#include +#include + +class QAction; +class QMenu; + +class UIInterface : public QObject { + Q_OBJECT + +public: + UIInterface(QObject* parent = 0); + + // Called from C++ + void RegisterActionLocation(const QString& id, QMenu* menu, QAction* before); + + // Called from scripts + void AddAction(const QString& id, QAction* action); + +private slots: + void MenuDestroyed(); + void MenuActionDestroyed(); + + void ActionDestroyed(); + +private: + struct Location { + Location() {} + Location(QMenu* menu, QAction* before) : menu_(menu), before_(before) {} + + QMenu* menu_; + QAction* before_; + }; + + typedef QPair IdAndAction; + + void DoAddAction(const QString& id, QAction* action); + +private: + QList pending_actions_; + QMap locations_; +}; + +#endif // UIINTERFACE_H diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index c024725ce..29ee5a343 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -99,6 +99,7 @@ #ifdef HAVE_SCRIPTING # include "scripting/scriptdialog.h" # include "scripting/scriptmanager.h" +# include "scripting/uiinterface.h" #endif #include @@ -640,12 +641,12 @@ MainWindow::MainWindow(QWidget* parent) #endif #ifdef HAVE_SCRIPTING + scripts_->ui()->RegisterActionLocation("help_menu", ui_->menu_help, NULL); scripts_->Init(ScriptManager::GlobalData(player_, playlists_)); connect(ui_->action_script_manager, SIGNAL(triggered()), SLOT(ShowScriptDialog())); #else ui_->action_script_manager->setEnabled(false); #endif - } MainWindow::~MainWindow() { diff --git a/src/ui/mainwindow.ui b/src/ui/mainwindow.ui index a23a2cd2c..803755763 100644 --- a/src/ui/mainwindow.ui +++ b/src/ui/mainwindow.ui @@ -393,7 +393,7 @@ 23 - + Music @@ -411,7 +411,7 @@ - + Playlist @@ -430,14 +430,14 @@ - + Help - + Extras @@ -445,7 +445,7 @@ - + Tools @@ -460,11 +460,11 @@ - - - - - + + + + +