Guide to porting Sugar Activities to GTK 3.
GTK is a library for creating graphical user interfaces. GTK is written in C. GTK for Python is a language binding.
GTK 2 is the previous major version of GTK. GTK 2 for Python is a static binding, and is called PyGTK. GTK 2 is soon to be obsolete and unavailable.
GTK 3 is the current major version of GTK. It breaks both API and ABI compared with GTK 2. GTK 3 for Python is a GObject Introspection binding, using PyGObject.
Sugar Toolkit provides services and a set of GTK widgets to build activities and other Sugar components on Linux based computers using Python.
- Sugar Toolkit for GTK 2, module name
sugar
, uses PyGTK, - Sugar Toolkit for GTK 3, module name
sugar3
, uses PyGObject, - Sugar Toolkit for GTK 3 Documentation
New Sugar activities are written in either;
- JavaScript using the Sugar Web tools, or
- Python using GTK 3 and Sugar Toolkit for GTK 3,
Old Sugar activities were written in Python using GTK 2 and Sugar Toolkit for GTK 2.
These old activities are to be ported to GTK 3. This guide explains how.
- application development in Python,
- application development in GTK 2 and GTK 3, using the event loop model,
- Sugar activity development,
- use of PyGObject API libraries.
General information for all GTK applications;
- PyGObject - Porting from Static Bindings part of the PyGObject documentation, focusing on Python,
- PyGObject - Introspection Porting on the GNOME Wiki, focusing on Python,
- Migrating from GTK 2 to GTK 3 part of the GTK documentation, focusing on the underlying C library and object classes, but is relevant to Python porting because the same classes are used.
-
Set up development environment for Sugar on GTK 3; such as Ubuntu 18.04,
-
Set up test environments capable of both GTK 2 and GTK 3 at the same time; such as Ubuntu 16.04,
-
Quiesce the activity source by making sure the activity works properly before porting, fixing any bugs, closing any solved issues, merging any pull requests or branches and releasing the last GTK 2 version; see the activity maintainer checklist.
-
Port to Sugar Toolkit for GTK 3 (see below),
-
Port to GTK 3, using the PyGObject script pygi-convert.sh to convert automatically much as it can.
-
Port to any other libraries, such as Sugargame, Cairo, Pango, GConf to Gio.Settings, GStreamer 0.10 to GStreamer 1.0,
-
Test and iterate until original functionality is reached.
Follow the Code Guidelines during all porting.
Write any comments in the code, by adding # README:, # TODO: and # FIXME: explaining what are the problems that you are having with that chunk of code. Put a link if it's necessary.
- The namespace is changed from
sugar
tosugar3
, which reflects that GTK 3 is the underlying technology, use a script to automate the rename of the importssugar
tosugar3
, sugar-convert.sh, - The keep button has been removed
- The old-style toolbar has been removed
set_toolbar_box
is used instead ofset_toolbox
- Remove import of deprecated ActivityToolbox (see hello-world)
- Support for
service_name
andclass
tags in activity.info has been removed. Usebundle_id
instead ofservice_name
andexec
instead ofclass
(see in Record) sugar3.activity.Activity
does not have the window attribute. Use the.get_window()
method instead.
To start, change the importing instruction for GTK from
import gtk
to
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
Note that require_version
needs to called only the first time when GTK
is being imported.
Similar imports that may be used are:
from gi.repository import Gdk, Pango, GObject
Then you have to change each call that involves GTK, for example creating a button will look now like this:
button = Gtk.Button()
A simple hello world program in GTK 3 looks like this:
from gi.repository import Gtk
def _destroy_cb(widget, data=None):
Gtk.main_quit()
w = Gtk.Window()
w.connect("destroy", _destroy_cb)
label = Gtk.Label('Hello World!')
w.add(label)
w.show_all()
Gtk.main()
The changes that were needed to port the hello-world activity can be seen in this commit.
Several common problems arise during a port.
A Sugar Activity has a toolbar. These are the relevant modules required to create a simple toolbar containing the activity button and the stop button.
from sugar3.activity import activity
from sugar3.graphics.toolbarbox import ToolbarBox
from sugar3.activity.widgets import ActivityToolbarButton
from sugar3.activity.widgets import StopButton
from sugar3.graphics.toolbarbox import ToolbarButton
from sugar3.graphics import style
Since the ActivityToolbar() module has been deprecated, the toolbar can
now be called using ToolbarBox()
Then, from the ToolbarBox(), include the ActivityButton and StopButton. In order for the StopButton to be align to the right as per Sugar activity interface, a separator has to be included as well.
toolbar_box = ToolbarBox()
activity_button = ActivityToolbarButton(self)
toolbar_box.toolbar.insert(activity_button, 0)
activity_button.show()
separator = Gtk.SeparatorToolItem()
separator.props.draw = False
separator.set_expand(True)
toolbar_box.toolbar.insert(separator, -1)
separator.show()
stop_button = StopButton(self)
toolbar_box.toolbar.insert(stop_button, -1)
stop_button.show()
self.set_toolbar_box(toolbar_box)
toolbar_box.show()
If you are having trouble finding how a particular GTK class/method/constant has been named in PyGI, run pygi-enumerate.py and grep the output. (this app lists all identified methods and constants). Usage example:
$ python pygi-enumerate.py | grep get_selection
Gtk.AccelLabel.get_selection_bounds() (instance method)
Gtk.Editable.get_selection_bounds() (instance method)
Gtk.Entry.get_selection_bounds() (instance method)
Gtk.IconView.get_selection_mode() (instance method)
Gtk.Label.get_selection_bounds() (instance method)
Gtk.SelectionData.get_selection() (instance method)
Gtk.SpinButton.get_selection_bounds() (instance method)
Gtk.TextBuffer.get_selection_bound() (instance method)
Gtk.TextBuffer.get_selection_bounds() (instance method)
Gtk.TreeView.get_selection() (instance method)
With PyGI it is possible to use Python-like constructors, or “new” functions e.g. the following are (usually) equivalent:
label = Gtk.Button()
label = Gtk.Button.new()
However, the first form is preferred: it is more Python-like.
Internally, the difference is that Gtk.Label.new()
translates to a call
to gtk_label_new()
, whereas Gtk.Label()
will
directly construct an instance of GtkLabel at the GObject level.
If the constructor takes parameters, they must be named. The parameters correspond to GObject properties in the API documentation which are usually marked as “Construct”. For example, the following code will not work:
expander = Gtk.Expander("my expander")
The (confusing) error is:
TypeError: GObject.__init__() takes exactly 0 arguments (1 given)
The solution is to go to the GtkExpander API documentation and find the appropriate property that we wish to set. In this case it is label (which is a Construct property, further increasing our confidence of success), so the code should be:
expander = Gtk.Expander(label="my expander")
Combining the two points above, if you wish to call a construct-like
function such as gtk_button_new_with_label(), you do have the option
of calling Gtk.Button.new_with_label(), however if we check the
GtkButton
properties
we see one called "label" which is equivalent. Therefore
gtk_button_new_with_label("foo")
should be called as:
button = Gtk.Button(label="foo")
GtkHBox and GtkVBox, commonly used containers in GTK 2 code, have
pack_start
and pack_end
methods. These take 4 parameters:
- widget: The widget to pack into the container
- expand: Whether the child should receive extra space when the container grows (default True)
- fill: True if space given to child by the expand option is actually allocated to child, rather than just padding it. This parameter has no effect if expand is set to False. A child is always allocated the full height of a gtk.HBox and the full width of a gtk.VBox. This option affects the other dimension. (default True)
- padding: extra space in pixels to put between child and its neighbor (default 0)
In PyGTK, the expand, fill and padding parameters were optional: if unspecified, the default values above were used. In PyGI, these parameters are not optional: all 4 must be specified. Hence the rules for adding in the extra parameters are:
- If
expand
was not set, use value True - If
fill
was not set, use value True. (however, if expand is False, this parameter gets ignored so False is an equally acceptable option when expand=False) - If
padding
was not set, use value 0.
These parameters can be specified either as positional arguments or as named keyword arguments, however all 4 must always be specified. Some developers prefer keyword arguments, arguing that the following:
box.pack_start(widget, expand=True, fill=False, padding=4)
is much more readable than:
box.pack_start(widget, True, False, 4)
If you are using pack_start
with the default values (expand=True,
fill=True and padding=0), you can avoid using pack_start
(and the
parameter pain that it brings with it) by just using .add for some added
cleanliness, e.g.
box.pack_start(widget, True, True, 0)
can be replaced with:
box.add(widget)
In GTK 3, GtkVBox
and GtkHBox
have been deprecated, which means they might be removed later. The replacement is to use GtkBox
directly, and you may wish to
make this change now. e.g.:
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=True, spacing=8)
However, it must be noted that if GtkBox
is used directly (instead of
using GtkHBox/GtkVBox), the default value of expand
is now
False
. The implications of this are:
- You need to check your .add() calls, as previously they would behave
as
pack_start
with expand=True, but now they will behave as expand=False (you need to change them to use pack_start with expand=True to retain the old behaviour) - Every single
pack_start
call that hasexpand=False
andpadding=0
(and any value of fill) can be converted to.add()
for cleanliness
In PyGTK, the gtk.Alignment
constructor takes four optional parameters:
- xalign: the fraction of horizontal free space to the left of the child widget. Ranges from 0.0 to 1.0. Default 0.0.
- yalign: the fraction of vertical free space above the child widget. Ranges from 0.0 to 1.0. Default 0.0.
- xscale: the fraction of horizontal free space that the child widget absorbs, from 0.0 to 1.0. Default 0.0.
- yscale: the fraction of vertical free space that the child widget absorbs, from 0.0 to 1.0. Default 0.0
In PyGI/GTK3, these parameters are still optional, however, the default values have changed. They are now:
- xalign: default 0.5
- yalign: default 0.5
- xscale: default 1
- yscale: default 1
Additionally, PyGTK accepted these construction parameters as positional arguments. As explained above, they must now be converted to keyword arguments.
The Gtk.Menu.popup function now works slightly differently. The user supplied positioning function now takes different parameters. These are menu, x, y, push_in and user_data.
Previously, gdk was an attribute of the GTK module, which means that it can be called through GTK. For example, if we want to use color_parse():
gtk.gdk.color_parse(color)
However, what we have to do now is:
from gi.repository import Gdk
Then we can modify the code to the following:
Gdk.color_parse(color)
Following the release of GTK 3, we should not be importing pango like this:
import pango
In fact, we can now import pango as an attribute within the GTK 3 library:
from gi.repository import Pango as pango
Any use of GConf should be ported to Gio.Settings.
self.allocation
property is no longer available. self.get_allocation()
should be used instead.
So to get the allocation size:
self.allocation.width
self.allocation.height
should be replaced by:
self.get_allocated_width()
self.get_allocated_height()
Most of the constants have slightly different formats, e.g.,
gtk.STATE_NORMAL
becameGtk.StateFlags.NORMAL
gtk.RESPONSE_ACCEPT
becameGtk.ResponseType.ACCEPT
gtk.JUSTIFY_CENTER
becameGtk.Justification.CENTER
gtk.RELIEF_NONE
becameGtk.ReliefStyle.NONE
The pixbuf libraries are in their own repository
from gi.repository import GdkPixbuf
GdkPixbuf.Pixbuf.new_from_file()
Two things to note:
- You need to specify a clipboard using get()
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
- You need to pass a length to set_text()
clipboard.set_text(string, len(string))
See python-gtk-3-tutorial/clipboard for more details.
Slightly different syntax:
self.drag_dest_set(Gtk.DestDefaults.ALL, [],
Gdk.DragAction.COPY)
self.drag_dest_set_target_list(Gtk.TargetList.new([]))
self.drag_dest_add_text_targets()
self.connect('drag_data_received', self._drag_data_received)
and:
data.get_text()
or:
data.get_image()
See python-gtk-3-tutorial/drag_and_drop for more details.
GTK 3 does not support GTK Drawable objects, so the first step is to get your activity running under Cairo.
import cairo
# From activity.Activity, you inherit a canvas.
# Create a Cairo context from the window.
cairo_context = self.canvas.get_window().cairo_create()
# Create an XLib surface to be used for drawing
xlib_surface = surface.create_similar(cairo.CONTENT_COLOR,
Gdk.Screen.width(),
Gdk.Screen.height())
# Although Gdk.Screen.width() and Gdk.Screen.height() have been
# deprecated from version 3.22 they can still be used.
#
# You'll need a Cairo context from which you'll build a GTK Cairo context
cairo_context = cairo.Context(xlib_surface)
# Use this context as you would a Drawable, substituting Cairo commands
# for gtk commands, e.g.,
# draw_line changes to line_to
cairo_context.move_to(0, 0)
cairo_context.line_to(100, 100)
# Cairo uses floats from 0 to 1 for RGB values
cairo_context.set_source_rgb(r, g, b)
cairo_context.rectangle(x, y, w, h)
cairo_context.fill()
# To invalidate a region to force a refresh, use:
self.canvas.queue_draw_area(x, y, w, h)
# Handle the expose event
# "expose-event" signal became "draw" signal for Gtk Widget
# And it takes a cairo context instead of an expose event
def draw(self, widget, cr):
x, y = cr.get_current_point()
width, height = widget.get_allocated_width(), widget.get_allocated_height()
cr.rectangle(x, y, width, height)
cr.clip()
cr.set_source_surface(xlib_surface)
cr.paint()
Pango is a bit different when used with Cairo:
import pango, pangocairo
# Again, from the xlib_surface...
cairo_context = cairo.Context(xlib_surface)
# Create a PangoCairo context
cairo_context = pangocairo.CairoContext(cairo_context)
# The pango layout is created from the Cairo context
pango_layout = cairo_context.create_layout()
# You still use pango to set up font descriptions.
fd = pango.FontDescription('Sans')
fd.set_size(12 * pango.SCALE)
# Tell your pango layout about your font description
pango_layout.set_font_description(fd)
# Write text to your pango layout
pango_layout.set_text('Hello world', -1)
# Position it within the Cairo context
cairo_context.save()
cairo_context.translate(x, y)
cairo_context.rotate(pi / 3) # You can rotate text and images in Cairo
cairo_context.set_source_rgb(1, 0, 0)
# Finally, draw the text
cairo_context.update_layout(pango_layout)
cairo_context.show_layout(pango_layout)
cairo_context.restore()
To draw a bitmap...
# Again, from the xlib_surface...
cairo_context = cairo.Context(xlib_surface)
Gdk.cairo_set_source_pixbuf(cairo_context, pixbuf, x, y)
cairo_context.rectangle(x, y, w, h)
cairo_context.fill()
To read a pixel from the xlib surface...
# create a new 1x1 cairo surface
cairo_surface = cairo.ImageSurface(cairo.FORMAT_RGB24, 1, 1);
cairo_context = cairo.Context(cairo_surface)
# translate xlib_surface so that target pixel is at 0, 0
cairo_context.set_source_surface(xlib_surface, -x, -y)
cairo_context.rectangle(0,0,1,1)
cairo_context.set_operator(cairo.OPERATOR_SOURCE)
cairo_context.fill()
cairo_surface.flush() # ensure all writing is done
# Read the pixel
return (ord(pixels[2]), ord(pixels[1]), ord(pixels[0]), 0)
The Cairo/Pango interaction is a little different:
from gi.repository import Pango, PangoCairo
cairo_context = ...
pango_layout = PangoCairo.create_layout(cairo_context)
fd = Pango.FontDescription('Sans')
fd.set_size(12 * Pango.SCALE)
pango_layout.set_font_description(fd)
pango_layout.set_text('Hello World', -1)
cairo_context.set_source_rgb(1, 0, 0)
PangoCairo.update_layout(cairo_context, pango_layout)
PangoCairo.show_layout(cairo_context, pango_layout)
The get_extents()
method if different in PangoCairo. It calculates an
extent as a Rectangle, but doesn't return anything. There is a method,
get_logical_extents()
that returns a Rectangle. Alas, it is not
necessarily available after v1.16. Note that Rectangle is not a list but
a class with methods for get_x()
, get_y()
, get_width()
, and
get_height()
, so you cannot iter over it.
(For more details, see http://developer.gnome.org/pangomm/2.28/annotated.html)
You need to replace your pixmaps with Cairo in GTK 3.
win = gtk.gdk.get_default_root_window()
gc = gtk.gdk.GC(win)
pix = gtk.gdk.pixbuf_new_from_file("filename.png")
map = gtk.gdk.Pixmap(win, pix.get_width(), pix.get_height())
map.draw_rectangle(gc, True, 0, 0, pix.get_width(), pix.get_height())
map.draw_pixbuf(gc, pix, 0, 0, 0, 0, pix.get_width(), pix.get_height(), gtk.gdk.RGB_DITHER_NONE)
Becomes;
pix = GdkPixbuf.Pixbuf.new_from_file("filename.png")
imagesurface = cairo.ImageSurface(
cairo.Format.ARGB32,
pix.get_width(),
pix.get_height())
context = cairo.Context(imagesurface)
context.rectangle(0, 0, pix.get_width(), pix.get_height())
Gdk.cairo_set_source_pixbuf(pix)
context.fill()
To make a screenshot of the window:
width, height = window.get_width(), window.get_height()
thumb_surface = Gdk.Window.create_similar_surface(window,
cairo.CONTENT_COLOR,
width, height)
thumb_width, thumb_height = style.zoom(100), style.zoom(80)
cairo_context = cairo.Context(thumb_surface)
thumb_scale_w = thumb_width * 1.0 / width
thumb_scale_h = thumb_height * 1.0 / height
cairo_context.scale(thumb_scale_w, thumb_scale_h)
Gdk.cairo_set_source_window(cairo_context, window, 0, 0)
cairo_context.paint()
thumb_surface.write_to_png(png_path_or_filelike_object)
Some necessary changes include:
Using
get_property('window').get_xid()
Instead of
window.xid
Using
set_double_buffered(False)
set_app_paintable(True)
Instead of
unset_flags(gtk.DOUBLE_BUFFERED)
set_flags(gtk.APP_PAINTABLE)
Use an editor with remote file access to a virtual machine, such as emacs with tramp.
Start Terminal inside Sugar and then start Screen. Change to the activity source directory. Use an SSH client to reach into the Terminal shell to run sugar-activity
.
Temporarily add code to detect when your editor rewrites files. For example in activity.Activity.__init__
;
# testing restarter
ct = os.stat('file.py').st_ctime
def restarter():
if os.stat('file.py').st_ctime != ct:
self.close()
return False
return True
GObject.timeout_add(233, restarter)
epdb
library is useful to inspect the code while the Activity is running.
sudo yum install python-epdb
You can put trace point in the code to stop and make tests by doing this:
import epdb;epdb.set_trace()
Finally I run Get Books Activity from the Terminal Activity to be able to write some code on a shell. This is the command that I use:
sugar-launch org.laptop.sugar.GetBooksActivity
See also: Development Team/Debugging
multitail is really helpful for developing Sugar Activities. It can be used to read the latest log that an Activity wrote and see how it's growing.
For example, if we run an Activity three times it will create 3 different .log files behind ~/.sugar/default/logs directory. With multitail we will be seeing the most recent version of the activity log.
Install multitail using:
sudo yum install multitail
Show the proper log files
cd ~/.sugar/default/logs
multitail -iw "*<Activity Name>*" 1 -m 0
-iw is to inform to multitail about the input files and check for them every 1 second -m is to let multitail know about the buffersize (0 is infinite)
pygobject is what we use to make GTK 3 activities. So, it's really useful to take a look at the code examples that are there. Even more, you can run some demo application that show how to use something specific about the library.
- Clone the code:
git clone git://git.gnome.org/pygobject
- Run an example
cd pygobject cd demos/gtk-demo/demos python pixbuf.py
- Grep the code to search for something useful
cd pygobject git grep GdkPixbuf
Not sure how this command works, but it can give us an interesting information. If you run this command and plug an USB drive you will see useful information
dbus-monitor --system
We are migrating towards Python 3. Python 3 does not support GTK 2. Hence, once the activity is ported to GTK 3, please consider porting the activity from Python 2 to Python 3.
Ref: Guide to port activities to Python 3
Once an activity is ported, a new release can be made. The major version should be greater than the existing one.
Please follow this guide for releasing a new version
These are the changes noted by developers while porting activities
-
Gtk.Widget.hide_all()
does not exist anymore. We should use just.hide
Ref -
If the code creates some own object, and it defines some properties, you should use
__gproperties__
dictionary. Ref -
Gtk.ListStore
doesn't have the method.reorder
. There is a ticket reported upstream about this. -
I replaced the use of
dbus
by Gio to monitor (dis)connection of pen drives -
Migrate custom signals: If you have defined custom gtk objects with custom signal you need to update them to the new way You should replace this:
from gobject import signal_new, TYPE_INT, TYPE_STRING, TYPE_BOOLEAN, TYPE_PYOBJECT, TYPE_NONESIGNAL_RUN_LAST, signal_new('extlistview-modified', gtk.TreeView, SIGNAL_RUN_LAST, TYPE_NONE, ())
by adding the signal definition inside the object that you are creating using the
__gsignals__
dictionary like this (in this case Gtk.TreeView is the class that our object inherits):from gi.repository import GObject class ExtListView(Gtk.TreeView): __gsignals__ = { 'extlistview-modified': (GObject.SignalFlags.RUN_LAST, None, ()), }
The last argument of the signal definition are the argument types that the callback will receive.
-
Change the mouse cursor
-
Example use case: When the activity is working and we want to show a work in progress cursor.
-
Replace this:
self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
with:
from gi.repository import Gdk self.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.WATCH))
-
- Python GTK 3 Tutorial: http://python-gtk-3-tutorial.readthedocs.org
- PyGTK or GTK 2: http://www.pygtk.org/docs/pygtk/
- Sugar Toolkit GTK 3 Documentation: https://developer.sugarlabs.org/sugar3/
- GTK 3 Reference Manual http://developer.gnome.org/gtk3/stable/
- OLPC Documentation: http://wiki.laptop.org/go/Activities/PortingToGtk3
- Pango documentation: http://developer.gnome.org/pangomm
- GStreamer-1.0 documentation: http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gstreamer/html/index.html
- GStreamer-1.0 porting hints: https://wiki.ubuntu.com/Novacut/GStreamer1.0