← Back to team overview

linuxdcpp-team team mailing list archive

[Branch ~dcplusplus-team/dcplusplus/trunk] Rev 3093: allow plugins to add commands

 

------------------------------------------------------------
revno: 3093
committer: poy <poy@xxxxxxxxxx>
branch nick: trunk
timestamp: Mon 2012-10-29 19:19:03 +0100
message:
  allow plugins to add commands
modified:
  changelog.txt
  dcpp/PluginApiImpl.cpp
  dcpp/PluginApiImpl.h
  dcpp/PluginDefs.h
  dwt/include/dwt/Widget.h
  dwt/src/Widget.cpp
  dwt/src/widgets/Menu.cpp
  plugins/Dev/Dialog.cpp
  plugins/Dev/Dialog.h
  plugins/Dev/Plugin.cpp
  plugins/Dev/Plugin.h
  win32/MainWindow.cpp
  win32/MainWindow.h
  win32/PluginApiImpl.cpp


--
lp:dcplusplus
https://code.launchpad.net/~dcplusplus-team/dcplusplus/trunk

Your team Dcplusplus-team is subscribed to branch lp:dcplusplus.
To unsubscribe from this branch go to https://code.launchpad.net/~dcplusplus-team/dcplusplus/trunk/+edit-subscription
=== modified file 'changelog.txt'
--- changelog.txt	2012-10-27 14:30:14 +0000
+++ changelog.txt	2012-10-29 18:19:03 +0000
@@ -3,6 +3,7 @@
 * Add "chunked" transfer encoding as per the HTTP/1.1 spec (crise)
 * [L#1072041] Fix DPI conversion problems (poy
 * Remove the "Windows UPnP" port mapper in favor of MiniUPnP (poy)
+* Add a UI interface to the plugin API (poy)
 
 -- 0.802 2012-10-20 --
 * Perf improvements using lock-free queues, requires P6 CPUs (poy)

=== modified file 'dcpp/PluginApiImpl.cpp'
--- dcpp/PluginApiImpl.cpp	2012-10-27 16:10:35 +0000
+++ dcpp/PluginApiImpl.cpp	2012-10-29 18:19:03 +0000
@@ -169,6 +169,9 @@
 DCUI PluginApiImpl::dcUI = {
 	DCINTF_DCPP_UI_VER,
 
+	&PluginApiImpl::addCommand,
+	&PluginApiImpl::removeCommand,
+
 	&PluginApiImpl::playSound
 };
 

=== modified file 'dcpp/PluginApiImpl.h'
--- dcpp/PluginApiImpl.h	2012-10-27 16:10:35 +0000
+++ dcpp/PluginApiImpl.h	2012-10-29 18:19:03 +0000
@@ -94,6 +94,9 @@
 	static void DCAPI addTag(TagDataPtr hTags, size_t start, size_t end, const char* id, const char* attributes);
 
 	// Functions for DCUI - the host has to define these
+	static void DCAPI addCommand(const char* name, void (*command)());
+	static void DCAPI removeCommand(const char* name);
+
 	static void DCAPI playSound(const char* path);
 
 	// Functions for DCQueue

=== modified file 'dcpp/PluginDefs.h'
--- dcpp/PluginDefs.h	2012-10-27 16:10:35 +0000
+++ dcpp/PluginDefs.h	2012-10-29 18:19:03 +0000
@@ -420,6 +420,9 @@
 	/* User interface API version */
 	uint32_t apiVersion;
 
+	void		(DCAPI *add_command)				(const char* name, void (*command)());
+	void		(DCAPI *remove_command)				(const char* name);
+
 	void		(DCAPI *play_sound)					(const char* path);
 } DCUI, *DCUIPtr;
 

=== modified file 'dwt/include/dwt/Widget.h'
--- dwt/include/dwt/Widget.h	2012-07-07 15:36:18 +0000
+++ dwt/include/dwt/Widget.h	2012-10-29 18:19:03 +0000
@@ -111,7 +111,6 @@
 	typedef std::function<bool(const MSG& msg, LRESULT& ret)> CallbackType;
 	typedef std::list<CallbackType> CallbackList;
 	typedef CallbackList::iterator CallbackIter;
-	typedef std::unordered_map<Message, CallbackList> CallbackCollectionType;
 
 	/// Adds a new callback - multiple callbacks for the same message will be called in the order they were added
 	CallbackIter addCallback(const Message& msg, const CallbackType& callback);
@@ -122,11 +121,12 @@
 	/// Clear a callback registered to msg
 	void clearCallback(const Message& msg, const CallbackIter& i);
 
+	/// Clear all callbacks registered to msg
+	void clearCallbacks(const Message& msg);
+
 	/** Run a function bound to this widget asynchronously */
 	void callAsync(const Application::Callback& f);
 
-	CallbackCollectionType &getCallbacks();
-
 	/// Returns true if handled, else false
 	virtual bool handleMessage(const MSG &msg, LRESULT &retVal);
 
@@ -241,7 +241,7 @@
 	static GlobalAtom propAtom;
 
 	// Contains the list of signals we're (this window) processing
-	CallbackCollectionType handlers;
+	std::unordered_map<Message, CallbackList> handlers;
 
 	HWND hwnd;
 
@@ -278,10 +278,6 @@
 	return (::GetWindowLong(handle(), GWL_EXSTYLE) & style) == style;
 }
 
-inline Widget::CallbackCollectionType& Widget::getCallbacks() {
-	return handlers;
-}
-
 inline HWND Widget::getParentHandle() {
 	return getParent() ? getParent()->handle() : NULL;
 }

=== modified file 'dwt/src/Widget.cpp'
--- dwt/src/Widget.cpp	2012-07-07 15:36:18 +0000
+++ dwt/src/Widget.cpp	2012-10-29 18:19:03 +0000
@@ -124,6 +124,13 @@
 void Widget::clearCallback(const Message& msg, const CallbackIter& i) {
 	CallbackList& callbacks = handlers[msg];
 	callbacks.erase(i);
+	if(callbacks.empty()) {
+		handlers.erase(msg);
+	}
+}
+
+void Widget::clearCallbacks(const Message& msg) {
+	handlers.erase(msg);
 }
 
 /// Make sure that handle is still valid before calling f

=== modified file 'dwt/src/widgets/Menu.cpp'
--- dwt/src/widgets/Menu.cpp	2012-06-18 16:32:14 +0000
+++ dwt/src/widgets/Menu.cpp	2012-10-29 18:19:03 +0000
@@ -755,17 +755,21 @@
 		}
 	}
 
-	// remove from the child list if this was a sub-menu.
 	if(child) {
+		// remove this sub-menu from the child list.
 		itsChildren.erase(std::remove_if(itsChildren.begin(), itsChildren.end(),
 			[child](std::unique_ptr<Menu>& sub) { return sub->handle() == child; }), itsChildren.end());
+
+	} else if(!getRootMenu()->popup) {
+		// in the menu bar: remove the callback.
+		getParent()->clearCallbacks(Message(WM_MENUCOMMAND, index * 31 + reinterpret_cast<LPARAM>(handle())));
 	}
 }
 
 void Menu::removeAllItems() {
-	//must be backwards, since bigger indexes change on remove
-	for(int i = size() - 1; i >= 0; i--) {
-		removeItem( i );
+	// must be backwards, since higher indexes change when removing lower ones
+	for(int i = size() - 1, end = itsTitle.empty() ? 0 : 1; i >= end; --i) {
+		removeItem(i);
 	}
 }
 

=== modified file 'plugins/Dev/Dialog.cpp'
--- plugins/Dev/Dialog.cpp	2012-08-02 17:49:34 +0000
+++ plugins/Dev/Dialog.cpp	2012-10-29 18:19:03 +0000
@@ -18,6 +18,7 @@
 
 #include "stdafx.h"
 #include "Dialog.h"
+#include "Plugin.h"
 #include "resource.h"
 #include "Util.h"
 
@@ -44,16 +45,14 @@
 }
 
 Dialog::~Dialog() {
-	if(hwnd) {
-		DestroyWindow(hwnd);
-	}
+	close();
 
 	dlg = nullptr;
 }
 
-void Dialog::create(HWND parent) {
+void Dialog::create() {
 	if(hwnd) {
-		MessageBox(parent, Util::toT("The dev plugin hasn't been properly shut down; you better restart " + Util::appName).c_str(),
+		MessageBox(0, Util::toT("The dev plugin hasn't been properly shut down; you better restart " + Util::appName).c_str(),
 			_T("Error creating the dev plugin's dialog"), MB_OK);
 		return;
 	}
@@ -63,7 +62,7 @@
 	if(!hwnd) {
 		TCHAR buf[256];
 		FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, 0, GetLastError(), 0, buf, 256, 0);
-		MessageBox(parent, buf, _T("Error creating the dev plugin's dialog"), MB_OK);
+		MessageBox(0, buf, _T("Error creating the dev plugin's dialog"), MB_OK);
 	}
 }
 
@@ -73,6 +72,13 @@
 	messages.push_back(move(msg));
 }
 
+void Dialog::close() {
+	if(hwnd) {
+		DestroyWindow(hwnd);
+		hwnd = nullptr;
+	}
+}
+
 INT_PTR CALLBACK Dialog::DialogProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM) {
 	switch(uMsg) {
 	case WM_INITDIALOG:
@@ -92,7 +98,7 @@
 		}
 	case WM_CLOSE:
 		{
-			delete dlg;
+			Plugin::dlgClosed();
 			break;
 		}
 	case WM_DESTROY:

=== modified file 'plugins/Dev/Dialog.h'
--- plugins/Dev/Dialog.h	2012-07-26 17:35:04 +0000
+++ plugins/Dev/Dialog.h	2012-10-29 18:19:03 +0000
@@ -40,8 +40,9 @@
 	Dialog();
 	~Dialog();
 
-	void create(HWND parent);
+	void create();
 	void write(bool hubOrUser, bool sending, string ip, string peer, string message);
+	void close();
 
 	static HINSTANCE instance;
 

=== modified file 'plugins/Dev/Plugin.cpp'
--- plugins/Dev/Plugin.cpp	2012-08-02 17:49:34 +0000
+++ plugins/Dev/Plugin.cpp	2012-10-29 18:19:03 +0000
@@ -22,13 +22,17 @@
 
 Plugin* Plugin::instance = nullptr;
 
+const char* switchText = "Dev plugin: enable/disable";
+
 Plugin::Plugin() {
 }
 
 Plugin::~Plugin() {
-	for(auto& i: events)
-		hooks->release_hook(i.second);
-	events.clear();
+	clearHooks();
+
+	if(ui) {
+		ui->remove_command(switchText);
+	}
 }
 
 Bool DCAPI Plugin::main(PluginState state, DCCorePtr core, dcptr_t) {
@@ -37,15 +41,16 @@
 	case ON_LOAD:
 		{
 			Bool res = True;
-			newInstance();
-			getInstance()->onLoad(core, (state == ON_INSTALL), res);
+			instance = new Plugin();
+			instance->onLoad(core, (state == ON_INSTALL), res);
 			return res;
 		}
 
 	case ON_UNINSTALL:
 	case ON_UNLOAD:
 		{
-			deleteInstance();
+			delete instance;
+			instance = nullptr;
 			return True;
 		}
 
@@ -56,6 +61,37 @@
 	}
 }
 
+void Plugin::dlgClosed() {
+	instance->close();
+}
+
+void Plugin::addHooks() {
+	events[HOOK_NETWORK_HUB_IN] = hooks->bind_hook(HOOK_NETWORK_HUB_IN, [](dcptr_t pObject, dcptr_t pData, dcptr_t, Bool*) {
+		return instance->onHubDataIn(reinterpret_cast<HubDataPtr>(pObject), reinterpret_cast<char*>(pData)); }, nullptr);
+	events[HOOK_NETWORK_HUB_OUT] = hooks->bind_hook(HOOK_NETWORK_HUB_OUT, [](dcptr_t pObject, dcptr_t pData, dcptr_t, Bool*) {
+		return instance->onHubDataOut(reinterpret_cast<HubDataPtr>(pObject), reinterpret_cast<char*>(pData)); }, nullptr);
+	events[HOOK_NETWORK_CONN_IN] = hooks->bind_hook(HOOK_NETWORK_CONN_IN, [](dcptr_t pObject, dcptr_t pData, dcptr_t, Bool*) {
+		return instance->onConnectionDataIn(reinterpret_cast<ConnectionDataPtr>(pObject), reinterpret_cast<char*>(pData)); }, nullptr);
+	events[HOOK_NETWORK_CONN_OUT] = hooks->bind_hook(HOOK_NETWORK_CONN_OUT, [](dcptr_t pObject, dcptr_t pData, dcptr_t, Bool*) {
+		return instance->onConnectionDataOut(reinterpret_cast<ConnectionDataPtr>(pObject), reinterpret_cast<char*>(pData)); }, nullptr);
+}
+
+void Plugin::clearHooks() {
+	for(auto& i: events)
+		hooks->release_hook(i.second);
+	events.clear();
+}
+
+void Plugin::start() {
+	dialog.create();
+	addHooks();
+}
+
+void Plugin::close() {
+	clearHooks();
+	dialog.close();
+}
+
 void Plugin::onLoad(DCCorePtr core, bool install, Bool& loadRes) {
 	dcpp = core;
 	hooks = reinterpret_cast<DCHooksPtr>(core->query_interface(DCINTF_HOOKS, DCINTF_HOOKS_VER));
@@ -63,38 +99,31 @@
 	auto utils = reinterpret_cast<DCUtilsPtr>(core->query_interface(DCINTF_DCPP_UTILS, DCINTF_DCPP_UTILS_VER));
 	auto config = reinterpret_cast<DCConfigPtr>(core->query_interface(DCINTF_CONFIG, DCINTF_CONFIG_VER));
 	auto logger = reinterpret_cast<DCLogPtr>(core->query_interface(DCINTF_LOGGING, DCINTF_LOGGING_VER));
+	ui = reinterpret_cast<DCUIPtr>(core->query_interface(DCINTF_DCPP_UI, DCINTF_DCPP_UI_VER));
 
-	if(!utils || !config || !logger) {
+	if(!utils || !config || !logger || !ui) {
 		loadRes = False;
 		return;
 	}
 
 	Util::initialize(core->host_name(), utils, config, logger);
 
-	/*if(install) {
-		// Default settings
-
-		Util::logMessage("Dev plugin installed, please restart " + Util::appName + " to begin using the plugin.");
-		return;
-	}*/
-
-	events[HOOK_HUB_OFFLINE]			= hooks->bind_hook(HOOK_HUB_OFFLINE, &hubOfflineEvent, NULL);
-	events[HOOK_HUB_ONLINE]				= hooks->bind_hook(HOOK_HUB_ONLINE, &hubOnlineEvent, NULL);
-
-	events[HOOK_NETWORK_HUB_IN]			= hooks->bind_hook(HOOK_NETWORK_HUB_IN, &netHubInEvent, NULL);
-	events[HOOK_NETWORK_HUB_OUT]		= hooks->bind_hook(HOOK_NETWORK_HUB_OUT, &netHubOutEvent, NULL);
-	events[HOOK_NETWORK_CONN_IN]		= hooks->bind_hook(HOOK_NETWORK_CONN_IN, &netConnInEvent, NULL);
-	events[HOOK_NETWORK_CONN_OUT]		= hooks->bind_hook(HOOK_NETWORK_CONN_OUT, &netConnOutEvent, NULL);
-
-	events[HOOK_UI_CREATED]				= hooks->bind_hook(HOOK_UI_CREATED, &uiCreatedEvent, NULL);
-}
-
-Bool Plugin::onHubDisconnected(HubDataPtr hHub) {
-	return False;
-}
-
-Bool Plugin::onHubConnected(HubDataPtr hHub) {
-	return False;
+	if(install) {
+		/// @todo config enabled/disabled
+
+		Util::logMessage("The dev plugin has been installed; check the \"" + string(switchText) + "\" command.");
+	}
+
+	start();
+	ui->add_command(switchText, [] { instance->onSwitched(); });
+}
+
+void Plugin::onSwitched() {
+	if(events.empty()) {
+		start();
+	} else {
+		close();
+	}
 }
 
 Bool Plugin::onHubDataIn(HubDataPtr hHub, const char* message) {
@@ -102,7 +131,7 @@
 	return False;
 }
 
-Bool Plugin::onHubDataOut(HubDataPtr hHub, const char* message, Bool* bBreak) {
+Bool Plugin::onHubDataOut(HubDataPtr hHub, const char* message) {
 	dialog.write(true, true, hHub->ip, "Hub " + string(hHub->url), message);
 	return False;
 }
@@ -116,8 +145,3 @@
 	dialog.write(false, true, hConn->ip, "User" /** @todo get user's nick */, message);
 	return False;
 }
-
-Bool Plugin::onUiCreated(HWND hwnd) {
-	dialog.create(hwnd);
-	return False;
-}

=== modified file 'plugins/Dev/Plugin.h'
--- plugins/Dev/Plugin.h	2012-07-19 22:00:20 +0000
+++ plugins/Dev/Plugin.h	2012-10-29 18:19:03 +0000
@@ -19,8 +19,6 @@
 #ifndef PLUGINS_DEV_PLUGIN_H
 #define PLUGINS_DEV_PLUGIN_H
 
-#include <Singleton.h>
-
 #include <map>
 
 #include "Dialog.h"
@@ -28,42 +26,40 @@
 using std::map;
 using std::string;
 
-class Plugin : public dcpp::Singleton<Plugin>
+class Plugin
 {
 public:
 	static Bool DCAPI main(PluginState state, DCCorePtr core, dcptr_t);
 
+	static void dlgClosed();
+
 private:
-	friend class dcpp::Singleton<Plugin>;
-
 	Plugin();
 	~Plugin();
 
+	void addHooks();
+	void clearHooks();
+
+	void start();
+	void close();
+
 	void onLoad(DCCorePtr core, bool install, Bool& loadRes);
-	Bool onHubDisconnected(HubDataPtr hHub);
-	Bool onHubConnected(HubDataPtr hHub);
+	void onSwitched();
 	Bool onHubDataIn(HubDataPtr hHub, const char* message);
-	Bool onHubDataOut(HubDataPtr hHub, const char* message, Bool* bBreak);
+	Bool onHubDataOut(HubDataPtr hHub, const char* message);
 	Bool onConnectionDataIn(ConnectionDataPtr hConn, const char* message);
 	Bool onConnectionDataOut(ConnectionDataPtr hConn, const char* message);
-	Bool onUiCreated(HWND hwnd);
-
-	// Event wrappers
-	static Bool DCAPI hubOfflineEvent(dcptr_t pObject, dcptr_t /*pData*/, dcptr_t, Bool* /*bBreak*/) { return getInstance()->onHubDisconnected(reinterpret_cast<HubDataPtr>(pObject)); }
-	static Bool DCAPI hubOnlineEvent(dcptr_t pObject, dcptr_t /*pData*/, dcptr_t, Bool* /*bBreak*/) { return getInstance()->onHubConnected(reinterpret_cast<HubDataPtr>(pObject)); }
-	static Bool DCAPI netHubInEvent(dcptr_t pObject, dcptr_t pData, dcptr_t, Bool* /*bBreak*/) { return getInstance()->onHubDataIn(reinterpret_cast<HubDataPtr>(pObject), reinterpret_cast<char*>(pData)); }
-	static Bool DCAPI netHubOutEvent(dcptr_t pObject, dcptr_t pData, dcptr_t, Bool* bBreak) { return getInstance()->onHubDataOut(reinterpret_cast<HubDataPtr>(pObject), reinterpret_cast<char*>(pData), bBreak); }
-	static Bool DCAPI netConnInEvent(dcptr_t pObject, dcptr_t pData, dcptr_t, Bool* /*bBreak*/) { return getInstance()->onConnectionDataIn(reinterpret_cast<ConnectionDataPtr>(pObject), reinterpret_cast<char*>(pData)); }
-	static Bool DCAPI netConnOutEvent(dcptr_t pObject, dcptr_t pData, dcptr_t, Bool* /*bBreak*/) { return getInstance()->onConnectionDataOut(reinterpret_cast<ConnectionDataPtr>(pObject), reinterpret_cast<char*>(pData)); }
-	static Bool DCAPI uiCreatedEvent(dcptr_t pObject, dcptr_t /*pData*/, dcptr_t, Bool* /*bBreak*/) { return getInstance()->onUiCreated(reinterpret_cast<HWND>(pObject)); }
 
 	map<string, subsHandle> events;
 
 	DCCorePtr dcpp;
 	DCHooksPtr hooks;
+	DCUIPtr ui;
 
 	Dialog dialog;
 
+	/** @todo switch to dcpp::Singleton when <http://gcc.gnu.org/bugzilla/show_bug.cgi?id=51494>
+	is fixed */
 	static Plugin* instance;
 };
 

=== modified file 'win32/MainWindow.cpp'
--- win32/MainWindow.cpp	2012-10-20 16:10:39 +0000
+++ win32/MainWindow.cpp	2012-10-29 18:19:03 +0000
@@ -90,6 +90,8 @@
 using dwt::Spinner;
 using dwt::ToolBar;
 
+map<tstring, function<void ()>, noCaseStringLess> MainWindow::pluginCommands;
+
 static dwt::IconPtr mainIcon(WinUtil::createIcon(IDI_DCPP, 32));
 static dwt::IconPtr mainSmallIcon(WinUtil::createIcon(IDI_DCPP, 16));
 
@@ -308,6 +310,10 @@
 		file->appendItem(T_("GeoIP database update"), [this] { updateGeo(); });
 		file->appendSeparator();
 
+		pluginMenu = file->appendPopup(T_("Plugins"));
+		refreshPluginMenu();
+		file->appendSeparator();
+
 		file->appendItem(T_("E&xit\tAlt+F4"), [this] { close(true); }, WinUtil::menuIcon(IDI_EXIT));
 	}
 
@@ -641,7 +647,40 @@
 	return false;
 }
 
-void MainWindow::notify(const tstring& title, const tstring& message, const std::function<void ()>& callback, const dwt::IconPtr& balloonIcon) {
+void MainWindow::addPluginCommand(const tstring& text, function<void ()> command) {
+	pluginCommands.emplace(text, command);
+
+	if(WinUtil::mainWindow && !WinUtil::mainWindow->closing()) {
+		WinUtil::mainWindow->pluginMenu->removeAllItems();
+		WinUtil::mainWindow->refreshPluginMenu();
+	}
+}
+
+void MainWindow::removePluginCommand(const tstring& text) {
+	auto i = pluginCommands.find(text);
+	if(i == pluginCommands.end()) return;
+	auto index = std::distance(pluginCommands.begin(), i);
+	pluginCommands.erase(i);
+
+	if(WinUtil::mainWindow && !WinUtil::mainWindow->closing()) {
+		WinUtil::mainWindow->pluginMenu->removeItem(index + 1 /* account for the menu title */);
+		if(pluginCommands.empty()) {
+			WinUtil::mainWindow->refreshPluginMenu();
+		}
+	}
+}
+
+void MainWindow::refreshPluginMenu() {
+	if(pluginCommands.empty()) {
+		pluginMenu->appendItem(T_("(No plugin command found)"), nullptr, nullptr, false);
+	} else {
+		for(auto& i: pluginCommands) {
+			pluginMenu->appendItem(i.first, i.second);
+		}
+	}
+}
+
+void MainWindow::notify(const tstring& title, const tstring& message, function<void ()> callback, const dwt::IconPtr& balloonIcon) {
 	notifier->addMessage(str(TF_("DC++ - %1%") % title), message, [this, callback] { handleRestore(); if(callback) callback(); }, balloonIcon);
 }
 
@@ -981,9 +1020,10 @@
 	// This should end immediately, as it only should be the stopper that sends another WM_CLOSE
 	WaitForSingleObject(stopperThread, 60*1000);
 	CloseHandle(stopperThread);
-	stopperThread = NULL;
 	::PostQuitMessage(0);
 	dcdebug("Quit message posted\n");
+
+	WinUtil::mainWindow = nullptr;
 	return true;
 }
 

=== modified file 'win32/MainWindow.h'
--- win32/MainWindow.h	2012-10-20 16:10:39 +0000
+++ win32/MainWindow.h	2012-10-29 18:19:03 +0000
@@ -30,6 +30,7 @@
 #include "forward.h"
 #include "AspectStatus.h"
 
+using std::function;
 using std::unique_ptr;
 
 class MainWindow :
@@ -64,8 +65,11 @@
 
 	void handleSettings();
 
+	static void addPluginCommand(const tstring& text, function<void ()> command);
+	static void removePluginCommand(const tstring& text);
+
 	/** show a balloon popup. refer to the dwt::Notification::addMessage doc for info about parameters. */
-	void notify(const tstring& title, const tstring& message, const std::function<void ()>& callback = nullptr, const dwt::IconPtr& balloonIcon = nullptr);
+	void notify(const tstring& title, const tstring& message, function<void ()> callback = nullptr, const dwt::IconPtr& balloonIcon = nullptr);
 	void setStaticWindowState(const string& id, bool open);
 	void TrayPM();
 
@@ -118,6 +122,7 @@
 	RebarPtr rebar;
 	SplitterContainerPtr paned;
 	MenuPtr mainMenu;
+	Menu* pluginMenu;
 	Menu* viewMenu;
 	TransferView* transfers;
 	ToolBarPtr toolbar;
@@ -128,6 +133,10 @@
 
 	bool tray_pm;
 
+	/* sorted list of plugin commands. static because they may be added before the window has
+	actually been created. */
+	static map<tstring, function<void ()>, noCaseStringLess> pluginCommands;
+
 	unique_ptr<HttpDownload> conns[CONN_LAST];
 
 	HANDLE stopperThread;
@@ -197,6 +206,7 @@
 	void layout();
 	void updateStatus();
 	void updateAwayStatus();
+	void refreshPluginMenu();
 	void showPortsError(const string& port);
 	void setSaveTimer();
 	void saveSettings();

=== modified file 'win32/PluginApiImpl.cpp'
--- win32/PluginApiImpl.cpp	2012-10-27 16:10:35 +0000
+++ win32/PluginApiImpl.cpp	2012-10-29 18:19:03 +0000
@@ -24,11 +24,20 @@
 
 #include <dcpp/Text.h>
 
+#include "MainWindow.h"
 #include "WinUtil.h"
 
 namespace dcpp {
 
 // Functions for DCUI
+void PluginApiImpl::addCommand(const char* name, void (*command)()) {
+	MainWindow::addPluginCommand(Text::toT(name), command);
+}
+
+void PluginApiImpl::removeCommand(const char* name) {
+	MainWindow::removePluginCommand(Text::toT(name));
+}
+
 void PluginApiImpl::playSound(const char* path) {
 	WinUtil::playSound(Text::toT(path));
 }