APR
5
2008

Ruby Clock Plasma Applet

We can't have too many plasma clocks in KDE4, and I'm pleased to say that the Ruby analog clock is now working pretty well. I've been using it to time brewing a pot of tea this morning, and there is certainly a more delicate taste to Earl Grey timed with a Ruby clock as opposed the the slightly coarser and more acidic flavour that using a C++ based clock applet as a timer, can give to your cuppa.

The clock code is in playground/base/plasma/applets/ruby-clock, and I'll explain how it all fits together. It has a Qt designer .ui file, that is identical to the one used by the C++ analog clock, for configuration - by the way, I've recently updated the rbuic4 .ui compiler so that the code is now fully up to date, and in line with the Qt 4.4's uic code. You just need to add these lines to the applet's CMakeLists.txt file to make the rbuic4 tool generate the Ruby code from the .ui file:


SET(UI_CLOCKCONFIG ${CMAKE_CURRENT_SOURCE_DIR}/clockConfig.ui)
FIND_PROGRAM(RBUIC4 rbuic4 PATHS ${BIN_INSTALL_DIR})

ADD_CUSTOM_COMMAND(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/clockConfig.rb COMMAND ${RBUIC4} ${UI_CLOCKCONFIG} -o ${CMAKE_CURRENT_BINARY_DIR}/clockConfig.rb COMMENT "Generating clockConfig.rb")

install(FILES ${CMAKE_CURRENT_BINARY_DIR}/clockConfig.rb DESTINATION ${DATA_INSTALL_DIR}/plasma-ruby-clock)

The .desktop file is very similar to a C++ applet one apart from these two lines:


X-KDE-Library=krubypluginfactory
X-KDE-PluginKeyword=plasma-ruby-clock/clock.rb

All Ruby KDE plugins are started with the same executable 'krubypluginfactory.so', and it is given the name of the particular Ruby code to invoke via the X-KDE-PluginKeyword entry, in this case 'plasma-ruby-clock/clock.rb' installed under /share/apps. Here is the constructor method to give an idea of how the code compares with the C++ version:


require 'plasma_applet'
require 'clockConfig.rb'

class Clock < Plasma::Applet

slots 'dataUpdated(QString,Plasma::DataEngine::Data)',
:showConfigurationInterface,
:configAccepted

def initialize(parent, args)
super

setHasConfigurationInterface(true)
setContentSize(125, 125)
setRemainSquare(true)

@theme = Plasma::Svg.new("widgets/clock", self)
@theme.contentType = Plasma::Svg::SingleImage
@theme.resize(size())

@timezone = ""
@showTimeString = false
@showSecondHand = false
@ui = Ui_ClockConfig.new
@lastTimeSeen = Qt::Time.new
end

Compared with the same code in C++:


public slots:
void dataUpdated(const QString &name, const Plasma::DataEngine::Data &data);
void showConfigurationInterface();

protected slots:
void configAccepted();
void moveSecondHand();
...

Clock::Clock(QObject *parent, const QVariantList &args)
: Plasma::Containment(parent, args),
m_showTimeString(false),
m_showSecondHand(false),
m_dialog(0),
m_secondHandUpdateTimer(0)
{
setHasConfigurationInterface(true);
setContentSize(125, 125);
setRemainSquare(true);

m_theme = new Plasma::Svg("widgets/clock", this);
m_theme->setContentType(Plasma::Svg::SingleImage);
m_theme->resize(size());
}

One issue I had problems with is that Ruby doesn't have implicit constructors, and in several places it meant that something like a Qt::Rect needed to be converted to a Qt::RectF in Ruby before it could be passed to a method called. Whereas in C++ a QRect will be automatically converted to a QRectF. For instance:


# Ruby painting
@theme.paint(p, Qt::RectF.new(rect), "ClockFace")

// C++ painting
m_theme->paint(p, rect, "ClockFace");

Another problem was that when Ruby threw an exception it took down Plasma, which was unfortunate, and I needed to put rb_protect() calls around virtual method callbacks and slot invocations to stop that happening:


static VALUE funcall2_protect_id = Qnil;
static int funcall2_protect_argc = 0;
static VALUE * funcall2_protect_args = 0;

static VALUE
funcall2_protect(VALUE obj)
{
return rb_funcall2(obj, funcall2_protect_id, funcall2_protect_argc, funcall2_protect_args);
}
...
funcall2_protect_id = _slotname;
funcall2_protect_argc = _items - 1;
funcall2_protect_args = _sp;
int state = 0;

VALUE result = rb_protect(funcall2_protect, _obj, &state);
if (state != 0) {
rb_backtrace();
}

So now when an exception is thrown, it is caught and a backtrace displayed with rb_backtrace(), and then the applet just carries on. There are still some occasional unexplained exceptions that are thrown, which might be a problem with the Ruby runtime not being thread safe, but they no longer kill Plasma and it isn't such a great problem anymore. Sebastian Sauer suggested setting a Ruby global variable called 'rb_thread_critical' to solve this, but I haven't found it makes any difference.

I changed kdebindings so that the plasma smoke library and plasma ruby extension are built by default, and it would be nice if other people can try it out. I'm particularly interested in knowing how to use QtWebKit to write applets, and I think that would be a neat combination with Ruby.