# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
#
# GNOME Activity Journal
#
# Copyright © 2009-2010 Seif Lotfy <seif@lotfy.com>
# Copyright © 2010 Randal Barlow <email.tehk@gmail.com>
# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com>
# Copyright © 2010 Markus Korn <thekorn@gmx.de>
# Copyright © 2010 Stefano Candori <stefano.candori@gmail.com>
# Copyright © 2020 The GNOME Activity Journal developers
#
# 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 3 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, see <http://www.gnu.org/licenses/>.

import gi
import datetime
import math
import urllib.request, urllib.parse, urllib.error
from urllib.parse import urlparse

gi.require_version('PangoCairo', '1.0')

from gi.repository import GLib, GObject, Pango, Gdk, PangoCairo

try:
    gi.require_version('Gst', '1.0')
    from gi.repository import Gst
    Gst.init(None)
    if Gst.ElementFactory.find("playbin") is None : Gst = None
except ImportError:
    Gst = None

from .common import *
from . import config, content_objects
from .config import event_exists, settings, bookmarker, preference_settings
from .sources import SUPPORTED_SOURCES
from .store import ContentStruct, CLIENT
from .supporting_widgets import DayLabel, ContextMenu, ContextMenuMolteplicity, StaticPreviewTooltip, VideoPreviewTooltip,\
SearchBox, AudioPreviewTooltip
from zeitgeist.datamodel import ResultType, StorageState, TimeRange

#DND support variables
TYPE_TARGET_TEXT = 80
TYPE_TARGET_URI = 81

class Draggable():

    def __init__(self, widget):
        target_text = Gtk.TargetEntry.new("text/plain", 0, TYPE_TARGET_TEXT)
        target_uri_list  = Gtk.TargetEntry.new("text/uri-list", 0, TYPE_TARGET_URI)
        targets = [ target_text, target_uri_list ]

        widget.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, targets,
                Gdk.DragAction.COPY)
        widget.connect("drag_data_get", self.on_drag_data_get)

class Droppable():

    def __init__(self, widget):
        target = Gtk.TargetEntry.new("text/plain", 0, TYPE_TARGET_TEXT)
        targets = [target,]
        widget.drag_dest_set(Gtk.DestDefaults.MOTION |
                                    Gtk.DestDefaults.HIGHLIGHT |
                                    Gtk.DestDefaults.DROP,
                                    targets, Gdk.DragAction.COPY)
        widget.connect("drag_data_received", self.on_drag_data_received)


class _GenericViewWidget(Gtk.Grid):
    day = None
    day_signal_id = None
    icon_path = "path to an icon"# get_data_path("relative_path")
    dsc_text = "Description for toolbutton"# _("Switch to MultiView")

    def __init__(self):
        Gtk.Grid.__init__(self)

        self.props.orientation = Gtk.Orientation.VERTICAL
        self.style = self.get_style_context()
        self.style.add_class("generic-view")

        self.daylabel = DayLabel()
        self.add(self.daylabel)
        self.connect("style-updated", self.change_style)

    def set_day(self, day, store, force_update=False):
        self.store = store
        if self.day:
            self.day.disconnect(self.day_signal_id)
        self.day = day
        self.day_signal_id = self.day.connect("update", self.update_day)
        self.update_day(day, force_update)

    def update_day(self, day, force_update=False):
        self.daylabel.set_date(day.date)
        self.view.set_day(self.day, force_update)

    def click(self, widget, event):
        if event.button in (1, 3):
            self.emit("unfocus-day")

    def change_style(self, widget):
        color = get_gtk_rgba(self.style, "bg", 0)
        color = shade_gdk_color(color, 102/100.0)
        self.view.override_background_color(Gtk.StateFlags.NORMAL, color)
        # FixMe: revisit this
        #self.view.modify_base(Gtk.StateFlags.NORMAL, color)

class MultiViewContainer(Gtk.HBox):

    __gsignals__ = {
        "view-ready" : (GObject.SignalFlags.RUN_LAST, None,())
    }

    days = []
    num_pages = preference_settings["multi-view-num-days"]
    day_signal_id = [None] * num_pages
    day_page_map = {}
    icon_path = get_data_path("multiview_icon.png")
    dsc_text = _("Switch to MultiView")
    name = "multi-view"

    def __init__(self):
        super(MultiViewContainer, self).__init__()
        self.pages = []
        for i in range(self.num_pages):
            group = DayViewContainer()
            self.pages.append(group)
            self.pack_end(group, True, True, 6)
        SearchBox.connect("matching-days", self.on_matching_days_set)
        SearchBox.connect("clear", self.on_search_cleared)
        self.matching_days = []

    def set_day(self, day, store, force_update=False):
        t = time.time()
        if self.days:
            for i, _day in enumerate(self.__days(self.days[0], store)):
                signal = self.day_signal_id[i]
                if signal:
                    _day.disconnect(signal)
        self.days = self.__days(day, store)
        for i, day in enumerate(self.days):
            self.day_signal_id[i] = day.connect("update", self.update_day, day)
        self.update_days()
        self.update_day_label_search_matches()

    def __days(self, day, store):
        days = []
        for i in range(self.num_pages):
            days += [day]
            day = day.previous(store)
        return days

    def get_page_containing_day(self, day):
        for page in self.pages:
            if page.day == day:
                return page

        raise Exception("No page matching day: %s" % day)

    # check if all days are in order
    def check_day_order(self):
        last_day = None

        for page in self.get_children():
            if last_day != None:
                this_day = last_day.next()
                if this_day != page.day:
                    return False

            last_day = page.day

        return True

    # print days in multiview container
    def print_children(self):
        last_day = None

        for page in self.get_children():
            print ("page '%d' has day '%s'" % (page.cid, page.day))
            if last_day != None:
                this_day = last_day.next()
                if this_day != page.day:
                    print("ERROR: '%s'.next() != %s" % (last_day, this_day))

            last_day = page.day

    def on_matching_days_set(self, searchbox, matching_days):
        self.matching_days = matching_days
        if not self.matching_days:
            self.update_day_label_search_matches()

    def on_search_cleared(self, searchbox):
        self.matching_days = []
        self.update_day_label_search_matches()

    def update_day_label_search_matches(self):
        for page in self.pages:
            page.daylabel.set_matching(False)

        for page in self.pages:
            for day in self.matching_days:
                if (page.day.date == day):
                    page.daylabel.set_matching(True)
                    break

    def update_days(self, *args):
        page_days = set([page.day for page in self.pages])
        diff = list(set(self.days).difference(page_days))
        i = 0
        day_page = {}

        for page in self.pages:
            if not page.day in self.days:
                page.set_day(diff[i])
                day_page[page.day] = page
                i += 1

        # self.days should contain later days first for this to work.
        for day in self.days:
            page = self.get_page_containing_day(day)
            self.reorder_child(page, self.days.index(page.day))

        # check if we have out of order days.
        if not self.check_day_order():
            self.print_children()

    def update_day(self, *args):
        day = args[1]
        for page in self.pages:
            if page.day == day:
                page.set_day(day)
        self.emit("view-ready")

    def set_zoom(self, zoom):
        pass

    def set_slider(self, slider):
        pass

    def toggle_erase_mode(self):
        #yeah that'ugly..don't mind: it's only a temp solution
        config.IN_ERASE_MODE = not config.IN_ERASE_MODE
        #for page in self.pages:
        #    page.in_erase_mode = not page.in_erase_mode

class DayViewContainer(Gtk.VBox):
    event_templates = (
        Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri),
        Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri),
        Event.new_for_values(interpretation=Interpretation.ACCESS_EVENT.uri),
        Event.new_for_values(interpretation=Interpretation.SEND_EVENT.uri),
        Event.new_for_values(interpretation=Interpretation.RECEIVE_EVENT.uri)
    )

    container_id = 0

    def __init__(self):
        super(DayViewContainer, self).__init__()

        self.cid = DayViewContainer.container_id
        DayViewContainer.container_id += 1

        self.style = self.get_style_context()
        self.style.add_class("day-view-container")

        self.set_vexpand(True)
        self.daylabel = DayLabel()
        self.pack_start(self.daylabel, False, False, 0)
        self.dayviews = [DayView(title) for title in DayParts.get_day_parts()]
        self.scrolled_window = Gtk.ScrolledWindow()
        self.scrolled_window.set_shadow_type(Gtk.ShadowType.NONE)
        self.empty_label = get_empty_label(size=16000)
        self._box = Gtk.VBox()
        for dayview in self.dayviews:
            self._box.pack_start(dayview, False, False, 0)
        self._box.pack_end(self.empty_label, True, True, 0)
        self.scrolled_window.add(self._box)
        self.pack_end(self.scrolled_window, True, True, 0)
        self.scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        self.show_all()
        self.day = None
        self.in_erase_mode = False
        self.connect("style-updated", self.change_style)

    def set_day(self, day):
        # TODO: Don't duplicate half of the code for each view, use common
        # base classes and shared interfaces instead!

        day_is_loaded = day.is_loaded()
        self.empty_label.hide()
        self.day = day
        if pinbox in self._box.get_children():
            self._box.remove(pinbox)
        if (day.date - datetime.date.today()) == datetime.timedelta(days=0):
            self._box.pack_start(pinbox, False, False, 0)
            self._box.reorder_child(pinbox, 0)
        self.daylabel.set_date(day.date)

        parts = [[] for i in DayParts.get_day_parts()]
        uris = [[] for i in parts]

        list = day.filter(self.event_templates, result_type=ResultType.MostRecentEvents)
        for item in list:
            if not item.content_object:
                continue
            i = DayParts.get_day_part_for_item(item)
            uri = item.event.subjects[0].uri
            interpretation = item.event.interpretation
            if not uri in uris[i]:
                uris[i].append(uri)
                parts[i].append(item)

        show_empty_label = True
        for i, part in enumerate(parts):
            have_events = self.dayviews[i].set_items(part)
            if have_events:
                show_empty_label = False

        # Don't update labels if we're not loaded yet, since the dbus
        # callback will return here with proper data.
        if not day_is_loaded:
            return

        if show_empty_label:
            self.empty_label.show()
            for dayview in self.dayviews:
                dayview.label.hide()
        else:
            self.empty_label.hide()
            if not config.HIDE_EMPTY_DAY_LABELS:
                for dayview in self.dayviews:
                    dayview.label.show()

    def change_style(self, widget):
        color = get_gtk_rgba(self.style, "bg", 0)
        bgcolor = shade_gdk_color(color, 102/100.0)
        self._box.override_background_color(Gtk.StateFlags.NORMAL, bgcolor)


class DayView(Gtk.VBox):

    def __init__(self, title=None, pinbox=False):
        super(DayView, self).__init__()

        self.style = self.get_style_context()
        self.style.add_class("day-view")
        self.pinbox = pinbox

        # Create the title label
        if title:
            self.label = Gtk.Label(label=title)
            self.label.set_alignment(0.03, 0.5)
            self.pack_start(self.label, False, False, 6)
        # Create the main container
        self.view = None
        # Connect to relevant signals
        self.connect("style-updated", self.on_style_change)
        self.show_all()

    def on_style_change(self, widget):
        """ Update used colors according to the system theme. """
        color = get_gtk_rgba(self.style, "bg", 0)
        fcolor = get_gtk_rgba(self.style, "fg", 0)
        color = combine_gdk_color(color, fcolor)
        if self.pinbox:
            shade = 0.5 if is_light_theme() else 2
            color = shade_gdk_color(color, shade)
        else:
            shade = 0.75 if is_light_theme() else 1.5
            color = shade_gdk_color(color, shade)

        if hasattr(self, "label"):
            self.label.override_color(Gtk.StateFlags.NORMAL, color)

    def clear(self):
        if self.view:
            if self.view in self.get_children():
                self.remove(self.view)
            self.view.destroy()
        self.view = Gtk.VBox()
        self.pack_start(self.view, False, True, 0)

    def set_items(self, items):
        self.clear()
        categories = {}
        for struct in items:
            if not struct.content_object: continue
            subject = struct.event.subjects[0]
            interpretation = subject.interpretation
            if interpretation in INTERPRETATION_PARENTS:
                interpretation = INTERPRETATION_PARENTS[interpretation]
            if struct.event.actor == "application://tomboy.desktop":
                interpretation = "aj://note"
            if struct.event.actor == "application://bzr.desktop":
                interpretation = "aj://vcs"
            if interpretation not in categories:
                categories[interpretation] = []
            categories[interpretation].append(struct)
        if not categories:
            if config.HIDE_EMPTY_DAY_LABELS:
                self.hide()
            else:
                self.view.hide()
        else:
            ungrouped_events = []
            for key in sorted(categories.keys()):
                events = categories[key]
                if len(events) > 3:
                    box = CategoryBox(key, list(reversed(events)))
                    self.view.pack_start(box, False, True, 0)
                else:
                    ungrouped_events += events
            box = CategoryBox(None, ungrouped_events)
            self.view.pack_start(box, False, True, 0)
            self.show_all()

        return True if categories else False

class CategoryBox(Gtk.HBox):
    set_up_done = False
    EXPANDED = {}
    event_count = 0

    def _set_up_box(self, event_structs):
        if not self.set_up_done:
            self.set_up_done = True
            for struct in event_structs:
                if not struct.content_object:continue
                if self.itemoff > 0:
                    item = Item(struct, self.pinnable, False)
                else:
                    item = Item(struct, self.pinnable, True)
                hbox = Gtk.HBox ()
                hbox.pack_start(item, True, True, 0)
                self.view.pack_start(hbox, False, False, 0)
                hbox.show_all()

    def __init__(self, category, event_structs, pinnable = False, itemoff = 0):
        super(CategoryBox, self).__init__()

        self.style = self.get_style_context()
        self.style.add_class("category-box")

        self.category = category
        self.event_structs = event_structs
        self.pinnable = pinnable
        self.itemoff = itemoff
        self.view = Gtk.VBox(True)
        self.vbox = Gtk.VBox()
        SearchBox.connect("search", self.__highlight)
        SearchBox.connect("clear", self.__clear)
        if len(event_structs) > 0:
            d = str(datetime.date.fromtimestamp(int(event_structs[0].event.timestamp)/1000)) \
              + " " + str((time.localtime(int(event_structs[0].event.timestamp)/1000).tm_hour)/8) + " " + str(category)
            if d not in self.EXPANDED:
                self.EXPANDED[d] = False
        # If this isn't a set of ungrouped events, give it a label
        if category or category == "":
            # Place the items into a box and simulate left padding
            self._box = Gtk.HBox()
            self._box.pack_start(self.view, False, True, 0)
            self._hbox = hbox = Gtk.HBox(False, 12)
            # Add the title button
            if category in SUPPORTED_SOURCES:
                text = SUPPORTED_SOURCES[category].group_label(len(event_structs))
            else:
                text = "Unknown"
            self.label = Gtk.Label()
            self.label.set_markup("<span>%s</span>" % text)
            self.label.set_ellipsize(Pango.EllipsizeMode.END)
            hbox.pack_start(self.label, False, False, 0)
            self.label_num = Gtk.Label()
            self.label_num.set_markup("<span>(%d)</span>" % len(event_structs))
            self.label_num.set_alignment(1.0,0.5)
            self.label_num.set_alignment(1.0,0.5)
            hbox.pack_end(self.label_num, False, False, 0)
            self.al = Gdk.Rectangle()

            # This call is currently a no-op in GTK3. Revisit this to
            # address this issue later. For details refer:
            # https://gitlab.gnome.org/GNOME/gtk/-/issues/3331
            # self.i = self.connect_after("size-allocate", self.set_size)

            hbox.set_border_width(6)
            self.expander = Gtk.Expander()
            self.expander.set_expanded(self.EXPANDED[d])

            # This call is currently a no-op in GTK3. Revisit this to
            # address this issue later. For details refer:
            # https://gitlab.gnome.org/GNOME/gtk/-/issues/3331
            self.expander.set_label_fill(True)

            if self.EXPANDED[d]:
                self._set_up_box(event_structs)
            self.expander.connect_after("activate", self.on_expand, d)
            self.expander.set_label_widget(hbox)
            self.vbox.pack_start(self.expander, True, True, 0)
            self.expander.add(self._box)
            self.pack_start(self.vbox, True, True, 24)
            self.expander.show_all()
            self.show()
            hbox.show_all()
            self.label_num.show_all()
            self.view.show()
            self.connect("style-updated", self.on_style_change, self.label_num)
            self.init_multimedia_tooltip()
        else:
            self._set_up_box(event_structs)
            self._box = self.view
            self.vbox.pack_end(self._box, False, True, 0)
            self._box.show()
            self.show()
            self.pack_start(self.vbox, True, True, 16 -itemoff)
        self.__highlight()
        self.show_all()

    def on_style_change(self, widget, label):
        """ Update used colors according to the system theme. """
        color = get_gtk_rgba(self.style, "bg", 0)
        fcolor = get_gtk_rgba(self.style, "fg", 0)
        color = combine_gdk_color(color, fcolor)

        shade = 0.6 if is_light_theme() else 2.5
        color = shade_gdk_color(color, shade)
        label.override_color(Gtk.StateFlags.NORMAL, color)

    def set_size(self, widget, allocation):
        if self.al != allocation:
            self.al = allocation
            self._hbox.set_size_request(self.al.width- 72, -1)

    def on_expand(self, widget, d):
        self.EXPANDED[d] = self.expander.get_expanded()
        self._set_up_box(self.event_structs)

    def init_multimedia_tooltip(self):
        """
        Show the items cointained in the category allowing to peek into those.
        """
        self.set_property("has-tooltip", True)
        self.text = ""
        self.text_title = "<b>" + self.label.get_text() +":</b>\n\n"
        for struct in self.event_structs[:15]:
            text = get_text_or_uri(struct.content_object)
            self.text += "<b>" + '\u2022' + " </b>" + text + "\n"
        if len(self.event_structs) > 15:
            self.text += "..."
        self.text = self.text.replace("&", "&amp;")

        self.connect("query-tooltip", self._handle_tooltip_category)

    def _handle_tooltip_category(self, widget, x, y, keyboard_mode, tooltip):
        """
        Create the tooltip for the categories
        """
        if self.expander.get_expanded(): return False
        if self.category in SUPPORTED_SOURCES:
            pix = get_icon_for_name(SUPPORTED_SOURCES[self.category].icon, 32)
        else:
            pix = None
        hbox = Gtk.HBox()
        label_title = Gtk.Label()
        label_title.set_markup(self.text_title)
        label_text = Gtk.Label()
        label_text.set_markup(self.text)
        al = Gtk.Alignment.new(0.5, 0.5, 1.0, 1.0) #center the label in the middle of the image
        al.set_padding(32, 0, 10, 0)
        al.add(label_title)
        img = Gtk.Image.new_from_pixbuf(pix)
        hbox.pack_start(img, False, False, 0)
        hbox.pack_start(al, False, False, 0)
        vbox = Gtk.VBox()
        al = Gtk.Alignment.new(0.5, 0.5, 0, 0) #align the title in the center
        al.add(hbox)
        vbox.pack_start(al, False, True, 0)
        al = Gtk.Alignment.new(0.5, 0.5, 1.0, 1.0) #align to the right ( FixMe: right align )
        al.add(label_text)
        vbox.pack_start(al, False, True, 0)
        vbox.show_all()
        tooltip.set_custom(vbox)
        return True

    def __highlight(self,*args):
        matches = False
        CategoryBox.event_count += 1

        # make ui responsive during search
        if CategoryBox.event_count % 50 == 0:
            while Gtk.events_pending():
                Gtk.main_iteration()

        match_count = 0
        if self.category:
            for struct in self.event_structs:
                if struct.content_object.matches_search:
                    match_count += 1

        if self.category:
            for struct in self.event_structs:
                if match_count:
                    text = self.label.get_text()
                    text = style_search_result_text(text)
                    self.label.set_markup(text)

                    # use custom color for highlighting search results
                    if config.SEARCH_RESULTS_USE_DEFAULT_COLOR:
                        color = get_gtk_rgba(self.style, "text", 0)
                    else:
                        color = get_gdk_rgba_from_string(config.SEARCH_RESULTS_TEXT_COLOR)

                    self.label.override_color(Gtk.StateFlags.NORMAL, color)
                    matching_text = "<span>(%d/%d)</span>" % (match_count, len(self.event_structs))
                    matching_text = style_search_result_text(matching_text)
                    self.label_num.set_markup(matching_text)
                    self.label_num.override_color(Gtk.StateFlags.NORMAL, color)
                    matches = True
                    break
            if not matches: self.__clear()

    def __clear(self, *args):
        CategoryBox.event_count += 1

        # make ui responsive during search
        if CategoryBox.event_count % 50 == 0:
            while Gtk.events_pending():
                Gtk.main_iteration()

        if self.category:
            self.label.set_markup("<span>" + self.label.get_text() + "</span>")
            color = get_gtk_rgba(self.style, "text", 0)
            self.label.override_color(Gtk.StateFlags.NORMAL, color)
            self.label_num.set_markup("<span>(%d)</span>" % len(self.event_structs))
            color = get_gtk_rgba(self.style, "bg", 0)
            fcolor = get_gtk_rgba(self.style, "fg", 0)
            color = combine_gdk_color(color, fcolor)
            self.label_num.override_color(Gtk.StateFlags.NORMAL, color)


class Item(Gtk.HBox, Draggable):

    event_count = 0
    size_group = Gtk.SizeGroup.new(Gtk.SizeGroupMode.BOTH)

    def __init__(self, content_struct, allow_pin = False, do_style=True):
        event = content_struct.event
        Gtk.HBox.__init__(self)

        self.style = self.get_style_context()
        self.style.add_class("item-hbox")

        self.set_border_width(2)
        self.allow_pin = allow_pin
        self.btn = Gtk.Button()
        Draggable.__init__(self, self.btn)
        self.search_results = []
        self.subject = event.subjects[0]
        self.content_obj = content_struct.content_object
        self.time = float(event.timestamp) / 1000
        self.time =  time.strftime("%H:%M", time.localtime(self.time))
        if self.content_obj is not None:
            if self.subject.uri.startswith("http"):
                self.icon = self.content_obj.get_actor_pixbuf(24)
            else:
                self.icon = self.content_obj.get_icon(
                    can_thumb=settings['small-thumbnails'], border=0)
        else:
            self.icon = None
        self.btn.set_relief(Gtk.ReliefStyle.NONE)
        self.btn.set_focus_on_click(False)
        self.__init_widget()
        self.show_all()
        self.o_style = None
        self.markup = None
        self.last_launch = 0
        self.__highlight()
        SearchBox.connect("search", self.__highlight)
        SearchBox.connect("clear", self.__clear)
        if do_style:
            self.connect("style-updated", self.change_style)

    def change_style(self, widget):
        color = get_gtk_rgba(self.style, "bg", 0)
        color = shade_gdk_color(color, 102/100.0)
        for w in self:
            w.override_background_color(Gtk.StateFlags.NORMAL, color)

    def __highlight(self, *args):
        Item.event_count += 1

        # make ui responsive during search
        if Item.event_count % 50 == 0:
            while Gtk.events_pending():
                Gtk.main_iteration()

        # FixMe: copy style code was here. Revisit this.
        text = self.content_obj.text.replace("&", "&amp;")
        if text.strip() == "":
            text = self.content_obj.uri.replace("&", "&amp;")
        if self.content_obj.matches_search:
            text = style_search_result_text(text)
            self.label.set_markup(text)

            # use custom color for highlighting search results
            if config.SEARCH_RESULTS_USE_DEFAULT_COLOR:
                color = get_gtk_rgba(self.style, "text", 0)
            else:
                color = get_gdk_rgba_from_string(config.SEARCH_RESULTS_TEXT_COLOR)

            self.label.override_color(Gtk.StateFlags.NORMAL, color)
        else:
            self.label.set_markup("<span>" + text + "</span>")
            color = get_gtk_rgba(self.style, "text", 0)
            self.label.override_color(Gtk.StateFlags.NORMAL, color)

    def __clear(self, *args):
        Item.event_count += 1

        # make ui responsive during search
        if Item.event_count % 50 == 0:
            while Gtk.events_pending():
                Gtk.main_iteration()

        self.content_obj.matches_search = False
        text = self.content_obj.text.replace("&", "&amp;")
        if text.strip() == "":
            text = self.content_obj.uri.replace("&", "&amp;")

        self.label.set_markup("<span>" + text + "</span>")
        color = get_gtk_rgba(self.style, "text", 0)
        self.label.override_color(Gtk.StateFlags.NORMAL, color)

    def __init_widget(self):
        self.label = Gtk.Label()
        text = self.content_obj.text.replace("&", "&amp;")
        text.strip()
        if self.content_obj.text.strip() == "":
            text = self.content_obj.uri.replace("&", "&amp;")
        self.label.set_markup(text)
        self.label.set_ellipsize(Pango.EllipsizeMode.END)
        self.label.set_alignment(0.0, 0.5)
        if self.icon: img = Gtk.Image.new_from_pixbuf(self.icon)
        else: img = None
        hbox = Gtk.HBox()

        if img:
            # Make sure we have even size
            self.size_group.add_widget(img)
            hbox.pack_start(img, False, False, 1)
        hbox.pack_start(self.label, True, True, 4)
        if self.allow_pin:
            self.pin = Gtk.Image.new_from_icon_name("view-pin-symbolic", Gtk.IconSize.BUTTON)
            self.trash = Gtk.Button.new_from_icon_name("user-trash-symbolic", Gtk.IconSize.BUTTON)
            self.trash.set_tooltip_text(_("Remove Pin"))
            self.trash.set_focus_on_click(False)
            self.trash.set_relief(Gtk.ReliefStyle.NONE)
            self.pack_start(self.pin, False, False, 12)
            self.pack_end(self.trash, False, False, 0)
            self.trash.connect("clicked", lambda x: self.set_bookmarked(False))
        evbox = Gtk.EventBox()
        self.btn.add(hbox)
        hbox.set_hexpand(True)
        evbox.add(self.btn)
        self.pack_start(evbox, False, True, 0)
        self.btn.connect("clicked", self.launch)
        self.btn.connect("enter-notify-event", update_cursor)
        self.btn.connect("button_press_event", self._show_item_popup)
        self.btn.connect("realize", self.realize_cb, evbox)
        self.init_multimedia_tooltip()


    def on_drag_data_get(self, treeview, context, selection, target_id, etime):
        uri = self.content_obj.uri
        if target_id == TYPE_TARGET_TEXT:
            selection.set_text(uri, -1)
        elif target_id == TYPE_TARGET_URI:
            if uri.startswith("file://"):
                unquoted_uri = urllib.parse.unquote(uri)
                if os.path.exists(unquoted_uri[7:]):
                    selection.set_uris([uri])

    def realize_cb(self, widget, evbox):
        widget.props.window.set_cursor(Gdk.Cursor.new(Gdk.CursorType.HAND2))

    def init_multimedia_tooltip(self):
        """
        Add multimedia tooltip to multimedia files.
        Multimedia tooltip is shown for all images, all videos and pdfs
        A generic tooltip with text and uri is showed for non-GioFile
        items. Audio files have a preview, too.

        TODO: make loading of multimedia thumbs async
        """
        self.set_property("has-tooltip", True)
        if isinstance(self.content_obj, GioFile) and self.content_obj.has_preview():
            icon_names = self.content_obj.icon_names
            if "video-x-generic" in icon_names and Gst is not None and VideoPreviewTooltip.has_video_preview():
                self.connect("query-tooltip", self._handle_tooltip_dynamic)
                self.set_tooltip_window(VideoPreviewTooltip)
            elif "audio-x-generic" in icon_names and Gst is not None and AudioPreviewTooltip.has_audio_preview():
                self.connect("query-tooltip", self._handle_tooltip_dynamic)
                self.set_tooltip_window(AudioPreviewTooltip)
            else:
                self.connect("query-tooltip", self._handle_tooltip_static)
        else:
            self.connect("query-tooltip", self._handle_tooltip_generic)

    def _handle_tooltip_generic(self, widget, x, y, keyboard_mode, tooltip):
        """
        Create the tooltip for non-GioFile events
        """
        text = "<b>" + self.label.get_text() + "</b>\n"
        if isinstance(self.content_obj, GioFile) and \
           self.content_obj.annotation is not None:
            if self.content_obj.annotation.strip() != "":
                note = _("Notes")
                note_text = "<b>%s:</b> %s" % (note, self.content_obj.annotation)
                text += note_text + "\n"
        uri = urllib.parse.unquote(self.content_obj.uri)
        if len(uri) > 90: uri = uri[:90] + "..." #ellipsize--it's ugly!
        if uri.startswith("file://"):
            text += str(uri[7:])
        else:
            text += str(uri)
        text = text.replace("&", "&amp;")

        tooltip.set_markup(text)
        tooltip.set_icon(self.icon)
        return True

    def _handle_tooltip_static(self, widget, x, y, keyboard_mode, tooltip):
        """
        Create the tooltip for static GioFile events (documents,pdfs,...)
        """
        gio_file = self.content_obj
        if not isinstance(gio_file, GioFile): return False
        pixbuf = gio_file.get_thumbnail(size=SIZE_LARGE, border=1)

        text = _("<b>Name: </b>") + self.label.get_text()
        uri = urllib.parse.unquote(self.content_obj.uri)
        descr = Gio.content_type_from_mime_type(self.content_obj.mime_type)
        descr = Gio.content_type_get_description(descr)
        mime_text = _("\n<b>MIME Type:</b> %(descr)s (%(mimetype)s)")% ({ 'descr' : descr, 'mimetype' : self.content_obj.mime_type })
        text += mime_text + "\n"
        if self.content_obj.annotation is not None:
            if self.content_obj.annotation.strip() != "":
                note = _("Notes")
                note_text = "<b>%s:</b> %s" % (note, self.content_obj.annotation)
                text += note_text + "\n"
                truncate_lenght = max(len(mime_text), len(note_text))
            else:
                truncate_lenght = len(mime_text)
        else:
            truncate_lenght = len(mime_text)
        text += "\n"
        if uri.startswith("file://"):
            uri = self.truncate_string(uri[7:], truncate_lenght)
            text += str(uri)
        else:
            uri = self.truncate_string(uri, truncate_lenght)
            text += str(uri)
        text = text.replace("&", "&amp;")

        label = Gtk.Label()
        label.set_markup(text)
        tooltip.set_custom(label)
        tooltip.set_icon(pixbuf)
        return True

    def _handle_tooltip_dynamic(self, widget, x, y, keyboard_mode, tooltip):
        """
        Create the tooltip for dynamic GioFile events (audio,video)
        """
        tooltip_window = self.get_tooltip_window()
        if tooltip_window.showing:
            return

        window = widget.props.window
        req = widget.get_allocation()

        (abs_x, abs_y, w, h) = window.get_geometry()
        (ret, origin_x, origin_y) = window.get_origin()

        move_x = x + origin_x + req.x + abs_x
        move_y = y + origin_y + req.y + abs_y

        return tooltip_window.preview(self.content_obj, move_x, move_y)

    def truncate_string(self, string, truncate_lenght):
        """
        Very simple recursive function that truncates strings in a nicely way
        """
        delta = 8
        if len(string) <= (truncate_lenght + delta) : return string
        else:
            i = string.find(os.path.sep, truncate_lenght - delta*2, truncate_lenght + delta*2)
            if i > 0: truncate_lenght_new = i + 1
            else: truncate_lenght_new = truncate_lenght
            t_string = string[:truncate_lenght_new]
            t_string += "\n" + self.truncate_string(string[truncate_lenght_new:], truncate_lenght)
            return t_string

    def _show_item_popup(self, widget, ev):
        if ev.button == 3 and not config.IN_ERASE_MODE:
            items = [self.content_obj]
            ContextMenu.do_popup(ev.time, items)

    def set_bookmarked(self, bool_):
        uri = str(self.subject.uri)
        bookmarker.bookmark(uri) if bool_ else bookmarker.unbookmark(uri)
        if not bool_: self.destroy()

    def launch(self, *discard):
        if config.IN_ERASE_MODE:
            if config.CONFIRM_ITEM_DELETION:
                delete = confirm_object_deletion(self.get_toplevel(), self.content_obj.uri)
                if not delete:
                    return
            ContextMenu.do_delete_object(self.content_obj)
        else:
            ev_time = time.time()
            #1 sec it's a good range imo...
            launch = True if (ev_time - self.last_launch)*1000 > 1000 else False
            if self.content_obj is not None and launch:
                self.last_launch = ev_time
                self.content_obj.launch()

#####################
## ThumbView code
#####################
class ThumbViewContainer(_GenericViewWidget):
    icon_path = get_data_path("thumbview_icon.png")
    dsc_text = _("Switch to ThumbView")
    name = "thumb-view"

    def __init__(self):
        _GenericViewWidget.__init__(self)
        self.scrolledwindow = Gtk.ScrolledWindow()
        self.scrolledwindow.props.expand = True
        self.scrolledwindow.set_shadow_type(Gtk.ShadowType.NONE)
        self.scrolledwindow.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        self.view = ThumbView()
        self.scrolledwindow.add(self.view)
        self.scrolledwindow.get_children()[0].set_shadow_type(Gtk.ShadowType.NONE)
        self.add(self.scrolledwindow)
        self.show_all()

    def set_zoom(self, zoom):
        self.view.set_zoom(zoom)

    def set_slider(self, slider):
        self.view.set_slider(slider)

    def toggle_erase_mode(self):
        self.view.toggle_erase_mode()

class _ThumbViewRenderer(Gtk.CellRenderer):
    """
    A IconView renderer to be added to a cellayout. It displays a pixbuf and
    data based on the event property
    """

    __gtype_name__ = "_ThumbViewRenderer"
    __gproperties__ = {
        "content-obj" :
        (GObject.TYPE_PYOBJECT,
         "event to be displayed",
         "event to be displayed",
         GObject.PARAM_READWRITE,
         ),
         "size-w" :
        (GObject.TYPE_INT,
         "Width of cell",
         "Width of cell",
         48,
         288,
         96,
         GObject.PARAM_READWRITE,
         ),
         "size-h" :
        (GObject.TYPE_INT,
         "Height of cell",
         "Height of cell",
         36,
         224,
         72,
         GObject.PARAM_READWRITE,
         ),
         "zoom-level" :
        (GObject.TYPE_INT,
         "Current zoom level index",
         "Current zoom level index",
         0,
         3,
         2,
         GObject.PARAM_READWRITE,
         ),
         "text-size" :
        (GObject.TYPE_STRING,
         "Size of the text",
         "Size of the text",
         "",
         GObject.PARAM_READWRITE,
         ),
         "molteplicity" :
        (GObject.TYPE_INT,
         "Molteplicity",
         "Number of similar item that are grouped into one",
         0,
         999,
         0,
         GObject.PARAM_READWRITE,
         )
    }

    properties = {}

    @property
    def width(self):
        return self.get_property("size-w")

    @property
    def height(self):
        return self.get_property("size-h")

    @property
    def text_size(self):
        return self.get_property("text-size")

    @property
    def content_obj(self):
        return self.get_property("content-obj")

    @property
    def molteplicity(self):
        return self.get_property("molteplicity")

    @property
    def zoom_level(self):
        return self.get_property("zoom-level")

    @property
    def emblems(self):
        return self.content_obj.emblems

    @property
    def pixbuf(self):
        # reduce w, h by emblem rendering w, h respectively
        return self.content_obj.get_thumbview_pixbuf_for_size(self.width - self.emblem_size,
                                                              self.height - self.emblem_size)

    @property
    def event(self):
        return self.content_obj.event

    @property
    def emblem_size(self):
        if self.zoom_level <= 1:
            emblem_size = 16
        elif self.zoom_level == 2:
            emblem_size = 24
        elif self.zoom_level == 3:
            emblem_size = 32
        else:
            raise Exception ("Unknown zoom level: %d" % self.zoom_level)

        return emblem_size

    @property
    def text_offset(self):
        if self.zoom_level == 0:
            text_offset = (2, 2)
        elif self.zoom_level == 1:
            text_offset = (3, 3)
        elif self.zoom_level == 2:
            text_offset = (4, 4)
        elif self.zoom_level == 3:
            text_offset = (5, 5)
        else:
            raise Exception ("Unknown zoom level: %d" % self.zoom_level)

        return text_offset

    @property
    def molteplicity_radius(self):
        if self.zoom_level == 0:
            radius = 8
        elif self.zoom_level == 1:
            radius = 8
        elif self.zoom_level == 2:
            radius = 12
        elif self.zoom_level == 3:
            radius = 16
        else:
            raise Exception ("Unknown zoom level: %d" % self.zoom_level)

        return radius

    def __init__(self):
        super(_ThumbViewRenderer, self).__init__()
        self.properties = {}
        self.set_property("mode", Gtk.CellRendererMode.ACTIVATABLE)

    def do_set_property(self, pspec, value):
        self.properties[pspec.name] = value

    def do_get_property(self, pspec):
        return self.properties[pspec.name]

    def do_get_size(self, widget, area):
        w = self.width
        h = self.height
        return (0, 0, w, h)

    def do_render(self, context, widget, background_area, cell_area, flags):
        """
        The primary rendering function. It calls either the classes rendering functions
        or special one defined in the rendering_functions dict
        """
        x = cell_area.x
        y = cell_area.y
        w = cell_area.width
        h = cell_area.height
        pixbuf, isthumb = self.pixbuf

        self.style = widget.get_style_context()
        emblem_size = self.emblem_size

        # We offset the frame so we can print emblems really to the
        # top-left, and bottom-right edges.
        x += emblem_size / 2
        y += emblem_size / 2
        w -= emblem_size
        h -= emblem_size

        if pixbuf and isthumb and "audio-x-generic" not in self.content_obj.icon_names:
            render_pixbuf(context, x, y, pixbuf, w, h)
        else:
            self.file_render_pixbuf(context, widget, pixbuf, x, y, w , h)
        render_emblems(context, x, y, w, h, self.emblems, emblem_size)
        return True

    @staticmethod
    def insert_file_markup(text, size):
        text = text.replace("&", "&amp;")
        text = "<span size='" + size + "'>" + text + "</span>"
        return text

    def file_render_pixbuf(self, context, widget, pixbuf, x, y, w, h):
        """
        Renders a icon and file name for non-thumb objects
        """
        if w == SIZE_THUMBVIEW[0][0]: pixbuf = None
        if pixbuf:
            imgw, imgh = pixbuf.get_width(), pixbuf.get_height()
            ix = x + (w - imgw)
            iy = y + (h - imgh)
        context.rectangle(x, y, w, h)
        context.set_source_rgb(1, 1, 1)
        context.fill_preserve()
        if pixbuf:
            Gdk.cairo_set_source_pixbuf (context, pixbuf, x, iy)
            context.fill()
        draw_frame(context, x, y, w, h)

        if self.molteplicity > 1:
            text_ = get_text_or_uri(self.content_obj, molteplicity=True)
            text_ = text_.split(" ")[-1]
        else:
            text_ = self.content_obj.thumbview_text
        text = self.insert_file_markup(text_, self.text_size)

        layout = PangoCairo.create_layout(context)
        layout.set_markup(text)
        top_left_emblem = self.emblems[0]
        (text_x_offset, text_y_offset) = self.text_offset

        max_width = w
        if top_left_emblem:
            text_x_offset += self.emblem_size / 2
            max_width -= text_x_offset

        draw_text(context, layout, text, x + text_x_offset, y + text_y_offset, max_width)

        if self.molteplicity > 1:
            render_molteplicity(context, x, y, w, h, self.molteplicity, self.style, self.molteplicity_radius)

    @staticmethod
    def render_info_box(context, widget, cell_area, event, content_obj, molteplicity):
        """
        Renders a info box when the item is active
        """
        x = cell_area.x
        y = cell_area.y - 10
        w = cell_area.width
        h = cell_area.height

        t0 = get_event_typename(event)
        if molteplicity > 1:
            t1 = get_text_or_uri(content_obj, molteplicity=True)
        else:
           t1 = event.subjects[0].text
        text = ("<span size='10240'>%s</span>\n<span size='8192'>%s</span>" % (t0, t1)).replace("&", "&amp;")
        layout = PangoCairo.create_layout(context)
        layout.set_markup(text)
        textw, texth = layout.get_pixel_size()
        popuph = max(h/3 + 5, texth)
        nw = w + 26
        x = x - (nw - w)/2
        window = widget.props.window
        width, height = window.get_geometry()[2:4]
        popupy = min(y+h+10, height-popuph-5-1) - 5
        draw_speech_bubble(context, layout, x, popupy, nw, popuph)
        context.fill()
        return False

    def do_start_editing(self, event, widget, path, background_area, cell_area, flags):
        pass

    def do_activate(self, event, widget, path, background_area, cell_area, flags):
        pass


class ThumbIconView(Gtk.IconView, Draggable):
    """
    A iconview which uses a custom cellrenderer to render square pixbufs
    based on zeitgeist events
    """
    last_active = -1
    child_width = _ThumbViewRenderer.width
    child_height = _ThumbViewRenderer.height

    def __init__(self):
        Gtk.IconView.__init__(self)
        Draggable.__init__(self, self)

        self.props.expand = True
        self.props.has_tooltip = True
        self.active_list = []
        self.current_size_index = 1
        self.in_erase_mode = False
        self.popupmenu = ContextMenu
        self.popupmenu_molteplicity = ContextMenuMolteplicity

        # Model fields
        # 1) content object
        # 2) preview width
        # 3) preview height
        # 4) text dimension
        # 5) number of similar items grouped into one
        #   (show the molteplicity number in the top-right corner)
        self.model = Gtk.ListStore(GObject.TYPE_PYOBJECT, int, int, str, int, int)
        self.set_model(self.model)

        self.add_events(Gdk.EventMask.LEAVE_NOTIFY_MASK |
                        Gdk.EventMask.ENTER_NOTIFY_MASK)

        self.connect("button-press-event", self.on_button_press)
        self.connect("button-release-event", self.on_button_release)
        self.connect("motion-notify-event", self.on_motion_notify)
        self.connect("enter-notify-event", update_cursor)
        self.connect("leave-notify-event", self.on_leave_notify)
        self.connect("query-tooltip", self.on_query_tooltip)

        self.set_selection_mode(Gtk.SelectionMode.SINGLE)
        self.set_column_spacing(6)
        self.set_row_spacing(6)
        render = _ThumbViewRenderer()
        self.pack_end(render, False)
        self.add_attribute(render, "content-obj", 0)
        self.add_attribute(render, "size-w", 1)
        self.add_attribute(render, "size-h", 2)
        self.add_attribute(render, "text-size", 3)
        self.add_attribute(render, "molteplicity", 4)
        self.add_attribute(render, "zoom-level", 5)
        self.set_margin(10)
        SearchBox.connect("search", lambda *args: self.queue_draw())
        SearchBox.connect("clear", lambda *args: self.queue_draw())

    def _set_model_in_idle(self, items, grouped_items):
        """
        A function which generates pixbufs and emblems for a list of events.
        It takes those properties and appends them to the view's model
        """
        self.active_list = []
        self.grouped_items = grouped_items
        for item in items:
            obj = item.content_object
            if not obj: continue
            self.active_list.append(False)
            self.model.append([obj,
                               SIZE_THUMBVIEW[self.current_size_index][0],
                               SIZE_THUMBVIEW[self.current_size_index][1],
                               SIZE_TEXT_THUMBVIEW[self.current_size_index],
                               0,
                               self.current_size_index])

        for item in list(grouped_items.values()):
            #i take the first element in the list
            obj = item[0].content_object
            if not obj: continue
            self.active_list.append(False)
            self.model.append([obj,
                               SIZE_THUMBVIEW[self.current_size_index][0],
                               SIZE_THUMBVIEW[self.current_size_index][1],
                               SIZE_TEXT_THUMBVIEW[self.current_size_index],
                               len(item),
                               self.current_size_index])

    def set_model_from_list(self, items, grouped_items):
        """
        Sets creates/sets a model from a list of zeitgeist events
        :param events: a list of :class:`Events <zeitgeist.datamodel.Event>`
        """
        self.model.clear()
        self.last_active = -1
        if not (items or grouped_items):
            return

        GLib.idle_add(self._set_model_in_idle, items, grouped_items)

    def set_zoom(self, size_index):
        self.current_size_index = size_index
        for row in self.model:
            row[1] = SIZE_THUMBVIEW[size_index][0]
            row[2] = SIZE_THUMBVIEW[size_index][1]
            row[3] = SIZE_TEXT_THUMBVIEW[size_index]
            row[5] = size_index
        self.queue_draw()

    def on_drag_data_get(self, iconview, context, selection, target_id, etime):
        model = iconview.get_model()
        selected = iconview.get_selected_items()
        content_object = model[selected[0]][0]
        uri = content_object.uri
        if target_id == TYPE_TARGET_TEXT:
            selection.set_text(uri, -1)
        elif target_id == TYPE_TARGET_URI:
            if uri.startswith("file://"):
                unquoted_uri = urllib.parse.unquote(uri)
                if os.path.exists(unquoted_uri[7:]):
                    selection.set_uris([uri])

    def on_button_press(self, widget, event):
        if event.button == 3 and not self.in_erase_mode:
            val = self.get_item_at_pos(int(event.x), int(event.y))
            if val:
                path, cell = val
                model = self.get_model()
                obj = model[path[0]][0]
                if model[path[0]][4] > 1:
                    key = urlparse(obj.uri).netloc
                    if key in list(self.grouped_items.keys()):
                        list_ = self.grouped_items[key]
                        self.popupmenu_molteplicity.do_popup(event.time, list_)
                else:
                    self.popupmenu.do_popup(event.time, [obj])

    def on_button_release(self, widget, event):
        if event.button == 1:
            val = self.get_item_at_pos(int(event.x), int(event.y))
            if val:
                path, cell = val
                model = self.get_model()
                obj = model[path[0]][0]
                if model[path[0]][4] > 1:
                    key = urlparse(obj.uri).netloc
                    if key in list(self.grouped_items.keys()):
                        list_ = self.grouped_items[key]
                        if self.in_erase_mode:
                            model.remove(model[path[0]].iter)
                            self.popupmenu_molteplicity.do_delete_list(list_)
                        else:
                            self.popupmenu_molteplicity.do_show_molteplicity_list(list_)
                else:
                    if self.in_erase_mode:
                        if config.CONFIRM_ITEM_DELETION:
                            delete = confirm_object_deletion(self.get_toplevel(), obj.uri)
                            if not delete:
                                return
                        model.remove(model[path[0]].iter)
                        self.popupmenu.do_delete_object(obj)
                    else:
                        obj.launch()

    @staticmethod
    def get_tooltip_markup_for_item(row):
        content_obj = row[0]
        size = row[3]
        molteplicity = row[4]
        event = content_obj.event

        if size == 'x-small':
            ratio = 1.1
        elif size == 'small':
            ratio = 1.2
        elif size == 'medium':
            ratio = 1.2
        elif size == 'large':
            ratio = 1.3
        elif size == 'x-large':
            ratio = 1.4
        else:
            raise Exception("Unknown size: %s" % (size))

        if molteplicity > 1:
            t0 = get_text_or_uri(content_obj, molteplicity=True)
        else:
           t0 = event.subjects[0].text

        t1 = get_event_typename(event)

        t0_size = 9 * 1024 * ratio
        t1_size = 10 * 1024 * ratio
        markup = ("<span size='%d'><b>%s</b></span>\n<span size='%d' color='lightgray'>%s</span>" % (t0_size, t0, t1_size, t1)).replace("&", "&amp;")

        return markup

    def on_query_tooltip(self, widget, x, y, mode, tooltip):
        (ret, x, y, model, path, iterator) = self.get_tooltip_context(x, y, mode)

        if ret:
            row = model[path[0]]
            markup = self.get_tooltip_markup_for_item(row)
            tooltip.set_markup(markup)
            self.set_tooltip_item(tooltip, path)
            return True

        return False

    def on_leave_notify(self, widget, event):
        try:
            self.active_list[self.last_active] = False
        except IndexError:pass
        self.last_active = -1
        self.queue_draw()

    def on_motion_notify(self, widget, event):
        val = self.get_item_at_pos(int(event.x), int(event.y))
        if val:
            path, cell = val
            if path[0] != self.last_active:
                self.active_list[self.last_active] = False
                self.active_list[path[0]] = True
                self.last_active = path[0]
                self.queue_draw()

    def query_tooltip(self, widget, x, y, keyboard_mode, tooltip):
        """
        Displays a tooltip based on x, y
        """
        path = self.get_path_at_pos(int(x), int(y))
        if path:
            model = self.get_model()
            uri = model[path[0]][3].uri
            interpretation = model[path[0]][3].subjects[0].interpretation
            tooltip_window = widget.get_tooltip_window()
            if interpretation == Zeitgeist.VIDEO:
                self.set_tooltip_window(VideoPreviewTooltip)
            else:
                self.set_tooltip_window(StaticPreviewTooltip)
            gio_file = content_objects.GioFile.create(uri)
            return tooltip_window.preview(gio_file)
        return False

def get_empty_label(size=22000):
    empty_label = Gtk.Label()
    empty_label.set_markup("<span size='%d' color='lightgrey'>No events</span>" % (size))
    empty_label.props.expand = True
    empty_label.set_no_show_all(True)

    return empty_label

class ThumbView(Gtk.Grid):
    """
    A container for three image views representing periods in time
    """
    event_templates = (
            Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri),
            Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri),
            Event.new_for_values(interpretation=Interpretation.ACCESS_EVENT.uri),
            Event.new_for_values(interpretation=Interpretation.SEND_EVENT.uri),
            Event.new_for_values(interpretation=Interpretation.RECEIVE_EVENT.uri),
        )
    def __init__(self):
        """Woo"""
        Gtk.Grid.__init__(self)

        self.props.orientation = Gtk.Orientation.VERTICAL
        self.style = self.get_style_context()
        self.style.add_class("thumbnail-view")

        self.views = []
        self.labels = []
        self.current_size_index = 1
        self.zoom_slider = None
        self.old_date = None

        self.empty_label = get_empty_label()
        self.add(self.empty_label)

        for text in DayParts.get_day_parts():
            label = Gtk.Label()
            label.set_markup("\n  <span size='10336'>%s</span>" % (text))
            label.set_justify(Gtk.Justification.RIGHT)
            label.set_alignment(0, 0)
            self.views.append(ThumbIconView())
            self.labels.append(label)
            self.add(label)
            self.add(self.views[-1])
        self.add_events(Gdk.EventMask.SCROLL_MASK)
        self.connect("scroll-event", self.on_scroll_event)
        self.connect("style-updated", self.change_style)

    def set_phase_items(self, i, items, grouped_items):
        """
        Set a time phases events

        :param i: a index for the three items in self.views. 0:Morning,1:AfterNoon,2:Evening
        :param events: a list of :class:`Events <zeitgeist.datamodel.Event>`
        """
        view = self.views[i]
        label = self.labels[i]
        if (not items or len(items) == 0) and \
           (not grouped_items or len(grouped_items) == 0):
            if config.HIDE_EMPTY_DAY_LABELS:
                label.hide()
            view.hide()
            return False
        view.show_all()
        label.show_all()
        view.set_model_from_list(items, grouped_items)

        return True

    def set_day(self, day, force_update=False):
        if not force_update and self.old_date is not None:
            if (day.date - self.old_date) == datetime.timedelta(days=0) and \
                self.views[0].in_erase_mode:
                #don't update the model when we are in ERASE_MODE
                return

        self.old_date = day.date
        day_is_loaded = day.is_loaded()
        parts = [[] for i in DayParts.get_day_parts()]
        uris = [[] for i in parts]
        grouped_items = [{} for i in parts]

        items = day.filter(self.event_templates, result_type=ResultType.MostRecentEvents)
        for item in items:
            if event_exists(item.event.subjects[0].uri):
                i = DayParts.get_day_part_for_item(item)
                uri = item.event.subjects[0].uri
                if not uri in uris[i]:
                    if item.content_object:
                        if item.content_object.molteplicity:
                            origin = urlparse(uri).netloc
                            if origin not in list(grouped_items[i].keys()):
                                grouped_items[i][origin] = [item,]
                            else:
                                grouped_items[i][origin].append(item)

                            uris[i].append(uri)
                            continue

                    uris[i].append(uri)
                    parts[i].append(item)

        show_empty_label = True
        for i, part in enumerate(parts):
            have_events = self.set_phase_items(i, part, grouped_items[i])
            if have_events:
                show_empty_label = False

        # Don't update labels if we're not loaded yet, since the dbus
        # callback will return here with proper data.
        if not day_is_loaded:
            return

        if show_empty_label:
            self.empty_label.show()
            for label in self.labels:
                label.hide()
        else:
            self.empty_label.hide()
            if not config.HIDE_EMPTY_DAY_LABELS:
                for label in self.labels:
                    label.show()

    def set_zoom(self, zoom):
         if zoom > len(SIZE_THUMBVIEW) - 1 or zoom < 0: return
         self.current_size_index = zoom
         for i in range(0, len(DayParts.get_day_parts())):
            self.views[i].set_zoom(zoom)

    def change_style(self, widget):
        parent = self.get_parent()
        if parent:
            parent = self.get_parent()

        color = get_gtk_rgba(self.style, "bg", 0)
        fcolor = get_gtk_rgba(self.style, "fg", 0)
        parent.override_background_color(Gtk.StateFlags.NORMAL, color)

        for view in self.views: view.override_background_color(Gtk.StateFlags.NORMAL, color)
        shade = 0.9 if is_light_theme() else 4.8
        color = get_gtk_rgba(self.style, "text", 4, shade)
        color = shade_gdk_color(color, 0.5)
        for label in self.labels:
            label.override_color(0, color)

    def on_scroll_event(self, widget, event):
        zoom_increment = 1

        if event.get_state() == Gdk.ModifierType.CONTROL_MASK:
            if event.direction == Gdk.ScrollDirection.SMOOTH:
                (ret, delta_x, delta_y) = event.get_scroll_deltas()
                if delta_y > 0:
                    zoom_increment = -1
            if event.direction == Gdk.ScrollDirection.DOWN:
                zoom_increment = -1

            self.zoom_slider.set_value(self.current_size_index + zoom_increment)

            return True

    def set_slider(self, slider):
        self.zoom_slider = slider

    def toggle_erase_mode(self):
        for view in self.views:
            view.in_erase_mode = not view.in_erase_mode


class TimelineViewContainer(_GenericViewWidget):
    icon_path = get_data_path("timelineview_icon.png")
    dsc_text = _("Switch to TimelineView")
    name = "timeline-view"

    def __init__(self):
        _GenericViewWidget.__init__(self)
        self.ruler = _TimelineHeader()
        self.scrolledwindow = Gtk.ScrolledWindow()
        self.scrolledwindow.set_shadow_type(Gtk.ShadowType.NONE)
        self.scrolledwindow.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        self.view = TimelineView(self)
        self.scrolledwindow.add(self.view)
        self.empty_label = get_empty_label()
        self.add(self.ruler)
        self.add(self.empty_label)
        self.add(self.scrolledwindow)
        self.view.set_events(Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.POINTER_MOTION_HINT_MASK)

    def update_view(self, show_empty_label):
        if show_empty_label:
            self.empty_label.show()
            self.scrolledwindow.hide()
        else:
            self.empty_label.hide()
            self.scrolledwindow.show()

    def change_style(self, widget):
        _GenericViewWidget.change_style(self, widget)
        color = get_gtk_rgba(self.style, "bg", 0)
        color = shade_gdk_color(color, 102/100.0)
        self.ruler.override_background_color(Gtk.StateFlags.NORMAL, color)

    def set_zoom(self, zoom):
        self.view.set_zoom(zoom)

    def set_slider(self, slider):
        self.view.set_slider(slider)

    def toggle_erase_mode(self):
        self.view.toggle_erase_mode()

class _TimelineRenderer(Gtk.CellRenderer):
    """
    Renders timeline columns, and text for a for properties
    """

    __gtype_name__ = "_TimelineRenderer"
    __gproperties__ = {
        "content-obj" :
        (GObject.TYPE_PYOBJECT,
         "event to be displayed",
         "event to be displayed",
         GObject.PARAM_READWRITE,
         ),
        "size-w" :
        (GObject.TYPE_INT,
         "Width of cell",
         "Width of cell",
         16,
         128,
         32,
         GObject.PARAM_READWRITE,
         ),
        "size-h" :
        (GObject.TYPE_INT,
         "Height of cell",
         "Height of cell",
         12,
         96,
         24,
         GObject.PARAM_READWRITE,
         ),
        "text-size" :
        (GObject.TYPE_STRING,
         "Size of the text",
         "Size of the text",
         "",
         GObject.PARAM_READWRITE,
         ),
    }

    barsize = 5
    properties = {}

    textcolor = {Gtk.StateFlags.NORMAL : ("#ff", "#ff"),
                 Gtk.StateFlags.SELECTED : ("#ff", "#ff")}

    @property
    def content_obj(self):
        return self.get_property("content-obj")

    @property
    def width(self):
        return self.get_property("size-w")

    @property
    def height(self):
        return self.get_property("size-h")

    @property
    def text_size(self):
        return self.get_property("text-size")

    @property
    def phases(self):
        return self.content_obj.phases

    @property
    def event(self):
        return self.content_obj.event

    @property
    def colors(self):
        """ A tuple of two colors, the first being the base the outer being the outline"""
        return self.content_obj.type_color_representation

    @property
    def text(self):
        return self.content_obj.timelineview_text

    @property
    def pixbuf(self):
        if config.SHOW_THUMBNAILS_IN_TIMELINE:
            return self.content_obj.get_thumbview_pixbuf_for_size(self.height, self.height)[0]
        else:
            return self.content_obj.get_icon(self.height)

    def __init__(self):
        super(_TimelineRenderer, self).__init__()
        self.properties = {}
        self.set_property("mode", Gtk.CellRendererMode.ACTIVATABLE)

    def do_set_property(self, pspec, value):
        self.properties[pspec.name] = value

    def do_get_property(self, pspec):
        return self.properties[pspec.name]

    def do_get_size(self, widget, area):
        w = self.width
        h = self.height + self.barsize*2 + 10
        return (0, 0, w, h)

    def do_render(self, context, widget, background_area, cell_area, flags):
        """
        The primary rendering function. It calls either the classes rendering functions
        or special one defined in the rendering_functions dict
        """
        x = int(cell_area.x)
        y = int(cell_area.y)
        w = int(cell_area.width)
        h = int(cell_area.height)
        self.render_phases(context, widget, x, y, w, h, flags)
        return True

    def render_phases(self, context, widget, x, y, w, h, flags):
        phases = self.phases
        for start, end in phases:
            context.set_source_rgb(*self.colors[0])
            start = int(start * w)
            end = max(int(end * w), 8)
            if start + 8 > w:
                start = w - 8
            context.rectangle(x+ start, y, end, self.barsize)
            context.fill()
            context.set_source_rgb(*self.colors[1])
            context.set_line_width(1)
            context.rectangle(x + start+0.5, y+0.5, end, self.barsize)
            context.stroke()
        x = int(phases[0][0]*w)
        # Pixbuf related junk which is really dirty
        self.render_text_with_pixbuf(context, widget, x, y, w, h, flags)
        return True

    def render_text_with_pixbuf(self, context, widget, x, y, w, h, flags):
        uri = self.content_obj.uri
        imgw, imgh = self.pixbuf.get_width(), self.pixbuf.get_height()
        x = max(x + imgw/2 + 4, 0 + imgw + 4)
        x, y = self.render_text(context, widget, x, y, w, h, flags)
        x -= imgw + 4
        y += self.barsize + 3
        pixbuf_w = self.pixbuf.get_width() if self.pixbuf else 0
        pixbuf_h = self.pixbuf.get_height() if self.pixbuf else 0
        if (pixbuf_w, pixbuf_h) == content_objects.SIZE_TIMELINEVIEW:
            drawframe = True
        else: drawframe = False
        render_pixbuf(context, x, y, self.pixbuf, w, h, drawframe=drawframe)

    def render_text(self, context, widget, x, y, w, h, flags):
        w = widget.get_allocated_width()
        y += 2
        x += 5
        state = Gtk.StateFlags.SELECTED if Gtk.CellRendererState.SELECTED & flags else Gtk.StateFlags.NORMAL
        color1, color2 = self.textcolor[state]
        text = self._make_timelineview_text(self.text, get_hex_color(color1), get_hex_color(color2), self.text_size)
        layout = PangoCairo.create_layout(context)
        fd = Pango.FontDescription.from_string("Cantarell")
        layout.set_font_description(fd)
        layout.set_markup(text)

        textw, texth = layout.get_pixel_size()
        if textw + x > w:
            layout.set_ellipsize(Pango.EllipsizeMode.END)
            layout.set_width(200*1024)
            textw, texth = layout.get_pixel_size()
            if x + textw > w:
                x = w - textw

        context.set_source_rgb(0, 0, 0)
        context.move_to(x, y + self.barsize)
        PangoCairo.update_layout(context, layout)
        PangoCairo.show_layout(context, layout)
        return x, y

    @staticmethod
    def _make_timelineview_text(text, color1, color2, size):
        """
        :returns: a string of text markup used in timeline widget and elsewhere
        """
        text = text.split("\n")
        if len(text) > 1:
            p1, p2 = text[0], text[1]
        else:
            p1, p2 = text[0], " "

        if size == 'x-large':
            type_size = 'large'
        elif size == 'large':
            type_size = 'medium'
        else:
            type_size = 'small'

        t1 = "<span size='"+ size + "' color='" + color1 + "'><b>" + p1 + "</b></span>"
        t2 = "<span size='"+ type_size + "' color='" + color2 + "'>" + p2 + "</span> "
        if size == 'small' or t2 == "":
            return (str(t1)).replace("&", "&amp;")
        return (str(t1) + "\n" + str(t2) + "").replace("&", "&amp;")

    def do_start_editing(self, event, widget, path, background_area, cell_area, flags):
        pass

    def do_activate(self, event, widget, path, background_area, cell_area, flags):
        pass


class TimelineView(Gtk.TreeView, Draggable):
    child_width = _TimelineRenderer.width
    child_height = _TimelineRenderer.height

    @staticmethod
    def make_area_from_event(timestamp, duration):
        """
        Generates a time box based on a objects timestamp and duration over 1.
        Multiply the results by the width to get usable positions

        :param timestamp: a timestamp int or string from which to calulate the start position
        :param duration: the length to calulate the width
        """
        w = max(duration/3600.0/1000.0/24.0, 0)
        x = ((int(timestamp)/1000.0 - time.timezone)%86400)/3600/24.0
        return [x, w]

    def __init__(self, container):
        Gtk.TreeView.__init__(self)
        Draggable.__init__(self, self)

        self.container = container
        self.style = self.get_style_context()
        self.style.add_class("timeline-view")

        self.props.expand = True
        self.model = Gtk.ListStore(GObject.TYPE_PYOBJECT, int, int, str)
        self.set_model(self.model)
        self.popupmenu = ContextMenu
        self.zoom_slider = None
        self.in_erase_mode = False
        self.old_date = None
        self.add_events(Gdk.EventMask.LEAVE_NOTIFY_MASK |
                        Gdk.EventMask.ENTER_NOTIFY_MASK |
                        Gdk.EventMask.SCROLL_MASK )
        self.connect("enter-notify-event", update_cursor)
        self.connect("button-press-event", self.on_button_press)
        self.connect("button-release-event", self.on_button_release)
        self.connect("row-activated" , self.on_activate)
        self.connect("style-updated", self.change_style)
        self.connect("drag-begin", self.on_drag_begin)
        self.connect("drag-end", self.on_drag_end)
        self.connect("scroll-event", self.on_scroll_event)
        pcolumn = Gtk.TreeViewColumn("Timeline")
        self.render = render = _TimelineRenderer()
        pcolumn.pack_start(render, False)
        self.append_column(pcolumn)
        pcolumn.add_attribute(render, "content-obj", 0)
        pcolumn.add_attribute(render, "size-w", 1)
        pcolumn.add_attribute(render, "size-h", 2)
        pcolumn.add_attribute(render, "text-size", 3)
        self.set_headers_visible(False)
        self.set_property("has-tooltip", True)
        self.set_tooltip_window(StaticPreviewTooltip)
        SearchBox.connect("search", lambda *args: self.queue_draw())
        SearchBox.connect("clear", lambda *args: self.queue_draw())
        self.on_drag = False
        self.current_size_index = 1

    def set_model_from_list(self, items):
        """
        Sets creates/sets a model from a list of zeitgeist events

        :param events: a list of :class:`Events <zeitgeist.datamodel.Event>`
        """
        self.model.clear()
        if not items:
            return
        for row in items:
            #take the last and more updated content_obj
            item = row[len(row)-1][0]
            obj = item.content_object
            if not obj: continue
            obj.phases = [self.make_area_from_event(item.event.timestamp, stop) for (item, stop) in row]
            obj.phases.sort(key=lambda x: x[0])
            self.model.append([obj, SIZE_TIMELINEVIEW[self.current_size_index][0],
                                    SIZE_TIMELINEVIEW[self.current_size_index][1],
                                    SIZE_TEXT_TIMELINEVIEW[self.current_size_index]])

    def set_day(self, day, force_update=False):
        if not force_update and self.old_date is not None:
            if (day.date - self.old_date) == datetime.timedelta(days=0) and \
                self.in_erase_mode:
                #don't update the model when we are in ERASE_MODE
                return

        day_is_loaded = day.is_loaded()

        self.old_date = day.date
        items = day.get_time_map()
        self.set_model_from_list(items)

        # Don't update labels if we're not loaded yet, since the dbus
        # callback will return here with proper data.
        if not day_is_loaded:
            return

        self.container.update_view(len(items) == 0)

    def set_zoom(self, size_index):
        if size_index > len(SIZE_TIMELINEVIEW) - 1 or size_index < 0: return
        self.current_size_index = size_index
        for row in self.model:
                row[1] = SIZE_TIMELINEVIEW[size_index][0]
                row[2] = SIZE_TIMELINEVIEW[size_index][1]
                row[3] = SIZE_TEXT_TIMELINEVIEW[size_index]
        self.queue_draw()

    def on_drag_data_get(self, treeview, context, selection, target_id, etime):
        tree_selection = treeview.get_selection()
        model, iter = tree_selection.get_selected()
        content_object = model.get_value(iter, 0)
        uri = content_object.uri
        if target_id == TYPE_TARGET_TEXT:
            selection.set_text(uri, -1)
        elif target_id == TYPE_TARGET_URI:
            if uri.startswith("file://"):
                unquoted_uri = urllib.parse.unquote(uri)
                if os.path.exists(unquoted_uri[7:]):
                    selection.set_uris([uri])

    def on_button_press(self, widget, event):
        if event.button == 3 and not self.in_erase_mode:
            path = self.get_dest_row_at_pos(int(event.x), int(event.y))
            if path:
                model = self.get_model()
                obj = model[path[0]][0]
                self.popupmenu.do_popup(event.time, [obj])
                return True

        return False

    def on_button_release (self, widget, event):
        if event.button == 1 and not self.on_drag:
            self.on_drag = False
            path = self.get_dest_row_at_pos(int(event.x), int(event.y))
            if path:
                model = self.get_model()
                obj = model[path[0]][0]
                if self.in_erase_mode:
                    if config.CONFIRM_ITEM_DELETION:
                        delete = confirm_object_deletion(self.get_toplevel(), obj.uri)
                        if not delete:
                            return
                    model.remove(model[path[0]].iter)
                    self.popupmenu.do_delete_object(obj)
                else:
                    obj.launch()
                return True

        return False

    def on_drag_begin(self, widget, context, *args):
        self.on_drag = True

    def on_drag_end(self, widget, context, *args):
        self.on_drag = False

    def on_scroll_event(self, widget, event):
        zoom_increment = 1

        if event.get_state() == Gdk.ModifierType.CONTROL_MASK:
            if event.direction == Gdk.ScrollDirection.SMOOTH:
                (ret, delta_x, delta_y) = event.get_scroll_deltas()
                if delta_y > 0:
                    zoom_increment = -1
            if event.direction == Gdk.ScrollDirection.DOWN:
                zoom_increment = -1

            self.zoom_slider.set_value(self.current_size_index + zoom_increment)

            return True

    def set_slider(self, slider):
        self.zoom_slider = slider

    def toggle_erase_mode(self):
        self.in_erase_mode = not self.in_erase_mode

    def on_activate(self, widget, path, column):
        model = self.get_model()
        obj = model[path][0]
        if self.in_erase_mode:
            if config.CONFIRM_ITEM_DELETION:
                delete = confirm_object_deletion(self.get_toplevel(), obj.uri)
                if not delete:
                    return
            model.remove(model[path[0]].iter)
            self.popupmenu.do_delete_object(obj)
        else:
            obj.launch()

    def change_style(self, widget):
        """
        Sets the widgets style and coloring
        """
        #layout = self.create_pango_layout("")
        #layout.set_markup("<b>qPqPqP|</b>\nqPqPqP|")
        #tw, th = layout.get_pixel_size()
        #self.render.set_property("size-h", max(self.render.height, th + 3 + _TimelineRenderer.barsize))
        if self.props.window:
            width = self.props.window.get_geometry()[2] - 4
            self.render.set_property("size-w", 128) # FixMe: max(self.render.width, width))
        def change_color(color, inc):
            color = shade_gdk_color(color, inc/100.0)
            return color

        shade = 70 if is_light_theme() else 380
        normal = (get_gtk_rgba(self.style, "text", 0), change_color(get_gtk_rgba(self.style, "text", 3), shade))
        selected = (get_gtk_rgba(self.style, "text", 2), get_gtk_rgba(self.style, "text", 2))
        self.render.textcolor[Gtk.StateFlags.NORMAL] = normal
        self.render.textcolor[Gtk.StateFlags.SELECTED] = selected


class _TimelineHeader(Gtk.DrawingArea):
    time_text = { 3: "3:00", 6 : "6:00", 9 : "9:00", 12 : "12:00", 15 : "15:00", 18 : "18:00", 21 : "21:00" }
    time_text_am_pm = { 3 : "3 AM", 6 : "6 AM", 9 : "9 AM", 12 : "12 PM", 15 : "3 PM", 18 : "6 PM", 21 : "9 PM" }
    odd_line_height = 6
    even_line_height = 12

    line_color = (0, 0, 0, 1)

    def __init__(self):
        super(_TimelineHeader, self).__init__()

        self.style = self.get_style_context()
        self.style.add_class("timeline-header")

        self.connect("draw", self.draw)
        self.connect("style-updated", self.change_style)
        self.set_size_request(100, 12)
        self.night_color = get_gtk_rgba(self.style, "bg", 0, 0.75)
        self.day_color = get_gtk_rgba(self.style, "bg", 0, 1)
        self.night_line_color = get_gtk_rgba(self.style, "bg", 0, 0.6)
        self.day_line_color = get_gtk_rgba(self.style, "bg", 0, 0.8)

    def draw(self, widget, context):
        window = widget.props.window
        width = widget.get_allocated_width()
        height = widget.get_allocated_height()
        self.style.set_background(window)
        context.set_line_width(2)
        self.draw_lines(context, width, height)

    def draw_text(self, context, x, text):
        x = int(x)
        color = get_gtk_rgba(self.style, "text", 0)
        markup = "<span color='%s'>%s</span>" % (get_hex_color(color), text)
        layout = PangoCairo.create_layout(context)
        fd = Pango.FontDescription.from_string("Cantarell")
        layout.set_font_description(fd)
        layout.set_markup(markup)

        xs, ys = layout.get_pixel_size()
        context.move_to(x - xs/2, 0)
        PangoCairo.update_layout(context, layout)
        PangoCairo.show_layout(context, layout)

    def draw_line(self, context, x, even, night):
        x = int(x)+0.5
        height = self.even_line_height

        line_color = self.night_line_color if night else self.day_line_color

        context.set_source_rgba(*line_color)
        context.move_to(x, 0)
        context.line_to(x, height)
        context.stroke()

    def draw_day_night_gradient(self, context, width, height):
        gradient = cairo.LinearGradient(0, 0, width, 0)

        gradient.add_color_stop_rgba(0.1, *self.night_color)
        gradient.add_color_stop_rgba(0.2, *self.night_color)
        gradient.add_color_stop_rgba(0.3, *self.day_color)
        gradient.add_color_stop_rgba(0.4, *self.day_color)
        gradient.add_color_stop_rgba(0.5, *self.day_color)
        gradient.add_color_stop_rgba(0.6, *self.day_color)
        gradient.add_color_stop_rgba(0.7, *self.day_color)
        gradient.add_color_stop_rgba(0.8, *self.night_color)
        gradient.add_color_stop_rgba(0.9, *self.night_color)

        context.rectangle(0, 0, width, height)
        context.set_source(gradient)
        context.fill()

    def draw_lines(self, context, width, height):
        xinc = width/24

        # Display time header as per locale settings
        if time.strftime("%p"):
            time_text = self.time_text_am_pm
        else:
            time_text = self.time_text

        if config.SHOW_DAY_NIGHT_GRADIENT_IN_TIMELINE:
            context.save()
            self.draw_day_night_gradient(context, width, height)
            context.restore()
            night_hours = list(range(1, 6)) + list(range(19, 24))
        else:
            night_hours = []

        for hour in range(1, 24):
            night = hour in night_hours
            if hour in time_text:
                self.draw_text(context, xinc*hour, time_text[hour])
            else:
                self.draw_line(context, xinc*hour, bool(hour % 2), night)

    def change_style(self, widget):
        layout = self.create_pango_layout("")
        layout.set_markup("<b>qPqPqP|</b>")
        tw, th = layout.get_pixel_size()
        self.set_size_request(tw*5, th+4)


class PinBox(DayView, Droppable):

    def __init__(self):
        self.event_timerange = TimeRange.until_now()
        DayView.__init__(self, title=_("Pinned Items"), pinbox=True)
        self.notebook = Gtk.Notebook()
        Droppable.__init__(self, self.notebook)

        bookmarker.connect("reload", self.set_from_templates)
        self.set_from_templates()

    @property
    def event_templates(self):
        if not bookmarker.bookmarks:
            # Abort, or we will query with no templates and get lots of
            # irrelevant events.
            return None
        templates = []
        for bookmark in bookmarker.bookmarks:
            subject = Subject.new_for_values(uri=bookmark)
            templates.append(Event.new_for_values(subjects=[subject]))
        return templates

    def set_from_templates(self, *args, **kwargs):
        if bookmarker.bookmarks:
            CLIENT.find_event_ids_for_templates(
                self.event_templates, self.do_set,
                self.event_timerange,
                StorageState.Any, 10000, ResultType.MostRecentSubjects)
        else:
            self.do_set([])

    def do_set(self, event_ids):
        objs = []
        for id_ in event_ids:
            objs += [ContentStruct(id_)]
        self.set_items(objs)
        # Make the pin icons visible
        self.view.show_all()
        self.show_all()

    def set_items(self, items):
        self.clear()
        box = CategoryBox(None, items, True, itemoff=4)
        self.view.pack_start(box, False, True, 0)
        for w in self:
            self.remove(w)
        self.notebook.append_page(self.view, self.label)
        self.label.set_alignment(0.01, 0.5)
        Gtk.Container.child_set_property(self.notebook, self.view, "tab-expand", True)
        Gtk.Container.child_set_property(self.notebook, self.view, "tab-fill", True)
        self.set_border_width(4)
        if len(items) > 0: self.pack_start(self.notebook, False, True, 0)

    def on_drag_data_received(self, wid, context, x, y, selection, target_type, time):
        uri = str(selection.data.strip())
        isbookmarked = bookmarker.is_bookmarked(uri)
        if not isbookmarked:
            bookmarker.bookmark(uri)


## gobject registration
GObject.type_register(_TimelineRenderer)
GObject.type_register(_ThumbViewRenderer)

pinbox = PinBox()
