[gnome-clocks/wip/vala] Vala rewrite (in progress)



commit bb7c52fa225d82f38e9d9a13e7c3593eb68a6193
Author: Paolo Borelli <pborelli gnome org>
Date:   Sat Feb 2 14:46:00 2013 +0100

    Vala rewrite (in progress)
    
    Python has served us well for prototyping the app, but we are now
    switching to Vala to overcome some problems (in particular lack of
    proper libcanberra bindings) and to take advantage of some other things
    (libgd, egglistbox, gtk resources) not readily available in python.

 .gitmodules                    |    3 +
 Makefile.am                    |   82 ++++++++--
 autogen.sh                     |   29 ++--
 configure.ac                   |   32 +++-
 data/css/gnome-clocks.css      |    9 +
 gnome-clocks                   |   33 ----
 libgd                          |    1 +
 src/alarm.ui                   |   81 ++++++++++
 src/alarm.vala                 |  335 ++++++++++++++++++++++++++++++++++++++++
 src/application.vala           |  102 ++++++++++++
 src/clock.vala                 |   31 ++++
 src/config.vapi                |    8 +
 src/gnome-clocks.gresource.xml |    9 +
 src/gnome-desktop-3.0.vapi     |   16 ++
 src/main.vala                  |   26 +++
 src/menu.ui                    |   24 +++
 src/stopwatch.ui               |  167 ++++++++++++++++++++
 src/stopwatch.vala             |  233 ++++++++++++++++++++++++++++
 src/timer.ui                   |  262 +++++++++++++++++++++++++++++++
 src/timer.vala                 |  225 +++++++++++++++++++++++++++
 src/utils.vala                 |  141 +++++++++++++++++
 src/widgets.vala               |  314 +++++++++++++++++++++++++++++++++++++
 src/window.vala                |  152 ++++++++++++++++++
 src/world.vala                 |  100 ++++++++++++
 24 files changed, 2348 insertions(+), 67 deletions(-)
---
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..bfd964e
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "libgd"]
+	path = libgd
+	url = git://git.gnome.org/libgd
diff --git a/Makefile.am b/Makefile.am
index 8f85924..5e57da3 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,6 +1,6 @@
-ACLOCAL_AMFLAGS = -I m4
+ACLOCAL_AMFLAGS = -I m4 -I libgd ${ACLOCAL_FLAGS}
 
-SUBDIRS = po
+SUBDIRS = libgd po
 
 # desktop file
 # (we can't use INTLTOOL_DESKTOP_RULE here due to lp#605826)
@@ -82,31 +82,78 @@ css_DATA = \
 	data/css/button-border-stop-active.svg \
 	data/css/button-border-stop.svg
 
-# main script
-bin_SCRIPTS = gnome-clocks
-
-# python module (nobase means dirname is preserved in site-packages
-packagesdir = $(pythondir)
-nobase_dist_packages_PYTHON = \
-	$(wildcard $(srcdir)/gnomeclocks/*.py)
+AM_CPPFLAGS = \
+	-I$(top_srcdir)/libgd \
+	-DGNOME_DESKTOP_USE_UNSTABLE_API \
+	-DGETTEXT_PACKAGE=\""$(GETTEXT_PACKAGE)"\" \
+	-DGNOMELOCALEDIR=\""$(prefix)/$(DATADIRNAME)/locale"\" \
+	-DDATADIR=\""$(datadir)"\"
+
+AM_VALAFLAGS = \
+	--vapidir=libgd \
+	--pkg gio-2.0 \
+	--pkg gtk+-3.0 \
+	--pkg libcanberra \
+	--pkg GWeather-3.0 \
+	--pkg libnotify \
+	--pkg gd-1.0
+
+bin_PROGRAMS = gnome-clocks
+
+BUILT_SOURCES = \
+	src/resources.c
+
+src/resources.c: $(top_srcdir)/src/gnome-clocks.gresource.xml $(shell $(GLIB_COMPILE_RESOURCES) --generate-dependencies --sourcedir=$(top_srcdir)/src $(top_srcdir)/src/gnome-clocks.gresource.xml)
+	$(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) --target=$@ --sourcedir=$(top_srcdir)/src --generate-source $<
+
+VALA_SOURCES = \
+	src/application.vala \
+	src/window.vala \
+	src/clock.vala \
+	src/world.vala \
+	src/alarm.vala \
+	src/stopwatch.vala \
+	src/timer.vala \
+	src/utils.vala \
+	src/widgets.vala \
+	src/main.vala
+
+gnome_clocks_SOURCES = \
+	$(BUILT_SOURCES) \
+	$(VALA_SOURCES) \
+	src/gnome-desktop-3.0.vapi \
+	src/config.vapi
+
+AM_CFLAGS = \
+	$(CLOCKS_CFLAGS) \
+	-Wall \
+	-Wno-unused-but-set-variable \
+	-Wno-unused-variable
+
+gnome_clocks_LDFLAGS = -export-dynamic -rpath $(libdir)
+gnome_clocks_LDADD = \
+	$(top_builddir)/libgd/libgd.la \
+	$(CLOCKS_LIBS) \
+	-lm
 
 EXTRA_DIST = \
 	$(icon_files) \
 	$(hcicon_files) \
 	$(images_DATA) \
 	$(css_DATA) \
-	$(bin_SCRIPTS) \
-	data/org.gnome.clocks.gschema.xml.in
+	data/org.gnome.clocks.gschema.xml.in \
+	src/gnome-clocks.gresource.xml \
+	src/menu.ui \
+	src/stopwatch.ui \
+	src/timer.ui
 
 CLEANFILES = \
-	$(applications_DATA) \
-	$(wildcard $(srcdir)/gnomeclocks/*.pyc)
+	$(applications_DATA)
 
 DISTCLEANFILES = \
-	gnomeclocks/defs.py \
 	$(gsettings_SCHEMAS)
 
-MAINTAINERCLEANFILES = 		\
+MAINTAINERCLEANFILES = \
 	ABOUT-NLS \
 	aclocal.m4 \
 	config.guess \
@@ -122,7 +169,8 @@ MAINTAINERCLEANFILES = 		\
 	po/insert-header.sin \
 	po/quot.sed \
 	po/remove-potcdate.sin \
-	py-compile \
-	$(gsettings_SCHEMAS:.xml=.valid)
+	$(gsettings_SCHEMAS:.xml=.valid) \
+	$(VALA_SOURCES:.vala=.c) \
+	*.stamp
 
 -include $(top_srcdir)/git.mk
diff --git a/autogen.sh b/autogen.sh
index a69b69b..f8c27f7 100755
--- a/autogen.sh
+++ b/autogen.sh
@@ -1,19 +1,26 @@
-#!/bin/bash
+#!/bin/sh
 # Run this to generate all the initial makefiles, etc.
 
-srcdir=`dirname $0`
-test -z "$srcdir" && srcdir=.
+test -n "$srcdir" || srcdir=`dirname "$0"`
+test -n "$srcdir" || srcdir=.
 
-PKG_NAME="gnome-clocks"
+OLDDIR=`pwd`
+cd $srcdir
 
-test -f $srcdir/configure.ac || {
-    echo "**Error**: Directory "\`$srcdir\'" does not look like the top-level $PKG_NAME directory"
+AUTORECONF=`which autoreconf`
+if test -z $AUTORECONF; then
+    echo "*** No autoreconf found, please install it ***"
     exit 1
-}
+fi
 
-which gnome-autogen.sh || {
-    echo "You need to install gnome-common from GNOME Git (or from your OS vendor's package manager)."
+INTLTOOLIZE=`which intltoolize`
+if test -z $INTLTOOLIZE; then
+    echo "*** No intltoolize found, please install the intltool package ***"
     exit 1
-}
+fi
 
-. gnome-autogen.sh "$@"
+autopoint --force || exit $?
+AUTOPOINT='intltoolize --automake --copy' autoreconf --force --install --verbose
+
+cd $OLDDIR
+test -n "$NOCONFIGURE" || "$srcdir/configure" "$@"
diff --git a/configure.ac b/configure.ac
index 03f83e5..20c6724 100644
--- a/configure.ac
+++ b/configure.ac
@@ -6,12 +6,12 @@ AC_INIT([gnome-clocks],
         [gnome-clocks])
 
 AC_CONFIG_MACRO_DIR([m4])
+AC_CONFIG_HEADERS(config.h)
+AC_CONFIG_SRCDIR(src/main.vala)
 
 AM_INIT_AUTOMAKE([1.11 tar-ustar dist-xz no-dist-gzip foreign])
 m4_ifdef([AM_SILENT_RULES],[AM_SILENT_RULES([yes])])
 
-AM_PATH_PYTHON([3.2])
-
 IT_PROG_INTLTOOL(0.40)
 AM_GNU_GETTEXT([external])
 AM_GNU_GETTEXT_VERSION([0.17])
@@ -19,18 +19,37 @@ GETTEXT_PACKAGE=AC_PACKAGE_NAME
 AC_SUBST([GETTEXT_PACKAGE])
 AC_DEFINE_UNQUOTED(GETTEXT_PACKAGE,"$GETTEXT_PACKAGE",[The name of the gettext domain])
 
+AC_PROG_CC
+AC_PROG_INSTALL
+AC_PROG_MAKE_SET
+AM_PROG_VALAC([0.17.4])
+
+AC_PATH_PROG(GLIB_COMPILE_RESOURCES, glib-compile-resources)
+
 GLIB_GSETTINGS
 
+LT_INIT([disable-static])
+
 PKG_PROG_PKG_CONFIG([0.22])
 
 PKG_CHECK_MODULES(CLOCKS, [
-    py3cairo
-    pygobject-3.0 >= 3.4.2
+    gio-2.0 >= 2.30.0
+    gtk+-3.0 >= 3.6.0
+    libcanberra >= 0.30
+    gweather-3.0 >= 3.7.4
+    gnome-desktop-3.0 >= 3.6.0
+    libnotify >= 0.7.0
+])
+
+LIBGD_INIT([
+    static
+    main-toolbar
+    vapi
 ])
 
 AC_CONFIG_FILES([
     Makefile
-    gnomeclocks/defs.py
+    libgd/Makefile
     po/Makefile.in
 ])
 
@@ -40,7 +59,8 @@ echo "
     gnome-clocks ${VERSION}
 
     prefix: ${prefix}
-    Python interpreter: ${PYTHON}
+    Vala compiler: ${VALAC}
+    C compiler: ${CC}
 
     Now type 'make' to build ${PACKAGE}
 "
diff --git a/data/css/gnome-clocks.css b/data/css/gnome-clocks.css
index 2f03ac8..3121533 100644
--- a/data/css/gnome-clocks.css
+++ b/data/css/gnome-clocks.css
@@ -98,10 +98,18 @@
     color: white;
 }
 
+.clocks-time-label {
+    font-size: 64px;
+}
+
 .clocks-button {
     font-size: 18px;
 }
 
+.clocks-spinbutton {
+    font-size: 64px;
+}
+
 .clocks-go {
     background-image: linear-gradient(to bottom,
                                       @clocks_go_color_a,
@@ -159,6 +167,7 @@
 }
 
 .view.clocks-laps {
+    font-size: larger;
     background-color: @theme_bg_color;
     color: @theme_fg_color;
 }
diff --git a/libgd b/libgd
new file mode 160000
index 0000000..41dfd2a
--- /dev/null
+++ b/libgd
@@ -0,0 +1 @@
+Subproject commit 41dfd2acbb5a84950b1956169051b9c362a50b49
diff --git a/src/alarm.ui b/src/alarm.ui
new file mode 100644
index 0000000..0722c9b
--- /dev/null
+++ b/src/alarm.ui
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.6 -->
+  <object class="GtkGrid" id="ringing_panel">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="halign">center</property>
+    <property name="valign">center</property>
+    <property name="hexpand">True</property>
+    <property name="vexpand">True</property>
+    <property name="row_spacing">48</property>
+    <property name="column_spacing">24</property>
+    <child>
+      <object class="GtkLabel" id="time_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <style>
+          <class name="clocks-time-label"/>
+        </style>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkGrid" id="button_grid">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="column_spacing">24</property>
+        <child>
+          <object class="GtkButton" id="left_button">
+            <property name="label" translatable="yes">Stop</property>
+            <property name="width_request">200</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <style>
+              <class name="clocks-button"/>
+              <class name="clocks-stop"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="right_button">
+            <property name="label" translatable="yes">Snooze</property>
+            <property name="width_request">200</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <style>
+              <class name="clocks-button"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/src/alarm.vala b/src/alarm.vala
new file mode 100644
index 0000000..d83f936
--- /dev/null
+++ b/src/alarm.vala
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2013  Paolo Borelli <pborelli gnome org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+namespace Clocks {
+namespace Alarm {
+
+private class Item : Object {
+    static const int SNOOZE_MINUTES = 9;
+    static const int RING_MINUTES = 3;
+
+    public enum State {
+        READY,
+        RINGING,
+        SNOOZING
+    }
+
+    public string name { get; set; }
+    public int hour { get; set; }
+    public int minute { get; set; }
+    public int[] days { get; set; }
+
+    public State state { get; private set; }
+
+    public string time_label {
+         get {
+            return "12:00";
+         }
+    }
+
+    public string repeat_label {
+         get {
+            return _("Every Day");
+         }
+    }
+
+    public bool active {
+        get {
+            return _active;
+        }
+
+        set {
+            if (value != _active) {
+                if (value) {
+                    reset ();
+                } else if (state == State.RINGING) {
+                    bell.stop ();
+                }
+            }
+            _active = value;
+        }
+    }
+
+    private bool _active;
+    private GLib.DateTime alarm_time;
+    private GLib.DateTime snooze_time;
+    private GLib.DateTime ring_end_time;
+    private Utils.Bell bell;
+
+    public Item (string name, bool active, int hour, int minute, int[]? days) {
+        Object (name: name, active: active, hour: hour, minute: minute);
+
+        reset ();
+
+        bell = new Utils.Bell ("alarm-clock-elapsed", _("Alarm"), name);
+    }
+
+    public bool equal (Item a) {
+        return a.name == name && a.hour == hour && a.minute == minute && compare_days ();
+    }
+
+    private bool compare_days () {
+        return true;
+    }
+
+    private void reset () {
+        update_alarm_time ();
+        update_snooze_time (alarm_time);
+        state = State.READY;
+    }
+
+    private void update_alarm_time () {
+        var wallclock = Utils.WallClock.get_default ();
+        var now = wallclock.date_time;
+        var dt = new GLib.DateTime(wallclock.time_zone,
+                                   now.get_year (),
+                                   now.get_month (),
+                                   now.get_day_of_month (),
+                                   now.get_hour (),
+                                   now.get_minute (),
+                                   0);
+
+        // check if it can ring later today
+        if (dt.compare (now) < 0) {
+            dt.add_days (1);
+        }
+
+        alarm_time = dt;
+    }
+
+    private void update_snooze_time(GLib.DateTime start_time) {
+        snooze_time = start_time.add_minutes (SNOOZE_MINUTES);
+    }
+
+    public virtual signal void ring () {
+        bell.ring ();
+    }
+
+    private void snooze () {
+        bell.stop ();
+        state = State.SNOOZING;
+    }
+
+    private void stop () {
+        bell.stop ();
+        update_snooze_time (alarm_time);
+        state = State.READY;
+    }
+
+    // Updates the state and ringing times of the AlarmItem and rings or stops
+    // the alarm as required, depending on the current time.
+    // Returns true if the state changed, false otherwise.
+    public bool tick () {
+        if (!active) {
+            return false;
+        }
+
+        State last_state = state;
+
+        var wallclock = Utils.WallClock.get_default ();
+        var now = wallclock.date_time;
+
+        if (state == State.RINGING && now.compare (ring_end_time) > 0) {
+            stop ();
+        }
+
+        if (now.compare (alarm_time) > 0) {
+            state = State.RINGING;
+            update_alarm_time (); // reschedule for the next repeat
+        } else if (now.compare (snooze_time) > 0) {
+            state = State.RINGING;
+        }
+
+        if (state == State.RINGING) {
+            update_snooze_time (now);
+            ring_end_time = now.add_minutes (RING_MINUTES);
+            ring ();
+        }
+
+        return state != last_state;
+    }
+
+    public GLib.Variant serialize () {
+        return new GLib.Variant.string ("");
+    }
+}
+
+private class Dialog : Gtk.Dialog {
+
+    private Gtk.Entry h_entry;
+    private Gtk.Entry m_entry;
+
+    public Dialog (Gtk.Window parent, Item? alarm) {
+        Object (transient_for: parent, modal: true, title: _("New Alarm"));
+
+        h_entry = new Gtk.Entry ();
+        m_entry = new Gtk.Entry ();
+
+        var content_area = get_content_area ();
+        content_area.add (h_entry);
+        content_area.add (m_entry);
+        content_area.show_all ();
+
+        add_buttons (Gtk.Stock.CANCEL, 0, Gtk.Stock.SAVE, 1);
+    }
+
+    public Item get_alarm_item () {
+        var h = h_entry.get_text ();
+        var m = h_entry.get_text ();
+        return new Item ("Alarm", true, int.parse (h), int.parse (m), null);
+    }
+}
+
+private class StandalonePanel : Gtk.Grid {
+    public Item alarm { get; set; }
+
+    public StandalonePanel () {
+    }
+
+    public void update () {
+        if (alarm != null) {
+        }
+    }
+}
+
+public class MainPanel : Gtk.Notebook, Clocks.Clock {
+    private enum Page {
+        OVERVIEW,
+        STANDALONE
+    }
+
+    private enum Column {
+        SELECTED,
+        NAME,
+        ITEM,
+        COLUMNS
+    }
+
+    public string label { get; protected set; }
+
+    private List<Item> alarms;
+    private GLib.Settings settings;
+    private Gtk.ListStore list_store;
+    private IconView icon_view;
+    private ContentView content_view;
+    private StandalonePanel standalone;
+
+    public MainPanel () {
+        Object (label: _("Alarms"), show_tabs: false);
+
+        alarms = new List<Item> ();
+        settings = new GLib.Settings("org.gnome.clocks");
+
+        list_store = new Gtk.ListStore (Column.COLUMNS, typeof (bool), typeof (string), typeof (Object));
+        icon_view = new IconView (list_store, Column.SELECTED, Column.NAME, (column, cell, model, iter) => {
+            var renderer = cell as DigitalClockRenderer;
+            Item alarm;
+            model.get (iter, Column.ITEM, out alarm, -1);
+            renderer.text = alarm.time_label;
+            renderer.subtext = alarm.repeat_label;
+            renderer.css_class = alarm.active ? "active" : "inactive";
+        });
+
+        content_view = new ContentView (icon_view, "alarm-symbolic", _("Select <b>New</b> to add an alarm"));
+        append_page (content_view);
+
+        standalone = new StandalonePanel ();
+        append_page (standalone);
+
+        set_current_page (Page.OVERVIEW);
+
+        show_all ();
+    }
+
+    public virtual signal void ring() {
+        set_current_page (Page.STANDALONE);
+    }
+
+    private void tick () {
+        foreach (var a in alarms) {
+            if (a.tick()) {
+                // a.tick() returns true if the state changed
+                if (a.state == Item.State.RINGING) {
+                    standalone.alarm = a;
+                    standalone.update ();
+                    ring ();
+                } else if (standalone.alarm == a) {
+                    // update the alarm shown in the standalone
+                    standalone.update ();
+                }
+            }
+        }
+    }
+
+    private void add_alarm (Item alarm) {
+        var label = "<b>%s</b>".printf (GLib.Markup.escape_text (alarm.name));
+        Gtk.TreeIter i;
+        list_store.append (out i);
+        list_store.set (i, Column.SELECTED, false, Column.NAME, label, Column.ITEM, alarm, -1);
+    }
+
+    public void activate_new () {
+        var dialog = new Dialog (get_toplevel () as Gtk.Window, null);
+        dialog.response.connect ((dialog, response) => {
+            if (response == 1) {
+                var alarm = (dialog as Dialog).get_alarm_item ();
+                add_alarm (alarm);
+            }
+            dialog.destroy ();
+        });
+        dialog.show ();
+    }
+
+    public void update_toolbar (Toolbar toolbar) {
+        toolbar.clear ();
+        switch (get_current_page ()) {
+        case Page.OVERVIEW:
+            if (icon_view.mode == IconView.Mode.SELECTION) {
+                toolbar.mode = Toolbar.Mode.SELECTION;
+                var done_button = toolbar.add_button (null, _("Done"), false) as Gtk.Button;
+                done_button.clicked.connect (() => {
+                    icon_view.mode = IconView.Mode.NORMAL;
+                    update_toolbar (toolbar);
+                });
+            } else {
+                toolbar.mode = Toolbar.Mode.NORMAL;
+                var new_button = toolbar.add_button (null, _("New"), true) as Gtk.Button;
+                new_button.clicked.connect (() => {
+                    activate_new ();
+                });
+                var select_button = toolbar.add_button ("object-select-symbolic", null, false) as Gtk.Button;
+                select_button.clicked.connect (() => {
+                    icon_view.mode = IconView.Mode.SELECTION;
+                    update_toolbar (toolbar);
+                });
+            }
+            break;
+        case Page.STANDALONE:
+            toolbar.mode = Toolbar.Mode.STANDALONE;
+                toolbar.add_button ("go-previous-symbolic", null, true);
+                toolbar.add_button (null, _("Edit"), false);
+                toolbar.set_labels (GLib.Markup.escape_text (standalone.alarm.name), null);
+            break;
+        default:
+            assert_not_reached ();
+        }
+    }
+}
+
+} // namespace Alarm
+} // namespace Clocks
diff --git a/src/application.vala b/src/application.vala
new file mode 100644
index 0000000..dd88fe5
--- /dev/null
+++ b/src/application.vala
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2013  Paolo Borelli <pborelli gnome org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+namespace Clocks {
+
+public class Application : Gtk.Application {
+    static bool print_version;
+    const OptionEntry[] option_entries = {
+        { "version", 'v', 0, OptionArg.NONE, ref print_version, N_("Print version information and exit"), null },
+        { null }
+    };
+
+    const GLib.ActionEntry[] action_entries = {
+        { "quit", on_quit_activate }
+    };
+
+    private Window window;
+
+    public Application () {
+        Object (application_id: "org.gnome.clocks");
+
+        add_action_entries (action_entries, this);
+    }
+
+    protected override void activate () {
+        if (window == null) {
+            window = new Window (this);
+        }
+        window.present ();
+    }
+
+    protected override void startup () {
+        base.startup ();
+
+        // FIXME: move the css in gnome-theme-extras
+        try {
+            var css_provider = new Gtk.CssProvider ();
+            css_provider.load_from_path (Utils.get_css_file("gnome-clocks.css"));
+            Gtk.StyleContext.add_provider_for_screen (Gdk.Screen.get_default(),
+                                                      css_provider,
+                                                      Gtk.STYLE_PROVIDER_PRIORITY_USER);
+        } catch (GLib.Error error) {
+            warning (error.message);
+        }
+
+        var builder = new Gtk.Builder ();
+        try {
+            builder.add_from_resource ("/org/gnome/clocks/ui/menu.ui");
+        } catch (GLib.Error error) {
+            warning (error.message);
+        }
+
+        var app_menu = builder.get_object ("appmenu") as MenuModel;
+        set_app_menu (app_menu);
+    }
+
+    protected override bool local_command_line ([CCode (array_length = false, array_null_terminated = true)] ref unowned string[] arguments, out int exit_status) {
+        var ctx = new OptionContext ("");
+
+        ctx.add_main_entries (option_entries, Config.GETTEXT_PACKAGE);
+        ctx.add_group (Gtk.get_option_group (true));
+
+        // Workaround for bug #642885
+        unowned string[] argv = arguments;
+
+        try {
+            ctx.parse (ref argv);
+        } catch (Error e) {
+            exit_status = 1;
+            return true;
+        }
+
+        if (print_version) {
+            print ("%s %s\n", Environment.get_application_name (), Config.VERSION);
+            exit_status = 0;
+            return true;
+        }
+
+        return base.local_command_line (ref arguments, out exit_status);
+    }
+
+    void on_quit_activate () {
+        quit ();
+    }
+}
+
+} // namespace Clocks
diff --git a/src/clock.vala b/src/clock.vala
new file mode 100644
index 0000000..ae07575
--- /dev/null
+++ b/src/clock.vala
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013  Paolo Borelli <pborelli gnome org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+namespace Clocks {
+
+public interface Clock : GLib.Object {
+    public abstract string label { get; protected set; }
+
+    public virtual void activate_new () {
+    }
+
+    public virtual void update_toolbar (Toolbar toolbar) {
+    }
+}
+
+} // namespace Clocks
diff --git a/src/config.vapi b/src/config.vapi
new file mode 100644
index 0000000..b286a75
--- /dev/null
+++ b/src/config.vapi
@@ -0,0 +1,8 @@
+[CCode (cprefix = "", lower_case_cprefix = "", cheader_filename = "config.h")]
+namespace Config {
+    public const string VERSION;
+    public const string GETTEXT_PACKAGE;
+    public const string GNOMELOCALEDIR;
+    public const string DATADIR;
+}
+
diff --git a/src/gnome-clocks.gresource.xml b/src/gnome-clocks.gresource.xml
new file mode 100644
index 0000000..7f311ee
--- /dev/null
+++ b/src/gnome-clocks.gresource.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/clocks/ui">
+    <file preprocess="xml-stripblanks">menu.ui</file>
+    <file preprocess="xml-stripblanks">alarm.ui</file>
+    <file preprocess="xml-stripblanks">stopwatch.ui</file>
+    <file preprocess="xml-stripblanks">timer.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/gnome-desktop-3.0.vapi b/src/gnome-desktop-3.0.vapi
new file mode 100644
index 0000000..0a8aa5c
--- /dev/null
+++ b/src/gnome-desktop-3.0.vapi
@@ -0,0 +1,16 @@
+/* gnome-desktop-3.0.vapi generated by vapigen, do not modify. */
+
+[CCode (cprefix = "Gnome", gir_namespace = "GnomeDesktop", gir_version = "3.0", lower_case_cprefix = "gnome_")]
+namespace Gnome {
+	[CCode (cheader_filename = "libgnome-desktop/gnome-wall-clock.h", type_id = "gnome_wall_clock_get_type ()")]
+	public class WallClock : GLib.Object {
+		[CCode (has_construct_function = false)]
+		protected WallClock ();
+		[CCode (cname = "gnome_wall_clock_get_clock")]
+		public unowned string get_clock ();
+		[NoAccessorMethod]
+		public string clock { owned get; }
+		[NoAccessorMethod]
+		public bool time_only { get; set; }
+	}
+}
diff --git a/src/main.vala b/src/main.vala
new file mode 100644
index 0000000..b28f613
--- /dev/null
+++ b/src/main.vala
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2013  Paolo Borelli <pborelli gnome org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+int main (string[] args) {
+    Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.GNOMELOCALEDIR);
+    Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8");
+    Intl.textdomain (Config.GETTEXT_PACKAGE);
+
+    var app = new Clocks.Application ();
+    return app.run (args);
+}
diff --git a/src/menu.ui b/src/menu.ui
new file mode 100644
index 0000000..105f87b
--- /dev/null
+++ b/src/menu.ui
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<interface>
+  <!-- interface-requires gtk+ 3.0 -->
+  <menu id="appmenu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_New</attribute>
+        <attribute name="action">win.new</attribute>
+        <attribute name="accel">&lt;Primary&gt;n</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_About</attribute>
+        <attribute name="action">win.about</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Quit</attribute>
+        <attribute name="action">app.quit</attribute>
+        <attribute name="accel">&lt;Primary&gt;q</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/src/stopwatch.ui b/src/stopwatch.ui
new file mode 100644
index 0000000..e34a305
--- /dev/null
+++ b/src/stopwatch.ui
@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.6 -->
+  <object class="GtkListStore" id="laps_model">
+    <columns>
+      <!-- column-name lap_n -->
+      <column type="gchararray"/>
+      <!-- column-name split -->
+      <column type="gchararray"/>
+      <!-- column-name total -->
+      <column type="gchararray"/>
+    </columns>
+  </object>
+  <object class="GtkGrid" id="stopwatch_panel">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="hexpand">True</property>
+    <child>
+      <object class="GtkGrid" id="top_grid">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">center</property>
+        <property name="margin_top">36</property>
+        <property name="margin_bottom">60</property>
+        <property name="row_spacing">24</property>
+        <property name="column_spacing">24</property>
+        <property name="column_homogeneous">True</property>
+        <child>
+          <object class="GtkLabel" id="time_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <style>
+              <class name="clocks-time-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+            <property name="width">2</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="left_button">
+            <property name="label" translatable="yes">Start</property>
+            <property name="width_request">200</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <style>
+              <class name="clocks-button"/>
+              <class name="clocks-go"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">1</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="right_button">
+            <property name="label" translatable="yes">Reset</property>
+            <property name="width_request">200</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <style>
+              <class name="clocks-button"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">1</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkScrolledWindow" id="scrolledwindow1">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <property name="shadow_type">in</property>
+        <style>
+          <class name="clocks-laps-scroll"/>
+        </style>
+        <child>
+          <object class="GtkTreeView" id="laps_view">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="model">laps_model</property>
+            <style>
+              <class name="clocks-laps"/>
+            </style>
+            <child internal-child="selection">
+              <object class="GtkTreeSelection" id="treeview-selection"/>
+            </child>
+            <child>
+              <object class="GtkTreeViewColumn" id="n_column">
+                <property name="title" translatable="yes">Lap</property>
+                <property name="alignment">1</property>
+                <property name="sort_column_id">0</property>
+                <child>
+                  <object class="GtkCellRendererText" id="cellrenderertext1">
+                    <property name="xalign">1.0</property>
+                  </object>
+                  <attributes>
+                    <attribute name="markup">0</attribute>
+                  </attributes>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkTreeViewColumn" id="split_column">
+                <property name="title" translatable="yes">Split</property>
+                <property name="expand">True</property>
+                <property name="alignment">1</property>
+                <property name="sort_column_id">0</property>
+                <child>
+                  <object class="GtkCellRendererText" id="cellrenderertext2">
+                    <property name="xalign">1.0</property>
+                  </object>
+                  <attributes>
+                    <attribute name="text">1</attribute>
+                  </attributes>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkTreeViewColumn" id="tot_column">
+                <property name="title" translatable="yes">Total</property>
+                <property name="expand">True</property>
+                <property name="alignment">1</property>
+                <property name="sort_column_id">0</property>
+                <child>
+                  <object class="GtkCellRendererText" id="cellrenderertext3">
+                    <property name="xalign">1.0</property>
+                  </object>
+                  <attributes>
+                    <attribute name="text">2</attribute>
+                  </attributes>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/src/stopwatch.vala b/src/stopwatch.vala
new file mode 100644
index 0000000..3c0071f
--- /dev/null
+++ b/src/stopwatch.vala
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2013  Paolo Borelli <pborelli gnome org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+namespace Clocks {
+namespace Stopwatch {
+
+public class MainPanel : Gtk.Box, Clocks.Clock {
+
+    private enum State {
+        RESET,
+        RUNNING,
+        STOPPED
+    }
+
+    private enum LapsColumn {
+        LAP,
+        SPLIT,
+        TOTAL
+    }
+
+    private State state;
+    private GLib.Timer timer;
+    private uint timeout_id;
+    private int current_lap;
+    private double last_lap_time;
+    private Gtk.Label time_label;
+    private Gtk.Button left_button;
+    private Gtk.Button right_button;
+    private Gtk.ListStore laps_model;
+    private Gtk.TreeView laps_view;
+
+    public string label { get; protected set; }
+
+    public MainPanel () {
+        Object (label: _("Stopwatch"));
+
+        state = State.RESET;
+        timer = new GLib.Timer ();
+        timeout_id = 0;
+        current_lap = 0;
+        last_lap_time = 0;
+
+        var builder = Utils.load_ui ("stopwatch.ui");
+
+        var stopwatch_panel = builder.get_object ("stopwatch_panel") as Gtk.Widget;
+        time_label = builder.get_object ("time_label") as Gtk.Label;
+        left_button = builder.get_object ("left_button") as Gtk.Button;
+        right_button = builder.get_object ("right_button") as Gtk.Button;
+        laps_model = builder.get_object ("laps_model") as Gtk.ListStore;
+        laps_view = builder.get_object ("laps_view") as Gtk.TreeView;
+
+        update_time_label ();
+
+        left_button.clicked.connect (on_left_button_clicked);
+        right_button.clicked.connect (on_right_button_clicked);
+
+        map.connect ((w) => {
+            if (state == State.RUNNING) {
+                update_time_label ();
+                add_timeout ();
+            }
+        });
+
+        unmap.connect ((w) => {
+            if (state == State.RUNNING) {
+                remove_timeout ();
+            }
+        });
+
+        add (stopwatch_panel);
+        show_all ();
+    }
+
+    private void on_left_button_clicked (Gtk.Button button) {
+        switch (state) {
+        case State.RESET:
+        case State.STOPPED:
+            start ();
+            break;
+        case State.RUNNING:
+            stop ();
+            break;
+        default:
+            assert_not_reached ();
+        }
+    }
+
+    private void on_right_button_clicked (Gtk.Button button) {
+        switch (state) {
+        case State.STOPPED:
+            reset ();
+            break;
+        case State.RUNNING:
+            lap ();
+            break;
+        default:
+            assert_not_reached ();
+        }
+    }
+
+    private void start () {
+        if (state == State.RESET) {
+            timer.start ();
+        } else {
+            timer.continue ();
+        }
+        state = State.RUNNING;
+        add_timeout ();
+        left_button.set_label (_("Stop"));
+        left_button.get_style_context ().add_class ("clocks-stop");
+        right_button.set_sensitive (true);
+        right_button.set_label (_("Lap"));
+    }
+
+    private void stop () {
+        timer.stop ();
+        state = State.STOPPED;
+        remove_timeout ();
+        left_button.set_label (_("Continue"));
+        left_button.get_style_context ().remove_class ("clocks-stop");
+        left_button.get_style_context ().add_class ("clocks-go");
+        right_button.set_sensitive (true);
+        right_button.set_label (_("Reset"));
+    }
+
+    private void reset () {
+        timer.reset ();
+        state = State.RESET;
+        remove_timeout ();
+        update_time_label ();
+        left_button.set_label (_("Start"));
+        left_button.get_style_context ().add_class ("clocks-go");
+        right_button.set_sensitive (false);
+        current_lap = 0;
+        laps_model.clear ();
+    }
+
+    private void lap () {
+        current_lap += 1;
+        var e = timer.elapsed ();
+        var split = e - last_lap_time;
+        last_lap_time = e;
+
+        int h;
+        int m;
+        double s;
+        Utils.time_to_hms (e, out h, out m, out s);
+
+        int split_h;
+        int split_m;
+        double split_s;
+        Utils.time_to_hms (split, out split_h, out split_m, out split_s);
+
+        var n_label = "<span color='dimgray'> %d </span>".printf (current_lap);
+
+        string split_label;
+        if (split_h > 0) {
+            split_label = "%i:%02i:%05.2f".printf (split_h, split_m, split_s);
+        } else {
+            split_label = "%02i:%05.2f".printf (split_m, split_s);
+        }
+
+        string tot_label;
+        if (h > 0) {
+            tot_label = "%i:%02i:%05.2f".printf (h, m, s);
+        } else {
+            tot_label = "%02i:%05.2f".printf (m, s);
+        }
+
+        Gtk.TreeIter i;
+        laps_model.append (out i);
+        laps_model.set (i,
+                        LapsColumn.LAP, n_label,
+                        LapsColumn.SPLIT, split_label,
+                        LapsColumn.TOTAL, tot_label,
+                       -1);
+        var p = laps_model.get_path (i);
+        laps_view.scroll_to_cell (p, null, false, 0, 0);
+    }
+
+    private void add_timeout () {
+        if (timeout_id == 0) {
+            timeout_id = Timeout.add (100, update_time_label);
+        }
+    }
+
+    private void remove_timeout () {
+        if (timeout_id != 0) {
+            Source.remove (timeout_id);
+            timeout_id = 0;
+        }
+    }
+
+    private bool update_time_label () {
+        int h = 0;
+        int m = 0;
+        double s = 0;
+        if (state != State.RESET) {
+            Utils.time_to_hms (timer.elapsed (), out h, out m, out s);
+        }
+
+        if (h > 0) {
+            time_label.set_text ("%i:%02i:%04.1f".printf (h, m, s));
+        } else {
+            time_label.set_text ("%02i:%04.1f".printf (m, s));
+        }
+
+        return true;
+    }
+
+    public void update_toolbar (Toolbar toolbar) {
+        toolbar.clear ();
+        toolbar.mode = Toolbar.Mode.NORMAL;
+    }
+}
+
+} // namespace Stopwatch
+} // namespace Clocks
diff --git a/src/timer.ui b/src/timer.ui
new file mode 100644
index 0000000..3590238
--- /dev/null
+++ b/src/timer.ui
@@ -0,0 +1,262 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.6 -->
+  <object class="GtkAdjustment" id="adjustment_hours">
+    <property name="upper">99</property>
+    <property name="step_increment">1</property>
+    <property name="page_increment">10</property>
+  </object>
+  <object class="GtkAdjustment" id="adjustment_minutes">
+    <property name="upper">59</property>
+    <property name="step_increment">1</property>
+    <property name="page_increment">10</property>
+  </object>
+  <object class="GtkAdjustment" id="adjustment_seconds">
+    <property name="upper">59</property>
+    <property name="step_increment">1</property>
+    <property name="page_increment">10</property>
+  </object>
+  <object class="GtkGrid" id="setup_panel">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="halign">center</property>
+    <property name="valign">center</property>
+    <property name="hexpand">True</property>
+    <property name="vexpand">True</property>
+    <property name="row_spacing">48</property>
+    <child>
+      <object class="GtkGrid" id="grid_spinbuttons">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <child>
+          <object class="GtkSpinButton" id="spinbutton_hours">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="invisible_char">â</property>
+            <property name="xalign">0.5</property>
+            <property name="invisible_char_set">True</property>
+            <property name="input_purpose">number</property>
+            <property name="orientation">vertical</property>
+            <property name="adjustment">adjustment_hours</property>
+            <property name="numeric">True</property>
+            <property name="wrap">True</property>
+            <style>
+              <class name="clocks-spinbutton"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkSpinButton" id="spinbutton_minutes">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="invisible_char">â</property>
+            <property name="xalign">0.5</property>
+            <property name="invisible_char_set">True</property>
+            <property name="input_purpose">number</property>
+            <property name="orientation">vertical</property>
+            <property name="adjustment">adjustment_minutes</property>
+            <property name="numeric">True</property>
+            <property name="wrap">True</property>
+            <style>
+              <class name="clocks-spinbutton"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">2</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkSpinButton" id="spinbutton_seconds">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="invisible_char">â</property>
+            <property name="xalign">0.5</property>
+            <property name="invisible_char_set">True</property>
+            <property name="input_purpose">number</property>
+            <property name="orientation">vertical</property>
+            <property name="adjustment">adjustment_seconds</property>
+            <property name="numeric">True</property>
+            <property name="wrap">True</property>
+            <style>
+              <class name="clocks-spinbutton"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">4</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="colon_label1">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="hexpand">True</property>
+            <property name="label" translatable="yes">:</property>
+            <attributes>
+              <attribute name="font-desc" value="64"/>
+            </attributes>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="colon_label2">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="hexpand">True</property>
+            <property name="label" translatable="yes">:</property>
+            <attributes>
+              <attribute name="font-desc" value="64"/>
+            </attributes>
+          </object>
+          <packing>
+            <property name="left_attach">3</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton" id="start_button">
+        <property name="label" translatable="yes">Start</property>
+        <property name="width_request">200</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <style>
+          <class name="clocks-go"/>
+          <class name="clocks-button"/>
+        </style>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkGrid" id="countdown_panel">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="halign">center</property>
+    <property name="valign">center</property>
+    <property name="hexpand">True</property>
+    <property name="vexpand">True</property>
+    <property name="row_spacing">48</property>
+    <property name="column_spacing">24</property>
+    <child>
+      <object class="GtkLabel" id="time_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <style>
+          <class name="clocks-time-label"/>
+        </style>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkGrid" id="button_grid">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="column_spacing">24</property>
+        <child>
+          <object class="GtkButton" id="left_button">
+            <property name="label" translatable="yes">Pause</property>
+            <property name="width_request">200</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <style>
+              <class name="clocks-button"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="right_button">
+            <property name="label" translatable="yes">Reset</property>
+            <property name="width_request">200</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <style>
+              <class name="clocks-button"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkSizeGroup" id="sizegroup1">
+    <widgets>
+      <widget name="grid_spinbuttons"/>
+      <widget name="start_button"/>
+      <widget name="time_label"/>
+      <widget name="button_grid"/>
+    </widgets>
+  </object>
+  <object class="GtkSizeGroup" id="sizegroup2">
+    <property name="mode">vertical</property>
+    <widgets>
+      <widget name="grid_spinbuttons"/>
+      <widget name="time_label"/>
+    </widgets>
+  </object>
+  <object class="GtkSizeGroup" id="sizegroup3">
+    <property name="mode">vertical</property>
+    <widgets>
+      <widget name="start_button"/>
+      <widget name="button_grid"/>
+    </widgets>
+  </object>
+</interface>
diff --git a/src/timer.vala b/src/timer.vala
new file mode 100644
index 0000000..0c33f40
--- /dev/null
+++ b/src/timer.vala
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2013  Paolo Borelli <pborelli gnome org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+namespace Clocks {
+namespace Timer {
+
+public class MainPanel : Gtk.Notebook, Clocks.Clock {
+    enum State {
+        STOPPED,
+        RUNNING,
+        PAUSED
+    }
+
+    enum Page {
+        SETUP,
+        COUNTDOWN
+    }
+
+    private State state;
+    private uint timeout_id;
+    private Utils.Bell bell;
+    private Gtk.SpinButton h_spinbutton;
+    private Gtk.SpinButton m_spinbutton;
+    private Gtk.SpinButton s_spinbutton;
+    private Gtk.Button start_button;
+    private Gtk.Label time_label;
+    private Gtk.Button left_button;
+    private Gtk.Button right_button;
+    private double duration;
+    private GLib.Timer timer;
+
+    public string label { get; protected set; }
+
+    public MainPanel () {
+        Object (label: _("Timer"), show_tabs: false);
+
+        bell = new Utils.Bell ("complete", _("Time is up!"), _("Timer countdown finished"));
+
+        timeout_id = 0;
+        duration = 0;
+        timer = new GLib.Timer ();
+
+        var builder = Utils.load_ui ("timer.ui");
+
+        var setup_panel = builder.get_object ("setup_panel") as Gtk.Widget;
+        h_spinbutton = builder.get_object ("spinbutton_hours") as Gtk.SpinButton;
+        m_spinbutton = builder.get_object ("spinbutton_minutes") as Gtk.SpinButton;
+        s_spinbutton = builder.get_object ("spinbutton_seconds") as Gtk.SpinButton;
+        start_button = builder.get_object ("start_button") as Gtk.Button;
+
+        h_spinbutton.output.connect (show_leading_zeros);
+        m_spinbutton.output.connect (show_leading_zeros);
+        s_spinbutton.output.connect (show_leading_zeros);
+
+        h_spinbutton.value_changed.connect (update_start_button);
+        m_spinbutton.value_changed.connect (update_start_button);
+        s_spinbutton.value_changed.connect (update_start_button);
+
+        start_button.clicked.connect (() => {
+            start ();
+        });
+
+        var countdown_panel = builder.get_object ("countdown_panel") as Gtk.Widget;
+        time_label = builder.get_object ("time_label") as Gtk.Label;
+        left_button = builder.get_object ("left_button") as Gtk.Button;
+        right_button = builder.get_object ("right_button") as Gtk.Button;
+
+        left_button.clicked.connect (() => {
+            switch (state) {
+            case State.RUNNING:
+                pause ();
+                left_button.set_label (_("Continue"));
+                left_button.get_style_context ().add_class ("clocks-go");
+                break;
+            case State.PAUSED:
+                restart ();
+                left_button.set_label (_("Pause"));
+                left_button.get_style_context ().remove_class("clocks-go");
+                break;
+            default:
+                assert_not_reached ();
+            }
+        });
+
+        right_button.clicked.connect (() => {
+            reset ();
+            left_button.set_label (_("Pause"));
+        });
+
+        insert_page (setup_panel, null, Page.SETUP);
+        insert_page (countdown_panel, null, Page.COUNTDOWN);
+
+        reset ();
+
+        set_current_page (Page.SETUP);
+        show_all ();
+    }
+
+    public virtual signal void ring () {
+        bell.ring ();
+    }
+
+    private bool show_leading_zeros (Gtk.SpinButton spin_button) {
+        spin_button.set_text ("%02i".printf(spin_button.get_value_as_int ()));
+        return true;
+    }
+
+    private void update_start_button () {
+        var h = h_spinbutton.get_value_as_int ();
+        var m = m_spinbutton.get_value_as_int ();
+        var s = s_spinbutton.get_value_as_int ();
+
+        start_button.set_sensitive (h != 0 || m != 0 || s != 0);
+    }
+
+    private void reset () {
+        state = State.STOPPED;
+        timer.reset ();
+        remove_timeout ();
+        duration = 0;
+        h_spinbutton.value = 0;
+        m_spinbutton.value = 0;
+        s_spinbutton.value = 0;
+        start_button.set_sensitive (false);
+        set_current_page (Page.SETUP);
+    }
+
+    private void start () {
+        if (state == State.STOPPED && timeout_id == 0) {
+            var h = h_spinbutton.get_value_as_int ();
+            var m = m_spinbutton.get_value_as_int ();
+            var s = s_spinbutton.get_value_as_int ();
+
+            state = State.RUNNING;
+            duration = h * 3600 + m * 60 + s;
+            timer.start ();
+
+            set_current_page (Page.COUNTDOWN);
+
+            update_countdown_label (h, m, s);
+            add_timeout ();
+        }
+    }
+
+    private void restart () {
+        state = State.RUNNING;
+        timer.start ();
+        add_timeout ();
+    }
+
+    private void pause () {
+        state = State.PAUSED;
+        timer.stop ();
+        duration -= timer.elapsed ();
+        remove_timeout ();
+    }
+
+    private void add_timeout () {
+        if (timeout_id == 0) {
+            timeout_id = Timeout.add (100, count);
+        }
+    }
+
+    private void remove_timeout () {
+        if (timeout_id != 0) {
+            Source.remove (timeout_id);
+            timeout_id = 0;
+        }
+    }
+
+    private bool count () {
+        var e = timer.elapsed ();
+        if (e >= duration) {
+            ring ();
+            state = State.STOPPED;
+            remove_timeout ();
+            update_countdown_label (0, 0, 0);
+            set_current_page (Page.SETUP);
+            return false;
+        }
+
+        update_countdown (duration - e);
+        return true;
+    }
+
+    private void update_countdown (double t) {
+        if (time_label.get_mapped ()) {
+            int h;
+            int m;
+            double s;
+            Utils.time_to_hms (t, out h, out m, out s);
+
+            // Math.ceil() because we count backwards: with 0.3 seconds
+            // we want to show 1 second remaining
+            update_countdown_label (h, m, (int)Math.ceil(s));
+        }
+    }
+
+    private void update_countdown_label (int h, int m, int s) {
+        time_label.set_text ("%02i:%02i:%02i".printf (h, m, s));
+    }
+
+    public void update_toolbar (Toolbar toolbar) {
+        toolbar.clear ();
+        toolbar.mode = Toolbar.Mode.NORMAL;
+    }
+}
+
+} // namespace Timer
+} // namespace Clocks
diff --git a/src/utils.vala b/src/utils.vala
new file mode 100644
index 0000000..da3a4a4
--- /dev/null
+++ b/src/utils.vala
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2013  Paolo Borelli <pborelli gnome org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+namespace Clocks {
+namespace Utils {
+
+public string get_css_file (string file_name) {
+    return Path.build_filename (Config.DATADIR, "gnome-clocks", "css", file_name);
+}
+
+public void time_to_hms (double t, out int h, out int m, out double s) {
+    s = t;
+    h = (int) s / 3600;
+    s = t % 3600;
+    m = (int) s / 60;
+    s = s % 60;
+}
+
+public Gtk.Builder load_ui (string ui) {
+    var builder = new Gtk.Builder ();
+    try {
+        builder.add_from_resource ("/org/gnome/clocks/ui/".concat (ui, null));
+    } catch (Error e) {
+        error ("loading main builder file: %s", e.message);
+    }
+    return builder;
+}
+
+// TODO: For now we are wrapping Gnome's clock, but we should probably
+// implement our own class, maybe using gnome-datetime-source
+// Especially if we want to try to use CLOCK_REALTIME_ALARM
+// see https://bugzilla.gnome.org/show_bug.cgi?id=686115
+public class WallClock : Object {
+    private static WallClock instance;
+
+    public static WallClock get_default () {
+        if (instance == null) {
+            instance = new WallClock ();
+        }
+        return instance;
+    }
+
+    public GLib.DateTime date_time { get; private set; }
+    public GLib.TimeZone time_zone { get; private set; }
+
+    private Gnome.WallClock wc;
+
+    private WallClock () {
+        wc = GLib.Object.new (typeof (Gnome.WallClock)) as Gnome.WallClock;
+        wc.notify["clock"].connect (() => {
+            update ();
+            tick ();
+        });
+
+        // we keep it around since it is relatively expesive to get
+        time_zone = new GLib.TimeZone.local ();
+
+        update ();
+    }
+
+    public signal void tick ();
+
+    // provide various types/objects of the same time, to be used directly
+    // in AlarmItem and ClockItem, so they don't need to call these
+    // functions themselves all the time (they only care about minutes).
+    private void update () {
+        date_time = new GLib.DateTime.now (time_zone);
+//            self.time = time.time()
+//            self.localtime = time.localtime(self.time)
+    }
+}
+
+public class Bell : Object {
+    private GLib.Settings settings;
+    private Canberra.Context? canberra;
+    private string soundtheme;
+    private string sound;
+    private Notify.Notification? notification;
+
+    public Bell (string soundid, string title, string msg) {
+        settings = new GLib.Settings("org.gnome.desktop.sound");
+
+        if (Canberra.Context.create (out canberra) < 0) {
+            warning ("Sound will not be available");
+            canberra = null;
+        }
+
+        soundtheme = settings.get_string ("theme-name");
+        sound = soundid;
+
+        notification = null;
+        if (Notify.is_initted() || Notify.init ("GNOME Clocks")) {
+            notification = new Notify.Notification (title, msg, "gnome-clocks");
+            notification.set_hint_string ("desktop-entry", "gnome-clocks");
+        } else {
+            warning ("Could not initialize notification");
+        }
+    }
+
+    public void ring () {
+        if (canberra != null) {
+            canberra.play (1,
+                           Canberra.PROP_EVENT_ID, sound,
+                           Canberra.PROP_CANBERRA_XDG_THEME_NAME, soundtheme,
+                           Canberra.PROP_MEDIA_ROLE, "alarm",
+                           null);
+        }
+
+        if (notification != null) {
+            try {
+                notification.show ();
+            } catch (GLib.Error error) {
+                warning (error.message);
+            }
+        }
+    }
+
+    public void stop () {
+        if (canberra != null) {
+            canberra.cancel (1);
+        }
+    }
+}
+
+} // namespace Utils
+} // namespace Clocks
diff --git a/src/widgets.vala b/src/widgets.vala
new file mode 100644
index 0000000..89bc03b
--- /dev/null
+++ b/src/widgets.vala
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2013  Paolo Borelli <pborelli gnome org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+namespace Clocks {
+
+public class Toolbar : Gd.MainToolbar {
+    public enum Mode {
+        NORMAL,
+        SELECTION,
+        STANDALONE
+    }
+
+    private int n_clocks;
+    private List<Gtk.Widget> buttons;
+
+    public Mode mode { get; set; }
+
+    public signal void clock_changed (int clock);
+
+    public Toolbar () {
+        Object (show_modes: true, vexpand: false);
+        n_clocks = 0;
+    }
+
+    public int add_clock (Clock clock) {
+        int n = n_clocks;
+        n_clocks++;
+        var button = add_mode (clock.label) as Gtk.ToggleButton;
+        button.toggled.connect(() => {
+            if (button.active) {
+                clock_changed (n);
+            }
+        });
+
+        return n;
+    }
+
+    // we wrap add_button so that we can keep track of which
+    // buttons to remove in clear() without removing the radio buttons
+    public new Gtk.Widget add_button (string? icon_name, string? label, bool pack_start) {
+        var button = base.add_button (icon_name, label, pack_start);
+        buttons.prepend (button);
+        return button;
+    }
+
+    public new void clear () {
+        foreach (Gtk.Widget button in buttons) {
+            button.destroy ();
+        }
+    }
+}
+
+private class DigitalClockRenderer : Gtk.CellRendererPixbuf {
+    public string text { get; set; }
+    public string subtext { get; set; }
+    public string css_class { get; set; }
+    public bool active { get; set; default = false; }
+    public bool toggle_visible { get; set; default = false; }
+
+    public DigitalClockRenderer () {
+    }
+
+    public override void render (Cairo.Context cr, Gtk.Widget widget, Gdk.Rectangle background_area, Gdk.Rectangle cell_area, Gtk.CellRendererState flags) {
+        var context = widget.get_style_context ();
+
+        context.save ();
+        context.add_class ("clocks-digital-renderer");
+        context.add_class (css_class);
+
+        cr.save ();
+        Gdk.cairo_rectangle (cr, cell_area);
+        cr.clip ();
+
+        // draw background
+        if (pixbuf != null) {
+            base.render (cr, widget, background_area, cell_area, flags);
+        } else {
+            context.render_frame (cr, cell_area.x, cell_area.y, cell_area.width, cell_area.height);
+            context.render_background (cr, cell_area.x, cell_area.y, cell_area.width, cell_area.height);
+        }
+
+        cr.translate (cell_area.x, cell_area.y);
+
+        // for now the space around the digital clock is hardcoded and relative
+        // to the image width (not the width of the cell which may be larger in
+        // case of long city names).
+        // We need to know the width to create the pango layouts
+        int pixbuf_margin = 0;
+        if (pixbuf != null) {
+            pixbuf_margin = (int) ((cell_area.width - pixbuf.get_width()) / 2);
+        }
+
+        int margin = 12 + pixbuf_margin;
+        int padding = 12;
+        int w = cell_area.width - 2 * margin;
+
+        // create the layouts so that we can measure them
+        var layout = widget.create_pango_layout ("");
+        layout.set_markup ("<span size='xx-large'><b>%s</b></span>".printf (text), -1);
+        layout.set_width (w * Pango.SCALE);
+        layout.set_alignment (Pango.Alignment.CENTER);
+        int text_w, text_h;
+        layout.get_pixel_size (out text_w, out text_h);
+
+        Pango.Layout? layout_subtext = null;
+        int subtext_w = 0;
+        int subtext_h = 0;
+        int subtext_pad = 0;
+        if (subtext != null) {
+            layout_subtext = widget.create_pango_layout ("");
+            layout_subtext.set_markup ("<span size='medium'>%s</span>".printf (subtext), -1);
+            layout_subtext.set_width (w * Pango.SCALE);
+            layout_subtext.set_alignment (Pango.Alignment.CENTER);
+            layout_subtext.get_pixel_size (out subtext_w, out subtext_h);
+            subtext_pad = 6;
+            // We just assume the first line is the longest
+            var line = layout_subtext.get_line (0);
+            Pango.Rectangle ink_rect, log_rect;
+            line.get_pixel_extents (out ink_rect, out log_rect);
+            subtext_w = log_rect.width;
+        }
+
+        // measure the actual height and coordinates (xpad is ignored for now)
+        int h = 2 * padding + text_h + subtext_h + subtext_pad;
+        int x = margin;
+        int y = (cell_area.height - h) / 2;
+
+        context.add_class ("inner");
+
+        // draw inner rectangle background
+        context.render_frame (cr, x, y, w, h);
+        context.render_background (cr, x, y, w, h);
+
+        // draw text
+        context.render_layout (cr, x, y + padding, layout);
+        if (subtext != null) {
+            context.render_layout (cr, x, y + padding + text_h + subtext_pad, layout_subtext);
+        }
+
+        context.restore ();
+
+        // draw the overlayed checkbox
+        if (toggle_visible) {
+            int icon_size;
+            widget.style_get (widget, "check-icon-size", out icon_size, null);
+
+            context.save ();
+            context.add_class (Gtk.STYLE_CLASS_CHECK);
+
+            int xpad, ypad, x_offset;
+            get_padding (out xpad, out ypad);
+
+            if (widget.get_direction () == Gtk.TextDirection.RTL) {
+                x_offset = xpad;
+            } else {
+                x_offset = cell_area.width - icon_size - xpad;
+            }
+
+            int check_x = x_offset;
+            int check_y = cell_area.height - icon_size - ypad;
+
+            if (active) {
+                context.set_state (Gtk.StateFlags.ACTIVE);
+            }
+
+            context.render_check (cr, check_x, check_y, icon_size, icon_size);
+
+            context.restore ();
+        }
+
+        cr.restore ();
+    }
+
+    public override void get_size (Gtk.Widget widget, Gdk.Rectangle? cell_area, out int x_offset, out int y_offset, out int width, out int height) {
+        int icon_size;
+        widget.style_get (widget, "check-icon-size", out icon_size, null);
+        base.get_size (widget, cell_area, out x_offset, out y_offset, out width, out height);
+        width += (int) (icon_size / 4);
+        height += (int) (icon_size / 4);
+    }
+}
+
+public class IconView : Gtk.IconView {
+    public enum Mode {
+        NORMAL,
+        SELECTION
+    }
+
+    public Mode mode {
+        get {
+            return _mode;
+        }
+
+        set {
+            if (_mode != value) {
+                _mode = value;
+
+                // clear selection
+                if (_mode != Mode.SELECTION) {
+                    unselect_all ();
+                    var model = get_model () as Gtk.ListStore;
+                    model.foreach ((model, path, iter) => {
+                        // model.set (iter, selection_col, false, -1);
+                        return false;
+                    });
+                }
+
+                thumb_renderer.set_property ("toggle-visible", (_mode == Mode.SELECTION));
+                queue_draw ();
+            }
+        }
+    }
+
+    private Mode _mode;
+    private int selection_col;
+    private DigitalClockRenderer thumb_renderer;
+
+    public IconView (Gtk.TreeModel model, int selection_column, int text_column, owned Gtk.CellLayoutDataFunc thumb_data_func) {
+        Object (model: model, mode: Mode.NORMAL);
+
+        selection_col = selection_column;
+
+        get_style_context ().add_class ("content-view");
+        set_column_spacing (20);
+        set_margin (16);
+
+        thumb_renderer = new DigitalClockRenderer ();
+        thumb_renderer.set_alignment (0.5f, 0.5f);
+        thumb_renderer.set_fixed_size (160, 160);
+
+        pack_start (thumb_renderer, false);
+        add_attribute (thumb_renderer, "active", selection_col);
+        set_cell_data_func (thumb_renderer, thumb_data_func);
+
+        var text_renderer = new Gtk.CellRendererText ();
+        text_renderer.set_alignment (0.5f, 0.5f);
+        text_renderer.set_fixed_size (160, -1);
+        text_renderer.wrap_width = 140;
+        text_renderer.wrap_mode = Pango.WrapMode.WORD_CHAR;
+        pack_start (text_renderer, true);
+        add_attribute (text_renderer, "markup", text_column);
+    }
+}
+
+public class ContentView : Gtk.Box {
+    private Gtk.ScrolledWindow scrolled_window;
+    private Gtk.Widget empty_page;
+
+    public ContentView (IconView icon_view, string icon, string empty_message) {
+        scrolled_window = new Gtk.ScrolledWindow (null, null);
+        scrolled_window.add (icon_view);
+        empty_page = create_empty_page (icon, empty_message);
+
+        var model = icon_view.get_model ();
+        model.row_inserted.connect(() => {
+            update_empty_view (model);
+        });
+        model.row_deleted.connect(() => {
+            update_empty_view (model);
+        });
+
+        pack_start (empty_page, true, true, 0);
+        show_all ();
+    }
+
+    private Gtk.Widget create_empty_page (string icon, string message) {
+        var gicon = new GLib.ThemedIcon.with_default_fallbacks (icon);
+        var image = new Gtk.Image.from_gicon (gicon, Gtk.IconSize.DIALOG);
+        image.set_sensitive (false);
+
+        var label = new Gtk.Label (message);
+        label.get_style_context ().add_class("dim-label");
+        label.set_use_markup (true);
+
+        var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
+        box.pack_start (new Gtk.Label (""), true, true, 0);
+        box.pack_start (image, false, false, 6);
+        box.pack_start (label, false, false, 6);
+        box.pack_start (new Gtk.Label (""), true, true, 0);
+        box.show_all ();
+
+        return box as Gtk.Widget;
+    }
+
+    private void update_empty_view (Gtk.TreeModel model) {
+        Gtk.TreeIter i;
+
+        if (model.get_iter_first (out i)) {
+            remove (empty_page);
+            pack_start (scrolled_window, true, true, 0);
+        } else {
+            remove (scrolled_window);
+            pack_start (empty_page, true, true, 0);
+        }
+        show_all ();
+    }
+}
+
+} // namespace Clocks
diff --git a/src/window.vala b/src/window.vala
new file mode 100644
index 0000000..125881d
--- /dev/null
+++ b/src/window.vala
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2013  Paolo Borelli <pborelli gnome org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+namespace Clocks {
+
+public class Window : Gtk.ApplicationWindow {
+    // Default size is enough for two rows of 4 clocks
+    private const int DEFAULT_WIDTH = 788;
+    private const int DEFAULT_HEIGHT = 540;
+
+    private const GLib.ActionEntry[] action_entries = {
+        { "new", on_new_activate },
+        { "about", on_about_activate }
+    };
+
+    private enum Page {
+        WORLD,
+        ALARM,
+        STOPWATCH,
+        TIMER
+    }
+
+    private Toolbar toolbar;
+    private Gtk.Notebook notebook;
+    private World.MainPanel world;
+    private Alarm.MainPanel alarm;
+    private Stopwatch.MainPanel stopwatch;
+    private Timer.MainPanel timer;
+
+    public Window (Application app) {
+        Object (application: app, title: _("Clocks"));
+
+        set_hide_titlebar_when_maximized (true);
+        add_action_entries (action_entries, this);
+
+        set_size_request (DEFAULT_WIDTH, DEFAULT_HEIGHT);
+
+        notebook = new Gtk.Notebook ();
+        notebook.set_show_tabs (false);
+        notebook.set_show_border (true);
+        notebook.get_style_context ().add_class ("clocks-content-view");
+        notebook.get_style_context ().add_class ("view");
+        notebook.get_style_context ().add_class ("content-view");
+
+        world = new World.MainPanel ();
+        alarm = new Alarm.MainPanel ();
+        stopwatch = new Stopwatch.MainPanel ();
+        timer = new Timer.MainPanel ();
+
+        toolbar = new Toolbar ();
+        toolbar.add_clock (world);
+        toolbar.add_clock (alarm);
+        toolbar.add_clock (stopwatch);
+        toolbar.add_clock (timer);
+
+        notebook.append_page (world, null);
+        notebook.append_page (alarm, null);
+        notebook.append_page (stopwatch, null);
+        notebook.append_page (timer, null);
+
+        toolbar.clock_changed.connect ((c) => {
+            notebook.set_current_page (c);
+        });
+
+        notebook.switch_page.connect ((w, i) => {
+            (w as Clock).update_toolbar (toolbar);
+        });
+
+        alarm.switch_page.connect ((w, i) => {
+            (w as Clock).update_toolbar (toolbar);
+        });
+        alarm.ring.connect ((w) => {
+            notebook.set_current_page (Page.ALARM);
+        });
+
+        timer.switch_page.connect ((w, i) => {
+            (w as Clock).update_toolbar (toolbar);
+        });
+        timer.ring.connect ((w) => {
+            notebook.set_current_page (Page.TIMER);
+        });
+
+        notebook.set_current_page (0);
+        world.update_toolbar (toolbar);
+
+        var vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
+        vbox.pack_start (toolbar, false, false, 0);
+        vbox.pack_end (notebook, true, true, 0);
+        add (vbox);
+
+        show_all ();
+    }
+
+    void on_new_activate () {
+        var clock = notebook.get_nth_page (notebook.get_current_page ()) as Clock;
+        clock.activate_new ();
+    }
+
+    void on_alarm_ringing () {
+    }
+
+    void on_about_activate () {
+        const string copyright = "Copyright \xc2\xa9 2011 Collabora Ltd.\n" +
+                                 "Copyright \xc2\xa9 2012-2013 Collabora Ltd., Seif Lotfy, Emily Gonyer\n" +
+                                 "Eslam Mostafa, Paolo Borelli, Volker Sobek\n";
+
+        const string authors[] = {
+            "Alex Anthony",
+            "Paolo Borelli",
+            "Allan Day",
+            "Piotr DrÄg",
+            "Emily Gonyer",
+            "MaÃl Lavault",
+            "Seif Lotfy",
+            "William Jon McCann",
+            "Eslam Mostafa",
+            "Bastien Nocera",
+            "Volker Sobek",
+            "Jakub Steiner",
+            null
+        };
+
+        Gtk.show_about_dialog (this,
+                               "program-name", _("Gnome Clocks"),
+                               "logo-icon-name", "gnome-clocks",
+                               "version", Config.VERSION,
+                               "comments", _("Utilities to help you with the time."),
+                               "copyright", copyright,
+                               "authors", authors,
+                               "license-type", Gtk.License.GPL_2_0,
+                               "wrap-license", false,
+                               "translator-credits", _("translator-credits"),
+                               null);
+    }
+}
+
+} // namespace Clocks
diff --git a/src/world.vala b/src/world.vala
new file mode 100644
index 0000000..7ba8cdf
--- /dev/null
+++ b/src/world.vala
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2013  Paolo Borelli <pborelli gnome org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+namespace Clocks {
+namespace World {
+
+private class Item : Object {
+}
+
+private class Dialog : Gtk.Dialog {
+}
+
+private class StandalonePanel : Gtk.Grid {
+}
+
+public class MainPanel : Gtk.Notebook, Clocks.Clock {
+    private enum Page {
+        OVERVIEW,
+        STANDALONE
+    }
+
+    private enum Column {
+        SELECTED,
+        NAME,
+        ITEM,
+        COLUMNS
+    }
+
+    public string label { get; protected set; }
+
+    private GLib.Settings settings;
+    private Gtk.ListStore list_store;
+    private IconView icon_view;
+    private ContentView content_view;
+    private StandalonePanel standalone;
+
+    public MainPanel () {
+        Object (label: _("World"), show_tabs: false);
+
+        settings = new GLib.Settings("org.gnome.clocks");
+
+        list_store = new Gtk.ListStore (Column.COLUMNS, typeof (bool), typeof (string), typeof (Object));
+        icon_view = new IconView (list_store, Column.SELECTED, Column.NAME, (column, cell, model, iter) => {
+            var renderer = cell as DigitalClockRenderer;
+            Item clock;
+            model.get (iter, Column.ITEM, out clock, -1);
+        });
+
+        content_view = new ContentView (icon_view, "document-open-recent-symbolic", _("Select <b>New</b> to add a world clock"));
+        append_page (content_view);
+
+        standalone = new StandalonePanel ();
+        append_page (standalone);
+
+        set_current_page (Page.OVERVIEW);
+
+        show_all ();
+    }
+
+    public void update_toolbar (Toolbar toolbar) {
+        toolbar.clear ();
+        switch (get_current_page ()) {
+        case Page.OVERVIEW:
+            if (icon_view.mode == IconView.Mode.SELECTION) {
+                toolbar.mode = Toolbar.Mode.SELECTION;
+                toolbar.add_button (null, _("Done"), false);
+            } else {
+                toolbar.mode = Toolbar.Mode.NORMAL;
+                toolbar.add_button (null, _("New"), true);
+                toolbar.add_button ("object-select-symbolic", null, false);
+            }
+            break;
+        case Page.STANDALONE:
+            toolbar.mode = Toolbar.Mode.STANDALONE;
+                toolbar.add_button ("go-previous-symbolic", null, true);
+//                toolbar.set_title (GLib.markup_escape_text (standalone.clock.name));
+            break;
+        default:
+            assert_not_reached ();
+        }
+    }
+}
+
+} // namespace World
+} // namespace Clocks


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]