diff --git a/pitivi/marks.py b/pitivi/marks.py
new file mode 100644
index 0000000..80f67bc
--- /dev/null
+++ b/pitivi/marks.py
@@ -0,0 +1,144 @@
+#!/usr/bin/python
+# PiTiVi , Non-linear video editor
+#
+#       marks.py
+#
+# Copyright (c) 2010, Robin Norwood <robin.norwood@gmail.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+"""
+Provide classes for a mark at a given time offset, a region, and a list of marks associated with a timeline.
+"""
+
+from pitivi.log.loggable import Loggable
+from pitivi.signalinterface import Signallable
+
+class Mark(Loggable):
+    """
+    Base class for L{TimeMark} and L{RegionMark}.
+
+    @ivar label: Label of this mark.
+    @type label: L{string}
+    """
+
+    def __init__(self, label=None):
+        Loggable.__init__(self)
+        self.label = label
+        self.rgb = (1.0, 0.0, 0.0)
+
+
+class TimeMark(Mark):
+    """
+    A Mark at a specific point in time.
+
+    @ivar time: Time at which the mark occurs (nanoseconds).
+    @type time: L{long}
+    """
+
+    def __init__(self, when, label=None, rgb=None):
+        Mark.__init__(self, label)
+        if not rgb:
+            rgb = (1.0, 0.0, 0.0)
+        self.time = when
+
+
+class RegionMark(Mark):
+    """
+    A Mark for a region on a timeline, with a start time and duration.
+
+    @ivar start: Time at which the marked region starts (nanoseconds).
+    @type start: L{long}
+    @ivar duration: Duration of marked region (nanoseconds).
+    @type duration: L{long}
+    """
+
+    def __init__(self, when, duration):
+        Mark.__init__(self)
+        self.start = when
+        self.duration = duration
+
+
+class MarkList(Loggable, Signallable):
+    """
+    A list of Mark objects attached to a Timeline.
+
+    @ivar timeline: Timeline the marks are for.
+    @type timeline: L{Timeline}
+    @ivar marks: List of Marks on the timeline.
+    @type marks: L{list}
+    """
+
+    def __init__(self, timeline):
+        Loggable.__init__(self)
+        Signallable.__init__(self)
+
+        self.timeline = timeline
+
+        self.marks = []
+
+    def addTimeMark(self, *args, **kwargs):
+        """
+        Add a mark at a specific time.
+
+        @returns: The mark just added.
+        @rtype: L{TimeMark}
+        """
+        mark = TimeMark(*args, **kwargs)
+        self.marks.append(mark)
+
+        return mark
+
+    def addRegionMark(self, *args, **kwargs):
+        """
+        Add a region mark at a specific time.
+
+        @returns: The mark just added.
+        @rtype: L{RegionMark}
+        """
+        mark = RegionMark(*args, **kwargs)
+        self.marks.append(mark)
+
+        return mark
+
+    def append(self, mark):
+        """
+        Add a Mark object.
+
+        @ivar mark: Mark object
+        @type mark: L{Mark}
+        """
+
+        # Delete existing mark, if any.
+        self.delete(mark)
+        self.marks.append(mark)
+
+        return
+
+    def delete(self, mark):
+        """
+        Delete a Mark object, if it exists.
+
+        @ivar mark: Mark object
+        @type mark: L{Mark}
+        """
+
+        self.marks = [ m for m in self.marks if m.time != mark.time ]
+
+        return
+
+    def __iter__(self):
+        return getattr(self.marks, '__iter__')()
diff --git a/pitivi/formatters/etree.py b/pitivi/formatters/etree.py
index 34de3ec..08dd480 100644
--- a/pitivi/formatters/etree.py
+++ b/pitivi/formatters/etree.py
@@ -29,6 +29,7 @@ from xml.etree.ElementTree import Element, SubElement, tostring, parse
 from pitivi.reflect import qual, namedAny
 from pitivi.factories.base import SourceFactory
 from pitivi.factories.file import FileSourceFactory
+from pitivi.marks import TimeMark
 from pitivi.timeline.track import Track
 from pitivi.timeline.timeline import TimelineObject
 from pitivi.formatters.base import Formatter, FormatterError
@@ -546,6 +547,9 @@ class ElementTreeFormatter(Formatter):
     def _saveTimeline(self, timeline):
         element = Element("timeline")
 
+        marks = self._saveMarks(timeline.marks)
+        element.append(marks)
+
         tracks = self._saveTracks(timeline.tracks)
         element.append(tracks)
 
@@ -560,6 +564,10 @@ class ElementTreeFormatter(Formatter):
 
         timeline = self.project.timeline
 
+        # Marks
+        marks_element = element.find("marks")
+        marks = self._loadMarks(marks_element)
+
         # Tracks
         tracks_element = element.find("tracks")
         tracks = self._loadTracks(tracks_element)
@@ -572,6 +580,9 @@ class ElementTreeFormatter(Formatter):
         for track in tracks:
             timeline.addTrack(track)
 
+        for mark in marks:
+            timeline.marks.append(mark)
+
         # add the timeline objects
         for timeline_object in timeline_objects:
             # NOTE: this is a low-level routine that simply appends the
@@ -581,6 +592,40 @@ class ElementTreeFormatter(Formatter):
 
         return timeline
 
+    # Marks
+
+    def _saveMarks(self, marks):
+        element = Element("marks")
+        for mark in marks:
+            mark_element = self._saveTimeMark(mark)
+            element.append(mark_element)
+
+        return element
+
+    def _loadMarks(self, element):
+        marks = []
+        for mark_element in element:
+            mark = self._loadTimeMark(mark_element)
+            marks.append(mark)
+
+        return marks
+
+    def _saveTimeMark(self, mark):
+        element = Element("time-mark")
+        element.attrib["time"] = str(mark.time)
+        element.attrib["label"] = str(mark.label)
+        element.attrib["red"] = str(mark.rgb[0])
+        element.attrib["green"] = str(mark.rgb[1])
+        element.attrib["blue"] = str(mark.rgb[2])
+
+        return element
+
+    def _loadTimeMark(self, element):
+        return TimeMark(int(element.attrib["time"]),
+                        element.attrib["label"],
+                        ([float(element.attrib[rgb]) for
+                          rgb in ("red", "green", "blue")]))
+
     ## Main methods
 
     def _saveMainTag(self):
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index b9bd4d8..77ac2d3 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -24,6 +24,7 @@ from bisect import bisect_left
 
 from pitivi.signalinterface import Signallable
 from pitivi.log.loggable import Loggable
+from pitivi.marks import MarkList
 from pitivi.utils import UNKNOWN_DURATION, closest_item, PropertyChangeTracker
 from pitivi.timeline.track import TrackObject, SourceTrackObject, TrackError
 from pitivi.stream import match_stream_groups_map
@@ -1410,6 +1411,8 @@ class Timeline(Signallable, Loggable):
     @type duration: C{long}
     @ivar selection: The currently selected TimelineObjects
     @type selection: L{Selection}
+    @ivar marks: List of marks on this timeline.
+    @type marks: List of L{Mark}
     """
     __signals__ = {
         'duration-changed': ['duration'],
@@ -1433,6 +1436,7 @@ class Timeline(Signallable, Loggable):
         self.dead_band = 10
         self.edges = TimelineEdges()
         self.property_trackers = {}
+        self.marks = MarkList(self)
 
     def addTrack(self, track):
         """
diff --git a/pitivi/ui/ruler.py b/pitivi/ui/ruler.py
index 6819f04..9e95694 100644
--- a/pitivi/ui/ruler.py
+++ b/pitivi/ui/ruler.py
@@ -30,6 +30,7 @@ import cairo
 from pitivi.ui.zoominterface import Zoomable
 from pitivi.log.loggable import Loggable
 from pitivi.utils import time_to_string
+from pitivi.marks import TimeMark
 
 class ScaleRuler(gtk.Layout, Zoomable, Loggable):
 
@@ -82,6 +83,11 @@ class ScaleRuler(gtk.Layout, Zoomable, Loggable):
         self.frame_rate = gst.Fraction(1/1)
         self.app = instance
 
+        self.set_has_tooltip(True)
+        self.connect("query-tooltip", self.do_query_tooltip_event)
+
+        self.markwin = None
+
     def _hadjValueChangedCb(self, hadj):
         self.pixel_position_offset = Zoomable.nsToPixel(self.position) - hadj.get_value()
 
@@ -153,8 +159,34 @@ class ScaleRuler(gtk.Layout, Zoomable, Loggable):
         # seek at position
         cur = self.pixelToNs(event.x)
         self._doSeek(cur)
+
+        if self.markwin:
+            self.markwin.destroy()
+
+        closest_mark = (100, None)
+
+        for mark in self.app.current.timeline.marks:
+            xpos = self.nsToPixel(mark.time) + self.border
+            distance = abs(xpos - event.x)
+            if event.x > xpos - 5 and event.x < xpos + 5 and distance < closest_mark[0]:
+                closest_mark = (distance, mark)
+
+        self.markwin = MarkTips(self, closest_mark[1], event.x, event.y)
+        self.markwin.show_all()
+
         return True
 
+    def addMark(self, mark):
+        if mark.label and len(mark.label) > 1:
+            self.app.current.timeline.marks.append(mark)
+        self.doPixmap()
+        self.queue_draw()
+
+    def deleteMark(self, mark):
+        self.app.current.timeline.marks.delete(mark)
+        self.doPixmap()
+        self.queue_draw()
+
     def do_button_release_event(self, event):
         self.debug("button released at x:%d", event.x)
         self.pressed = False
@@ -166,8 +198,22 @@ class ScaleRuler(gtk.Layout, Zoomable, Loggable):
             # seek at position
             cur = self.pixelToNs(event.x)
             self._doSeek(cur)
+
         return False
 
+    def do_query_tooltip_event(self, ruler, x, y, keybd, tip):
+        close_marks = []
+        for mark in self.app.current.timeline.marks:
+            xpos = self.nsToPixel(mark.time) + self.border
+            if x > xpos - 5 and x < xpos + 5:
+                close_marks.append(mark)
+        if len(close_marks) > 0:
+            tip.set_text("\n".join([ time_to_string(mark.time) + ": " + mark.label for mark in close_marks ]))
+            return True
+
+        return False
+
+
     def do_scroll_event(self, event):
         if event.direction == gtk.gdk.SCROLL_UP:
             Zoomable.zoomIn()
@@ -300,6 +346,7 @@ class ScaleRuler(gtk.Layout, Zoomable, Loggable):
 
         zoomRatio = self.zoomratio
         self.drawFrameBoundaries(allocation)
+        self.drawMarks(allocation, offset)
         self.drawTicks(allocation, offset, spacing, scale)
         self.drawTimes(allocation, offset, spacing, scale, layout)
 
@@ -347,6 +394,25 @@ class ScaleRuler(gtk.Layout, Zoomable, Loggable):
             paintpos += spacing
             seconds += interval
 
+    def drawMarks(self, allocation, offset):
+        if not self.app.current:
+            return
+
+        for mark in self.app.current.timeline.marks:
+            xpos = self.nsToPixel(mark.time) + self.border
+
+            if (xpos < self.pixmap_offset or
+                xpos > self.pixmap_offset + self.pixmap_allocated_width):
+                continue
+
+            xgc = self.bin_window.new_gc()
+            xgc.set_rgb_fg_color(gtk.gdk.Color(*mark.rgb))
+            xgc.set_line_attributes(10, gtk.gdk.LINE_SOLID, gtk.gdk.CAP_BUTT, gtk.gdk.JOIN_MITER)
+            self.pixmap.draw_line(
+                xgc,
+                xpos, 0,
+                xpos, allocation.height)
+
     def drawFrameBoundaries(self, allocation):
         ns_per_frame = float(1 / self.frame_rate) * gst.SECOND
         frame_width = self.nsToPixel(ns_per_frame)
@@ -380,3 +446,94 @@ class ScaleRuler(gtk.Layout, Zoomable, Loggable):
         context.stroke()
 
         context.restore()
+
+
+class MarkTips(gtk.Window, Loggable):
+    def __init__(self, ruler, mark, x, y):
+        # FIXME: if the next line is used, I can't get the MarkTips window to receive key press events.
+#        gtk.Window.__init__(self, gtk.WINDOW_POPUP)
+        gtk.Window.__init__(self)
+        Loggable.__init__(self)
+
+        self.ruler = ruler
+        self.timer_id = None
+
+        self.log("Creating mark tip window")
+
+        self.set_border_width(4)
+        self.set_decorated(False)
+        self.set_resizable(False)
+        self.set_app_paintable(True)
+        self.set_position(gtk.WIN_POS_MOUSE)
+        self.connect("expose-event", self.on_expose_event)
+        self.connect("focus-out-event", self.on_focus_out_event)
+
+        self.box = gtk.HBox()
+        self.add(self.box)
+
+        self.time_label = gtk.Label()
+        self.time_label.set_line_wrap(True)
+        self.time_label.set_alignment(0.5, 0.5)
+        self.time_label.set_use_markup(True)
+
+        if mark:
+            self.time_label.set_label(time_to_string(mark.time) + ": ")
+        else:
+            self.time_label.set_label(time_to_string(self.ruler.pixelToNs(x)) + ": ")
+            mark = TimeMark(self.ruler.pixelToNs(x))
+
+        self.mark = mark
+
+        self.box.add(self.time_label)
+
+        self.mark_entry = gtk.Entry(max=32)
+        self.mark_entry.add_events(gtk.gdk.KEY_PRESS_MASK)
+
+        if mark.label:
+            self.mark_entry.set_text(mark.label)
+
+        self.mark_entry.connect("key-press-event", self.on_key_press_event)
+
+        self.box.add(self.mark_entry)
+
+
+    def on_expose_event(self, window, event):
+        w, h = self.size_request()
+        self.style.paint_flat_box(self.window, gtk.STATE_NORMAL,
+                                  gtk.SHADOW_OUT, None, self,
+                                  'tooltip', 0, 0, w, h)
+
+
+    def on_focus_out_event(self, window, event):
+        self.timer_id = gobject.timeout_add(4000, self.delete)
+
+
+    def on_key_press_event(self, entry, event):
+        self.reset_timer()
+        keyval_name = gtk.gdk.keyval_name(event.keyval)
+
+        if keyval_name == 'Return':
+            self.mark.label = self.mark_entry.get_text()
+            self.ruler.addMark(self.mark)
+            self.delete()
+
+            return True
+
+        if keyval_name == 'Escape':
+            self.ruler.deleteMark(self.mark)
+            self.delete()
+
+            return True
+
+        return False
+
+
+    def delete(self):
+        self.destroy()
+        self.ruler.markwin = None
+
+
+    def reset_timer(self):
+        if self.timer_id:
+            gobject.source_remove(self.timer_id)
+            self.timer_id = None
