In this tutorial we will write a simple plugin (called Auscrie for “Auto Screenshooter”) for LeechCraft. This way we will illustrate basic concepts of plugin writing. Our plugin would be able to take a screenshot of LeechCraft’s window and save it somewhere or upload to a imagebin. Screenshooting would be initiated by a button in the toolbar.
We will learn:
how to create dummy new plugins and build them;
how to add UI written in Qt Designer to plugins;
preferable ways of working with network and HTTP in particular in LeechCraft;
using LeechCraft messages to notify user about various events in your plugin.
Understanding plugins
C++ plugins for LeechCraft are just dynamic libraries with main plugin instance exported from it. Plugin instance is exported with Q_EXPORT_PLUGIN2. Plugin instance should also implement IInfo (/src/interfaces/iinfo.h from the repository root) interface to be recognized by LeechCraft as a proper plugin.
To know more about writing plugins for C++/Qt applications please visit How to Create Qt Plugins guide.
You could also refer to the general LeechCraft overview for more information about plugins and LeechCraft architecture.
Paths
For our convenience we will work right in source tree, in /src/plugins/auscrie. Usually you will develop your plugin separately from the source tree (see Workflow documentation for more information), and the only difference would be that you will need to adjust the paths accordingly. For example, you will need to make sure that CMAKE_MODULE_PATH points to the directory containing FindLeechCraft.cmake when running CMake. CMake variables are set with -D command line switch. For example:
This is *NIX-oriented tutorial. Refer to the corresponding parts of Windows building guide for the differences in the building process.
General skeleton
There is a convenience Python script in LeechCraft repository: /tools/scripts/genplugin.py which generates very basic CMakeLists.txt file for your plugin and plugin instance declaration/implementation files. It is invoked as:
genplugin.py -a "Plugin Author" -p PluginNameWithoutSpaces -i Comma,Separated,List,Of,Base,Interfaces
It would also give a short help message when run with -h option.
So, we create the /src/plugins/auscrie directory, cd into it and run the script like this (please note that in your case path to genplugin.py can be different):
../../../tools/scripts/genplugin.py -a "Your Name" -p Auscrie -i IToolBarEmbedder
We also derive from the IToolBarEmbedder (/src/interfaces/itoolbarembedder.h) because we want to embed our button there.
This script would generate basic files, but that would be enough to produce a minimal working (more precisely, minimal loading) plugin. Let's try to compile and run it! To do that, we create a directory in which the plugin will be built, run cmake over it, then make to build the plugin and then make install from root to install the plugin. From the directory with the sources issue:
mkdir build
cd build
cmake ../
make
sudo make install
Here we used out-of-source builds. Generally, it’s preferable to use out-of-source builds since you can easily clean up the source tree (by removing the build directory) or have multiple builds with different configurations simultaneously.
Now run LeechCraft and open Settings dialog, then select the Plugins page. You should now see your plugin in the list. If you don’t, check the logs (~/.leechcraft/warning.log) and contact us.
Basic stuff
Now we have a very basic plugin that is recognizable by LeechCraft. Let us fill the gaps.
First, we should fill the GetInfo stub with some sensible description, like “Simple auto screenshooter.”.
Then let’s create the action that would make a screenshot. First, we define some internal data members and methods:
Proxy_ of type ICoreProxy_ptr, where we will store the pointer to the core proxy object passed to Init. We need this proxy, because it’s the object through which all the communication with LeechCraft Core is done, and we will need it later;
QAction *ShotAction_, which would initiate screenshooting;
private slot makeScreenshot which would be invoked when our action fires up.
It’s better to do such initialization in our Init function, so we write there the following code for our action:
Proxy_ = proxy;
Dialog_ = new ShooterDialog (Proxy_->GetMainWindow ());
ShotAction_ = new QAction (Proxy_->GetIcon ("screenshot"),
tr ("Make a screenshot"),
this);
connect (ShotAction_,
SIGNAL (triggered ()),
this,
SLOT (makeScreenshot ()));
We use ICoreProxy_ptr to get the right icon from the right theme for our action. When you will develop your own plugins you will need to carry your icons with you unless you get into the official source tree. We also use the proxy to get the pointer to the main window. The ShooterDialog stuff will be explained later.
Then, let’s fill the GetActions stub and return our ShotAction_. GetActions would look like:
If you compile and install your plugin now, you will see the icon of your screenshoter in a toolbar, but it does nothing yet.
Initiating screenshooting
In our screenshooting slot we run a simple dialog asking for screenshot parameters. If user accepts the dialog, we disable the shooting action (we will reenable it again when the screenshot is ready) and start a timer according to the timeout set by user in the dialog:
We created the Dialog_ in Init in order to keep it between different calls to makeScreenshot. Having this dialog avaliable in any function has another benefit: we don’t have to store screenshot parameters like format or quality after executing the dialog since we can ask it anytime.
Writing this dialog is quite a trivial task for anyone who ever used Qt Designer, so it won’t be documented here. However, it’s worth noting how forms should be added to CMake-based project. We define a variable which will hold the list of forms (FORMS in our case), we add the .h and .cpp-files to the list of headers and sources, we call the QT4_WRAP_UI to run uic on the forms and we add the result of uic to the list of dependencies of our plugin. So, the middle of the CMakeLists.txt file would look like:
SET (SRCS
auscrie.cpp
shooterdialog.cpp
)
SET (HEADERS
auscrie.h
shooterdialog.h
)
SET (FORMS
shooterdialog.ui
)
QT4_WRAP_CPP (MOC_SRCS ${HEADERS})
QT4_WRAP_UI (UIS_H ${FORMS})
ADD_LIBRARY (leechcraft_auscrie SHARED
${COMPILED_TRANSLATIONS}
${SRCS}
${MOC_SRCS}
${UIS_H}
)
Shooting, it’s fun!
Let’s finally take a look at the shoot slot. We will split our discussion of it into logical parts.
To grab the window, we use the Proxy_ object we stored earlier to access the main LeechCraft window. Don’t forget to #include the QMainWindow header, otherwise casting from QMainWindow* to QWidget* would fail.
Here we used Core’s settings manager which is basically a wrapper around QSettings. Keys starting with PluginsStorage can be used by plugins, the Core won’t use them for its own tasks. It is generally ok to use Core’s settings manager for storing a setting or two, but if you need more, and especially if you need to build your own settings dialogs, you will need to add one for your plugin.
If user selected uploading to an imagebin, we call a separate function, Post(), that would take care of it:
case ShooterDialog::AUpload:
{
QBuffer buf;
pm.save (&buf,
fmt,
quality);
Post (buf.data ());
}
break;
}
}
We also should now add the following to our CMakeLists.txt just before INCLUDE (${QT_USE_FILE}):
SET (QT_USE_QTNETWORK TRUE)
This would add the networking abilities to our plugin, making visible the includes from the QtNetwork module and linking our plugin to the QtNetwork library. We will definitely need it since our plugin uses QtNetwork (for example, QNetworkAccessManager and QNetworkReply) to do actualy posting of screenshots.
Because we aren’t interested in uploading implementation now, we’ve moved all the posting code into separate Poster class, so our Post function looks quite simple:
Here we used Proxy_ once again to obtain the application-wide instance of the QNetworkAccessManager with the GetNetworkAccessManager method. It’s always better to use the application-wide QNetworkAccessManager thus getting access to the application-wide network cache and cookie database, as well as allowing it to optimize requests by reusing connections, for example.
It’s also worth noting that in case if you need to just download a file, which is not our case but just a very common task, you can just emit the corresponding signal without caring about network access, managers, replies and stuff. This approach is discusses in details in the Overview document.
It is also ok to create the Poster on the heap without caring about memory deallocation now. It will be freed in the corresponding slots.
We connect to the Poster’s signals
finished(
QNetworkReply *reply``);
and
error(
QNetworkReply *reply``);
to get notified when our uploading finishes or whether an error occurs. Poster emits the QNetworkReply that originally emitted the corresponding signal as the parameter of those signals.
Let’s take a look at
handleFinished(
QNetworkReply *reply``);
(by the way, don’t forget to declare all the introduced members in class definition):
void Plugin::handleFinished (QNetworkReply *reply)
{
sender ()->deleteLater ();
QString result = reply->readAll ();
QRegExp re ("You can find this at ([^<]+)");
if (!re.exactMatch (result))
{
Entity e = Util::MakeNotification ("Auscrie",
tr ("Page parse failed"),
PWarning_);
emit gotEntity (e);
return;
}
QString pasteUrl = re.cap (1);
Entity e = Util::MakeNotification ("Auscrie",
tr ("Image pasted: %1, the URL was copied to the clipboard")
.arg (pasteUrl),
PInfo_);
QApplication::clipboard ()->setText (pasteUrl, QClipboard::Clipboard);
QApplication::clipboard ()->setText (pasteUrl, QClipboard::Selection);
emit gotEntity (e);
}
First we shedule the sender object (Poster instance created earlier) to delete itself once control gets to the event loop. We don’t plainly use something like delete sender (); because we can’t delete objects inside their signals’ handlers.
Then we get the page returned by server via readAll and try to get the link to our newly uploaded image by quite a simple regexp. If it fails, we emit a notification about our failure with Warning priority and stop processing. Please note that in your class signals and slots that use LeechCraft’s data structures must use fully qualified names with namespaces, like
void gotEntity(
const LeechCraft::Entity& entity``);
Otherwise Qt’s meta object system won’t recognize them.
Here we also use the LeechCraft messages system to send out a notification about various events in your plugin, like errors or just information messages, to be shown to the user. We use the LeechCraft::Util::MakeNotification (which can be added by #include) utility function to create such an entity. This function takes three parameters: notification title, notification body, notification priority. Notifications and LeechCraft messages are discussed in details in the Overview document.
Conclusion
Basically that’s all. Now we have a working, usable and useful plugin. Of course there is something to extend: for example, we could keep history of all the pasted screenshots or show a fancy progressbar notifying about upload progress, and you will almost surely want to add localization support, but our goal here is to get used to LeechCraft’s concept of plugins.