Development/Tutorials/Plasma4/PythonPlasmoid: Difference between revisions

From KDE TechBase
(Written!)
 
 
(12 intermediate revisions by 8 users not shown)
Line 1: Line 1:
In this example, we're going to be writing a plasmoid that shows your laptop's battery charge in a neat graph as time goes by. Then we'll package it up and install it.
In this example, we're going to be writing a plasmoid that shows your laptop's battery charge in a neat graph as time goes by. Then we'll package it up and install it.


=Building=
= Building =
First and foremost, make sure you have [Getting Started/Build/KDE4/Python Support|Python support enabled].
First and foremost, make sure you have [[Getting Started/Build/KDE4/Python Support|Python support enabled]].


Next, you need to setup your plasmoid's development environment. For this example, our plasmoid will be called 'powerchart'. Make a directory to put everything in. Plasma expects a certain set of files and directories to exist when loading a plasmoid package:
Next, you need to setup your plasmoid's development environment. For this example, our plasmoid will be called 'powerchart'. Make a directory to put everything in. Plasma expects a certain set of files and directories to exist when loading a plasmoid package:
Line 11: Line 11:
*** ui/ - 'ui' type resources (not covered here)
*** ui/ - 'ui' type resources (not covered here)
*** code/ - 'code' type resources
*** code/ - 'code' type resources
    def connectToEngine(self):
**** main.py - The plugin's code. You can change this in metadata.desktop.
        self.engine = self.dataEngine('soliddevice')
        battery = self.engine.query('IS Battery').values()[0].toString()
        print "Connecting to battery %s"%battery
        self.engine.connectSource(battery, self)


    @pyqtSignature("dataUpdated(const QString &, const Plasma::DataEngine::Data &)")
To run it, try
    def dataUpdated(self, sourceName, data):
plasmoidviewer [root of the package]
        charge = data[QString("Charge Percent")].toInt()[0]
        print "Charge: %s%%"%charge
        samples = [charge,]
        self.chart.addSample(samples)**** main.py - The plugin's code. You can change this in metadata.desktop.


==metadata.desktop==
== metadata.desktop ==
metadata.desktop contains the plasmoid's metadata such as it's name, author, and scripting engine.
metadata.desktop contains the plasmoid's metadata such as its name, author, and scripting engine.


<syntaxhighlight lang="ini">
  [Desktop Entry]
  [Desktop Entry]
  Encoding=UTF-8 ;Encoding of this file
#Encoding of this file
  Name=Battery Graph ;The name to be shown in the 'Add Widgets' dialog
  Encoding=UTF-8
  ServiceTypes=Plasma/Applet ;Tells KDE this is provides the 'Plasma applet' service
  #The name to be shown in the 'Add Widgets' dialog
  Type=Service ;Tells KDE this is a service of sorts
  Name=Battery Graph
  Icon=battery ;The icon to show in the 'Add Widgets' dialog
#Tells KDE this is provides the 'Plasma applet'  
  X-Plasma-API=python ;The language your plugin is written in
  ServiceTypes=Plasma/Applet
  X-Plasma-MainScript=code/main.py ;The main entrypoint for your plasmoid
#Tells KDE this is a service of sorts
  Type=Service
#The icon to show in the 'Add Widgets' dialog
Icon=battery
#The language your plugin is written in
  X-Plasma-API=python
#The main entrypoint for your plasmoid
  X-Plasma-MainScript=code/main.py
 
   
   
  X-KDE-PluginInfo-Author=John Doe
  X-KDE-PluginInfo-Author=John Doe
  X-KDE-PluginInfo-Name=powerchart ;The internal name of the plasmoid. Your plasmoid gets installed into ~/.kde/share/apps/plasma/<Name>/
  #The internal name of the plasmoid. Your plasmoid gets installed into ~/.kde/share/apps/plasma/<Name>/
X-KDE-PluginInfo-Name=powerchart
  X-KDE-PluginInfo-Version=pre0.1
  X-KDE-PluginInfo-Version=pre0.1
  X-KDE-PluginInfo-Website=http://plasma.kde.org/
  X-KDE-PluginInfo-Website=http://plasma.kde.org/
Line 44: Line 46:
  X-KDE-PluginInfo-Depends=
  X-KDE-PluginInfo-Depends=
  X-KDE-PluginInfo-License=GPL
  X-KDE-PluginInfo-License=GPL
  X-KDE-PluginInfo-EnabledByDefault=true ;Tells KDE if this plasmoid should be available by default, or if the user needs to jump through hoops to enable it.
  #Tells KDE if this plasmoid should be available by default, or if the user needs to jump through hoops to enable it.
X-KDE-PluginInfo-EnabledByDefault=true
</syntaxhighlight>


Once you have that, you can start hacking away.
Once you have that, you can start hacking away.
Line 51: Line 55:


The first thing you do in a python file of course is import your dependencies:
The first thing you do in a python file of course is import your dependencies:
 
<syntaxhighlight lang="python">
  # -*- coding: utf-8 -*-
  # -*- coding: utf-8 -*-
  from PyQt4.QtCore import *
  from PyQt4.QtCore import *
Line 57: Line 61:
  from PyKDE4.plasma import Plasma
  from PyKDE4.plasma import Plasma
  from PyKDE4 import plasmascript
  from PyKDE4 import plasmascript
</syntaxhighlight>


The last import contains the bridge between your python code and the underlying C++ API. If you were writing this in C++, you'd inherit the Plasma::Applet class. It isn't as straightforward in Python though. There is some python wrapper code (in /usr/lib/python2.5/site-packages/PyKDE4/plasmascript.py) that needs to work some magic so your python script can access protected Plasma::Applet members.
The last import contains the bridge between your python code and the underlying C++ API. If you were writing this in C++, you'd inherit the Plasma::Applet class. It isn't as straightforward in Python though. There is some python wrapper code (in /usr/lib/python2.5/site-packages/PyKDE4/plasmascript.py) that needs to work some magic so your python script can access protected Plasma::Applet members.
<syntaxhighlight lang="python">
# Continued from above


  class PowerChart(plasmascript.Applet):
  class PowerChart(plasmascript.Applet):
Line 74: Line 82:
         self.chart.setTitle("Battery Charge")
         self.chart.setTitle("Battery Charge")
         self.connectToEngine()
         self.connectToEngine()
</syntaxhighlight>


The init() method is where you should put the majority of your initialization code such as creating widgets and loading data engines.
The init() method is where you should put the majority of your initialization code such as creating widgets and loading data engines.
Line 82: Line 91:


===Connecting to the engine===
===Connecting to the engine===
<syntaxhighlight lang="python">
# Continued from above


     def connectToEngine(self):
     def connectToEngine(self):
Line 88: Line 99:
         print "Connecting to battery %s"%battery
         print "Connecting to battery %s"%battery
         self.engine.connectSource(battery, self)
         self.engine.connectSource(battery, self)
 
</syntaxhighlight>
As mentioned earlier, the plasmascript module contains some magic bridge code that lets you access protected members while python thinks you're outside the Applet class. With the odd exception of self.applet, everything else works like normal.
As mentioned earlier, the plasmascript module contains some magic bridge code that lets you access protected members while python thinks you're outside the Applet class. With the odd exception of self.applet, everything else works like normal.


Line 94: Line 105:


===Reading updates===
===Reading updates===
<syntaxhighlight lang="python">
# Continued from above


     @pyqtSignature("dataUpdated(const QString &, const Plasma::DataEngine::Data &)")
     @pyqtSignature("dataUpdated(const QString &, const Plasma::DataEngine::Data &)")
Line 101: Line 115:
         samples = [charge,]
         samples = [charge,]
         self.chart.addSample(samples)
         self.chart.addSample(samples)
</syntaxhighlight>


A problem with mixing Python with Qt's signals is the lack of type safety. To get around this, you need to manually declare the dataUpdated slot's signature with the pyqtSignature decorator. Another problem is the use of QStrings. It makes things take a little more typing but it doesn't add a whole lot more effort.
A problem with mixing Python with Qt's signals is the lack of type safety. To get around this, you need to manually declare the dataUpdated slot's signature with the pyqtSignature decorator. Another problem is the use of QStrings. It makes things take a little more typing but it doesn't add a whole lot more effort.
Line 110: Line 125:
Our applet class is all written. The last thing we need to do is tell plasma how to get our class. It expects our main code file to contain a CreateApplet method that returns our plasmascript.Applet class.
Our applet class is all written. The last thing we need to do is tell plasma how to get our class. It expects our main code file to contain a CreateApplet method that returns our plasmascript.Applet class.


Two simple lines accomplishes this:
Two simple lines accomplish this:


<syntaxhighlight lang="python">
  def CreateApplet(parent):
  def CreateApplet(parent):
     return PowerChart(parent)
     return PowerChart(parent)
</syntaxhighlight>


=Packaging=
=Packaging=


Plasma packages are zip files that adhere to the expected directory structure explained at the start of this tutorial. You can call your file anything you want, but a good naming scheme is <Name>-<Version>.plasmoid eg powerchart-pre0.1.plasmoid. Remember while creating it, that the root of the package structure explained earlier means the root of the zip file.
Plasma packages are zip files that adhere to the expected directory structure explained at the start of this tutorial ([[Development/Tutorials/Plasma/Python/GettingStarted#Packaging.2C_installing_.26_running|link]]). You can call your file anything you want, but a good naming scheme is <Name>-<Version>.plasmoid eg powerchart-pre0.1.plasmoid. Remember while creating it, that the root of the package structure explained earlier means the root of the zip file.


Create it, install it, and add it to your desktop. Makes you feel proud, huh?
Create it, install it, and add it to your desktop. Makes you feel proud, huh?
Line 124: Line 141:
For reference, here is the complete source of main.py:
For reference, here is the complete source of main.py:


<syntaxhighlight lang="python">
  # -*- coding: utf-8 -*-
  # -*- coding: utf-8 -*-
  from PyQt4.QtCore import *
  from PyQt4.QtCore import *
Line 149: Line 167:
         battery = self.engine.query('IS Battery').values()[0].toString()
         battery = self.engine.query('IS Battery').values()[0].toString()
         print "Connecting to battery %s"%battery
         print "Connecting to battery %s"%battery
         self.engine.connectSource(battery, self)
         if not battery:
            print("you don't appear to have a battery.")
            [self.chart.addSample([v]) for v in [1,  2,  3,  1]]
        else:
            self.engine.connectSource(battery, self)
   
   
     @pyqtSignature("dataUpdated(const QString &, const Plasma::DataEngine::Data &)")
     @pyqtSignature("dataUpdated(const QString &, const Plasma::DataEngine::Data &)")
Line 160: Line 182:
  def CreateApplet(parent):
  def CreateApplet(parent):
     return PowerChart(parent)
     return PowerChart(parent)
</syntaxhighlight>


Happy hacking!
Happy hacking!

Latest revision as of 23:27, 11 September 2014

In this example, we're going to be writing a plasmoid that shows your laptop's battery charge in a neat graph as time goes by. Then we'll package it up and install it.

Building

First and foremost, make sure you have Python support enabled.

Next, you need to setup your plasmoid's development environment. For this example, our plasmoid will be called 'powerchart'. Make a directory to put everything in. Plasma expects a certain set of files and directories to exist when loading a plasmoid package:

  • / - The root of the package
    • metadata.desktop - Metadata about the plasmoid
    • contents/ - The directory plasma looks in for all your resources
      • ui/ - 'ui' type resources (not covered here)
      • code/ - 'code' type resources
        • main.py - The plugin's code. You can change this in metadata.desktop.

To run it, try

plasmoidviewer [root of the package]

metadata.desktop

metadata.desktop contains the plasmoid's metadata such as its name, author, and scripting engine.

 [Desktop Entry]
 #Encoding of this file
 Encoding=UTF-8
 #The name to be shown in the 'Add Widgets' dialog
 Name=Battery Graph
 #Tells KDE this is provides the 'Plasma applet' 
 ServiceTypes=Plasma/Applet
 #Tells KDE this is a service of sorts
 Type=Service
 #The icon to show in the 'Add Widgets' dialog
 Icon=battery
 #The language your plugin is written in
 X-Plasma-API=python
 #The main entrypoint for your plasmoid
 X-Plasma-MainScript=code/main.py

 
 X-KDE-PluginInfo-Author=John Doe
 X-KDE-PluginInfo-Email=[email protected]
 #The internal name of the plasmoid. Your plasmoid gets installed into ~/.kde/share/apps/plasma/<Name>/
 X-KDE-PluginInfo-Name=powerchart
 X-KDE-PluginInfo-Version=pre0.1
 X-KDE-PluginInfo-Website=http://plasma.kde.org/
 X-KDE-PluginInfo-Category=Examples
 X-KDE-PluginInfo-Depends=
 X-KDE-PluginInfo-License=GPL
 #Tells KDE if this plasmoid should be available by default, or if the user needs to jump through hoops to enable it.
 X-KDE-PluginInfo-EnabledByDefault=true

Once you have that, you can start hacking away.

The Code

The first thing you do in a python file of course is import your dependencies:

 # -*- coding: utf-8 -*-
 from PyQt4.QtCore import *
 from PyQt4.QtGui import *
 from PyKDE4.plasma import Plasma
 from PyKDE4 import plasmascript

The last import contains the bridge between your python code and the underlying C++ API. If you were writing this in C++, you'd inherit the Plasma::Applet class. It isn't as straightforward in Python though. There is some python wrapper code (in /usr/lib/python2.5/site-packages/PyKDE4/plasmascript.py) that needs to work some magic so your python script can access protected Plasma::Applet members.

# Continued from above

 class PowerChart(plasmascript.Applet):
     def __init__(self, parent, args=None):
         plasmascript.Applet.__init__(self, parent)
 
     def init(self):
         self.layout = QGraphicsGridLayout(self.applet)
         self.chart = Plasma.SignalPlotter(self.applet)
         self.chart.addPlot(QColor(0,255,0))
         self.layout.addItem(self.chart, 0, 0)
         self.setAspectRatioMode(Plasma.IgnoreAspectRatio)
         self.resize(200, 150)
         self.setHasConfigurationInterface(False)
         self.chart.setTitle("Battery Charge")
         self.connectToEngine()

The init() method is where you should put the majority of your initialization code such as creating widgets and loading data engines.

self.applet is the actual C++ Applet object your PowerChart class represents. When creating widgets and similar objects, you need to pass in self.applet instead of self when the good old fashioned C++ API reference says you need a QGraphicsWidget or Applet. If not, your applet will fail to load and you'll be puzzled why.

The code above is fairly self-explainatory as well. A basic layout is created, a plotter widget is added to it, a plot is added to the plotter, and some administrivia is performed. The next step is to connect to our dataengine.

Connecting to the engine

# Continued from above

     def connectToEngine(self):
         self.engine = self.dataEngine('soliddevice')
         battery = self.engine.query('IS Battery').values()[0].toString()
         print "Connecting to battery %s"%battery
         self.engine.connectSource(battery, self)

As mentioned earlier, the plasmascript module contains some magic bridge code that lets you access protected members while python thinks you're outside the Applet class. With the odd exception of self.applet, everything else works like normal.

Above, we requested the soliddevice engine, used a solid predicate to search for battery devices, and told the engine we're interested in updates from that battery. When updates come along, your applet's dataUpdated method is called.

Reading updates

# Continued from above

     @pyqtSignature("dataUpdated(const QString &, const Plasma::DataEngine::Data &)")
     def dataUpdated(self, sourceName, data):
         charge = data[QString("Charge Percent")].toInt()[0]
         print "Charge: %s%%"%charge
         samples = [charge,]
         self.chart.addSample(samples)

A problem with mixing Python with Qt's signals is the lack of type safety. To get around this, you need to manually declare the dataUpdated slot's signature with the pyqtSignature decorator. Another problem is the use of QStrings. It makes things take a little more typing but it doesn't add a whole lot more effort.

In our dataUpdated method, we find the data we want, pry it out of the Qt code, and put it in our plotter's chart. Other data can be found by poking around with plasmaengineexplorer.

Finishing up

Our applet class is all written. The last thing we need to do is tell plasma how to get our class. It expects our main code file to contain a CreateApplet method that returns our plasmascript.Applet class.

Two simple lines accomplish this:

 def CreateApplet(parent):
     return PowerChart(parent)

Packaging

Plasma packages are zip files that adhere to the expected directory structure explained at the start of this tutorial (link). You can call your file anything you want, but a good naming scheme is <Name>-<Version>.plasmoid eg powerchart-pre0.1.plasmoid. Remember while creating it, that the root of the package structure explained earlier means the root of the zip file.

Create it, install it, and add it to your desktop. Makes you feel proud, huh?

Complete Source

For reference, here is the complete source of main.py:

 # -*- coding: utf-8 -*-
 from PyQt4.QtCore import *
 from PyQt4.QtGui import *
 from PyKDE4.plasma import Plasma
 from PyKDE4 import plasmascript
 
 class PowerChart(plasmascript.Applet):
     def __init__(self, parent, args=None):
         plasmascript.Applet.__init__(self, parent)
 
     def init(self):
         self.layout = QGraphicsGridLayout(self.applet)
         self.chart = Plasma.SignalPlotter(self.applet)
         self.chart.addPlot(QColor(0,255,0))
         self.layout.addItem(self.chart, 0, 0)
         self.setAspectRatioMode(Plasma.IgnoreAspectRatio)
         self.resize(200, 150)
         self.setHasConfigurationInterface(False)
         self.chart.setTitle("Battery Charge")
         self.connectToEngine()
         
     def connectToEngine(self):
         self.engine = self.dataEngine('soliddevice')
         battery = self.engine.query('IS Battery').values()[0].toString()
         print "Connecting to battery %s"%battery
         if not battery:
            print("you don't appear to have a battery.")
            [self.chart.addSample([v]) for v in [1,  2,  3,  1]]
         else:
            self.engine.connectSource(battery, self)
 
     @pyqtSignature("dataUpdated(const QString &, const Plasma::DataEngine::Data &)")
     def dataUpdated(self, sourceName, data):
         charge = data[QString("Charge Percent")].toInt()[0]
         print "Charge: %s%%"%charge
         samples = [charge,]
         self.chart.addSample(samples)
 
 def CreateApplet(parent):
     return PowerChart(parent)

Happy hacking!