Writing Your Own GIO Jobs
After having moved into a new apartment I'm now back at working on my thesis about Thunar. There are a few things which are solved very differently in GIO than in ThunarVFS. One of them is the way jobs are handled. A job basically is a task which may take a while to run and thus is executed in a separate thread (so it doesn't block the GUI).
ThunarVFS has a framework called ThunarVfsJob
. It lets you create different kinds of jobs e.g. for changing file permissions recursively or for computing the total number of files and the total size of a directory. These jobs report back to the GUI thread using signals such as "new-files" (when new files are added and need to picked up by the GUI) or "progress".
GIO has something similar ... but it's not so obvious how it works. When I tried to figure out how to migrate ThunarVfsJob
to GIO I thought: hey, GIO must have something like this already! It contains several job-like functions such as g_file_copy_async()
after all.
So here's what I found out after spending some time on reading gfile.c
and glocalfile.c
: there is a job framework in GIO ... but it's hidden behind easy-to-use asynchronous functions. It's based on GCancellable
, GAsyncResult
and a bunch of callback types. It uses GIOScheduler
internally to glue everything together to something that is actually pretty convenient (but still kinda tricky).
So, what do you need in orderto write your own jobs in the GIO style?
First of all, you need an example task. I picked counting files and computing the total size of a directory to understand how it works. What we want is an asynchronous function which does exactly that and uses a callback to report the progress back to the GUI thread ... just like g_file_copy_async()
does.
First of all, you define the callback type and two functions for starting the job (sync and async version):
The Public API
typedef void (*GFileCountProgressCallback) (goffset current_num_files, goffset current_num_bytes, gpointer user_data); static gboolean g_file_deep_count (GFile *file, GCancellable *cancellable, GFileCountProgressCallback progress_callback, gpointer progress_callback_data, GError **error); static void g_file_deep_count_async (GFile *file, int io_priority, GCancellable *cancellable, GFileCountProgressCallback progress_callback, gpointer progress_callback_data, GAsyncReadyCallback callback, gpointer callback_data);
The Implementation
All the function g_file_deep_count_async()
will do is to create a GSimpleAsyncResult
, put
the callback information into it and then tell the GIOScheduler
to run the job. Here's how
that looks like:
static void g_file_deep_count_async (GFile *file, int io_priority, GCancellable *cancellable, GFileCountProgressCallback progress_callback, gpointer progress_callback_data, GAsyncReadyCallback callback, gpointer callback_data) { GSimpleAsyncResult *result; DeepCountAsyncData *data; g_return_if_fail (G_IS_FILE (file)); data = g_new0 (DeepCountAsyncData, 1); data->file = g_object_ref (file); data->progress_cb = progress_callback; data->progress_cb_data = progress_callback_data; result = g_simple_async_result_new (G_OBJECT (file), callback, callback_data, g_file_deep_count_async);
g_simple_async_result_set_op_res_gpointer (result, data, (GDestroyNotify) deep_count_async_data_free); g_io_scheduler_push_job (deep_count_async_thread, result, g_object_unref, io_priority, cancellable); }
DeepCountAsyncData
is a simple struct
which needs no further explanation, I think.
First data
with callback and user data information is added to the GSimpleAsyncResult
and then the job is added to the GIOScheduler
. As you can see, there is another function
involved: deep_count_async_thread
. This is the function which runs in a separate thread
and does most of the work (well, not quite ... but almost). Here's how it looks like:
static gboolean deep_count_async_thread (GIOSchedulerJob *job, GCancellable *cancellable, gpointer user_data) { GSimpleAsyncResult *res; DeepCountAsyncData *data; gboolean result; GError *error = NULL; res = user_data; data = g_simple_async_result_get_op_res_gpointer (res); data->job = job; result = g_file_deep_count (data->file, cancellable, data->progress_cb != NULL ? deep_count_async_progress_callback : NULL, data, &error); if (data->progress_cb != NULL) g_io_scheduler_job_send_to_mainloop (job, (GSourceFunc) gtk_false, NULL, NULL); if (!result && error != NULL) { g_simple_async_result_set_from_error (res, error); g_error_free (error); } g_simple_async_result_complete_in_idle (res); return FALSE; }
As you can see it runs the synchronous function g_file_deep_count()
and makes sure the progress
callback is called at least once. It does one more thing though: it defines it's own progress
callback: deep_count_async_progress_callback
. This is required for the real progress
callback to be called inside the GUI thread. This is the code for the internal callback:
static gboolean deep_count_async_progress_in_main (gpointer user_data) { ProgressData *progress = user_data; DeepCountAsyncData *data = progress->data; data->progress_cb (progress->current_num_files, progress->current_num_bytes, data->progress_cb_data); return FALSE; } static void deep_count_async_progress_callback (goffset current_num_files, goffset current_num_bytes, gpointer user_data) { DeepCountAsyncData *data = user_data; ProgressData *progress; progress = g_new (ProgressData, 1); progress->data = data; progress->current_num_files = current_num_files; progress->current_num_bytes = current_num_bytes; g_io_scheduler_job_send_to_mainloop_async (data->job, deep_count_async_progress_in_main, progress, g_free); }
deep_count_async_progress_callback()
is called from within the job thread. It then tells
the scheduler to call deep_count_async_progress_in_main
from the GUI thread. And finally
deep_count_async_progress_in_main
calls the real progress callback e.g. to update
the GUI.
Now you still haven't seen any code related to counting files and computing the total file size of a directory ... let's get to that now. Here's the synchronous deep count function which is called from within the job thread:
static gboolean g_file_deep_count (GFile *file, GCancellable *cancellable, GFileCountProgressCallback progress_callback, gpointer progress_callback_data, GError **error) { ProgressData data = { .data = NULL, .current_num_files = 0, .current_num_bytes = 0, }; g_return_val_if_fail (G_IS_FILE (file), FALSE); if (g_cancellable_set_error_if_cancelled (cancellable, error)) return FALSE; return g_file_real_deep_count (file, cancellable, progress_callback, progress_callback_data, &data, error); }
Damn ... it still doesn't do any real work! Ok, but this time there's no big rat-tail
of nested function calls anymore, I promise. There's just one function left:
g_file_real_deep_count()
.
Before we can call it, however, g_file_deep_count()
has to initialize the progress data.
After that we can call g_file_real_deep_count()
recursively and do something useful.
Here we go:
static gboolean g_file_real_deep_count (GFile *file, GCancellable *cancellable, GFileCountProgressCallback progress_callback, gpointer progress_callback_data, ProgressData *progress_data, GError **error) { GFileEnumerator *enumerator; GFileInfo *info; GFileInfo *child_info; GFile *child; gboolean success = TRUE; g_return_val_if_fail (G_IS_FILE (file), FALSE);
if (g_cancellable_set_error_if_cancelled (cancellable, error)) return FALSE; info = g_file_query_info (file, "standard::*", G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, cancellable, error); if (g_cancellable_is_cancelled (cancellable)) return FALSE; if (info == NULL) return FALSE; progress_data->current_num_files += 1; progress_data->current_num_bytes += g_file_info_get_size (info); if (progress_callback != NULL) { /* Here we call the internal callback */ progress_callback (progress_data->current_num_files, progress_data->current_num_bytes, progress_callback_data); } if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) { enumerator = g_file_enumerate_children (file, "standard::*", G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, cancellable, error); if (!g_cancellable_is_cancelled (cancellable)) { if (enumerator != NULL) { while (!g_cancellable_is_cancelled (cancellable) && success) { child_info = g_file_enumerator_next_file (enumerator, cancellable, error); if (g_cancellable_is_cancelled (cancellable)) break; if (child_info == NULL) { if (*error != NULL) success = FALSE; break; } child = g_file_resolve_relative_path (file, g_file_info_get_name (child_info)); success = success && g_file_real_deep_count (child, cancellable, progress_callback, progress_callback_data, progress_data, error); g_object_unref (child); g_object_unref (child_info); } g_object_unref (enumerator); } } } g_object_unref (info); return !g_cancellable_is_cancelled (cancellable) && success; }
And that's it. We can now compute the number of files and the total size of a directory
recursively using a GCancellable
and one or two callbacks. All of this is done using
threads, so you don't have to worry about blocking your GUI main loop.
If you want to see this in action, visit the job framework page in my thesis wiki and download deepcount.c and the Makefile.
The prettiest of the *buntu’s is coming
Ubuntu’s brownish/orangy theme has been the target of much criticism. What is often forgotten is that one of its derivatives has been looking very good for quite a while now: Xubuntu. A complete visual overhaul for the upcoming version 9.04 is all set to continue this trend.
Pasi Lallinaho, picking up the baton from Jozsef Mak, leverages Xfce’s extensive theming capabilities to provide a coherent look and feel with wonderful new themes.
Update: This article previously contained a paragraph about a new GTK theme, but unfortunately, that didn’t make it in in time, meaning the visual overhaul is now “just” a new wallpaper, login window theme and icon theme, so scrap the part about extensive theming capabilities
The theme for the login screen pictures a misty forest set next to a lake, in Xubuntu’s characteristic colour scheme.
Then there is the gorgeous new wallpaper, bearing a watermark of the Xubuntu logo on a canvas of the same defining shades of blue.
To complement it all, the GNOME-Colors icon theme is included. GNOME-Colors is a very complete theme, including a wide range of good-looking icons that very well match the rest of Xubuntu’s artwork.
Of course, a release this pretty deserves some buzz – if not for the great looks, then at least for the host of new features, such as the new version of Xfce – and you can help! If you own a website or blog, you can include a countdown banner with the following HTML code:
<a href="http://xubuntu.org/" title="Learn more about Xubuntu..."><img src="http://xubuntu.org/xubuntu-static/jaunty-countdown/" alt="Countdown to Xubuntu (xubuntu.org) 9.04, by Pasi Lallinaho"></a>
Alternatively, if you are a member of a forum or other online community, you can include it in your signature, forum posts or other places you’d like to promote Xubuntu, with the following BBCode:
[url=http://xubuntu.org/][img]http://xubuntu.org/xubuntu-static/jaunty-countdown/[/img][/url]
Of course, the countdown banner, too, was created by Pasi in the style of the new Xubuntu desktop.
![](http://stats.wordpress.com/b.gif?host=xubuntublog.wordpress.com&blog=806196&post=117&subd=xubuntublog&ref=&feed=1)
defender graphs
This blog post is the funniest thing I read today (scroll down for the screenshot).
How about some Xfce download statistics in that style? ;-)
What’s up in the Goodies ?
There have been a bunch of new releases in the Goodies during the last few days.
Midori 1.5.0
This new release mainly brings changes under the hood which aim at easing the future development of the application and its stability. A lot of old code which was used to provide backward compatibility for the old Webkit releases was removed, as well as the internal source viewer: Midori now uses your default text editor.
There are also some visible changes: it is now possible to download files directly, without using an external application and a new extension called 'Colorful Tabs' makes your browser shiny!
Xfce Power Manager 0.6.5 and 0.8.0 alpha
Two releases during the last few days: a stable release and an alpha release for the future 0.8.0.
The 0.6.5 version provides the standard Freedesktop.org inhibit interface which allows you to prevent power managers to suspend, shutdown, etc your computer under certain conditions.
The 0.8.0 version contains far more changes. The user interface was simplified and the inhibit and power management standard interfaces have also been added. Xfce Power Manager is now able to detect if your session is idle and to reduce the brightness in that case, it can also detect if you are watching a movie and then disables screen suspending or brightness reduction. Two new panel plugins are available: one to set the brightness of the screen and the other one to manage the inhibit stuff. Your suspend, sleep and brightness keys should also be detected correctly.
The developer of this applications would appreciate if people could test this alpha release and report any bug on the Xfce Bugzilla.
Xfmpc 0.1.0
Xfmpc now has an additional developer who will speed the development up. This release features a preferences dialog for the MPD server and the appearance of the player, and improved play list with new context menus to show information about the songs, to add and remove songs, etc. A search entry was also added to the song database browser to find songs easily.
Xfce Clipman Plugin 0.9.1
This is a minor bug fix release which fixes some little issues. The next release (1.0) should be far more exciting with the actions support: the plugin will be able to execute some actions when it detects a given pattern in your clipboard.
Translating Xfce
Having Xfce translated into as many languages as possible and maintaining translations of high quality is essential. People will not use a desktop environment that they do not understand! The Xfce i18n Project is in charge of this task and always needs new contributors. You do not need any particular skill, except understanding English, writing your own language correctly and motivation.
Let's see how you can contribute to this project.
First step: contact any existing translators for your language
Reference: the Language Maintainers page on the Xfce i18n Wiki.
Their might already be contributors to the translation of Xfce into your language listed on this page. If this is the case, you should get in touch with them and ask them how you can help. If the page is not up to date, you should warn us or update it yourself.
Let's see how to translate an application or to update a translation now.
Second step: download the translation files
The applications are translated using pot files, with a 'po' extension: those files basically contain each English string and its translation. There is also a template for creating a pot file for a new translation: it is the file which has a 'pot' extension. Every time a developper changes a string in the application, he will update the pot files and the translation will have to be updated accordingly.
Those files are located on the Xfce SVN to ease cooperation and distribution. We will use a script to ease the downloading of those files, this script will also allow you to update your pot files easily. This script requires Subversion to work.
Download the script and move it to a folder where you want to store the pot files. Then run the following command in this directory to make the script executable:
chmod +x xdt-i18n
Then, use this command to get the pot files for the core applications for the first time:
xdt-i18n init xfce/trunk
And for the goodies:
xdt-i18n init goodies/trunk
You should always make sure that you are using the latest pot files when starting to translate a project. This command allows you to update the pot files quickly:
xdt-i18n update
Third step: start translating
The script above downloaded the latest translation files and put them in separate folder. Go to the folder of the application you want to translate or or of the application whose translation you want to improve. The translation files will be available in a po folder. If there is a file for your language, you will be able to start translating directly. If not you will have to create a file for your language by copying the pot file and renaming it to your_locale_identifier.po. To find the locale identifier, you can use this page.
To translate, open the po file using your favorite text editor. You will see a main header composed of likes similar to this:
"Project-Id-Version: Thunar\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2009-01-11 00:20+0100\n" "PO-Revision-Date: 2009-02-11 13:20+0100\n" "Last-Translator: Mike Massonnet <mmassonnet@gmail.com>\n" "Language-Team: French <xfce-i18n@xfce.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
Update the PO-Revision-Date field with the current date and time and the Last-Translator field with your name and your email so that the next translator can contact you if he has some questions. After that you can start the real translation job.
Every sentence already translated is displayed in the po file as the following one:
msgid "Invalid file name" msgstr "Nom de fichier non valide"
Untranslated strings are displayed as this one:
msgid "Only local files may be renamed" msgstr ""
Some strings may also be marked as fuzzy, which means that the original sentence has changed a bit and that the translation is not totally wrong but still needs to be checked and/or fixed. Make sure to remove the '#,fuzzy' after fixing the string.
#, fuzzy msgid "Screenshot.png" msgstr " Screenshot"
msgid allows you to identify the original string and msgstr to identify the translated strings. Modifying the msgid strings will have no effect, if you see a typo in one of them, file a bug. To translate a string or update a translation, just modify the text between the "" in the msgstr field.
Some strings may contain slashes or weird characters such as "%s" or "%i". You should always make sure that those characters are also present in the translated string in the same order, or the application will be broken in your language. To check if your translation is valid, the xdt-i18n script offers a check option which will check all po files for your language:
xdt-i18n check your_language_code.po
If it displays any warning, you must fix it before sharing your translation.
You can also use this script to see the stats for a given language, that is to say the number of translated messages, the number of fuzzy and untranslated ones:
xdt-i18n stat your_language_code.po
You can also use Poedit to modify the po files. It updates the po headers automatically, sorts the strings by category (translated, fuzzy and untranslated) and provides a friendly interface.
Fourth step: get your translation in
Once you are done with your translations, go to the root of your xfce/trunk folder or goodies/trunk folder and use the following command to create a patch named my_language.diff containing your translations:
svn diff > my_language.diff
If there is a maintainer for your language, you should send him this file asking him politely to review it and get it in. If not, you should send it to the Xfce i18n mailing list where someone will review it and get it in.
Fourth step: this tutorial sucks and you need more information
The Xfce i18n Wiki contains a lot of information but some of it may be outdated. If you have any questions, please ask them on the Xfce i18n mailing list.
I hope that I was clear enough and that we will be flooded with new or improved translations! We need you!
libxfce4menu to be renamed to gdesktopmenu
Now that all Xfce dependencies have been removed from libxfce4menu, I'm planning to rename it to something more generic. Travis Watkins from Alacarte has expressed interest in helping with writing the XfceMenuEditor
(err GDesktopMenuEditor
) part. He is also working on Vala bindings for the library.
With a more complete API and polished code perhaps we can even move gdesktopmenu over to freedesktop.org ... who knows.
Xfce4 Screenshooter news
During the last few days, I found some time to improve Xfce4 Screenshooter and implement some new features.
Interface
The interface has been slightly improved, it should be more user friendly and take less space. I also added an option allowing you to chose whether the mouse pointer should be displayed on the screenshot.
Improved 'Region' mode
When selecting a region to be screenshooted using the mouse, you can now use the Escape key to cancel the operation.
Saving screenshots to remote locations
It is now possible to save screenshots to remote locations such as FTP, computers accessible via SSH, a Samba shared folder... The window below is displayed to show the progress of the upload and to cancel it.
This is totally transparent for the user. Remote locations connected with Gigolo or gvfs-connect are automatically available in the save dialog, as any other local folder.
I still have to update the documentation for those new features and to improve various other points, a new release might be out in May.
Thesis: Migrating Thunar to GIO
I've semi-officially (whatever that means) started working on my thesis on migrating Thunar from ThunarVFS to GIO. I'll work on it in public. That means research material, testing results, status reports and of course the code will be publically available in a read-only wiki and our Subversion repository in a special branch respectively.
I'm always open to suggestions and opinions. Just drop me a mail if you have something to say.
If everything goes as planned (err, did I really sayplanned?) GIO will land in Thunar 1.2 which is supposed to be released along with Xfce 4.8. There's no warranty for this though.
Clipboard Managers Done Right
I just had a look at the screenshots of Parcellite, a GTK+ clipboard manager, mainly because I didn't know what it is.
And just like with Getting Things Done tools before, I immediately had a few ideas how clipboard managers like this could be improved.
If you add a file or a URI to the clipboard, what do you expect to see in a visualization of the clipboard? Do you expect to see the path or filename or do you expect to see an icon and the filename together with a button to open or launch that file? Maybe even a possiblity to drag the item into a file manager or some other program? It's very easy to do things like that with GIO or even ThunarVFS.
What if you copy an email address into the clipboard? Do you expect only to see the email address or would you rather like to see the email address with one or two buttons (add to addressbook, send email)?
What are your experiences with clipboard tools? Are you using them at all? If not, why? What features would you like to see in a clipboard manager to make it attractive to use?
Git Weirdness, Part 2
Ok, now this is just ridiculous:
[brian@machine1 airconfig $] pwd /home/brian/src/airconfig [brian@machine1 airconfig $] git branch -a advanced-ip-settings * master nm-frontend notification-rework reconnect origin/master origin/pre-hal [brian@machine1 airconfig $] cd .. && mkdir t && cd t [brian@machine1 t $] git clone ../airconfig Initialized empty Git repository in /home/brian/src/t/airconfig/.git/ [brian@machine1 t $] cd airconfig [brian@machine1 airconfig $] git branch -a * master origin/HEAD origin/advanced-ip-settings origin/master origin/nm-frontend origin/notification-rework origin/reconnect
Ok, that makes sense! Now:
[brian@machine1 airconfig $] cd .. [brian@machine1 t $] rm -rf airconfig [brian@machine1 t $] git clone file:///home/brian/src/airconfig Initialized empty Git repository in /home/brian/src/t/airconfig/.git/ remote: Counting objects: 1272, done. remote: Compressing objects: 100% (486/486), done. remote: Total 1272 (delta 778), reused 1270 (delta 776) Receiving objects: 100% (1272/1272), 360.40 KiB, done. Resolving deltas: 100% (778/778), done. [brian@machine1 t $] cd airconfig [brian@machine1 airconfig $] git branch -a * master origin/HEAD origin/advanced-ip-settings origin/master origin/pre-hal
What. The. Fuck.
Yes, the git-clone man page tells me that, when using a local pathname (and not using a file: URI), it assumes the other repo is local and uses hardlinks between the repos. But hey, if I specify –no-hardlinks when cloning, I still get all branches if I use the “../airconfig” method.
And if I go to machine2 and do “git clone machine1:src/airconfig” I get the same broken result with the file:// method.
Git, I think you rock. But why does your user interface suck so much? And why do you just appear to be broken right now? I can’t seem to find anything else in the man page to help here. (I’m using git 1.6.1.3 if that matters.)