Difference between revisions of "Features/GTK3/Porting"

From Sugar Labs
< Features‎ | GTK3
Jump to navigation Jump to search
Line 175: Line 175:
 
GTK-3 does not support gtk Drawable objects, so the first step is to get your activity running under Cairo.
 
GTK-3 does not support gtk Drawable objects, so the first step is to get your activity running under Cairo.
  
 +
<pre>
 
  # From activity.Activity, you inherit a canvas. Get its window.
 
  # From activity.Activity, you inherit a canvas. Get its window.
 
  win = self.canvas.get_window()
 
  win = self.canvas.get_window()
Line 199: Line 200:
 
  cr.fill()
 
  cr.fill()
  
  # To invalidate a region to forse a refresh, use:
+
  # To invalidate a region to force a refresh, use:
 
  self.canvas.queue_draw_area(x, y, w, h)
 
  self.canvas.queue_draw_area(x, y, w, h)
 +
</pre>
 +
 +
Pango is a bit different when used with Cairo:
 +
 +
<pre>
 +
# Again, from the xlib_surface...
 +
cr = cairo.Context(xlib_surface)
 +
 +
# Create a PangoCairo context
 +
cr = pangocairo.CairoContext(cr)
 +
 +
# The pango layout is created from the Cairo context
 +
pl = cr.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
 +
pl.set_font_description(fd)
  
   
+
  # Write text to your pango layout
 +
pl.set_text('Hello world')
 +
 
 +
# Position it within the Cairo context
 +
cr.save()
 +
cr.translate(x, y)
 +
cr.rotate(pi / 3)  # You can rotate text and images in Cairo
 +
cr.set_source_rgb(1, 0, 0)
 +
 
 +
# Finally, draw the text
 +
cr.update_layout(pl)
 +
cr.show_layout(pl)
 +
cr.restore()
 +
</pre>
 +
 
 +
To draw a bitmap...
 +
 
 +
<pre>
 +
# Again, from the xlib_surface...
 +
cr = cairo.Context(xlib_surface)
 +
 
 +
# Create a gtk context
 +
cr = gtk.gdk.CairoContext(cr)
 +
cr.set_source_pixbuf(pixbuf, x, y)
 +
cr.rectangle(x, y, w, h)
 +
cr.fill()
 +
</pre>
 +
 
 +
To read a pixel...
 +
 
 +
<pre>
 +
# Map the xlib surface onto a pixmap
 +
pixmap = gtk.gdk.Pixmap(None, w, h, 24)
 +
cr = pixmap.cairo_create()
 +
cr.set_source_surface(xlib_surface, 0, 0)
 +
cr.paint()
 +
 
 +
# Read the pixel
 +
pixel = pixmap.get_image(x, y, 1, 1).get_pixel(0, 0)
 +
return(int((pixel & 0xFF0000) >> 16),  # red
 +
        int((pixel & 0x00FF00) >> 8),  # green
 +
        int((pixel & 0x0000FF) >> 0), 0)  # blue
 +
</pre>
  
:* Example: abacus-cairo
+
===Going from Cairo in GTK-2 to Cairo in GTK-3===
:* Example: abacus-gtk3
 
* The conversion script (here) leaves a few things to be done by hand:
 
:* Notes from conversion from abacus-cairo to abacus-gtk3
 
  
* Notes from the TurtleArt conversion
+
Not much changes, but...
* Removing Hippo
 
  
 
===Taking a screenshot and making a thumbnail===
 
===Taking a screenshot and making a thumbnail===

Revision as of 13:33, 11 November 2011

Porting an exisiting activity to GTK3

In this guide we will handle the porting of the an existing activity from using GTK2 to use GTK3. Furthermore we will show you what changes you need to do to make usage of the new sugar toolkit that uses now GTK3 as well. In this guide we will use the hello-world activity as a simple example.

Preparation

Before you start porting your activity you are encouraged to branch off a stable branch. This will allow you to keep on doing stable releases on the stable branch and new releases on the master branch.

The latest release was version 3. We highly recommend that you use the 'sugar-0.94' as the stable branch name because this will keep the repositories consistent and eases the development work. In git you can create a branch like this:

git branch sugar-0.94

This has created a local branch. You can show the result by running 'git branch', you should see the following:

[erikos@T61 helloworld]$ git branch
* master
  sugar-0.94

The 'sugar-0.94' branch is only locally available so far which can be seen by running 'git branch -r' which shows the remote branches:

[erikos@T61 helloworld]$ git branch -r
  origin/HEAD -> origin/master
  origin/master
  origin/sucrose-0.84

The only branch available besides the master branch is the 'sucrose-0.84' branch. Let's push now our new branch to the remote repository to make it available for others:

git push origin sugar-0.94

The branch is now listed as a remote branch. You can verify as well on your gitorious page.

[erikos@T61 helloworld]$ git branch -r
  origin/HEAD -> origin/master
  origin/master
  origin/sucrose-0.84
  origin/sugar-0.94

You can switch now between those branches using 'git checkout <branch>'. And you can use 'git branch' to see which branch you are on (the one with the * before is the branch you are currently on).

git checkout sugar-0.94
git checkout master

Cleanup, adopt to API changes in the sugar-toolkit

This should be done only on the master branch! In the new sugar-toolkit we have removed old API, you should adjust your activity accordingly:

  • the keep button has been removed completely
  • the old-style toolbar has been removed

Port the activity from GTK-2 to GTK-3

The first thing that changes is the importing instruction for GTK,

import gtk

has to be replaced by

from gi.repository import Gtk

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 does look 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()

For porting your activity you do have to change your calls for accesing widgets and services in the new GTK3 sugar-toolkit as well. The new namespace is called sugar3, trying to reflect that GTK3 is the underlying technology. For example the import of the base activity class has to be changed from

from sugar.activity import activity

to

from sugar3.activity import activity

The changes that were needed to port the hello-world activity can be seen in this commit.

Ok, let's do these changes now for your activity. Make sure you are in your master branch using the 'git branch' command (the master branch should have a '*' before it). Make your changes, commit them ('git commit -a') and push them to the remote repository ('git push origin master').

Tools

There are tools to help you do the porting. There is a script in the pygobject repository for porting called pygi-convert.sh, more info about the script can be found in the PyGObject Introspection Porting guide.

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).


Constructor considerations

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() (the preferred form) 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")

HBox, VBox, pack_start and pack_end

GtkHBox and GtkVBox, commonly used containers in GTK2 code, have pack_start and pack_end methods. These take 4 parameters:

  1. The widget to pack into the container
  2. expand: Whether the child should receive extra space when the container grows (default True)
  3. 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)
  4. 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:

  1. If expand was not set, use value True
  2. 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)
  3. 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)

However, these functions are called extremely often; any mildly seasoned GTK developer will have memorized the order and meaning of the parameters. Some developers therefore prefer to avoid the extra work of dropping in hundreds of keyword arguments throughout the code and just use the positional ones. This is really up to you.

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)

This is as far as you need to go for now. However, in GTK3, GtkVBox and GtkHBox have been deprecated, which means they might be removed in GTK4. 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)

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:

  1. 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)
  2. Every single pack_start call that has expand=False and padding=0 (and any value of fill) can be converted to .add() for cleanliness

GtkAlignment considerations

In PyGTK, the gtk.Alignment constructor takes four optional parameters:

  1. xalign: the fraction of horizontal free space to the left of the child widget. Ranges from 0.0 to 1.0. Default value 0.0.
  2. yalign: the fraction of vertical free space above the child widget. Ranges from 0.0 to 1.0. Default value 0.0.
  3. xscale: the fraction of horizontal free space that the child widget absorbs, from 0.0 to 1.0. Default value 0.0.
  4. yscale: the fraction of vertical free space that the child widget absorbs, from 0.0 to 1.0. Default value 0.0

In PyGI/GTK3, these parameters are still optional when used in the Gtk.Alignment constructor (as keyword arguments, as explained above). However, the default values have changed. They are now:

  1. xalign: default value 0.5
  2. yalign: default value 0.5
  3. xscale: default value 1
  4. yscale: default value 1

If your code was relying on the default value of 0 for any of these parameters in PyGTK, you will now need to explicitly specify that in your constructor. Similarly, if you were previously using construction parameters to select the now-default values, those parameters can be dropped.

Additionally, PyGTK accepted these construction parameters as positional arguments. As explained above, they must now be converted to keyword arguments.

Make a release

Versioning

If you do new releases the versioning of the GTK2 and GTK3 release should be different. For GTK2 releases you should use dotted versions for new development releases major versions. Let's have a look at hello-world as an example. The latest release of hello-world was version 3. Bug fix releases should be named 3.1 then 3.2 and so on. The new releases for the new development branch should be starting with a major number, in this case 4.

Tips to Activity Developers

Going from Drawable to Cairo

GTK-3 does not support gtk Drawable objects, so the first step is to get your activity running under Cairo.

 # From activity.Activity, you inherit a canvas. Get its window.
 win = self.canvas.get_window()

 # Create a Cairo context from the window.
 cr = win.cairo_create()

 # Create a Cairo surface from the context
 surface = cr.get_target()

 # Create an XLib surface to be used for drawing
 xlib_surface = surface.create_similar(cairo.CONTENT_COLOR,
                                       gtk.gdk.screen_width(),
                                       gtk.gdk.screen_height())

 # You'll need a Cairo context from which you'll build a GTK Cairo context
 cr = cairo.Context(xlib_surface)
 cr = gtk.gdk.CairoContext(cr)

 # Use this context as you would a Drawable, substituting Cairo commands for gtk commands, e.g.,
 cr.move_to(0, 0)
 cr.set_source_rgb(r, g, b)  # Cairo uses floats from 0 to 1 for RGB values
 cr.rectangle(x, y, w, h)
 cr.fill()

 # To invalidate a region to force a refresh, use:
 self.canvas.queue_draw_area(x, y, w, h)

Pango is a bit different when used with Cairo:

 # Again, from the xlib_surface...
 cr = cairo.Context(xlib_surface)

 # Create a PangoCairo context
 cr = pangocairo.CairoContext(cr)

 # The pango layout is created from the Cairo context
 pl = cr.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
 pl.set_font_description(fd)

 # Write text to your pango layout
 pl.set_text('Hello world')

 # Position it within the Cairo context
 cr.save()
 cr.translate(x, y)
 cr.rotate(pi / 3)  # You can rotate text and images in Cairo
 cr.set_source_rgb(1, 0, 0)

 # Finally, draw the text
 cr.update_layout(pl)
 cr.show_layout(pl)
 cr.restore()

To draw a bitmap...

 # Again, from the xlib_surface...
 cr = cairo.Context(xlib_surface)

 # Create a gtk context
 cr = gtk.gdk.CairoContext(cr)
 cr.set_source_pixbuf(pixbuf, x, y)
 cr.rectangle(x, y, w, h)
 cr.fill()

To read a pixel...

 # Map the xlib surface onto a pixmap
 pixmap = gtk.gdk.Pixmap(None, w, h, 24)
 cr = pixmap.cairo_create()
 cr.set_source_surface(xlib_surface, 0, 0)
 cr.paint()

 # Read the pixel
 pixel = pixmap.get_image(x, y, 1, 1).get_pixel(0, 0)
 return(int((pixel & 0xFF0000) >> 16),  # red
        int((pixel & 0x00FF00) >> 8),  # green
        int((pixel & 0x0000FF) >> 0), 0)  # blue

Going from Cairo in GTK-2 to Cairo in GTK-3

Not much changes, but...

Taking a screenshot and making a thumbnail

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)