NOV
28
2008

Writing Plasma Data Engines in C# and Ruby

I feel a bit stuck in a time warp, having already written blogs with much the same title and subject as this one, back in April. The difference is that it is now possible to use the Plasma Script Engine api and associated packaging mechanism, as opposed to the earlier bindings, which were based on the C++ plugin api. Of course, being able to write engines in C# as well as Ruby is something new.

And don't forget that for Python fans, Simon Edwards has implemented similarly comprehensive bindings very close to the C++ api, that you can use to write both applets and data engines with KDE 4.2.

There is very little change needed in the code compared with the earlier Ruby bindings. You now need to call the main class 'Main' and it needs to be a subclass of PlasmaScripting::DataEngine instead of Plasma::DataEngine.

Here is what the Ruby code for the time engine now looks like:

require 'plasma_applet'

module RubyTime

class Main < PlasmaScripting::DataEngine

  def initialize(parent, args = nil)
    super(parent)
  end

  def init
    setMinimumPollingInterval(333)

    # To have translated timezone names
    # (effectively a noop if the catalog is already present).
    KDE::Global.locale.insertCatalog("timezones4")
    dbus = Qt::DBusConnection.sessionBus
    dbus.connect("", "", "org.kde.KTimeZoned", 
                 "configChanged", this, SLOT(:updateAllSources))
  end

  def sources
    timezones = KDE::SystemTimeZones.zones.keys
    timezones << "Local"
    return timezones
  end

  def sourceRequestEvent(name)
    return updateSourceEvent(name)
  end

  def updateSourceEvent(tz)
    # puts "TimeEngine#updateTime"
    localName = I18N_NOOP("Local")
    if tz == localName
        setData(localName, I18N_NOOP("Time"), Qt::Time.currentTime)
        setData(localName, I18N_NOOP("Date"), Qt::Date.currentDate)
        # this is relatively cheap - KSTZ.local is cached
        timezone = KDE::SystemTimeZones.local.name
    else
        newTz = KDE::SystemTimeZones.zone(tz)
        unless newTz.valid?
            return false
        end

        dt = KDE::DateTime.currentDateTime(KDE::DateTime::Spec.new(newTz))
        setData(tz, I18N_NOOP("Time"), dt.time)
        setData(tz, I18N_NOOP("Date"), dt.date)
        timezone = tz
    end

    trTimezone = i18n(timezone)
    setData(tz, I18N_NOOP("Timezone"), trTimezone)
    tzParts = trTimezone.split("/")

    setData(tz, I18N_NOOP("Timezone Continent"), tzParts[0])
    setData(tz, I18N_NOOP("Timezone City"), tzParts[1])

    return true
  end
end

end

You need to put the code into a standard plasmoid directory structure like this:

time
  metadata.desktop
  contents
    code
      main.rb

To install a data engine you use the plasmapkg tool from the command line like this:

# Initial installation:
$ plasmapkg --install time --type dataengine

# To reinstall:
$ plasmapkg --upgrade time --type dataengine

The Ruby desktop file for the time engine looks like this:

[Desktop Entry]
Name=Date and Time
Comment=Time data for Plasmoids

Type=Service
ServiceTypes=Plasma/DataEngine
X-Plasma-API=ruby-script

X-KDE-PluginInfo-Author=Richard Dale
X-KDE-PluginInfo-Email=richard.j.dale@gmail.com
X-KDE-PluginInfo-Name=ruby-time
X-KDE-PluginInfo-Version=1.0
X-KDE-PluginInfo-Website=http://plasma.kde.org/
X-KDE-PluginInfo-Category=Date and Time
X-KDE-PluginInfo-Depends=
X-KDE-PluginInfo-License=GPL
X-KDE-PluginInfo-EnabledByDefault=true

The 'X-KDE-PluginInfo-Name=ruby-time' line is used to name where the engine gets installed, and it is also the name you use to invoke it.

Here is the C# version of the same engine for comparison:

public class TimeEngine : PlasmaScripting.DataEngine, IDisposable {
    private static string localName = "Local";

    public TimeEngine(DataEngineScript parent) : base(parent) {
        SetMinimumPollingInterval(333);

        // To have translated timezone names
        // (effectively a noop if the catalog is already present).
        KGlobal.Locale().InsertCatalog("timezones4");
    }

    public override void Init() {
        base.Init();
        QDBusConnection dbus = QDBusConnection.SessionBus();
        dbus.Connect("", "", "org.kde.KTimeZoned", 
                     "configChanged", this, SLOT("UpdateAllSources()"));
    }

    public override List Sources() {
        List timezones = new List(KSystemTimeZones.Zones().Keys);
        timezones.Add("Local");
        return timezones;
    }

    public override bool SourceRequestEvent(string name) {
        return UpdateSourceEvent(name);
    }

    public override bool UpdateSourceEvent(string tz) {
        string timezone;
        if (tz == localName) {
            SetData(localName, "Time", QTime.CurrentTime());
            SetData(localName, "Date", QDate.CurrentDate());
            // this is relatively cheap - KSTZ::local() is cached
            timezone = KSystemTimeZones.Local().Name();
        } else {
            KTimeZone newTz = KSystemTimeZones.Zone(tz);
            if (!newTz.IsValid()) {
                return false;
            }

            KDateTime dt = KDateTime.CurrentDateTime(new KDateTime.Spec(newTz));
            SetData(tz, "Time", dt.Time());
            SetData(tz, "Date", dt.Date());
            timezone = tz;
        }

        string trTimezone = KDE.I18n(timezone);
        SetData(tz, "Timezone", trTimezone);
        string[] tzParts = trTimezone.Split(new char[] { '/' });

        SetData(tz, "Timezone Continent", tzParts[0]);
        SetData(tz, "Timezone City", tzParts[1]);

        return true;
    }
}

And the metadate.desktop file for C# looks like this:

[Desktop Entry]
Name=Date and Time
Comment=Time data for Plasmoids
Type=Service
ServiceTypes=Plasma/DataEngine
X-Plasma-API=mono-script

X-KDE-PluginInfo-Author=Richard Dale
X-KDE-PluginInfo-Email=richard.j.dale@gmail.com
X-KDE-PluginInfo-Name=csharp-time
X-KDE-PluginInfo-Version=1.0
X-KDE-PluginInfo-Website=http://plasma.kde.org/
X-KDE-PluginInfo-Category=Date and Time
X-KDE-PluginInfo-Depends=
X-KDE-PluginInfo-License=GPL
X-KDE-PluginInfo-EnabledByDefault=true

Whereas you can just edit the Ruby code when it is in the standard plasmoid directory structure, for C# it is a bit more tricky and you need to create a cmake file. Arno Rehn has done an ingenious CMakeFile.txt that will allow you to work on the source files in your $src directory, and then compile them into the plasmoid directory structure in the $build directory that can be directly installed by 'plasmapkg':

project(cs-time-engine)
include(CSharpMacros)

set(SRC_TIMEENGINE timeengine.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(time-engine "${SRC_TIMEENGINE}" ALL)

add_dependencies(time-engine plasma)

file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/csharp-time/contents/code)
install( FILES ${LIBRARY_OUTPUT_PATH}/time-engine.dll 
         DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/csharp-time/contents/code 
         RENAME main )
install(FILES metadata.desktop DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/csharp-time)

Hopefully, by the time KDE 4.2 is released we get can this kind of info copied over to the Tech Base Wiki, and also add tutorials and other examples to try and get the non-C++ Plasmoid community boot strapped.- if anyone wants to help out on that it would be great. I'm really looking forward to seeing what sort of scripting Plasmoids people will come up with.

Comments

Hello,
I am trying to make your example works on my PC running linux Fedora 11 (rawhide) and KDE 4.1.85 proposed with the rawhide.
Question: after running plasmapkg --install time -- type dataengine i can effectively find the files copied to my $HOME/.kde/share directories but the widget is not proposed in my KDE "add widgets" list. What is the way to make this happen?
Problem: when running plasmaengineexplorer i can retrieve my widget but when selecting it i got the following messages:
plasmaengineexplorer
RubyAppletScript::DataEngine#init mainScript: /root/.kde/share/apps/plasma/dataengines/ruby-time/contents/code/main.rb
RubyAppletScript::DataEngine#init instantiating: RubyTime::Main
/root/.kde/share/apps/plasma/dataengines/ruby-time/contents/code/main.rb:19:in `method_missing': undefined local variable or method `this' for # (NameError)
/root/.kde/share/apps/plasma/dataengines/ruby-time/contents/code/main.rb:19:in `init'
/usr/share/kde4/apps/plasma_scriptengine_ruby/data_engine.rb:128:in `init'

Thank you very much if you could have a quick look at this.


By bege at Thu, 01/08/2009 - 09:07