Skip to content

Writing Plasma PopupApplets in Ruby and C#

Wednesday, 29 July 2009  |  richard dale

Several people have wanted to be able to write Plasma PopupApplets in scripting languages. I'm pleased to announce that for KDE 4.3 you will be able to write them in Ruby and C#.

I've translated the C++ example on TechBase Plasma/UsingExtenders. First, here is how it looks in Ruby. In the metadata.desktop file you specify a service of 'Plasma/PopupApplet' instead of the usual 'Plasma/Applet' like this:


[Desktop Entry]
Name=Ruby Extender Tutorial
Comment=An example of a popup applet with extender
Type=Service
ServiceTypes=Plasma/PopupApplet
X-KDE-PluginInfo-Author=The Plasma Team
X-KDE-PluginInfo-Email=plasma-devel@kde.org
X-KDE-PluginInfo-Name=ruby-extender-tutorial
X-KDE-PluginInfo-Version=pre0.1
X-KDE-PluginInfo-Website=http://plasma.kde.org/
X-KDE-PluginInfo-Category=
X-KDE-PluginInfo-Depends=
X-KDE-PluginInfo-License=GPL
X-KDE-PluginInfo-EnabledByDefault=true
X-Plasma-API=ruby-script
X-Plasma-MainScript=code/extender_tutorial.rb

And here is the code:


require 'plasma_applet'

module RubyExtenderTutorial
  class ExtenderTutorial < PlasmaScripting::PopupApplet
    slots 'sourceAdded(QString)'
    
    def initialize(parent, args = nil)
      super

      # We want to collapse into an icon when put into a panel.
      # If you don't call this function, you can display another 
      # widget, or draw something yourself.      
      setPopupIcon("extendertutorial")
    end

    def init
      # Calling extender() instantiates an extender for you if you
      # haven't already done so. Never instantiate an extender 
      # before init() since Extender needs access to applet.config()
      # to work.
 
      # The message to be shown when there are no ExtenderItems in
      # this extender.
      extender.emptyExtenderMessage = KDE::i18n("no running jobs...")
 
      # Notify ourself whenever a new job is created.
      connect(dataEngine("kuiserver"),  SIGNAL('sourceAdded(QString)'),
              self, SLOT('sourceAdded(QString)'))    
    end 

    def initExtenderItem(item)
      # Create a Meter widget and wrap it in the ExtenderItem
      meter = Plasma::Meter.new(item) do |m|
        m.meterType = Plasma::Meter::BarMeterHorizontal
        m.svg = "widgets/bar_meter_horizontal"
        m.maximum = 100
        m.value = 0
  
        m.minimumSize = Qt::SizeF.new(250, 45)
        m.preferredSize = Qt::SizeF(250, 45)
      end

      # often, you'll want to connect dataengines or set properties
      # depending on information contained in item.config().
      # In this situation that won't be necessary though.    
      item.widget = meter
  
      # Job names are not unique across plasma restarts (kuiserver
      # engine just starts with Job1 again), so avoid problems and
      # just don't give reinstantiated items a name.
      item.name = ""
  
      # Show a close button.
      item.showCloseButton
    end

    def sourceAdded(source)
      # Add a new ExtenderItem
      item = Plasma::ExtenderItem.new(extender)
      initExtenderItem(item)
  
      # We give this item a name, which we don't use in this
      # example, but allows us to look up extenderItems by calling
      # extenderItem(name). That function is useful to avoid 
      # duplicating detached ExtenderItems between session, because 
      # you can check if a certain item already exists.
      item.name = source
  
      # And we give this item a title. Titles, along with icons and
      # names are persistent between sessions.
      item.title = source
 
      # Connect a dataengine. If this applet would display data where 
      # datasources would have unique names, even between sessions, 
      # you should do this in initExtenderItem, so that after a plasma 
      # restart, datasources would still get connected to the 
      # appropriate sources. Kuiserver jobs are not persistent however, 
      # so we connect them here.
      dataEngine("kuiserver").connectSource(source, item.widget, 200)
  
      # Show the popup for 5 seconds if in panel, so the user notices
      # that there's a new job running.
      showPopup(5000)
    end
  end
end

It's obviously pretty similar to the original C++ version, and so there isn't a lot to say. Here is the same example in C# for comparison. The metadata.desktop file again has a ServiceType=Plasma/PopupApplet line:


[Desktop Entry]
Name=Extender Tutorial
Comment=An example of a Plasma PopupApplet Extender
Type=Service
ServiceTypes=Plasma/PopupApplet

X-KDE-PluginInfo-Author=Richard Dale
X-KDE-PluginInfo-Email=panel-devel@kde.org
X-KDE-PluginInfo-Name=csharp-extender-tutorial
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
X-KDE-PluginInfo-EnabledByDefault=true
X-Plasma-API=mono-script

And the code with comments removed, as they are the same as for Ruby:


namespace Tutorials {
    using Qyoto;
    using Kimono;
    using Plasma;

    public class ExtenderTutorial : PlasmaScripting.PopupApplet {
        public ExtenderTutorial(AppletScript parent) : base(parent) {
            SetPopupIcon("extendertutorial");
        }

        public override void Init() {
            Extender().EmptyExtenderMessage = KDE.I18n("no running jobs...");
            Connect(DataEngine("kuiserver"), 
                    SIGNAL("sourceAdded(const QString&)"),
                    this, SLOT("SourceAdded(const QString&)"));
        }
 
        public override void InitExtenderItem(Plasma.ExtenderItem item) {
            Plasma.Meter meter = new Plasma.Meter(item) {
                meterType = Plasma.Meter.MeterType.BarMeterHorizontal,
                Svg = "widgets/bar_meter_horizontal",
                Maximum = 100,
                Value = 0
            };
        
            meter.SetMinimumSize(new QSizeF(250, 45));
            meter.SetPreferredSize(new QSizeF(250, 45));

            item.Widget = meter;
            item.Name = "";
            item.ShowCloseButton();
        }
 
        [Q_SLOT()]
        public void SourceAdded(string source) {
            Plasma.ExtenderItem item = new Plasma.ExtenderItem(Extender());
            InitExtenderItem(item);        
            item.Name = source;
            item.Title = source;
        
            DataEngine("kuiserver").ConnectSource(source, (QObject) item.Widget, 200);
            ShowPopup(5000);
        }
    }
}

For C# you can either just put the source code in the plasmoid, and have it compiled when the Applet is started (just put '// language:csharp' as the first line of code), or you can define a traditional CMakeLists.txt file to compile it in advance, and package the compiled code in the plasmoid:


project(cs-extender-tutorial)
include(CSharpMacros)

set(SRC_EXTENDER_TUTORIAL extender_tutorial.cs)

set(CS_FLAGS -warn:0 "-r:${LIBRARY_OUTPUT_PATH}/qt-dotnet.dll,
        ${LIBRARY_OUTPUT_PATH}/kde-dotnet.dll,
        ${LIBRARY_OUTPUT_PATH}/plasma-dll.dll")
add_cs_library(csharp-extender-tutorial "${SRC_EXTENDER_TUTORIAL}" ALL)

add_dependencies(csharp-extender-tutorial plasma-dll)

file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/csharp-extender-tutorial/contents/code)
install(FILES ${LIBRARY_OUTPUT_PATH}/csharp-extender-tutorial.dll 
        DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/csharp-extender-tutorial/contents/code 
        RENAME main)

install(FILES metadata.desktop 
        DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/csharp-extender-tutorial)

Both Ruby and C# have nice ways of initializing the newly created Plasma::Meter instance. With QtRuby you can pass a block to a constructor with an optional argument meaning 'the newly created instance' like this:


      meter = Plasma::Meter.new(item) do |m|
        m.meterType = Plasma::Meter::BarMeterHorizontal
        m.svg = "widgets/bar_meter_horizontal"
        m.maximum = 100
        m.value = 0
        m.minimumSize = Qt::SizeF.new(250, 45)
        m.preferredSize = Qt::SizeF(250, 45)
      end

I think the combination of the nice short name 'm', and being able to use foo=() methods instance of setFoo() style method calls makes for very readable code. In C# 3.0 there is a new syntax called 'Object initializers' which allows you to do something similar:


            Plasma.Meter meter = new Plasma.Meter(item) {
                meterType = Plasma.Meter.MeterType.BarMeterHorizontal,
                Svg = "widgets/bar_meter_horizontal",
                Maximum = 100,
                Value = 0
            };

You can pass a list of properties being initialized to the constructor. Although it looks like a block or a lamda it doesn't actually have its own context though, and is just a simple list of assignments.

The only slightly unfortunate thing about the extender tutorial examples is that if you run them in the plasmoidviewer, none of them seem to actually do anything! I thought at first that it was a bug in Ruby and C#, but the C++ one is exactly the same. So I'm not sure what the best way to test popup applets is at the moment.