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



commit c646a721383d9be8dca7e6fceeae8c8e52ade20e
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                    |    6 +
 Makefile.am                    |   88 +++++--
 autogen.sh                     |   31 ++-
 configure.ac                   |   44 +++-
 data/css/gnome-clocks.css      |   13 +
 egg-list-box                   |    1 +
 gnome-clocks                   |   33 ---
 libgd                          |    1 +
 po/POTFILES.in                 |   22 +-
 src/alarm.ui                   |  301 +++++++++++++++++++++
 src/alarm.vala                 |  569 ++++++++++++++++++++++++++++++++++++++++
 src/application.vala           |   91 +++++++
 src/clock.vala                 |   38 +++
 src/config.vapi                |    8 +
 src/cutils.c                   |   75 ++++++
 src/gnome-clocks.gresource.xml |   10 +
 src/gnome-desktop-3.0.vapi     |   20 ++
 src/gweather-3.0.vapi          |  292 ++++++++++++++++++++
 src/main.vala                  |   26 ++
 src/menu.ui                    |   37 +++
 src/stopwatch.ui               |  167 ++++++++++++
 src/stopwatch.vala             |  227 ++++++++++++++++
 src/timer.ui                   |  262 ++++++++++++++++++
 src/timer.vala                 |  218 +++++++++++++++
 src/utils.vala                 |  322 +++++++++++++++++++++++
 src/widgets.vala               |  520 ++++++++++++++++++++++++++++++++++++
 src/window.vala                |  167 ++++++++++++
 src/world.ui                   |  213 +++++++++++++++
 src/world.vala                 |  392 +++++++++++++++++++++++++++
 29 files changed, 4119 insertions(+), 75 deletions(-)
---
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..0ec7a39
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "libgd"]
+       path = libgd
+       url = ../libgd
+[submodule "egg-list-box"]
+       path = egg-list-box
+       url = ../egg-list-box
diff --git a/Makefile.am b/Makefile.am
index 8f85924..c98c194 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,84 @@ 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)/egg-list-box \
+       -I$(top_srcdir)/libgd \
+       -DGWEATHER_I_KNOW_THIS_IS_UNSTABLE \
+       -DGNOME_DESKTOP_USE_UNSTABLE_API \
+       -DGETTEXT_PACKAGE=\""$(GETTEXT_PACKAGE)"\" \
+       -DGNOMELOCALEDIR=\""$(prefix)/$(DATADIRNAME)/locale"\" \
+       -DDATADIR=\""$(datadir)"\"
+
+AM_VALAFLAGS = \
+       --vapidir=egg-list-box \
+       --vapidir=libgd \
+       --pkg gio-2.0 \
+       --pkg gtk+-3.0 \
+       --pkg libcanberra \
+       --pkg libnotify \
+       --pkg gd-1.0 \
+       --pkg eggflowbox
+
+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) \
+       egg-list-box/egg-flow-box.c \
+       src/cutils.c \
+       src/gweather-3.0.vapi \
+       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 +175,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..81af577 100755
--- a/autogen.sh
+++ b/autogen.sh
@@ -1,19 +1,28 @@
-#!/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 "$@"
+git submodule update --init --recursive
+
+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..d796392 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,49 @@ 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])
+
+# _NL_TIME_FIRST_WEEKDAY is an enum and not a define
+AC_MSG_CHECKING([for _NL_TIME_FIRST_WEEKDAY])
+AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include <langinfo.h>]],
+                                   [[nl_langinfo(_NL_TIME_FIRST_WEEKDAY);]])],
+               [langinfo_ok=yes], [langinfo_ok=no])
+AC_MSG_RESULT($langinfo_ok)
+if test "$langinfo_ok" = "yes"; then
+  AC_DEFINE([HAVE__NL_TIME_FIRST_WEEKDAY], [1],
+            [Define if _NL_TIME_FIRST_WEEKDAY is available])
+fi
+
+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
+    stack
+    vapi
 ])
 
 AC_CONFIG_FILES([
     Makefile
-    gnomeclocks/defs.py
+    libgd/Makefile
     po/Makefile.in
 ])
 
@@ -40,7 +71,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..24a3a1b 100644
--- a/data/css/gnome-clocks.css
+++ b/data/css/gnome-clocks.css
@@ -98,10 +98,22 @@
     color: white;
 }
 
+.clocks-time-label {
+    font-size: 64px;
+}
+
+.clocks-secondary-time-label {
+    font-size: 18px;
+}
+
 .clocks-button {
     font-size: 18px;
 }
 
+.clocks-spinbutton {
+    font-size: 64px;
+}
+
 .clocks-go {
     background-image: linear-gradient(to bottom,
                                       @clocks_go_color_a,
@@ -159,6 +171,7 @@
 }
 
 .view.clocks-laps {
+    font-size: larger;
     background-color: @theme_bg_color;
     color: @theme_fg_color;
 }
diff --git a/egg-list-box b/egg-list-box
new file mode 160000
index 0000000..491db4b
--- /dev/null
+++ b/egg-list-box
@@ -0,0 +1 @@
+Subproject commit 491db4b5eb181f1e13b909644dfd0ad971231099
diff --git a/libgd b/libgd
new file mode 160000
index 0000000..de382fe
--- /dev/null
+++ b/libgd
@@ -0,0 +1 @@
+Subproject commit de382feb378429d716080ce020818f1d587bb3df
diff --git a/po/POTFILES.in b/po/POTFILES.in
index b0cae1f..6bc6508 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -1,10 +1,16 @@
 data/gnome-clocks.desktop.in
 data/org.gnome.clocks.gschema.xml.in
-gnomeclocks/alarm.py
-gnomeclocks/app.py
-gnomeclocks/clocks.py
-gnomeclocks/stopwatch.py
-gnomeclocks/timer.py
-gnomeclocks/utils.py
-gnomeclocks/widgets.py
-gnomeclocks/world.py
+src/alarm.vala
+src/application.vala
+src/clocks.vala
+src/stopwatch.vala
+src/timer.vala
+src/utils.vala
+src/widgets.vala
+src/window.vala
+src/world.vala
+[type: gettext/glade]src/alarm.ui
+[type: gettext/glade]src/menu.ui
+[type: gettext/glade]src/stopwatch.ui
+[type: gettext/glade]src/timer.ui
+[type: gettext/glade]src/world.ui
diff --git a/src/alarm.ui b/src/alarm.ui
new file mode 100644
index 0000000..40c10c4
--- /dev/null
+++ b/src/alarm.ui
@@ -0,0 +1,301 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.6 -->
+  <object class="GtkGrid" id="empty_panel">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="halign">center</property>
+    <property name="valign">center</property>
+    <property name="row_spacing">6</property>
+    <child>
+      <object class="GtkImage" id="image1">
+        <property name="visible">True</property>
+        <property name="sensitive">False</property>
+        <property name="can_focus">False</property>
+        <property name="icon_name">alarm-symbolic</property>
+        <property name="icon-size">6</property>
+      </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="GtkLabel" id="label1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label" translatable="yes">Select &lt;b&gt;New&lt;/b&gt; to add an alarm</property>
+        <property name="use_markup">True</property>
+        <style>
+          <class name="dim-label"/>
+        </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="GtkAdjustment" id="h_adjustment">
+    <property name="upper">100</property>
+    <property name="step_increment">1</property>
+    <property name="page_increment">10</property>
+  </object>
+  <object class="GtkAdjustment" id="m_adjustment">
+    <property name="upper">59</property>
+    <property name="step_increment">1</property>
+    <property name="page_increment">10</property>
+  </object>
+  <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="stop_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="snooze_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>
+  <object class="GtkGrid" id="setup_dialog_content">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="margin_left">5</property>
+    <property name="margin_right">5</property>
+    <property name="margin_top">5</property>
+    <property name="margin_bottom">5</property>
+    <property name="hexpand">True</property>
+    <property name="vexpand">True</property>
+    <property name="row_spacing">9</property>
+    <property name="column_spacing">6</property>
+    <child>
+      <object class="GtkLabel" id="time_labe">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">1</property>
+        <property name="label" translatable="yes">Time</property>
+      </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="GtkBox" id="time_box">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="spacing">6</property>
+        <child>
+          <object class="GtkSpinButton" id="h_spinbutton">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="hexpand">True</property>
+            <property name="invisible_char">â</property>
+            <property name="invisible_char_set">True</property>
+            <property name="input_purpose">number</property>
+            <property name="adjustment">h_adjustment</property>
+            <property name="numeric">True</property>
+            <property name="wrap">True</property>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label2">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="label" translatable="yes">:</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkSpinButton" id="m_spinbutton">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="hexpand">True</property>
+            <property name="invisible_char">â</property>
+            <property name="invisible_char_set">True</property>
+            <property name="input_purpose">number</property>
+            <property name="adjustment">m_adjustment</property>
+            <property name="numeric">True</property>
+            <property name="wrap">True</property>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+      </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="name_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">1</property>
+        <property name="label" translatable="yes">Name</property>
+      </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="GtkEntry" id="name_entry">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="invisible_char">â</property>
+        <property name="invisible_char_set">True</property>
+      </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>
+    <child>
+      <object class="GtkLabel" id="repeat_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">1</property>
+        <property name="label" translatable="yes">Repeat Every</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">2</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkBox" id="day_buttons_box">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="homogeneous">True</property>
+        <style>
+          <class name="linked"/>
+        </style>
+        <child>
+          <placeholder/>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">2</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkSwitch" id="active_switch">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="halign">end</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">3</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+  </object>
+</interface>
diff --git a/src/alarm.vala b/src/alarm.vala
new file mode 100644
index 0000000..23fdb94
--- /dev/null
+++ b/src/alarm.vala
@@ -0,0 +1,569 @@
+/*
+ * 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 {
+            return _name;
+        }
+
+        set {
+            _name = value;
+            bell = new Utils.Bell ("alarm-clock-elapsed", _("Alarm"), _name);
+        }
+    }
+
+    public int hour { get; set; }
+    public int minute { get; set; }
+    public Utils.Weekdays days { get; construct set; }
+
+    public string repeat_label {
+        owned get {
+            return days.get_label ();
+        }
+    }
+
+    public State state { get; private set; }
+
+    public string time_label {
+         owned get {
+            return Utils.WallClock.get_default ().format_time (alarm_time);
+         }
+    }
+
+    public bool active {
+        get {
+            return _active;
+        }
+
+        set {
+            if (value != _active) {
+                _active = value;
+                if (_active) {
+                    reset ();
+                } else if (state == State.RINGING) {
+                    bell.stop ();
+                }
+            }
+        }
+    }
+
+    private string _name;
+    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.with_data (string name, bool active, int hour, int minute, Utils.Weekdays days) {
+        Object (name: name, active: active, hour: hour, minute: minute, days: days);
+
+        bell = new Utils.Bell ("alarm-clock-elapsed", _("Alarm"), name);
+
+        reset ();
+    }
+
+    public Item () {
+        bell = new Utils.Bell ("alarm-clock-elapsed", _("Alarm"), "");
+    }
+
+    public bool equal (Item a) {
+        return a.name == name && a.hour == hour && a.minute == minute && days.equal (a.days);
+    }
+
+    public 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.timezone,
+                                   now.get_year (),
+                                   now.get_month (),
+                                   now.get_day_of_month (),
+                                   hour,
+                                   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 ();
+    }
+
+    public void snooze () {
+        bell.stop ();
+        state = State.SNOOZING;
+    }
+
+    public void stop () {
+        bell.stop ();
+        update_snooze_time (alarm_time);
+        state = State.READY;
+    }
+
+    // Update the state and ringing time. Ring or stop
+    // 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 void serialize (GLib.VariantBuilder builder) {
+        builder.open (new GLib.VariantType ("a{sv}"));
+        builder.add ("{sv}", "name", new GLib.Variant.string ("name"));
+        builder.add ("{sv}", "active", new GLib.Variant.boolean (active));
+        builder.add ("{sv}", "hour", new GLib.Variant.int32 (hour));
+        builder.add ("{sv}", "minute", new GLib.Variant.int32 (minute));
+        builder.add ("{sv}", "days", days.serialize ());
+        builder.close ();
+    }
+
+    public static Item? deserialize (GLib.Variant alarm_variant) {
+        string? name = null;
+        bool active = true;
+        int hour = -1;
+        int minute = -1;
+        Utils.Weekdays days = new Utils.Weekdays ();
+        foreach (var v in alarm_variant) {
+            var key = v.get_child_value (0).get_string ();
+            if (key == "name") {
+                name = v.get_child_value (1).get_child_value (0).get_string ();
+            } else if (key == "active") {
+                active = v.get_child_value (1).get_child_value (0).get_boolean ();
+            } else if (key == "hour") {
+                hour = v.get_child_value (1).get_child_value (0).get_int32 ();
+            } else if (key == "minute") {
+                minute = v.get_child_value (1).get_child_value (0).get_int32 ();
+            } else if (key == "days") {
+                days = Utils.Weekdays.deserialize (v.get_child_value (1).get_child_value (0));
+            }
+        }
+        if (name != null && hour > 0 && minute > 0) {
+            return new Item.with_data (name, active, hour, minute, days);
+        }
+        return null;
+    }
+}
+
+private class SetupDialog : Gtk.Dialog {
+    private Utils.WallClock.Format format;
+    private Gtk.SpinButton h_spinbutton;
+    private Gtk.SpinButton m_spinbutton;
+    private Gtk.Entry name_entry;
+    private AmPmToggleButton am_pm_button;
+    private Gtk.ToggleButton[] day_buttons;
+    private Gtk.Switch active_switch;
+
+    public SetupDialog (Gtk.Window parent, Item? alarm) {
+        Object (transient_for: parent, modal: true, title: _("New Alarm"));
+
+        add_buttons (Gtk.Stock.CANCEL, 0, Gtk.Stock.SAVE, 1);
+        set_default_response (1);
+
+        format  = Utils.WallClock.get_default ().format;
+        am_pm_button = new AmPmToggleButton ();
+
+        // Get objects from the ui file
+        var builder = Utils.load_ui ("alarm.ui");
+        var grid = builder.get_object ("setup_dialog_content") as Gtk.Grid;
+        var time_box = builder.get_object ("time_box") as Gtk.Box;
+        var day_buttons_box = builder.get_object ("day_buttons_box") as Gtk.Box;
+        h_spinbutton = builder.get_object ("h_spinbutton") as Gtk.SpinButton;
+        m_spinbutton = builder.get_object ("m_spinbutton") as Gtk.SpinButton;
+        name_entry = builder.get_object ("name_entry") as Gtk.Entry;
+        active_switch = builder.get_object ("active_switch") as Gtk.Switch;
+
+        h_spinbutton.output.connect (show_leading_zeros);
+        m_spinbutton.output.connect (show_leading_zeros);
+        if (format == Utils.WallClock.Format.TWENTYFOUR)
+            // 24h format
+            h_spinbutton.set_range (0, 23);
+        else {
+            // 12h format
+            h_spinbutton.set_range (1, 12);
+            time_box.pack_start (am_pm_button);
+        }
+
+        // Create an array with the weekday buttons with
+        // day_buttons[0] referencing the button for Monday, and so on.
+        day_buttons = new Gtk.ToggleButton[7];
+        for (int i = 0; i < 7; i++) {
+            var button = new Gtk.ToggleButton.with_label (Utils.Weekdays.abbreviation ((Utils.Weekdays.Day) 
i));
+            day_buttons[i] = button;
+        }
+
+        // Pack the buttons, starting with the first day of the week
+        // depending on the locale.
+        var first_weekday = Utils.Weekdays.get_first_weekday ();
+        for (int i = 0; i < 7; i++) {
+            var day_number = (first_weekday + i) % 7;
+            day_buttons_box.pack_start (day_buttons[day_number]);
+        }
+
+        get_content_area ().add (grid);
+        set_from_alarm (alarm);
+    }
+
+    // Sets up the dialog to show the values of alarm.
+    public void set_from_alarm (Item? alarm) {
+        string name;
+        bool active;
+        int hour;
+        int minute;
+        unowned Utils.Weekdays? days;
+
+        if (alarm == null) {
+            var wc = Utils.WallClock.get_default ();
+            name = _("New Alarm");
+            hour = wc.date_time.get_hour();
+            minute = wc.date_time.get_minute();
+            days = null;
+            active = true;
+        } else {
+            name = alarm.name;
+            hour = alarm.hour;
+            minute = alarm.minute;
+            days = alarm.days;
+            active = alarm.active;
+        }
+
+        // Set the time.
+        if (format == Utils.WallClock.Format.TWELVE) {
+            if (hour < 12) {
+                am_pm_button.choice = AmPmToggleButton.AmPm.AM;
+            } else {
+                am_pm_button.choice = AmPmToggleButton.AmPm.PM;
+                hour -= 12;
+            }
+
+            if (hour == 0) {
+                hour = 12;
+            }
+        }
+        h_spinbutton.set_value (hour);
+        m_spinbutton.set_value (minute);
+
+        // Set the name.
+        name_entry.set_text (name);
+
+        // Set the toggle buttons for weekdays.
+        if (days != null) {
+            for (int i = 0; i < 7; i++) {
+                day_buttons[i].active = days.get ((Utils.Weekdays.Day) i);
+            }
+        }
+
+        // Set On/Off switch.
+        active_switch.active = active;
+    }
+
+    // Sets alarm according to the current dialog settings.
+    public void apply_to_alarm (Item alarm) {
+        var name = name_entry.get_text();
+        var active = active_switch.active;
+        var hour = h_spinbutton.get_value_as_int ();
+        var minute = m_spinbutton.get_value_as_int ();
+        if (format == Utils.WallClock.Format.TWELVE) {
+            var choice = am_pm_button.choice;
+            if (choice == AmPmToggleButton.AmPm.AM && hour == 12) {
+                hour = 0;
+            } else if (choice == AmPmToggleButton.AmPm.PM && hour != 12) {
+                hour += 12;
+            }
+        }
+
+        alarm.name = name;
+        alarm.active = active;
+        alarm.hour = hour;
+        alarm.minute = minute;
+
+        for (int i = 0; i < 7; i++) {
+            alarm.days.set ((Utils.Weekdays.Day) i, day_buttons[i].active);
+        }
+    }
+
+    private bool show_leading_zeros (Gtk.SpinButton spin_button) {
+        spin_button.set_text ("%02i".printf (spin_button.get_value_as_int ()));
+        return true;
+    }
+}
+
+private class StandalonePanel : Gtk.EventBox {
+    public Item alarm { get; set; }
+
+    private Gtk.Label time_label;
+    private Gtk.Button stop_button;
+    private Gtk.Button snooze_button;
+
+    public StandalonePanel () {
+        get_style_context ().add_class ("view");
+        get_style_context ().add_class ("content-view");
+
+        var builder = Utils.load_ui ("alarm.ui");
+        var grid = builder.get_object ("ringing_panel") as Gtk.Grid;
+        time_label = builder.get_object ("time_label") as Gtk.Label;
+        stop_button = builder.get_object ("stop_button") as Gtk.Button;
+        snooze_button = builder.get_object ("snooze_button") as Gtk.Button;
+
+        stop_button.clicked.connect (() => {
+            alarm.stop ();
+        });
+
+        snooze_button.clicked.connect (() => {
+            alarm.snooze ();
+            snooze_button.sensitive = false;
+        });
+
+        add (grid);
+    }
+
+    public void update () {
+        if (alarm != null) {
+            time_label.set_text (alarm.time_label);
+            snooze_button.sensitive = (alarm.state == Item.State.RINGING);
+        }
+    }
+}
+
+public class MainPanel : Gd.Stack, Clocks.Clock {
+    private enum Column {
+        SELECTED,
+        NAME,
+        ITEM,
+        COLUMNS
+    }
+
+    public string label { get; construct set; }
+    public Toolbar toolbar { get; construct 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 (Toolbar toolbar) {
+        Object (label: _("Alarm"), toolbar: toolbar);
+
+        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) => {
+            Item alarm;
+            model.get (iter, Column.ITEM, out alarm);
+            var renderer = (DigitalClockRenderer) cell;
+            renderer.text = alarm.time_label;
+            renderer.subtext = alarm.repeat_label;
+            renderer.css_class = alarm.active ? "active" : "inactive";
+        });
+
+        icon_view.item_activated.connect ((path) => {
+            Gtk.TreeIter i;
+            if (list_store.get_iter (out i, path)) {
+                Item alarm;
+                list_store.get (i, Column.ITEM, out alarm);
+                var dialog = new SetupDialog ((Gtk.Window) get_toplevel (), alarm);
+                dialog.response.connect ((dialog, response) => {
+                    if (response == 1) {
+                        ((SetupDialog) dialog).apply_to_alarm (alarm);
+                        alarm.reset ();
+                        save ();
+                    }
+                    dialog.destroy ();
+                });
+                dialog.show_all ();
+            }
+        });
+
+        var builder = Utils.load_ui ("alarm.ui");
+        var empty_view = builder.get_object ("empty_panel") as Gtk.Widget;
+        content_view = new ContentView (empty_view, icon_view, toolbar);
+        add (content_view);
+
+        standalone = new StandalonePanel ();
+        add (standalone);
+
+        load ();
+
+        var id = notify["visible-child"].connect (() => {
+            if (visible_child == content_view) {
+                toolbar.mode = Toolbar.Mode.NORMAL;
+            } else if (visible_child == standalone) {
+                toolbar.mode = Toolbar.Mode.STANDALONE;
+            }
+        });
+        toolbar.destroy.connect(() => {
+            disconnect (id);
+            id = 0;
+        });
+
+        visible_child = content_view;
+        show_all ();
+
+        // Start ticking...
+        Utils.WallClock.get_default ().tick.connect (() => {
+            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 ();
+                    }
+                }
+            }
+        });
+    }
+
+    public virtual signal void ring () {
+        visible_child = standalone;
+    }
+
+    private void load () {
+        foreach (var a in settings.get_value ("alarms")) {
+            Item alarm = Item.deserialize (a);
+            alarms.prepend (alarm);
+            add_alarm_to_store (alarm);
+        }
+        alarms.reverse ();
+    }
+
+    private void save () {
+        var builder = new GLib.VariantBuilder (new VariantType ("aa{sv}"));
+        foreach (Item i in alarms) {
+            i.serialize (builder);
+        }
+        settings.set_value ("alarms", builder.end ());
+    }
+
+    private void add_alarm_to_store (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);
+    }
+
+    public void activate_new () {
+        var dialog = new SetupDialog ((Gtk.Window) get_toplevel (), null);
+        dialog.response.connect ((dialog, response) => {
+            if (response == 1) {
+                var alarm = new Item ();
+                ((SetupDialog) dialog).apply_to_alarm (alarm);
+                alarms.append (alarm);
+                add_alarm_to_store (alarm);
+                alarm.reset();
+                save ();
+            }
+            dialog.destroy ();
+        });
+        dialog.show_all ();
+    }
+
+    public void activate_select_all () {
+        icon_view.select_all ();
+    }
+
+    public void activate_select_none () {
+        icon_view.unselect_all ();
+    }
+
+    public void update_toolbar () {
+        switch (toolbar.mode) {
+        case Toolbar.Mode.NORMAL:
+            // Translators: "New" refers to an alarm
+            var new_button = toolbar.add_button (null, _("New"), true);
+            new_button.clicked.connect (() => {
+                activate_new ();
+            });
+            content_view.update_toolbar ();
+            break;
+        case Toolbar.Mode.SELECTION:
+            content_view.update_toolbar ();
+            break;
+        case Toolbar.Mode.STANDALONE:
+            var back_button = toolbar.add_button ("go-previous-symbolic", null, true);
+            back_button.clicked.connect (() => {
+                visible_child = content_view;
+            });
+            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..11f7d13
--- /dev/null
+++ b/src/application.vala
@@ -0,0 +1,91 @@
+/*
+ * 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
+        var css_provider = Utils.load_css ("gnome-clocks.css");
+        Gtk.StyleContext.add_provider_for_screen (Gdk.Screen.get_default(),
+                                                  css_provider,
+                                                  Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+        var builder = Utils.load_ui ("menu.ui");
+        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..96cede9
--- /dev/null
+++ b/src/clock.vala
@@ -0,0 +1,38 @@
+/*
+ * 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 construct set; }
+    public abstract Toolbar toolbar { get; protected construct set; }
+
+    public virtual void activate_new () {
+    }
+
+    public virtual void activate_select_all () {
+    }
+
+    public virtual void activate_select_none () {
+    }
+
+    public virtual void update_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/cutils.c b/src/cutils.c
new file mode 100644
index 0000000..4d7f458
--- /dev/null
+++ b/src/cutils.c
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#ifdef HAVE__NL_TIME_FIRST_WEEKDAY
+#include <langinfo.h>
+#endif
+#include <glib/gi18n-lib.h>
+#include <gtk/gtk.h>
+
+/* Copied from gtkcalendar.c */
+int
+clocks_cutils_get_week_start (void)
+{
+  int week_start;
+#ifdef HAVE__NL_TIME_FIRST_WEEKDAY
+  union { unsigned int word; char *string; } langinfo;
+  int week_1stday = 0;
+  int first_weekday = 1;
+  guint week_origin;
+#else
+  char *gtk_week_start;
+#endif
+
+#ifdef HAVE__NL_TIME_FIRST_WEEKDAY
+  langinfo.string = nl_langinfo (_NL_TIME_FIRST_WEEKDAY);
+  first_weekday = langinfo.string[0];
+  langinfo.string = nl_langinfo (_NL_TIME_WEEK_1STDAY);
+  week_origin = langinfo.word;
+  if (week_origin == 19971130) /* Sunday */
+    week_1stday = 0;
+  else if (week_origin == 19971201) /* Monday */
+    week_1stday = 1;
+  else
+    g_warning ("Unknown value of _NL_TIME_WEEK_1STDAY.\n");
+
+  week_start = (week_1stday + first_weekday - 1) % 7;
+#else
+  /* Use a define to hide the string from xgettext */
+# define GTK_WEEK_START "calendar:week_start:0"
+  gtk_week_start = dgettext ("gtk30", GTK_WEEK_START);
+
+  if (strncmp (gtk_week_start, "calendar:week_start:", 20) == 0)
+    week_start = *(gtk_week_start + 20) - '0';
+  else
+    week_start = -1;
+
+  if (week_start < 0 || week_start > 6)
+    {
+      g_warning ("Whoever translated calendar:week_start:0 for GTK+ "
+                 "did so wrongly.\n");
+      week_start = 0;
+    }
+#endif
+
+  return week_start;
+}
+
diff --git a/src/gnome-clocks.gresource.xml b/src/gnome-clocks.gresource.xml
new file mode 100644
index 0000000..dacf3a2
--- /dev/null
+++ b/src/gnome-clocks.gresource.xml
@@ -0,0 +1,10 @@
+<?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">world.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..c249a5a
--- /dev/null
+++ b/src/gnome-desktop-3.0.vapi
@@ -0,0 +1,20 @@
+/* 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 (cname = "gnome_wall_clock_new", has_construct_function = false)]
+               public WallClock ();
+               [CCode (cname = "gnome_wall_clock_get_clock")]
+               public unowned string get_clock ();
+               [CCode (cname = "gnome_wall_clock_get_timezone")]
+               public unowned GLib.TimeZone get_timezone ();
+               [NoAccessorMethod]
+               public string clock { owned get; }
+               [NoAccessorMethod]
+               public bool time_only { get; set; }
+               [NoAccessorMethod]
+               public GLib.TimeZone timezone { owned get; }
+       }
+}
diff --git a/src/gweather-3.0.vapi b/src/gweather-3.0.vapi
new file mode 100644
index 0000000..af0082f
--- /dev/null
+++ b/src/gweather-3.0.vapi
@@ -0,0 +1,292 @@
+/* gweather-3.0.vapi generated by vapigen, do not modify. */
+
+[CCode (cprefix = "GWeather", gir_namespace = "GWeather", gir_version = "3.0", lower_case_cprefix = 
"gweather_")]
+namespace GWeather {
+       [CCode (cheader_filename = "libgweather/gweather.h", type_id = "gweather_info_get_type ()")]
+       public class Info : GLib.Object {
+               [CCode (has_construct_function = false)]
+               public Info (GWeather.Location? location, GWeather.ForecastType forecast_type);
+               public void abort ();
+               [CCode (has_construct_function = false)]
+               public Info.for_world (GWeather.Location world, GWeather.Location? location, 
GWeather.ForecastType forecast_type);
+               public string get_apparent ();
+               public unowned string get_attribution ();
+               public string get_conditions ();
+               public string get_dew ();
+               public GWeather.Provider get_enabled_providers ();
+               public string get_forecast ();
+               public unowned GLib.SList<GWeather.Info> get_forecast_list ();
+               public string get_humidity ();
+               public unowned string get_icon_name ();
+               public unowned GWeather.Location get_location ();
+               public string get_location_name ();
+               public string get_pressure ();
+               public unowned Gdk.PixbufAnimation get_radar ();
+               public string get_sky ();
+               public string get_sunrise ();
+               public string get_sunset ();
+               public string get_temp ();
+               public string get_temp_max ();
+               public string get_temp_min ();
+               public string get_temp_summary ();
+               public bool get_upcoming_moonphases (long phases);
+               public string get_update ();
+               public bool get_value_apparent (GWeather.TemperatureUnit unit, out double value);
+               public bool get_value_conditions (out GWeather.ConditionPhenomenon phenomenon, out 
GWeather.ConditionQualifier qualifier);
+               public bool get_value_dew (GWeather.TemperatureUnit unit, out double value);
+               public bool get_value_moonphase (out GWeather.MoonPhase value, out GWeather.MoonLatitude lat);
+               public bool get_value_pressure (GWeather.PressureUnit unit, out double value);
+               public bool get_value_sky (out GWeather.Sky sky);
+               public bool get_value_sunrise (out ulong value);
+               public bool get_value_sunset (out ulong value);
+               public bool get_value_temp (GWeather.TemperatureUnit unit, out double value);
+               public bool get_value_temp_max (GWeather.TemperatureUnit unit, out double value);
+               public bool get_value_temp_min (GWeather.TemperatureUnit unit, out double value);
+               public bool get_value_update (out long value);
+               public bool get_value_visibility (GWeather.DistanceUnit unit, out double value);
+               public bool get_value_wind (GWeather.SpeedUnit unit, out double speed, out 
GWeather.WindDirection direction);
+               public string get_visibility ();
+               public string get_weather_summary ();
+               public string get_wind ();
+               public bool is_daytime ();
+               public bool is_valid ();
+               public bool network_error ();
+               public int next_sun_event ();
+               public void set_enabled_providers (GWeather.Provider providers);
+               public void set_location (GWeather.Location? location);
+               public void update ();
+               public GWeather.Provider enabled_providers { get; set; }
+               [NoAccessorMethod]
+               public GWeather.ForecastType forecast_type { get; construct; }
+               public GWeather.Location location { get; construct; }
+               [NoAccessorMethod]
+               public GWeather.Location world { owned get; construct; }
+               public virtual signal void updated ();
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", copy_function = "g_boxed_copy", free_function = 
"g_boxed_free", type_id = "gweather_location_get_type ()")]
+       [Compact]
+       public class Location {
+               public GWeather.Location deserialize (GLib.Variant serialized);
+               public bool equal (GWeather.Location two);
+               public GWeather.Location find_by_station_code (string station_code);
+               public void free_children (GWeather.Location children);
+               public void free_timezones (GWeather.Timezone zones);
+               [CCode (array_length = false, array_null_terminated = true)]
+               public unowned GWeather.Location[] get_children ();
+               public string get_city_name ();
+               public unowned string get_code ();
+               public void get_coords (out double latitude, out double longitude);
+               public unowned string get_country ();
+               public double get_distance (GWeather.Location loc2);
+               public GWeather.LocationLevel get_level ();
+               public unowned string get_name ();
+               public unowned GWeather.Location get_parent ();
+               public unowned string get_sort_name ();
+               public unowned GWeather.Timezone get_timezone ();
+               [CCode (array_length = false, array_null_terminated = true)]
+               public GWeather.Timezone[] get_timezones ();
+               public bool has_coords ();
+               public GWeather.Location @ref ();
+               public GWeather.Location ref_world ();
+               public unowned GLib.Variant serialize ();
+               public void unref ();
+               [CCode (has_construct_function = false)]
+               public Location.world (bool use_regions);
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", type_id = "gweather_location_entry_get_type ()")]
+       public class LocationEntry : Gtk.Entry, Atk.Implementor, Gtk.Buildable, Gtk.CellEditable, 
Gtk.Editable {
+               [CCode (has_construct_function = false, type = "GtkWidget*")]
+               public LocationEntry (GWeather.Location top);
+               public GWeather.Location get_location ();
+               public bool has_custom_text ();
+               public bool set_city (string? city_name, string code);
+               public void set_location (GWeather.Location? loc);
+               public GWeather.Location location { owned get; set; }
+               public GWeather.Location top { construct; }
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", copy_function = "g_boxed_copy", free_function = 
"g_boxed_free", type_id = "gweather_timezone_get_type ()")]
+       [Compact]
+       public class Timezone {
+               public int get_dst_offset ();
+               public unowned string get_name ();
+               public int get_offset ();
+               public unowned string get_tzid ();
+               public static GWeather.Timezone get_utc ();
+               public bool has_dst ();
+               public GWeather.Timezone @ref ();
+               public void unref ();
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", type_id = "gweather_timezone_menu_get_type ()")]
+       public class TimezoneMenu : Gtk.ComboBox, Atk.Implementor, Gtk.Buildable, Gtk.CellEditable, 
Gtk.CellLayout {
+               [CCode (has_construct_function = false, type = "GtkWidget*")]
+               public TimezoneMenu (GWeather.Location top);
+               public unowned string get_tzid ();
+               public void set_tzid (string? tzid);
+               public void* top { construct; }
+               public string tzid { get; set; }
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", has_type_id = false)]
+       public struct Conditions {
+               public bool significant;
+               public GWeather.ConditionPhenomenon phenomenon;
+               public GWeather.ConditionQualifier qualifier;
+               public unowned string to_string ();
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h")]
+       [SimpleType]
+       public struct MoonLatitude : double {
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h")]
+       [SimpleType]
+       public struct MoonPhase : double {
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", cprefix = "GWEATHER_PHENOMENON_", type_id = 
"gweather_phenomenon_get_type ()")]
+       public enum ConditionPhenomenon {
+               INVALID,
+               NONE,
+               DRIZZLE,
+               RAIN,
+               SNOW,
+               SNOW_GRAINS,
+               ICE_CRYSTALS,
+               ICE_PELLETS,
+               HAIL,
+               SMALL_HAIL,
+               UNKNOWN_PRECIPITATION,
+               MIST,
+               FOG,
+               SMOKE,
+               VOLCANIC_ASH,
+               SAND,
+               HAZE,
+               SPRAY,
+               DUST,
+               SQUALL,
+               SANDSTORM,
+               DUSTSTORM,
+               FUNNEL_CLOUD,
+               TORNADO,
+               DUST_WHIRLS,
+               LAST
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", cprefix = "GWEATHER_QUALIFIER_", type_id = 
"gweather_qualifier_get_type ()")]
+       public enum ConditionQualifier {
+               INVALID,
+               NONE,
+               VICINITY,
+               LIGHT,
+               MODERATE,
+               HEAVY,
+               SHALLOW,
+               PATCHES,
+               PARTIAL,
+               THUNDERSTORM,
+               BLOWING,
+               SHOWERS,
+               DRIFTING,
+               FREEZING,
+               LAST
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", cprefix = "GWEATHER_DISTANCE_UNIT_", type_id = 
"gweather_distance_unit_get_type ()")]
+       public enum DistanceUnit {
+               INVALID,
+               DEFAULT,
+               METERS,
+               KM,
+               MILES
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", cprefix = "GWEATHER_FORECAST_", type_id = 
"gweather_forecast_type_get_type ()")]
+       public enum ForecastType {
+               STATE,
+               ZONE,
+               LIST
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", cprefix = "GWEATHER_LOCATION_", type_id = 
"gweather_location_level_get_type ()")]
+       public enum LocationLevel {
+               WORLD,
+               REGION,
+               COUNTRY,
+               ADM1,
+               ADM2,
+               CITY,
+               WEATHER_STATION,
+               DETACHED
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", cprefix = "GWEATHER_PRESSURE_UNIT_", type_id = 
"gweather_pressure_unit_get_type ()")]
+       public enum PressureUnit {
+               INVALID,
+               DEFAULT,
+               KPA,
+               HPA,
+               MB,
+               MM_HG,
+               INCH_HG,
+               ATM
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", cprefix = "GWEATHER_PROVIDER_", type_id = 
"gweather_provider_get_type ()")]
+       [Flags]
+       public enum Provider {
+               NONE,
+               METAR,
+               IWIN,
+               YAHOO,
+               YR_NO,
+               ALL
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", cprefix = "GWEATHER_SKY_", type_id = 
"gweather_sky_get_type ()")]
+       public enum Sky {
+               INVALID,
+               CLEAR,
+               BROKEN,
+               SCATTERED,
+               FEW,
+               OVERCAST,
+               LAST;
+               public static unowned string to_string (GWeather.Sky sky);
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", cprefix = "GWEATHER_SPEED_UNIT_", type_id = 
"gweather_speed_unit_get_type ()")]
+       public enum SpeedUnit {
+               INVALID,
+               DEFAULT,
+               MS,
+               KPH,
+               MPH,
+               KNOTS,
+               BFT
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", cprefix = "GWEATHER_TEMP_UNIT_", type_id = 
"gweather_temperature_unit_get_type ()")]
+       public enum TemperatureUnit {
+               INVALID,
+               DEFAULT,
+               KELVIN,
+               CENTIGRADE,
+               FAHRENHEIT
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", cprefix = "GWEATHER_WIND_", type_id = 
"gweather_wind_direction_get_type ()")]
+       public enum WindDirection {
+               INVALID,
+               VARIABLE,
+               N,
+               NNE,
+               NE,
+               ENE,
+               E,
+               ESE,
+               SE,
+               SSE,
+               S,
+               SSW,
+               SW,
+               WSW,
+               W,
+               WNW,
+               NW,
+               NNW,
+               LAST;
+               public static unowned string to_string (GWeather.WindDirection wind);
+       }
+       [CCode (cheader_filename = "libgweather/gweather.h", cname = "GWEATHER_LOCATION_ENTRY_H")]
+       public const int LOCATION_ENTRY_H;
+       [CCode (cheader_filename = "libgweather/gweather.h", cname = "GWEATHER_TIMEZONE_MENU_H")]
+       public const int TIMEZONE_MENU_H;
+}
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..37d050f
--- /dev/null
+++ b/src/menu.ui
@@ -0,0 +1,37 @@
+<?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>
+  <menu id="selection-menu">
+    <section>
+      <item>
+        <attribute name="action">win.select-all</attribute>
+        <attribute name="label" translatable="yes">Select All</attribute>
+        <attribute name="accel">&lt;Primary&gt;a</attribute>
+      </item>
+      <item>
+        <attribute name="action">win.select-none</attribute>
+        <attribute name="label" translatable="yes">Select None</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..6abe129
--- /dev/null
+++ b/src/stopwatch.vala
@@ -0,0 +1,227 @@
+/*
+ * 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
+    }
+
+    public string label { get; construct set; }
+    public Toolbar toolbar { get; construct set; }
+
+    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 MainPanel (Toolbar toolbar) {
+        Object (label: _("Stopwatch"), toolbar: toolbar);
+
+        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);
+        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;
+    }
+}
+
+} // 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..111442c
--- /dev/null
+++ b/src/timer.vala
@@ -0,0 +1,218 @@
+/*
+ * 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 : Gd.Stack, Clocks.Clock {
+    enum State {
+        STOPPED,
+        RUNNING,
+        PAUSED
+    }
+
+    public string label { get; construct set; }
+    public Toolbar toolbar { get; construct set; }
+
+    private State state;
+    private uint timeout_id;
+    private Utils.Bell bell;
+    private Gtk.Widget setup_panel;
+    private Gtk.SpinButton h_spinbutton;
+    private Gtk.SpinButton m_spinbutton;
+    private Gtk.SpinButton s_spinbutton;
+    private Gtk.Button start_button;
+    private Gtk.Widget countdown_panel;
+    private Gtk.Label time_label;
+    private Gtk.Button left_button;
+    private Gtk.Button right_button;
+    private double span;
+    private GLib.Timer timer;
+
+    public MainPanel (Toolbar toolbar) {
+        Object (label: _("Timer"), toolbar: toolbar);
+
+        bell = new Utils.Bell ("complete", _("Time is up!"), _("Timer countdown finished"));
+
+        timeout_id = 0;
+        span = 0;
+        timer = new GLib.Timer ();
+
+        var builder = Utils.load_ui ("timer.ui");
+
+        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 ();
+        });
+
+        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"));
+        });
+
+        add (setup_panel);
+        add (countdown_panel);
+
+        reset ();
+
+        visible_child = setup_panel;
+        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 ();
+        span = 0;
+        h_spinbutton.value = 0;
+        m_spinbutton.value = 0;
+        s_spinbutton.value = 0;
+        start_button.set_sensitive (false);
+        visible_child = setup_panel;
+    }
+
+    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;
+            span = h * 3600 + m * 60 + s;
+            timer.start ();
+
+            visible_child = countdown_panel;
+
+            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 ();
+        span -= 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 >= span) {
+            ring ();
+            state = State.STOPPED;
+            remove_timeout ();
+            update_countdown_label (0, 0, 0);
+            visible_child = setup_panel;
+            return false;
+        }
+
+        update_countdown (span - 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));
+    }
+}
+
+} // namespace Timer
+} // namespace Clocks
diff --git a/src/utils.vala b/src/utils.vala
new file mode 100644
index 0000000..92c1367
--- /dev/null
+++ b/src/utils.vala
@@ -0,0 +1,322 @@
+/*
+ * 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.
+ */
+
+extern int clocks_cutils_get_week_start ();
+
+namespace Clocks {
+namespace Utils {
+
+public Gtk.CssProvider load_css (string css) {
+    var provider = new Gtk.CssProvider ();
+    try {
+        var path = Path.build_filename (Config.DATADIR, "gnome-clocks", "css", css);
+        provider.load_from_path (path);
+    } catch (Error e) {
+        warning ("loading css: %s", e.message);
+    }
+    return provider;
+}
+
+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;
+}
+
+public Gdk.Pixbuf? load_image (string image) {
+    try {
+        var path = Path.build_filename (Config.DATADIR, "gnome-clocks", "images", image);
+        return new Gdk.Pixbuf.from_file (path);
+    } catch (Error e) {
+        warning ("loading image file: %s", e.message);
+    }
+    return null;
+}
+
+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;
+}
+
+// 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 {
+    public enum Format {
+        TWELVE,
+        TWENTYFOUR
+    }
+
+    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 timezone { get; private set; }
+    public Format format { get; private set; }
+
+    private GLib.Settings settings;
+    private Gnome.WallClock wc;
+
+    private WallClock () {
+        wc = new Gnome.WallClock ();
+        wc.notify["clock"].connect (() => {
+            update ();
+            tick ();
+        });
+
+        // mirror the wallclock's timezone property
+        timezone = wc.timezone;
+        wc.notify["timezone"].connect (() => {
+            timezone = wc.timezone;
+        });
+
+        // system-wide settings about clock format
+        settings = new GLib.Settings ("org.gnome.desktop.interface");
+        settings.changed["clock-format"].connect (() => {
+            update_format ();
+        });
+        update_format ();
+
+        update ();
+    }
+
+    public signal void tick ();
+
+    private void update_format () {
+        var sys_format = settings.get_string ("clock-format");
+        format = sys_format == "12h" ? Format.TWELVE : Format.TWENTYFOUR;
+    }
+
+    // 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 (timezone);
+    }
+
+    public string format_time (GLib.DateTime date_time) {
+        return date_time.format (format == Format.TWELVE ? "%I:%M %p" : "%H:%M");
+    }
+}
+
+public class Weekdays {
+    public enum Day {
+        MON,
+        TUE,
+        WED,
+        THU,
+        FRI,
+        SAT,
+        SUN
+    }
+
+    private const bool[] weekdays = {
+        true, true, true, true, true, false, false
+    };
+
+    private const string[] plurals = {
+        N_("Mondays"),
+        N_("Tuesdays"),
+        N_("Wednesdays"),
+        N_("Thursdays"),
+        N_("Fridays"),
+        N_("Saturdays"),
+        N_("Sundays")
+    };
+
+    private static string[] abbreviations = {
+         (new GLib.DateTime.utc (1, 1, 1, 0, 0, 0)).format ("%a"),
+         (new GLib.DateTime.utc (1, 1, 2, 0, 0, 0)).format ("%a"),
+         (new GLib.DateTime.utc (1, 1, 3, 0, 0, 0)).format ("%a"),
+         (new GLib.DateTime.utc (1, 1, 4, 0, 0, 0)).format ("%a"),
+         (new GLib.DateTime.utc (1, 1, 5, 0, 0, 0)).format ("%a"),
+         (new GLib.DateTime.utc (1, 1, 6, 0, 0, 0)).format ("%a"),
+         (new GLib.DateTime.utc (1, 1, 7, 0, 0, 0)).format ("%a"),
+    };
+
+    public static Day get_first_weekday () {
+        var d = clocks_cutils_get_week_start ();
+        return (Day) ((d + 6) % 7);
+    }
+
+    public static string plural (Day d) {
+        assert (d >= 0 && d < 7);
+        return plurals[d].dup ();
+    }
+
+    public static string abbreviation (Day d) {
+        assert (d >= 0 && d < 7);
+        return abbreviations[d].dup ();
+    }
+
+    private bool[] days= {
+        false, false, false, false, false, false, false
+    };
+
+    public Weekdays() {
+    }
+
+    private bool days_equal (bool[] d) {
+        assert (d.length == 7);
+        return (Memory.cmp (d, days, days.length * sizeof (bool)) == 0);
+    }
+
+    public bool equal (Weekdays d) {
+        return days_equal (d.days);
+    }
+
+    public bool get (Day d) {
+        assert (d >= 0 && d < 7);
+        return days[d];
+    }
+
+    public void set (Day d, bool on) {
+        assert (d >= 0 && d < 7);
+        days[d] = on;
+    }
+
+    public string get_label () {
+        string r = null;
+        int n = 0;
+        int first = -1;
+        for (int i = 0; i < 7; i++) {
+            if (get ((Day) i)) {
+                if (first < 0) {
+                    first = i;
+                }
+                n++;
+            }
+        }
+
+        if (n == 0) {
+            r = "".dup ();
+        } else if (n == 1) {
+            r = plural ((Day) first);
+        } else if (n == 7) {
+            r = _("Every Day").dup ();
+        } else if (days_equal (weekdays)) {
+            r = _("Weekdays").dup ();
+        } else {
+            string[] abbrs = {};
+            for (int i = 0; i < 7; i++) {
+                Day d = (get_first_weekday () + i) % 7;
+                if (get (d)) {
+                    abbrs += abbreviation (d);
+                }
+            }
+            r = string.joinv (", ", abbrs);
+        }
+        return r;
+    }
+
+    // Note that we serialze days according to ISO 8601
+    // (1 is Monday, 2 is Tuesday... 7 is Sunday)
+
+    public GLib.Variant serialize () {
+        var builder = new GLib.VariantBuilder (new VariantType ("ai"));
+        int32 i = 1;
+        foreach (var d in days) {
+            if (d) {
+                builder.add ("i", i);
+            }
+            i++;
+        }
+        return builder.end ();;
+    }
+
+    public static Weekdays deserialize (GLib.Variant days_variant) {
+        Weekdays d = new Weekdays ();
+        foreach (var v in days_variant) {
+            int32 i = v.get_int32 ();
+            if (i > 1 && i <= 7) {
+                d.set ((Day) (i - 1), true);
+            } else {
+                warning ("Invalid days %d", i);
+            }
+        }
+        return d;
+    }
+}
+
+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");
+        }
+
+        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..dd7224e
--- /dev/null
+++ b/src/widgets.vala
@@ -0,0 +1,520 @@
+/*
+ * 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 List<Gtk.Widget> buttons;
+    private List<Clock> clocks;
+
+    [CCode (notify = false)]
+    public Mode mode {
+        get {
+            return _mode;
+        }
+
+        set {
+            if (_mode != value) {
+                _mode = value;
+
+                show_modes = (_mode == Mode.NORMAL);
+
+                if (_mode == Mode.SELECTION) {
+                    get_style_context ().add_class ("selection-mode");
+                } else {
+                    get_style_context ().remove_class ("selection-mode");
+                }
+
+                notify_property ("mode");
+            }
+        }
+    }
+
+    private Mode _mode;
+
+    public Toolbar () {
+        Object (show_modes: true, vexpand: false);
+        get_style_context ().add_class (Gtk.STYLE_CLASS_MENUBAR);
+    }
+
+    public signal void clock_changed (Clock clock);
+
+    public void add_clock (Clock clock) {
+        var button = add_mode (clock.label) as Gtk.ToggleButton;
+        clocks.prepend (clock);
+        button.toggled.connect(() => {
+            if (button.active) {
+                clock_changed (clock);
+            }
+        });
+    }
+
+    // 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.Button add_button (string? icon_name, string? label, bool pack_start) {
+        var button = base.add_button (icon_name, label, pack_start);
+        buttons.prepend (button);
+        return (Gtk.Button) button;
+    }
+
+    public new void clear () {
+        foreach (Gtk.Widget button in buttons) {
+            button.destroy ();
+        }
+    }
+}
+
+private class DigitalClockRenderer : Gtk.CellRendererPixbuf {
+    const int CHECK_ICON_SIZE = 40;
+
+    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.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 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 - CHECK_ICON_SIZE - xpad;
+            }
+
+            int check_x = x_offset;
+            int check_y = cell_area.height - CHECK_ICON_SIZE - ypad;
+
+            context.save ();
+            context.add_class (Gtk.STYLE_CLASS_CHECK);
+
+            if (active) {
+                context.set_state (Gtk.StateFlags.ACTIVE);
+            }
+
+            context.render_check (cr, check_x, check_y, CHECK_ICON_SIZE, CHECK_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) {
+        base.get_size (widget, cell_area, out x_offset, out y_offset, out width, out height);
+        width += CHECK_ICON_SIZE / 4;
+        height += CHECK_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 ();
+                }
+
+                thumb_renderer.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, selection_mode: Gtk.SelectionMode.NONE, 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, (owned) 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 override bool button_press_event (Gdk.EventButton event) {
+        var path = get_path_at_pos ((int) event.x, (int) event.y);
+        if (path != null) {
+            if (mode == Mode.SELECTION) {
+                var store = (Gtk.ListStore) model;
+                Gtk.TreeIter i;
+                if (store.get_iter (out i, path)) {
+                    bool selected;
+                    store.get (i, selection_col, out selected);
+                    store.set (i, selection_col, !selected);
+                    selection_changed ();
+                }
+            } else {
+                item_activated (path);
+            }
+        }
+
+        return false;
+    }
+
+    // Redefine selection handling methods since we handle selection manually
+
+    public new List<Gtk.TreePath>? get_selected_items () {
+        List<Gtk.TreePath>? items = null;
+        model.foreach ((model, path, iter) => {
+            bool selected;
+            ((Gtk.ListStore) model).get (iter, selection_col, out selected);
+            if (selected) {
+                items.prepend (path);
+            }
+            return false;
+        });
+        items.reverse ();
+        return (owned) items;
+    }
+
+    public new void select_all () {
+        var model = get_model () as Gtk.ListStore;
+        model.foreach ((model, path, iter) => {
+            ((Gtk.ListStore) model).set (iter, selection_col, true);
+            return false;
+        });
+        selection_changed ();
+    }
+
+    public new void unselect_all () {
+        var model = get_model () as Gtk.ListStore;
+        model.foreach ((model, path, iter) => {
+            ((Gtk.ListStore) model).set (iter, selection_col, false);
+            return false;
+        });
+        selection_changed ();
+    }
+
+    public void remove_selected () {
+        foreach (Gtk.TreePath path in get_selected_items ()) {
+            Gtk.TreeIter i;
+            if (((Gtk.ListStore) model).get_iter (out i, path)) {
+                ((Gtk.ListStore) model).remove (i);
+            }
+        }
+        selection_changed ();
+    }
+}
+
+public class ContentView : Gtk.Bin {
+    private const int SELECTION_TOOLBAR_WIDTH = 300;
+
+    public bool empty { get; private set; default = true; }
+
+    private Gtk.Widget empty_page;
+    private IconView icon_view;
+    private Toolbar main_toolbar;
+    private GLib.MenuModel selection_menu;
+    private Gtk.Toolbar selection_toolbar;
+    private Gtk.Overlay overlay;
+    private Gtk.ScrolledWindow scrolled_window;
+
+    public ContentView (Gtk.Widget e, IconView iv, Toolbar t) {
+        empty_page = e;
+        icon_view = iv;
+        main_toolbar = t;
+
+        var builder = Utils.load_ui ("menu.ui");
+        selection_menu = builder.get_object ("selection-menu") as GLib.MenuModel;
+
+        overlay = new Gtk.Overlay ();
+        overlay.add (icon_view);
+
+        selection_toolbar = create_selection_toolbar ();
+        overlay.add_overlay (selection_toolbar);
+
+        scrolled_window = new Gtk.ScrolledWindow (null, null);
+        scrolled_window.add (overlay);
+
+        var model = icon_view.get_model ();
+        model.row_inserted.connect(() => {
+            update_empty_view (model);
+        });
+        model.row_deleted.connect(() => {
+            update_empty_view (model);
+        });
+
+        icon_view.notify["mode"].connect (() => {
+            if (icon_view.mode == IconView.Mode.SELECTION) {
+                main_toolbar.mode = Toolbar.Mode.SELECTION;
+            } else if (icon_view.mode == IconView.Mode.NORMAL) {
+                main_toolbar.mode = Toolbar.Mode.NORMAL;
+            }
+        });
+
+        icon_view.selection_changed.connect (() => {
+            var items = icon_view.get_selected_items ();
+            var n_items = items.length ();
+
+            string label;
+            if (n_items == 0) {
+                label = _("Click on items to select them");
+            } else {
+                label = ngettext ("%d selected", "%d selected", n_items).printf (n_items);
+            }
+            main_toolbar.set_labels (label, null);
+
+            selection_toolbar.set_visible (n_items != 0);
+        });
+
+        add (empty_page);
+    }
+
+    public signal void delete_selected ();
+
+    private Gtk.Toolbar create_selection_toolbar () {
+        var toolbar = new Gtk.Toolbar ();
+        toolbar.show_arrow = false;
+        toolbar.icon_size = Gtk.IconSize.LARGE_TOOLBAR;
+        toolbar.halign = Gtk.Align.CENTER;
+        toolbar.valign = Gtk.Align.END;
+        toolbar.margin_bottom = 40;
+        toolbar.get_style_context ().add_class ("osd");
+        toolbar.set_size_request (SELECTION_TOOLBAR_WIDTH, -1);
+        toolbar.no_show_all = true;
+
+        var delete_button = new Gtk.Button.with_label (_("Delete"));
+        delete_button.hexpand = true;
+        delete_button.clicked.connect (() => {
+            delete_selected ();
+        });
+
+        var hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
+        hbox.hexpand = true;
+        hbox.add (delete_button);
+
+        var item = new Gtk.ToolItem ();
+        item.set_expand (true);
+        item.add (hbox);
+        item.show_all ();
+
+        toolbar.insert (item, -1);
+
+        return toolbar;
+    }
+
+    private void update_empty_view (Gtk.TreeModel model) {
+        Gtk.TreeIter i;
+
+        var child = get_child ();
+        if (model.get_iter_first (out i)) {
+            if (child != scrolled_window) {
+                remove (child);
+                add (scrolled_window);
+                empty = false;
+            }
+        } else {
+            if (child != empty_page) {
+                remove (child);
+                add (empty_page);
+                empty = true;
+            }
+        }
+        show_all ();
+    }
+
+    public void update_toolbar () {
+        switch (main_toolbar.mode) {
+        case Toolbar.Mode.SELECTION:
+            var done_button = main_toolbar.add_button (null, _("Done"), false);
+            main_toolbar.set_labels (_("Click on items to select them"), null);
+            main_toolbar.set_labels_menu (selection_menu);
+            done_button.get_style_context ().add_class ("suggested-action");
+            done_button.clicked.connect (() => {
+                selection_toolbar.set_visible (false);
+                icon_view.mode = IconView.Mode.NORMAL;
+            });
+            break;
+        case Toolbar.Mode.NORMAL:
+            main_toolbar.set_labels (null, null);
+            main_toolbar.set_labels_menu (null);
+            var select_button = main_toolbar.add_button ("object-select-symbolic", null, false);
+            select_button.clicked.connect (() => {
+                icon_view.mode = IconView.Mode.SELECTION;
+            });
+            bind_property ("empty", select_button, "sensitive", BindingFlags.SYNC_CREATE | 
BindingFlags.INVERT_BOOLEAN);
+            break;
+        }
+    }
+}
+
+public class AmPmToggleButton : Gtk.Button {
+    public enum AmPm {
+        AM,
+        PM
+    }
+
+    public AmPm choice {
+        get {
+            return _choice;
+        }
+        set {
+            if (_choice != value) {
+                _choice = value;
+                stack.visible_child = _choice == AmPm.AM ? am_label : pm_label;
+            }
+        }
+    }
+
+    private AmPm _choice;
+    private Gd.Stack stack;
+    private Gtk.Label am_label;
+    private Gtk.Label pm_label;
+
+    public AmPmToggleButton () {
+        stack = new Gd.Stack ();
+
+        // TODO: get translated AM/PM strings from the system
+        am_label = new Gtk.Label ("AM");
+        pm_label = new Gtk.Label ("PM");
+
+        stack.add (am_label);
+        stack.add (pm_label);
+        add (stack);
+
+        clicked.connect (() => {
+            choice = choice == AmPm.AM ? AmPm.PM : AmPm.AM;
+        });
+
+        choice = AmPm.AM;
+        stack.visible_child = am_label;
+        show_all ();
+    }
+}
+
+} // namespace Clocks
diff --git a/src/window.vala b/src/window.vala
new file mode 100644
index 0000000..dfefa64
--- /dev/null
+++ b/src/window.vala
@@ -0,0 +1,167 @@
+/*
+ * 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 = {
+        // app menu
+        { "new", on_new_activate },
+        { "about", on_about_activate },
+
+        // selection menu
+        { "select-all", on_select_all_activate },
+        { "select-none", on_select_none_activate }
+    };
+
+    private Toolbar toolbar;
+    private Gd.Stack stack;
+    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);
+
+        toolbar = new Toolbar ();
+
+        world = new World.MainPanel (toolbar);
+        alarm = new Alarm.MainPanel (toolbar);
+        stopwatch = new Stopwatch.MainPanel (toolbar);
+        timer = new Timer.MainPanel (toolbar);
+
+        toolbar.add_clock (world);
+        toolbar.add_clock (alarm);
+        toolbar.add_clock (stopwatch);
+        toolbar.add_clock (timer);
+
+        stack = new Gd.Stack ();
+        stack.duration = 0;
+        stack.add (world);
+        stack.add (alarm);
+        stack.add (stopwatch);
+        stack.add (timer);
+
+        toolbar.clock_changed.connect ((c) => {
+            stack.visible_child = (Gtk.Widget) c;
+        });
+
+        var id = stack.notify["visible-child"].connect (() => {
+            update_toolbar ();
+        });
+
+        toolbar.notify["mode"].connect (() => {
+            update_toolbar ();
+        });
+
+        toolbar.destroy.connect(() => {
+            stack.disconnect (id);
+            id = 0;
+        });
+
+        alarm.ring.connect ((w) => {
+            stack.visible_child = w;
+        });
+
+        timer.ring.connect ((w) => {
+            stack.visible_child = w;
+        });
+
+        stack.visible_child = world;
+        world.update_toolbar ();
+
+        var frame = new Gtk.Frame (null);
+        frame.get_style_context ().add_class ("clocks-content-view");
+        frame.get_style_context ().add_class ("view");
+        frame.get_style_context ().add_class ("content-view");
+        frame.add (stack);
+
+        var vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
+        vbox.pack_start (toolbar, false, false, 0);
+        vbox.pack_end (frame, true, true, 0);
+        add (vbox);
+
+        show_all ();
+    }
+
+    private void on_new_activate () {
+        ((Clock) stack.visible_child).activate_new ();
+    }
+
+    private void on_select_all_activate () {
+        ((Clock) stack.visible_child).activate_select_all ();
+    }
+
+    private void on_select_none_activate () {
+        ((Clock) stack.visible_child).activate_select_none ();
+    }
+
+    private 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);
+    }
+
+    private void update_toolbar () {
+        toolbar.clear ();
+        var clock = (Clock) stack.visible_child;
+        if (clock != null) {
+            clock.update_toolbar ();
+        }
+    }
+}
+
+} // namespace Clocks
diff --git a/src/world.ui b/src/world.ui
new file mode 100644
index 0000000..cecd2ce
--- /dev/null
+++ b/src/world.ui
@@ -0,0 +1,213 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.0 -->
+  <object class="GtkGrid" id="empty_panel">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="halign">center</property>
+    <property name="valign">center</property>
+    <property name="row_spacing">6</property>
+    <child>
+      <object class="GtkImage" id="image1">
+        <property name="visible">True</property>
+        <property name="sensitive">False</property>
+        <property name="can_focus">False</property>
+        <property name="icon_name">document-open-recent-symbolic</property>
+        <property name="icon-size">6</property>
+      </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="GtkLabel" id="label1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label" translatable="yes">Select &lt;b&gt;New&lt;/b&gt; to add a world 
clock</property>
+        <property name="use_markup">True</property>
+        <style>
+          <class name="dim-label"/>
+        </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="location_dialog_content">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="halign">center</property>
+    <property name="valign">center</property>
+    <property name="border_width">5</property>
+    <property name="row_spacing">6</property>
+    <child>
+      <object class="GtkLabel" id="label2">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">0</property>
+        <property name="label" translatable="yes">Search for a city:</property>
+      </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>
+  </object>
+  <object class="GtkGrid" id="standalone_content">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkGrid" id="grid2">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <property name="margin_left">34</property>
+        <property name="margin_right">34</property>
+        <property name="margin_bottom">12</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <child>
+          <object class="GtkLabel" id="dummy_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="label" translatable="yes">  </property>
+          </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="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">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="day_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <style>
+              <class name="clocks-secondary-time-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">2</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="GtkGrid" id="grid3">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">center</property>
+        <property name="margin_bottom">24</property>
+        <property name="column_spacing">12</property>
+        <style>
+          <class name="clocks-secondary-time-label"/>
+        </style>
+        <child>
+          <object class="GtkLabel" id="label3">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="label" translatable="yes">Sunrise</property>
+            <style>
+              <class name="dim-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="GtkLabel" id="label4">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="label" translatable="yes">Sunset</property>
+            <style>
+              <class name="dim-label"/>
+            </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="GtkLabel" id="sunrise_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="label" translatable="yes">label</property>
+          </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="sunset_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+          </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">1</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkSizeGroup" id="sizegroup1">
+    <property name="mode">vertical</property>
+    <widgets>
+      <widget name="dummy_label"/>
+      <widget name="day_label"/>
+    </widgets>
+  </object>
+</interface>
diff --git a/src/world.vala b/src/world.vala
new file mode 100644
index 0000000..1b6daa2
--- /dev/null
+++ b/src/world.vala
@@ -0,0 +1,392 @@
+/*
+ * 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 {
+
+static GWeather.Location? gweather_world = null;
+
+private GWeather.Location get_world_location () {
+    if (gweather_world == null) {
+        gweather_world = new GWeather.Location.world (true);
+    }
+    return gweather_world;
+}
+
+private class Item : Object {
+    public GWeather.Location location { get; set; }
+    public string name {
+        owned get {
+            var nation = location;
+
+            while (nation != null && nation.get_level () != GWeather.LocationLevel.COUNTRY) {
+                nation = nation.get_parent ();
+            }
+
+            if (nation != null) {
+                return "%s, %s".printf (location.get_city_name (), nation.get_name ());
+            } else {
+                return location.get_city_name ();
+            }
+        }
+    }
+
+    public bool is_daytime {
+         get {
+            return weather_info.is_daytime ();
+        }
+    }
+
+    public string sunrise_label {
+        owned get {
+            return weather_info.get_sunrise ();
+        }
+    }
+
+    public string sunset_label {
+        owned get {
+            return weather_info.get_sunset ();
+        }
+    }
+
+    public string time_label {
+        owned get {
+            return Utils.WallClock.get_default ().format_time (date_time);
+        }
+    }
+
+    public string? day_label {
+        get {
+            var d = date_time.get_day_of_year ();
+            var t = local_time.get_day_of_year ();
+
+            // FIXME: last day of the year
+            if (d < t) {
+                return _("Yesterday");
+            } else if (d > t) {
+                return _("Yesterday");
+            } else {
+                return null;
+            }
+        }
+    }
+
+    private GLib.TimeZone time_zone;
+    private GLib.DateTime local_time;
+    private GLib.DateTime date_time;
+    private GWeather.Info weather_info;
+
+    public Item (GWeather.Location location) {
+        Object (location: location);
+
+        var weather_time_zone = location.get_timezone ();
+        time_zone = new GLib.TimeZone (weather_time_zone.get_tzid());
+
+        tick ();
+    }
+
+    private void tick () {
+        var wallclock = Utils.WallClock.get_default ();
+        local_time = wallclock.date_time;
+        date_time = local_time.to_timezone (time_zone);
+
+        // We don't need to call update(), we're using only astronomical data
+        weather_info = new GWeather.Info.for_world (get_world_location (), location, 
GWeather.ForecastType.LIST);
+    }
+
+    public void serialize (GLib.VariantBuilder builder) {
+        builder.open (new GLib.VariantType ("a{sv}"));
+        builder.add ("{sv}", "location", location.serialize ());
+        builder.close ();
+    }
+
+    public static Item deserialize (GLib.Variant location_variant) {
+        GWeather.Location? location = null;
+        foreach (var v in location_variant) {
+            var key = v.get_child_value (0).get_string ();
+            if (key == "location") {
+                location = get_world_location ().deserialize (v.get_child_value (1).get_child_value (0));
+            }
+        }
+        return location != null ? new Item (location) : null;
+    }
+}
+
+private class LocationDialog : Gtk.Dialog {
+    private GWeather.LocationEntry entry;
+    private GLib.ThemedIcon find_icon;
+    private GLib.ThemedIcon clear_icon;
+
+    public LocationDialog (Gtk.Window parent) {
+        Object (transient_for: parent, modal: true, title: _("Add a New World Clock"));
+
+        add_buttons (Gtk.Stock.CANCEL, 0, Gtk.Stock.ADD, 1);
+        set_default_response (1);
+        set_response_sensitive (1, false);
+
+        var builder = Utils.load_ui ("world.ui");
+
+        var grid = builder.get_object ("location_dialog_content") as Gtk.Grid;
+        get_content_area ().add (grid);
+
+        entry = new GWeather.LocationEntry (get_world_location ());
+        entry.set_size_request (400, -1);
+        find_icon = new GLib.ThemedIcon.with_default_fallbacks ("edit-find-symbolic");
+        clear_icon = new GLib.ThemedIcon.with_default_fallbacks ("edit-clear-symbolic");
+        entry.set_icon_from_gicon (Gtk.EntryIconPosition.SECONDARY, find_icon);
+        entry.set_activates_default (true);
+        entry.show ();
+        grid.attach (entry, 0, 1, 1, 1);
+
+        entry.changed.connect (() => {
+            location_changed ();
+        });
+        entry.activate.connect (() => {
+            location_changed ();
+        });
+        entry.icon_release.connect (() => {
+            if (entry.get_icon_gicon (Gtk.EntryIconPosition.SECONDARY) == clear_icon) {
+                entry.set_text ("");
+            }
+        });
+    }
+
+    private void location_changed () {
+        GWeather.Location? l = null;
+        if (entry.get_text () == "") {
+            entry.set_icon_from_gicon (Gtk.EntryIconPosition.SECONDARY, find_icon);
+        } else {
+            entry.set_icon_from_gicon (Gtk.EntryIconPosition.SECONDARY, clear_icon);
+            l = entry.get_location ();
+        }
+
+        set_response_sensitive(1, l != null);
+    }
+
+    public Item? get_location () {
+        var location = entry.get_location ();
+        return location != null ? new Item (location) : null;
+    }
+}
+
+private class StandalonePanel : Gtk.EventBox {
+    public Item location { get; set; }
+
+    private Gtk.Label time_label;
+    private Gtk.Label day_label;
+    private Gtk.Label sunrise_label;
+    private Gtk.Label sunset_label;
+
+    public StandalonePanel () {
+        get_style_context ().add_class ("view");
+        get_style_context ().add_class ("content-view");
+
+        var builder = Utils.load_ui ("world.ui");
+
+        var grid = builder.get_object ("standalone_content") as Gtk.Grid;
+        time_label = builder.get_object ("time_label") as Gtk.Label;
+        day_label = builder.get_object ("day_label") as Gtk.Label;
+        sunrise_label = builder.get_object ("sunrise_label") as Gtk.Label;
+        sunset_label = builder.get_object ("sunset_label") as Gtk.Label;
+
+        add (grid);
+    }
+
+    public void update () {
+        if (location != null) {
+            time_label.label = location.time_label;
+            day_label.label = location.day_label;
+            sunrise_label.label = location.sunrise_label;
+            sunset_label.label = location.sunset_label;
+        }
+    }
+}
+
+public class MainPanel : Gd.Stack, Clocks.Clock {
+    private enum Column {
+        SELECTED,
+        NAME,
+        ITEM,
+        COLUMNS
+    }
+
+    public string label { get; construct set; }
+    public Toolbar toolbar { get; construct set; }
+
+    private List<Item> locations;
+    private GLib.Settings settings;
+    private Gdk.Pixbuf? day_pixbuf;
+    private Gdk.Pixbuf? night_pixbuf;
+    private Gtk.ListStore list_store;
+    private IconView icon_view;
+    private ContentView content_view;
+    private StandalonePanel standalone;
+
+    public MainPanel (Toolbar toolbar) {
+        Object (label: _("World"), toolbar: toolbar, homogeneous: true);
+
+        locations = new List<Item> ();
+        settings = new GLib.Settings("org.gnome.clocks");
+
+        day_pixbuf = Utils.load_image ("day.png");
+        night_pixbuf = Utils.load_image ("night.png");
+
+        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) => {
+            Item location;
+            model.get (iter, Column.ITEM, out location);
+            var renderer = (DigitalClockRenderer) cell;
+            renderer.text = location.time_label;
+            renderer.subtext = location.day_label;
+            if (location.is_daytime) {
+                renderer.pixbuf = day_pixbuf;
+                renderer.css_class = "light";
+            } else {
+                renderer.pixbuf = night_pixbuf;
+                renderer.css_class = "dark";
+            }
+        });
+
+        icon_view.item_activated.connect ((path) => {
+            Gtk.TreeIter i;
+            if (list_store.get_iter (out i, path)) {
+                Item location;
+                list_store.get (i, Column.ITEM, out location);
+                standalone.location = location;
+                standalone.update ();
+                visible_child = standalone;
+            }
+        });
+
+        var builder = Utils.load_ui ("world.ui");
+        var empty_view = builder.get_object ("empty_panel") as Gtk.Widget;
+        content_view = new ContentView (empty_view, icon_view, toolbar);
+        add (content_view);
+
+        content_view.delete_selected.connect (() => {
+            // FIXME: this is not efficient, but we have few itesm and
+            // we are probably going to drop the TreeModel soon
+            foreach (Gtk.TreePath path in icon_view.get_selected_items ()) {
+                Gtk.TreeIter i;
+                if (list_store.get_iter (out i, path)) {
+                    Item location;
+                    list_store.get (i, Column.ITEM, out location);
+                    locations.remove (location);
+                }
+            }
+            icon_view.remove_selected ();
+            save ();
+        });
+
+        standalone = new StandalonePanel ();
+        add (standalone);
+
+        load ();
+
+        var id = notify["visible-child"].connect (() => {
+            if (visible_child == content_view) {
+                toolbar.mode = Toolbar.Mode.NORMAL;
+            } else if (visible_child == standalone) {
+                toolbar.mode = Toolbar.Mode.STANDALONE;
+            }
+        });
+        toolbar.destroy.connect(() => {
+            disconnect (id);
+            id = 0;
+        });
+
+        visible_child = content_view;
+        show_all ();
+    }
+
+    private void load () {
+        foreach (var l in settings.get_value ("world-clocks")) {
+            Item location = Item.deserialize (l);
+            locations.prepend (location);
+            add_location_to_store (location);
+        }
+        locations.reverse ();
+    }
+
+    private void save () {
+        var builder = new GLib.VariantBuilder (new VariantType ("aa{sv}"));
+        foreach (Item i in locations) {
+            i.serialize (builder);
+        }
+        settings.set_value ("world-clocks", builder.end ());
+    }
+
+    private void add_location_to_store (Item location) {
+        var label = "<b>%s</b>".printf (GLib.Markup.escape_text (location.name));
+        Gtk.TreeIter i;
+        list_store.append (out i);
+        list_store.set (i, Column.SELECTED, false, Column.NAME, label, Column.ITEM, location);
+    }
+
+    public void activate_new () {
+        var dialog = new LocationDialog ((Gtk.Window) get_toplevel ());
+
+        dialog.response.connect ((dialog, response) => {
+            if (response == 1) {
+                var location = ((LocationDialog) dialog).get_location ();
+                locations.append (location);
+                add_location_to_store (location);
+                save ();
+            }
+            dialog.destroy ();
+        });
+        dialog.show ();
+    }
+
+    public void activate_select_all () {
+        icon_view.select_all ();
+    }
+
+    public void activate_select_none () {
+        icon_view.unselect_all ();
+    }
+
+    public void update_toolbar () {
+        toolbar.clear ();
+        switch (toolbar.mode) {
+        case Toolbar.Mode.NORMAL:
+            // Translators: "New" refers to a world clock
+            var new_button = toolbar.add_button (null, _("New"), true);
+            new_button.clicked.connect (() => {
+                activate_new ();
+            });
+            content_view.update_toolbar ();
+            break;
+        case Toolbar.Mode.SELECTION:
+            content_view.update_toolbar ();
+            break;
+        case Toolbar.Mode.STANDALONE:
+            var back_button = toolbar.add_button ("go-previous-symbolic", null, true);
+            back_button.clicked.connect (() => {
+                visible_child = content_view;
+            });
+            toolbar.set_labels (GLib.Markup.escape_text (standalone.location.name), null);
+            break;
+        default:
+            assert_not_reached ();
+        }
+    }
+}
+
+} // namespace World
+} // namespace Clocks


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