# -*- 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 Siegfried Gevatter <siegfried@gevatter.com>
# Copyright © 2010 Markus Korn <thekorn@gmx.de>
# Copyright © 2010 Randal Barlow <email.tehk@gmail.com>
# 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 cairo
import os
import gettext
import datetime
import math
import time
import threading
from dbus.exceptions import DBusException

import gi
from gi.repository import GObject, Gtk, Gio, Gdk, GdkPixbuf

try:
    gi.require_version('Gst', '1.0')
    from gi.repository import Gst
    Gst.init(None)
except ImportError:
    Gst = None

from zeitgeist.datamodel import Event, Subject, StorageState

from .common import *
from . import content_objects
from .config import BASE_PATH, VERSION, settings, PluginManager, get_icon_path, get_data_path, bookmarker
from .sources import SUPPORTED_SOURCES
from . import external
from .store import STORE, get_related_events_for_uri, CLIENT

class DayLabel(Gtk.DrawingArea):

    _events = (
        Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK |
        Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.BUTTON_MOTION_MASK |
        Gdk.EventMask.POINTER_MOTION_HINT_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK |
        Gdk.EventMask.BUTTON_PRESS_MASK
    )

    def __init__(self,date=None):
        super(DayLabel, self).__init__()

        self.matches_search = False
        self.style = self.get_style_context()
        self.style.add_class("day-label")
        self.font_name = self.get_pango_context().get_font_description().get_family()

        self.set_events(self._events)
        self.connect("draw", self.draw)
        if date:
            self.date = date
        else:
            self.date = datetime.date.today()
        self.set_size_request(100, 60)

    @property
    def date_string(self):
        return self.date.strftime("%A %d %B %Y")

    @property
    def weekday_string(self):
        if self.date == datetime.date.today():
            return _("Today")
        timedelta = datetime.date.today() -self.date
        if timedelta.days == 1:
            return _("Yesterday")
        return self.date.strftime("%A")

    @property
    def leading(self):
        if self.date == datetime.date.today():
            return True

    def set_date(self, date):
        self.date = date
        self.queue_draw()

    def set_matching(self, matching):
        self.matches_search = matching
        self.queue_draw()

    def day_text(self, widget, context):
        layout = widget.create_pango_layout(self.weekday_string)
        layout.set_font_description(Pango.FontDescription.from_string(self.font_name + " Bold 15"))
        layout_width, layout_height = layout.get_pixel_size()

        Gtk.render_layout(self.style, context, (self.width - layout_width)/2, self.height/2 - layout_height, layout)

        self.date_text(widget, context, self.height/2)

    def date_text(self, widget, context, last_font_height):
        layout = widget.create_pango_layout(self.date_string)
        layout.set_font_description(Pango.FontDescription.from_string(self.font_name + " 10"))
        layout_width, layout_height = layout.get_pixel_size()

        Gtk.render_layout(self.style, context, (self.width - layout_width)/2, last_font_height, layout)

    def draw(self, widget, context):
        if self.leading:
            self.style.add_class('white-fg-color')
        else:
            self.style.remove_class('white-fg-color')

        self.width = widget.get_allocated_width()
        self.height = widget.get_allocated_height()

        if self.leading:
            (found, bg) = self.style.lookup_color("theme_selected_bg_color")

            if found:
                red = bg.red
                green = bg.green
                blue = bg.blue
            else:
                # GNOME blue
                c = Gdk.RGBA()
                Gdk.RGBA.parse(c, "#3584e4")

                red = c.red
                green = c.green
                blue = c.blue
        else:
            (found, bg) = self.style.lookup_color("theme_base_color")

            if found:
                red = bg.red
                green = bg.green
                blue = bg.blue
            else:
                red = 1.0
                green = 1.0
                blue = 1.0

        x = 0; y = 0
        r = 5

        transparent = 1
        context.new_sub_path()
        context.arc(r+x, r+y, r, math.pi, 3 * math.pi /2)
        context.arc(self.width-r, r+y, r, 3 * math.pi / 2, 0)
        context.close_path()
        context.set_source_rgba(red, green, blue, transparent)
        context.rectangle(0, r, self.width, self.height)
        context.fill()

        color = get_gtk_rgba(self.style, "bg", 0)
        separator_red = color.red
        separator_green = color.green
        separator_blue = color.blue

        context.set_source_rgba(separator_red, separator_green, separator_blue, transparent)
        context.rectangle(0, r + self.height - 8, self.width, 8)
        context.fill()

        if self.matches_search:
            color = get_histogram_search_color(self.style)
            highlight_red = color.red
            highlight_green = color.green
            highlight_blue = color.blue
            context.set_source_rgba(highlight_red, highlight_green, highlight_blue, transparent)
            context.rectangle(0, r + self.height - 11, self.width, 3)
            context.fill()

        self.day_text(widget, context)

        return False

class DayButton(Gtk.DrawingArea):
    leading = False
    pressed = False
    today_pressed = False
    sensitive = True
    today_hover = False
    hover = False
    header_size = 60
    bg_color = (0, 0, 0, 0)
    header_color = (1, 1, 1, 1)
    leading_header_color = (1, 1, 1, 1)
    internal_color = (0, 1, 0, 1)
    arrow_color = (1, 1, 1, 1)
    arrow_color_selected = (1, 1, 1, 1)

    __gsignals__ = {
        "clicked":  (GObject.SignalFlags.RUN_LAST, None,()),
        "jump-to-today": (GObject.SignalFlags.RUN_LAST, None,()),
        }
    _events = (
        Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK |
        Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.BUTTON_PRESS_MASK |
        Gdk.EventType.MOTION_NOTIFY |   Gdk.EventMask.POINTER_MOTION_MASK
    )

    @classmethod
    def new(cls, side = 0, sensitive=True):
        button = DayButton(side, sensitive)
        def query_tooltip(widget, x, y, keyboard_mode, tooltip, ebutton):
            if not ebutton.sensitive:
                return False
            elif y < ebutton.header_size and ebutton.side == 1:
                text = _("Go to Today")
            elif y >= ebutton.header_size:
                text = _("Go to the previous day") if ebutton.side == 0 else _("Go to the next day")
            else:
                return False
            tooltip.set_text(text)
            return True
        evbox = Gtk.EventBox()
        evbox.connect("query-tooltip", query_tooltip, button)
        evbox.set_property("has-tooltip", True)
        evbox.add(button)

        return button, evbox

    def __init__(self, side = 0, sensitive=True):
        super(DayButton, self).__init__()

        self.style = self.get_style_context()
        self.style.add_class("day-button")

        self.set_events(self._events)
        self.set_can_focus(True)
        self.side = side
        self.connect("button_press_event", self.on_press)
        self.connect("button_release_event", self.clicked_sender)
        self.connect("key_press_event", self.keyboard_clicked_sender)
        self.connect("motion_notify_event", self.on_hover)
        self.connect("leave_notify_event", self._enter_leave_notify, False)
        self.connect("draw", self.draw)
        self.connect("style-updated", self.change_style)
        self.set_size_request(20, -1)
        self.set_sensitive(sensitive)

    def set_leading(self, leading):
        self.leading = leading

    def set_sensitive(self, case):
        self.sensitive = case
        self.queue_draw()

    def _enter_leave_notify(self, widget, event, bol):
        self.hover = bol
        self.today_hover = bol
        self.queue_draw()

    def on_hover(self, widget, event):
        if event.y > self.header_size:
            if not self.hover:
                self.hover = True
            else:
                self.today_hover = False
        else:
            if self.hover:
                self.hover = False
            else:
                self.today_hover = True
        self.queue_draw()
        return False

    def on_press(self, widget, event):
        if event.y > self.header_size:
            self.pressed = True
        else:
            self.today_pressed = True
        self.queue_draw()

    def keyboard_clicked_sender(self, widget, event):
        if event.keyval in (Gdk.KEY_Return, Gdk.KEY_space):
            if self.sensitive:
                self.emit("clicked")
            self.pressed = False
            self.queue_draw()
            return True
        return False

    def clicked_sender(self, widget, event):
        if event.y > self.header_size:
            if self.sensitive:
                self.emit("clicked")
        elif event.y < self.header_size:
            self.emit("jump-to-today")
        self.pressed = False
        self.today_pressed = False;
        self.queue_draw()
        return True

    def change_style(self, widget):
        self.bg_color = get_gtk_rgba(self.style, "bg", 0)
        self.header_color = get_gtk_rgba(self.style, "bg", 0, 1.25)
        self.leading_header_color = get_gtk_rgba(self.style, "bg", 3)
        self.internal_color = get_gtk_rgba(self.style, "bg", 0, 1.02)
        self.arrow_color = get_gtk_rgba(self.style, "text", 0, 0.6)
        self.arrow_color_selected = get_gtk_rgba(self.style, "bg", 3)
        self.arrow_color_insensitive = get_gtk_rgba(self.style, "text", 4)

    def draw(self, widget, context):
        width = self.get_allocated_width()
        height = self.get_allocated_height()
        context.set_source_rgba(*self.bg_color)
        context.set_operator(cairo.Operator.SOURCE)
        context.paint()
        context.rectangle(0, 0, width, height)
        context.clip()

        x = 0; y = 0
        r = 5

        w = widget.get_allocated_width()
        h = widget.get_allocated_height()

        angle = GLib.PI_2 if self.side else 3 * GLib.PI_2

        size = 20
        if self.sensitive:
            context.set_source_rgba(*(self.leading_header_color if self.leading else self.header_color))
            context.new_sub_path()
            context.move_to(x+r,y)
            context.line_to(x+w-r,y)
            context.curve_to(x+w,y,x+w,y,x+w,y+r)
            context.line_to(x+w,y+h-r)
            context.curve_to(x+w,y+h,x+w,y+h,x+w-r,y+h)
            context.line_to(x+r,y+h)
            context.curve_to(x,y+h,x,y+h,x,y+h-r)
            context.line_to(x,y+r)
            context.curve_to(x,y,x,y,x+r,y)
            # What's this for, exactly? Appears to have been already set.
            #context.set_source_rgba(*(self.leading_header_color if self.leading else self.header_color))
            context.close_path()
            context.rectangle(0, r, w,  self.header_size)
            context.fill()
            context.set_source_rgba(*self.internal_color)
            context.rectangle(0, self.header_size, w,  h)
            context.fill()
            if self.hover:
                Gtk.render_frame(self.style, context,
                                 0, self.header_size,
                                 w, h-self.header_size)
            if self.side > 0 and self.today_hover:
                Gtk.render_frame(self.style, context,
                                 0, 0,
                                 w, self.header_size)
        size = 10
        if not self.sensitive:
            state = Gtk.StateFlags.INSENSITIVE
        elif self.is_focus() or self.pressed:
            Gtk.render_focus(self.style, context,
                             0, self.header_size,
                             w, h-self.header_size)
            state = Gtk.StateFlags.SELECTED
        else:
            state = Gtk.StateFlags.NORMAL
        Gtk.render_arrow(self.style, context, angle,
                         w/2-size/2, h/2 + size/2, size)
        size = 7

        # Paint today button arrows.
        if self.sensitive and self.side > 0:
            if self.today_hover:
                if self.today_pressed:
                    Gtk.render_arrow(self.style, context, angle,
                                     w/2, self.header_size/2 - size/2, size)
                    Gtk.render_arrow(self.style, context, angle,
                                     w/2-size/2, self.header_size/2 - size/2, size)
                else:
                    Gtk.render_arrow(self.style, context, angle,
                                     w/2, self.header_size/2 - size/2, size)
                    Gtk.render_arrow(self.style, context, angle,
                                     w/2-size/2, self.header_size/2 - size/2, size)
            else:
                Gtk.render_arrow(self.style, context, angle,
                                 w/2, self.header_size/2 - size/2, size)
                Gtk.render_arrow(self.style, context, angle,
                                 w/2-size/2, self.header_size/2 - size/2, size)

class SearchBox(Gtk.ToolItem):

    __gsignals__ = {
        "clear" : (GObject.SignalFlags.RUN_FIRST,
                   None,
                   ()),
        "search" : (GObject.SignalFlags.RUN_FIRST,
                    None,
                    (GObject.TYPE_PYOBJECT,)),
        "matching-day" : (GObject.SignalFlags.RUN_FIRST,
                          None,
                          (GObject.TYPE_PYOBJECT,)),
        "matching-days" : (GObject.SignalFlags.RUN_FIRST,
                           None,
                           (GObject.TYPE_PYOBJECT,)),
    }

    search_cleared = False
    search_in_pending = False
    search_in_progress = False

    @property
    def use_fts(self):
        #if STORE.fts_search_enabled:
        #    return self.fts_checkbutton.get_active()
        return False

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

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

        self.text = ""
        self.previous_text = ""
        self.callback = None
        self.set_border_width(3)
        self.hbox = Gtk.HBox(False, 6)
        self.search_hbox = Gtk.HBox(False, 0)
        self.add(self.hbox)
        self.results = []
        self.search = Gtk.SearchEntry()
        self.search_style = self.search.get_style_context()
        self.search.set_placeholder_text(_("Type to search"))
        self.search.set_width_chars(35);
        self.category = {}
        self.matching_dates = None
        self.matching_date_index = 0
        self.total_matching_dates = 0
        self.search_progress_timeout = 0
        self.warning_timeout = 0

        #if STORE.fts_search_enabled:
        #    self.fts_checkbutton = Gtk.CheckButton(_("Use Zeitgeist FTS"))

        for source in list(SUPPORTED_SOURCES.keys()):
            s = SUPPORTED_SOURCES[source]._desc_pl
            self.category[s] = source
        self.combobox = Gtk.ComboBoxText()

        self.padding = Gtk.Label()
        self.padding.set_width_chars(10)

        self.label = Gtk.Label()
        self.label.set_width_chars(30)
        self.label.set_alignment(0.0, 0.5)

        # Previous matching day button
        self.previous_button = previous_button = Gtk.Button.new_from_icon_name("go-previous-symbolic", Gtk.IconSize.BUTTON)
        previous_button.connect("clicked", self.on_previous_match_clicked)
        previous_button.set_tooltip_text(_("Go to the previous matching day"))
        previous_button.set_sensitive(False)

        # Next matching day button
        self.next_button = next_button = Gtk.Button.new_from_icon_name("go-next-symbolic", Gtk.IconSize.BUTTON)
        next_button.connect("clicked", self.on_next_match_clicked)
        next_button.set_sensitive(False)
        next_button.set_tooltip_text(_("Go to the next matching day"))

        self.search_hbox.get_style_context().add_class("linked")

        self.combobox.set_focus_on_click(False)
        self.search_hbox.pack_start(self.search, False, True, 0)
        self.search_hbox.pack_start(self.previous_button, False, True, 0)
        self.search_hbox.pack_start(self.next_button, False, True, 0)

        self.hbox.pack_start(self.padding, False, True, 0)
        self.hbox.pack_start(self.combobox, False, False, 0)
        self.hbox.pack_start(self.search_hbox, False, True, 0)
        self.hbox.pack_start(self.label, False, False, 0)
        #if STORE.fts_search_enabled:
        #    self.hbox.pack_end(self.fts_checkbutton, False, True, 0)
        self.combobox.append_text("All activities")
        self.combobox.set_active(0)
        for cat in list(self.category.keys()):
            self.combobox.append_text(cat)

        def combo_changed(widget, self):
            self.set_search(self.search)

        self.combobox.connect("changed", combo_changed, self)
        self.show_all()

        def change_style(widget):
            color = get_gtk_rgba(self.style, "bg", 0)
            fcolor = get_gtk_rgba(self.style, "fg", 0)

            color = shade_gdk_color(color, 102/100.0)
            self.override_background_color(Gtk.StateFlags.NORMAL, color)

            color = combine_gdk_color(color, fcolor)
            self.search.override_color(Gtk.StateFlags.NORMAL, color)

        # self.hbox.connect("style-updated", change_style)
        self.search.connect("search-changed", self.set_search)

    def update_navigation_label(self):
        # do not update ui if another search is in progress or search
        # is cleared.
        if self.ignore_search("labelling", self.previous_text):
            self.search_finished()
            return

        label_uris = gettext.ngettext("{} match", "{} matches", self.total_matching_uri).format(self.total_matching_uri)
        label_days = gettext.ngettext("{} day", "{} days", self.total_matching_dates).format(self.total_matching_dates)
        label_events = gettext.ngettext("{} event", "{} events", self.total_valid_events).format(self.total_valid_events)
        link = gettext.ngettext(" in ", " over ", self.total_matching_dates)

        display_label = label_uris + link + label_days
        tooltip_label = label_events

        self.label.set_text(display_label)
        self.label.set_tooltip_text(tooltip_label)

    def reset_navigation(self):
        self.previous_button.set_sensitive(False)
        self.next_button.set_sensitive(False)

        if self.total_matching_dates:
            if self.total_matching_dates > 1:
                self.previous_button.set_sensitive(True)
            self.update_navigation_label()

    def update_navigation(self):
        self.update_navigation_label()

        if self.matching_date_index == self.total_matching_dates - 1:
            self.previous_button.set_sensitive(False)
        else:
            self.previous_button.set_sensitive(True)

        if self.matching_date_index == 0:
            self.next_button.set_sensitive(False)
        else:
            self.next_button.set_sensitive(True)

    def get_previous_match(self):
        self.matching_date_index +=1
        date = self.matching_dates[self.matching_date_index]
        return date

    def get_next_match(self):
        self.matching_date_index -=1
        date = self.matching_dates[self.matching_date_index]
        return date

    def on_previous_match_clicked(self, button):
        date = self.get_previous_match()
        self.update_navigation()
        self.emit("matching-day", date)

    def on_next_match_clicked(self, button):
        date = self.get_next_match()
        self.update_navigation()
        self.emit("matching-day", date)

    def single_char_search_warning(self):
        if self.dont_search:
            self.search_style.add_class('warning')
        self.warning_timeout = 0

    def set_search(self, entry):
        text = entry.get_text()
        self.text = text

        # clear error style if no matches found
        self.search_style.remove_class('error')
        self.search_style.remove_class('warning')

        interpretation = None
        self.matching_dates = None
        self.total_matching_uri = 0
        self.total_matching_dates = 0
        self.total_valid_events = 0
        self.matching_date_index = 0

        no_search = False
        self.dont_search = False

        if config.SEARCH_IGNORE_SINGLE_CHAR and len(text) == 1:
            self.dont_search = True
            if self.warning_timeout:
                GObject.source_remove(self.warning_timeout)
            self.warning_timeout = GLib.timeout_add(1000, self.single_char_search_warning)

        if not text:
            no_search = True

        if self.dont_search or no_search:
            self.results = []
            self.emit("clear")
            self.label.set_text("")
            self.label.set_tooltip_text("")
            SearchBox.search_cleared = True

            if config.DEBUG:
                print("all search cleared")

            # make sure to clear the flag in content objects
            content_objects.ContentObject.clear_search_matches()
            self.reset_navigation()
            return

        SearchBox.search_cleared = False

        def callback(text, results):
            self.results = results

            # mark first before emitting 'search' signal
            marked = self.__mark_search_results(text, results)
            if marked:
                self.emit("search", results)

        cat = self.combobox.get_active()
        if cat != 0:
            cat = self.category[self.combobox.get_active_text()]
            interpretation = self.category[self.combobox.get_active_text()]
            if interpretation:
                return self.do_search(text, callback, interpretation)

        return self.do_search(text, callback)

    def do_search(self, text, callback=None, interpretation=None):
        if not callback: return
        self.do_search_objs(text, callback, interpretation)

    def search_progress(self):
        self.search.progress_pulse()
        return GLib.SOURCE_CONTINUE

    def search_finished(self):
        self.search.set_progress_fraction(0.0)
        if self.search_progress_timeout:
            GObject.source_remove(self.search_progress_timeout)
            self.search_progress_timeout = 0

    def do_search_objs(self, text, callback, interpretation=None):
        def _search(text, callback):
            if SearchBox.search_in_progress:
                if config.DEBUG:
                    print ("search '%s' in progress. re-scheduling" % (self.previous_text))
                GLib.timeout_add(200, _search, text, callback)
                SearchBox.search_in_pending = True
                return

            self.previous_text = text
            SearchBox.search_in_pending = False
            SearchBox.search_in_progress = True
            _do_search(text, callback)
            SearchBox.search_in_progress = False

        def _do_search(text, callback):
            if config.DEBUG:
                print ("searching for '%s'" % (text))

            # display search progress
            self.search.set_progress_fraction(0.1)
            self.search_progress_timeout = GLib.timeout_add(100, self.search_progress)

            if STORE.fts_search_enabled and self.use_fts:
                matching = STORE.search_using_zeitgeist_fts(text, [Event.new_for_values(subject_interpretation=interpretation)] if interpretation else [])
            else:
                def matching_test_function(obj):
                    subject = obj.event.subjects[0]

                    # match filenames
                    if not config.SEARCH_IS_CASE_SENSITIVE:
                        match_name = text.lower() in subject.text.lower()
                    else:
                        match_name = text in subject.text

                    match_uri = False
                    # match path in uri
                    if config.SEARCH_IN_SUBJECT_URI and not match_name:
                        if not config.SEARCH_IS_CASE_SENSITIVE:
                            match_uri = text.lower() in subject.uri.lower()
                        else:
                            match_uri = text in subject.uri

                    if match_name or match_uri:
                        if interpretation:
                            try:
                                if subject.interpretation != interpretation:
                                    return False
                            except Exception:
                                return False
                        return True
                    return False
                matching = STORE.search_store_using_matching_function(matching_test_function)

            # do not update ui if another search is in progress or search
            # is cleared.
            if self.ignore_search("search-completion", text):
                self.search_finished()
                return

            if config.DEBUG:
                print("updating search results for '%s'" % (text))

            callback(text, matching)

        GLib.idle_add(_search, text, callback)

    def do_search_using_zeitgeist(self, text, callback=None, interpretation=""):
        if not text: return
        self.callback = callback
        templates = [
            Event.new_for_values(subject_text="*"+text+"*", subject_interpretation=interpretation),
            Event.new_for_values(subject_uri="*"+text+"*", subject_interpretation=interpretation)
        ]
        CLIENT.find_event_ids_for_templates(templates, self._search_callback,
            storage_state=StorageState.Available, num_events=20, result_type=0)

    def _search_callback(self, ids):
        objs = []
        for id_ in ids:
            try:
                obj.append(STORE[id_])
            except KeyError:
                continue
        if self.callback:
            self.callback(objs)

    def toggle_visibility(self):
        if self.get_property("visible"):
            self.hide()
            return False
        self.show()
        return True

    def ignore_search(self, marker, text):
        if SearchBox.search_in_pending or SearchBox.search_cleared:
            if config.DEBUG:
                print("[%s] - ignoring search results for '%s' as new search "
                      "is in progress" % (marker, text))
            return True

        if config.DEBUG:
            print("[%s] - search '%s' proceeding" % (marker, text))

        return False

    def __mark_search_results(self, text, results):
        content_objects.ContentObject.clear_search_matches()

        unique_days = []
        unique_uris = []
        timestamps_millis = []

        unique_day_count = 0
        unique_uri_count = 0
        valid_result_count = 0
        all_result_count = len(results)

        for obj in results:
            if obj.content_object:
                # do not update ui if another search is in progress or
                # search is cleared.
                if self.ignore_search("marking", text):
                    self.search_finished()
                    content_objects.ContentObject.clear_search_matches()
                    return False

                valid_result_count += 1

                # non-unique timestamp is needed for day spread count
                timestamp_millis = int(obj.event.timestamp)
                timestamps_millis.append(timestamp_millis)
                timestamp_secs = timestamp_millis/1000.0

                # unique uris
                if obj.content_object.uri not in unique_uris:
                    matching_day = datetime.date.fromtimestamp(timestamp_millis/1000)
                    unique_uris.append(obj.content_object.uri)
                    unique_uri_count += 1

                # mark as match
                setattr(obj.content_object, "matches_search", True)

        # get all matching dates
        timestamps_millis = list(set(timestamps_millis))
        timestamps_millis.sort(reverse=True)

        for timestamp in timestamps_millis:
            matching_day = datetime.date.fromtimestamp(timestamp/1000)
            if matching_day not in unique_days:
                unique_day_count += 1
                unique_days.append(matching_day)

        self.matching_dates = unique_days
        self.total_matching_uri = unique_uri_count
        self.total_matching_dates = unique_day_count
        self.total_valid_events = valid_result_count
        self.matching_date_index = 0

        self.emit("matching-days", self.matching_dates)
        if unique_day_count:
            self.emit("matching-day", self.matching_dates[0])
        else:
            self.search_style.add_class('error')
            self.label.set_tooltip_text("")
            self.label.set_text("")

        if config.DEBUG:
            print("matches for text '%s': %d (all), %d (valid), %d (unique uris), %d (unique days)" %
                  (text, all_result_count, valid_result_count, unique_uri_count, unique_day_count))

        self.reset_navigation()
        self.search_finished()

        return True

class PreviewTooltip(Gtk.Window):

    # per default we are using thumbs at a size of 128 * 128 px
    # in tooltips. For preview of text files we are using 256 * 256 px
    # which is dynamically defined in StaticPreviewTooltip.preview()
    TOOLTIP_SIZE = SIZE_NORMAL

    def __init__(self):
        Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP)

    def preview(self, gio_file):
        return False


class StaticPreviewTooltip(PreviewTooltip):

    def __init__(self):
        super(StaticPreviewTooltip, self).__init__()
        self.__current = None
        self.__monitor = None

    def replace_content(self, content):
        children = self.get_children()
        if children:
            self.remove(children[0])
            # hack to force the tooltip to have the exact same size
            # as the child image
            self.resize(1,1)
        self.add(content)

    def preview(self, gio_file):
        if gio_file == self.__current:
            return bool(self.__current)
        if self.__monitor is not None:
            self.__monitor.cancel()
        self.__current = gio_file
        self.__monitor = gio_file.get_monitor()
        self.__monitor.connect("changed", self._do_update_preview)
        # for text previews we are always using SIZE_LARGE
        if "text-x-generic" in gio_file.icon_names or "text-x-script" in gio_file.icon_names:
            size = SIZE_LARGE
        else:
            size = self.TOOLTIP_SIZE
        if not isinstance(gio_file, GioFile): return False
        pixbuf = gio_file.get_thumbnail(size=size, border=1)
        if pixbuf is None:
            self.__current = None
            return False
        img = Gtk.Image.new_from_pixbuf(pixbuf)
        img.set_alignment(0.5, 0.5)
        img.show_all()
        self.replace_content(img)
        del pixbuf, size
        return True

    def _do_update_preview(self, monitor, file, other_file, event_type):
        if event_type == Gio.FileMonitorEvent.CHANGES_DONE_HINT:
            if self.__current is not None:
                self.__current.refresh()
            self.__current = None
            Gtk.tooltip_trigger_tooltip_query(Gdk.Display.get_default())


class VideoPreviewTooltip(PreviewTooltip):

    def __init__(self):
        PreviewTooltip.__init__(self)
        hbox = Gtk.HBox()
        self.movie_window = Gtk.DrawingArea()
        hbox.pack_start(self.movie_window, False, True, 0)
        self.add(hbox)
        self.has_preview = True
        self.player = Gst.ElementFactory.make("playbin", "player")
        self.sink = Gst.ElementFactory.make("gtksink", "gtksink")

        self.showing = False

        # no video preview if elements are missing -or- if user doesn't want it
        if self.player == None or self.sink == None or not config.SHOW_VIDEO_PREVIEW:
            self.has_preview = False
            return

        self.player.props.video_sink = self.sink
        self.gst_widget = self.sink.props.widget

        bus = self.player.get_bus()
        bus.add_signal_watch()
        bus.enable_sync_message_emission()
        bus.connect("message", self.on_message)
        self.connect("hide", self._handle_hide)
        self.connect("show", self._handle_show)
        # FixMe: hack to hide the preview window. rework this.
        self.set_default_size(0, 0)
        self.gst_widget.set_size_request(*SIZE_LARGE)
        self.window = None

        display = Gdk.Display.get_default()
        monitor = display.get_monitor(0) # Monitor 0 for now
        geometry = monitor.get_geometry()

        self.monitor_width = geometry.width
        self.monitor_height = geometry.height

    @staticmethod
    def check_dependencies():
        player = Gst.ElementFactory.make("playbin", "player")
        sink = Gst.ElementFactory.make("gtksink", "gtksink")

        # no video preview if elements are missing
        return (player and sink)

    def has_video_preview(self):
        return self.has_preview

    def _handle_hide(self, widget):
        if not self.has_preview:
            return

        self.showing = False
        self.hide()
        self.window.hide()
        self.player.set_state(Gst.State.NULL)

    def _handle_show(self, widget):
        if not self.has_preview:
            return

        self.showing = True
        self.show_all()
        self.window.show_all()
        self.player.set_state(Gst.State.PLAYING)

    def preview(self, gio_file, x, y):
        if not self.has_preview:
            return

        frame_w = 320
        frame_h = 240

        # Use custom gst gtk window
        if self.window:
            self.window.remove(self.gst_widget)
        self.window = window = Gtk.Window()

        window.set_default_size(frame_w, frame_h)
        window.add(self.gst_widget)
        window.set_title(_("Video Preview"))

        x_offset = 50
        y_offset = 50

        if self.monitor_width - x < frame_w + 3 * x_offset:
            x_offset = -(frame_w + 3 * x_offset)

        if self.monitor_height - y < frame_h + 3 * y_offset:
            y_offset = -(frame_h + 3 * y_offset)

        # hope window manager honors the request.
        self.window.move(x + x_offset, y + y_offset)

        if gio_file.uri == self.player.get_property("uri"):
            return True
        self.player.set_property("uri", gio_file.uri)
        return True

    def on_message(self, bus, message):
        t = message.type
        if t == Gst.MessageType.EOS:
            self.player.set_state(Gst.State.NULL)
            self.hide()
        elif t == Gst.MessageType.ERROR:
            self.player.set_state(Gst.State.NULL)
            err, debug = message.parse_error()
            print("Error: %s" % err, debug)

class AudioPreviewTooltip(PreviewTooltip):

    def __init__(self):
        PreviewTooltip.__init__(self)
        #Playing label stuffs

        if False:
            screen = self.get_screen()
            map_ = screen.get_rgba_colormap()
            if map_ is None:
                map_ = screen.get_rgb_colormap()
                self.set_colormap(map_)

        self.has_preview = True
        self.showing = False

        # GStreamer stuffs
        self.player = Gst.ElementFactory.make("playbin", "player")
        fakesink = Gst.ElementFactory.make("fakesink", "fakesink")

        # no audio preview if gstreamer elements are missing -or- if user doesn't want it
        if self.player == None or fakesink == None or not config.SHOW_AUDIO_PREVIEW:
            self.has_preview = False
            return

        self.set_app_paintable(True)
        img = Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PLAY,Gtk.IconSize.LARGE_TOOLBAR)
        self.image = AnimatedImage(get_data_path("zlogo/zg%d.png"), 150, size=20)
        self.image.set_valign(Gtk.Align.CENTER)
        self.image.start()
        label = Gtk.Label()
        label.set_markup(_("<b><span color='white'>Audio Preview</span></b>"))
        label.set_valign(Gtk.Align.CENTER)
        hbox = Gtk.HBox()
        hal = Gtk.Alignment.new(0.5, 0.5, 1.0, 1.0)
        hal.set_padding(12, 12, 0, 6)
        hal.add(label)
        hbox.pack_start(self.image, False, True, 6)
        hbox.pack_end(hal, False, True, 0)
        self.resize(1,1)
        self.add(hbox)

        self.player.set_property("video-sink", fakesink)
        bus = self.player.get_bus()
        bus.add_signal_watch()
        bus.connect("message", self.on_message)
        self.connect("hide", self._handle_hide)
        self.connect("show", self._handle_show)
        self.connect("draw", self.transparent_draw)

    def transparent_draw(self, widget, context):
        window = widget.props.window
        context = window.cairo_create()
        context.set_source_rgba(0, 0, 0, 0.75)
        context.paint()

        return False

    @staticmethod
    def check_dependencies():
        player = Gst.ElementFactory.make("playbin", "player")
        fakesink = Gst.ElementFactory.make("fakesink", "fakesink")

        # no audio preview if elements are missing
        return (player and fakesink)

    def has_audio_preview(self):
        return self.has_preview

    def _handle_hide(self, widget):
        if not self.has_preview:
            return

        self.showing = False
        self.image.stop()
        self.player.set_state(Gst.State.NULL)

    def _handle_show(self, widget):
        if not self.has_preview:
            return

        self.showing = True
        self.image.start()
        self.show_all()
        self.player.set_state(Gst.State.PLAYING)

    def preview(self, gio_file, x=0, y=0):
        if not self.has_preview:
            return

        if gio_file.uri == self.player.get_property("uri"):
            return True
        self.player.set_property("uri", gio_file.uri)
        return True

    def on_message(self, bus, message):
        t = message.type
        if t == Gst.MessageType.EOS:
            self.player.set_state(Gst.State.NULL)
        elif t == Gst.MessageType.ERROR:
            self.player.set_state(Gst.State.NULL)
            err, debug = message.parse_error()
            print("Error: %s" % err, debug)

    def replace_content(self, content):
        children = self.get_children()
        if children:
            self.remove(children[0])
            # hack to force the tooltip to have the exact same size
            # as the child image
            self.resize(1,1)
        self.add(content)

class AnimatedImage(Gtk.Image):
    animating = None
    mod = 7
    i = 0
    speed = 100
    def __init__(self, uri, speed = 0, size = 16):
        super(AnimatedImage, self).__init__()
        if speed: self.speed = speed
        self.frames = []
        for i in (6, 5, 4, 3, 2, 1, 0):
            self.frames.append(GdkPixbuf.Pixbuf.new_from_file_at_size(get_icon_path(uri % i), size, size))
        self.set_from_pixbuf(self.frames[0])

    def next(self):
        """
        Move to next frame
        """
        self.set_from_pixbuf(self.frames[self.i % self.mod])
        self.i += 1
        return True

    def start(self):
        """
        start the image's animation
        """
        if self.animating: GObject.source_remove(self.animating)
        self.animating = GObject.timeout_add(self.speed, self.next)

    def stop(self):
        """
        stop the image's animation
        """
        if self.animating: GObject.source_remove(self.animating)
        self.animating = None
        return False

    def animate_for_seconds(self, seconds):
        """
        :param seconds: int seconds for the amount of time when you want
        animate the throbber
        """
        self.start()
        GObject.timeout_add_seconds(seconds, self.stop)


class ThrobberPopupButton(Gtk.ToolItem):

    def __init__(self):
        super(ThrobberPopupButton, self).__init__()
        self.button = button = Gtk.MenuButton()
        self.button.set_direction(Gtk.ArrowType.NONE)
        self.button.set_valign(Gtk.Align.CENTER)
        self.button.set_margin_end(6)
        self.add(button)

        builder = Gtk.Builder.new_from_file(get_data_path("ui/app-menu.ui"))
        menu_model = builder.get_object("app-menu")
        self.popover = Gtk.Popover.new_from_model(button, menu_model)

        button.set_popover(self.popover)

class AboutDialog(Gtk.AboutDialog):
    name = "Activity Journal"
    authors = (
        "Seif Lotfy <seif@lotfy.com>",
        "Randal Barlow <email.tehk@gmail.com>",
        "Siegfried-Angel Gevatter <siegfried@gevatter.com>",
        "Peter Lund <peterfirefly@gmail.com>",
        "Hylke Bons <hylkebons@gmail.com>",
        "Markus Korn <thekorn@gmx.de>",
        "Mikkel Kamstrup <mikkel.kamstrup@gmail.com>",
        "Thorsten Prante <thorsten@prante.eu>",
        "Stefano Candori <stefano.candori@gmail.com>",
        "crvi <crvisqr@gmail.com>"
    )

    artists = (
        "Hylke Bons <hylkebons@gmail.com>",
        "Thorsten Prante <thorsten@prante.eu>"
    )

    client_version = "{}.{}.{}".format(*external.CLIENT_VERSION)
    copyright = "Copyright 2009-2020 - The Activity Journal authors"
    comment = _("Activity Journal for the GNOME desktop environment\nPowered by Zeitgeist {}".format(client_version))
    website_label = _("Activity Journal website")
    website = "https://wiki.gnome.org/Apps/ActivityJournal"
    version = VERSION

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_program_name(self.name)
        self.set_version(self.version)
        self.set_comments(self.comment)
        self.set_copyright(self.copyright)
        self.set_authors(self.authors)
        self.set_artists(self.artists)
        self.set_license_type(Gtk.License.GPL_3_0)
        self.set_translator_credits(_("translator-credits"))
        self.set_website(self.website)
        self.set_website_label(self.website_label)

        self.set_logo_icon_name("org.gnome.ActivityJournal")
        #self.set_logo(GdkPixbuf.Pixbuf.new_from_file_at_size(get_icon_path(
         #   APP_ICON_NORMAL), 128, 128))


class ContextMenu(Gtk.Menu):
    subjects = []# A list of Zeitgeist event uris
    infowindow = None
    parent_window = None

    def __init__(self):
        super(ContextMenu, self).__init__()
        self.menuitems = {
            "open" : Gtk.ImageMenuItem(_("Open")),
            "unpin" : Gtk.MenuItem(_("Remove Pin")),
            "pin" : Gtk.MenuItem(_("Add Pin")),
            "delete" : get_menu_item_with_stock_id_and_text(Gtk.STOCK_DELETE, _("Delete item from Journal")),
            "delete_uri" : Gtk.MenuItem(_("Delete all events with this URI")),
            "info" : get_menu_item_with_stock_id_and_text(Gtk.STOCK_INFO, _("More Information")),
            }
        callbacks = {
            "open" : self.do_open,
            "unpin" : self.do_unbookmark,
            "pin" : self.do_bookmark,
            "delete" : self.do_delete,
            "delete_uri" : self.do_delete_events_with_shared_uri,
            "info" : self.do_show_info,
            }
        names = ["open", "unpin", "pin", "delete", "delete_uri", "info"]
        if is_command_available("nautilus-sendto"):
            self.menuitems["sendto"] = get_menu_item_with_stock_id_and_text(Gtk.STOCK_CONNECT, _("Send To..."))
            callbacks["sendto"] = self.do_send_to
            names.append("sendto")
        for name in names:
            item = self.menuitems[name]
            self.append(item)
            item.connect("activate", callbacks[name])
        self.show_all()
        self.confirmed = False

    def do_popup(self, time, subjects):
        """
        Call this method to popup the context menu

        :param time: the event time from the button press event
        :param subjects: a list of uris
        """
        self.subjects = subjects
        if len(subjects) == 1:
            uri = subjects[0].uri
            if bookmarker.is_bookmarked(uri):
                self.menuitems["pin"].hide()
                self.menuitems["unpin"].show()
            else:
                self.menuitems["pin"].show()
                self.menuitems["unpin"].hide()

        self.popup(None, None, None, None, 3, time)

    def do_open(self, menuitem):
        for obj in self.subjects:
            obj.launch()

    def do_show_info(self, menuitem):
        if self.subjects:
            if self.infowindow:
                self.infowindow.destroy()
            self.infowindow = InformationContainer(parent=self.parent_window)
            self.infowindow.set_content_object(self.subjects[0])
            self.infowindow.show_all()

    def do_bookmark(self, menuitem):
        for obj in self.subjects:
            uri = obj.uri
            uri = str(uri)
            isbookmarked = bookmarker.is_bookmarked(uri)
            if not isbookmarked:
                bookmarker.bookmark(uri)

    def do_unbookmark(self, menuitem):
        for obj in self.subjects:
            uri = obj.uri
            uri = str(uri)
            isbookmarked = bookmarker.is_bookmarked(uri)
            if isbookmarked:
                bookmarker.unbookmark(uri)

    def do_delete(self, menuitem):
        for obj in self.subjects:
            self.do_delete_object(obj, False)

    # Delete all matching events for object within its day part time range
    def do_delete_object(self, obj, confirmed=True):
        if obj is None: return

        self.confirmed = confirmed
        self.delete_all = False
        self.deleted_uri = obj.uri

        timerange=DayParts.get_day_part_range_for_item(obj)
        self.from_time = int(timerange[0])/1000
        self.to_time = int(timerange[1])/1000

        CLIENT.find_event_ids_for_template(
            Event.new_for_values(subject_uri=obj.uri),
            self._delete_objects,
            timerange=timerange)

    # Delete all matching events for object
    def do_delete_events_with_shared_uri(self, menuitem):
        self.delete_all = True

        for uri in [obj.uri for obj in self.subjects]:
            self.deleted_uri = uri
            CLIENT.find_event_ids_for_template(
                Event.new_for_values(subject_uri=uri),
                self._delete_objects)

    # confirm deletion and proceed
    def _delete_objects(self, ids):
        ids = list(map(int, ids))
        count = len(ids)

        # confirm deletion
        if not self.confirmed and config.CONFIRM_ITEM_DELETION:
            delete = confirm_object_deletion(self.get_toplevel(),
                                             self.deleted_uri,
                                             self.delete_all,
                                             len(ids))
            if not delete:
                return

        if config.DEBUG:
            if self.delete_all:
                print("Deleting all '%d' events for '%s'" %
                      (count, get_readable_uri(self.deleted_uri)))
            else:
                print("Deleting '%d' matching events for '%s' in time range [%s to %s]" %
                      (count, get_readable_uri(self.deleted_uri),
                       time.ctime(self.from_time),
                       time.ctime(self.to_time)))

        CLIENT.delete_events(ids)

    def do_send_to(self, menuitem):
        launch_command("nautilus-sendto", [obj.uri for obj in self.subjects])

    def set_parent_window(self, parent):
        self.parent_window = parent

class ContextMenuMolteplicity(Gtk.Menu):
    subjects = []# A list of Zeitgeist event uris
    infowindow = None
    parent_window = None

    def __init__(self):
        super(ContextMenuMolteplicity, self).__init__()
        self.menuitems = {
            "delete" : get_menu_item_with_stock_id_and_text(Gtk.STOCK_DELETE, _("Delete items from Journal")),
            "info" : get_menu_item_with_stock_id_and_text(Gtk.STOCK_INFO, _("Show all grouped items"))
            }
        callbacks = {
            "delete" : self.do_delete,
            "info" : self.do_show_info
            }
        names = ["delete", "info"]
        for name in names:
            item = self.menuitems[name]
            self.append(item)
            item.connect("activate", callbacks[name])
        self.show_all()

    def do_popup(self, time, subjects):
        """
        Call this method to popup the context menu

        :param time: the event time from the button press event
        :param subjects: a list of uris
        """
        self.subjects = subjects
        if len(subjects) == 1:
            uri = subjects[0].uri

        self.popup(None, None, None, None, 3, time)

    def do_show_info(self, menuitem):
        if self.subjects:
            self.do_show_molteplicity_list(self.subjects)

    def do_show_molteplicity_list(self, item_list):
        if item_list:
            if self.infowindow:
                self.infowindow.destroy()
            self.infowindow = MolteplicityInformationContainer(parent=self.parent_window)
            self.infowindow.set_item_list(item_list)
            self.infowindow.show_all()

    def do_delete(self, menuitem):
        for obj_ in self.subjects:
            obj = obj_.content_object
            CLIENT.find_event_ids_for_template(
                Event.new_for_values(subject_uri=obj.uri),
                lambda ids: CLIENT.delete_events(list(map(int, ids))),
                timerange=DayParts.get_day_part_range_for_item(obj))

    def do_delete_list(self, list_):
        for obj_ in list_:
            obj = obj_.content_object
            CLIENT.find_event_ids_for_template(
                Event.new_for_values(subject_uri=obj.uri),
                lambda ids: CLIENT.delete_events(list(map(int, ids))),
                timerange=DayParts.get_day_part_range_for_item(obj))


    def set_parent_window(self, parent):
        self.parent_window = parent


class ToolButton(Gtk.RadioToolButton):
    def __init__(self, *args, **kwargs):
        super(ToolButton, self).__init__(*args, **kwargs)

    def set_label(self, text):
        super(ToolButton, self).set_label(text)
        self.set_tooltip_text(text)

    @staticmethod
    def new_from_widget(widget):
        return Gtk.RadioToolButton.new_from_widget(widget)

    def set_tooltip_text(self, text):
        Gtk.Widget.set_tooltip_text(self, text)

class InformationToolButton(Gtk.ToolButton):
    def __init__(self, icon=None, *args, **kwargs):
        super(InformationToolButton, self).__init__(*args, **kwargs)
        if icon:
            image = Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.SMALL_TOOLBAR)
            self.set_icon_widget(image)

    def set_label(self, text):
        super(InformationToolButton, self).set_label(text)
        self.set_tooltip_text(text)

    def set_tooltip_text(self, text):
        Gtk.Widget.set_tooltip_text(self, text)


class Toolbar(Gtk.HBox):

    first_tool_button = None

    __gsignals__ = {
        "previous" : (GObject.SignalFlags.RUN_FIRST,
                   None,
                   ()),
        "jump-to-today" : (GObject.SignalFlags.RUN_FIRST,
                   None,
                   ()),
        "jump-to-selected-day" : (GObject.SignalFlags.RUN_FIRST,
                   None,
                   (GObject.TYPE_PYOBJECT,)),
        "next" : (GObject.SignalFlags.RUN_FIRST,
                   None,
                   ()),
    }

    @staticmethod
    def get_toolbutton(path, label_string, radio=True):
        if radio:
            # FixMe: not the right way, find the reason for crash, and
            # rework this.
            if not Toolbar.first_tool_button:
                button = ToolButton()
                Toolbar.first_tool_button = button
            else:
                button = ToolButton.new_from_widget(Toolbar.first_tool_button)

        else:
            button = InformationToolButton()
        pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
        image = Gtk.Image()
        image.set_from_pixbuf(pixbuf)
        button.set_icon_widget(image)
        button.set_tooltip_text(label_string)
        return button

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

        # Previous-day button
        self.previousd_button = previous_button = Gtk.Button.new_from_icon_name("go-previous-symbolic", Gtk.IconSize.BUTTON)
        previous_button.connect("clicked", lambda x: self.emit("previous"))
        previous_button.set_tooltip_text(_("Go to the previous day"))

        # Next-day button
        self.nextd_button = next_button = Gtk.Button.new_from_icon_name("go-next-symbolic", Gtk.IconSize.BUTTON)
        next_button.connect("clicked", lambda x: self.emit("next"))
        next_button.set_sensitive(False)
        next_button.set_tooltip_text(_("Go to the next day"))

        navigation_box = Gtk.HBox()
        navigation_box.get_style_context().add_class("linked")

        for item in (previous_button, next_button):
            navigation_box.pack_start(item, False, False, 0)

        # Jump-to-today button
        self.home_button = home_button = Gtk.Button.new_from_icon_name("go-home-symbolic", Gtk.IconSize.BUTTON)
        home_button.connect("clicked", lambda x: self.emit("jump-to-today"))
        home_button.set_sensitive(False)
        home_button.set_tooltip_text(_("Go to Today"))

        self.syncing_date = False
        self.calendar = calendar = Gtk.Calendar()
        calendar.connect("day-selected-double-click", self.on_day_selected)
        calendar.get_style_context().add_class("no-border")
        calendar.show()

        self.popover = popover = Gtk.Popover()
        popover.add(calendar)

        # Jump-to-selected-day button
        self.jump_button = jump_button = Gtk.MenuButton()
        image = Gtk.Image.new_from_icon_name("go-jump-symbolic", Gtk.IconSize.BUTTON)
        jump_button.connect("clicked", self.popup_calendar)
        jump_button.set_tooltip_text(_("Go to selected day"))
        jump_button.set_popover(popover)
        jump_button.set_image(image)

        home_box = Gtk.HBox()
        home_box.pack_start(home_button, False, False, 6)
        home_box.pack_start(jump_button, False, False, 6)

        self.pack_start(navigation_box, False, False, 6)
        self.pack_start(home_box, False, False, 6)

        self.view_index = 3 # FixMe: Switch to StackSwitcher later

    def sync_date(self, date):
        self.syncing_date = True
        self.calendar.select_month(date.month - 1, date.year)
        self.calendar.select_day(date.day)
        self.syncing_date = False

    def on_day_selected(self, calendar):
        if self.syncing_date:
            return

        self.emit("jump-to-selected-day", calendar.get_date())
        self.popover.popdown()

    def popup_calendar(self, button):
        self.popover.popup()

    def add_new_view_button(self, button, i=0):
        self.pack_start(button, False, False, 6)
        self.reorder_child(button, self.view_index)
        self.view_index += 1
        button.show()

class StockIconButton(Gtk.Button):
    def __init__(self, stock_id, label=None, size=Gtk.IconSize.BUTTON):
        super(StockIconButton, self).__init__()
        self.size = size
        self.set_alignment(0, 0)
        self.set_relief(Gtk.ReliefStyle.NONE)
        self.image = Gtk.Image.new_from_stock(stock_id, size)
        if not label:
            self.add(self.image)
        else:
            box = Gtk.HBox()
            self.label = Gtk.Label(label=label)
            box.pack_start(self.image, False, False, 2)
            box.pack_start(self.label, False, True, 0)
            self.add(box)

    def set_stock(self, stock_id):
        self.image.set_from_stock(stock_id, self.size)


#######################
# More Information Pane
##

class InformationBox(Gtk.VBox):
    """
    Holds widgets which display information about a uri

    obj: the content object to be shown
    is_molteplicity: bool used to discriminate grouped items from single ones
    """
    obj = None
    is_molteplicity = False

    class _ImageDisplay(Gtk.Image):
        """
        A display based on GtkImage to display a uri's thumb or icon using GioFile
        """
        def set_content_object(self, obj):
            if obj:
                if isinstance(obj, GioFile) and obj.has_preview():
                    pixbuf = obj.get_thumbnail(size=SIZE_NORMAL, border=3)
                    if pixbuf is None: pixbuf = obj.get_icon(size=64)
                else:
                    pixbuf = obj.get_icon(size=64)

                if pixbuf is None: pixbuf = obj.get_actor_pixbuf(size=64)
                self.set_from_pixbuf(pixbuf)

    def __init__(self):
        super(InformationBox, self).__init__()
        vbox = Gtk.VBox()
        self._box = Gtk.Frame()
        self.label = Gtk.Label()
        self.pathlabel = Gtk.Label()
        self.pathlabel.modify_font(Pango.FontDescription.from_string("Monospace 7"))
        self.box_label = Gtk.EventBox()
        self.box_label.add(self.pathlabel)
        self.box_label.connect("button-press-event", self.on_path_label_clicked)
        self.box_label.connect("enter-notify-event", self.on_path_label_enter)
        self.box_label.connect("leave-notify-event", self.on_path_label_leave)
        self.box_label.connect("realize", self.on_realize_event)
        labelvbox = Gtk.VBox()
        labelvbox.pack_start(self.label, False, True, 0)
        labelvbox.pack_end(self.box_label, False, True, 0)
        self.pack_start(labelvbox, True, True, 5)
        self._box.set_shadow_type(Gtk.ShadowType.NONE)
        vbox.pack_start(self._box, True, True, 0)
        self.pathlabel.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
        self.label.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
        self.add(vbox)
        self.display_widget = self._ImageDisplay()
        self._box.add(self.display_widget)
        self.show_all()

    def set_displaytype(self, obj):
        """
        Determines the ContentDisplay to use for a given uri
        """
        self.display_widget.set_content_object(obj)
        self.show_all()

    def set_content_object(self, obj):
        self.obj = obj
        self.set_displaytype(obj)
        text = get_text_or_uri(obj,molteplicity=self.is_molteplicity)
        self.label.set_markup("<span size='10336'>" + text + "</span>")
        path = obj.uri.replace("&", "&amp;").replace("%20", " ")
        self.is_file = path.startswith("file://")
        if self.is_file:
            self.textpath = os.path.dirname(path)[7:]
        else:
            if self.is_molteplicity:
                path = text.split(" ")[-1]  #take only the uri
            self.textpath = path

        self.pathlabel.set_markup("<span color='#979797'>" + self.textpath + "</span>")

    def on_realize_event(self, parms):
        if self.is_file:
            hand = Gdk.Cursor.new(Gdk.CursorType.HAND2)
            self.box_label.props.window.set_cursor(hand)

    def on_path_label_clicked(self, wid, e):
        if self.is_file:
            os.system('xdg-open "%s"' % self.textpath)

    def on_path_label_enter(self, wid, e):
        if self.is_file:
            self.pathlabel.set_markup("<span color='#970000'><b>" + self.textpath+ "</b></span>")

    def on_path_label_leave(self, wid, e):
        if self.is_file:
            self.pathlabel.set_markup("<span color='#999797'>" + self.textpath + "</span>")

class _RelatedPane(Gtk.TreeView):
    """
    . . . . .
    .       .
    .       . <--- Related files
    .       .
    . . . . .

    Displays related events using a widget based on Gtk.TreeView
    """
    def __init__(self, column_name):
        super(_RelatedPane, self).__init__()
        self.popupmenu = ContextMenu
        self.connect("button-press-event", self.on_button_press)
        self.connect("row-activated", self.row_activated)
        pcolumn = Gtk.TreeViewColumn(_(column_name))
        pcolumn.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
        pcolumn.set_spacing(6)
        pixbuf_render = Gtk.CellRendererPixbuf()
        pcolumn.pack_start(pixbuf_render, False)
        pcolumn.set_cell_data_func(pixbuf_render, self.celldatamethod, "pixbuf")
        text_render = Gtk.CellRendererText()
        text_render.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE)
        pcolumn.pack_end(text_render, True)
        pcolumn.set_cell_data_func(text_render, self.celldatamethod, "text")
        self.append_column(pcolumn)
        self.set_headers_visible(True)
        self.set_fixed_height_mode(True)

    def celldatamethod(self, column, cell, model, iter_, user_data):
        if model:
            obj = model.get_value(iter_, 0)
            if user_data == "text":
                text = get_text_or_uri(obj)
                cell.set_property("text", text)
            elif user_data == "pixbuf":
                cell.set_property("pixbuf", obj.icon)

    def _set_model_in_thread(self, structs):
        """
        A threaded which generates pixbufs and emblems for a list of structs.
        It takes those properties and appends them to the view's model
        """
        lock = threading.Lock()
        self.active_list = []
        liststore = Gtk.ListStore(GObject.TYPE_PYOBJECT)
        self.set_model(liststore)
        for struct in structs:
            if not struct.content_object: continue
            self.active_list.append(False)
            liststore.append((struct.content_object,))

    def set_model_from_list(self, structs):
        self.last_active = -1
        if not structs:
            self.set_model(None)
            return
        thread = threading.Thread(target=self._set_model_in_thread, args=(structs,))
        thread.start()

    def on_button_press(self, widget, event):
        if event.button == 3:
            path = self.get_path_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 False

    def row_activated(self, widget, path, col, *args):
        if path:
            model = self.get_model()
            if model:
                obj = model[path[0]][0]
                obj.launch()

class InformationContainer(Gtk.Dialog):
    """
    . . . . .
    .  URI  .
    . Info  .
    .       .
    . Tags  .
    . . . . .
    . . . . .
    .       .
    .       . <--- Related files
    .       .
    . . . . .

    A pane which holds the information pane and related pane
    """
    __gsignals__ = {
        "content-object-set":  (GObject.SignalFlags.RUN_LAST, None,()),
        }

    class _InformationToolbar(Gtk.Toolbar):
        def __init__(self):
            Gtk.Toolbar.__init__(self)
            self.set_icon_size(Gtk.IconSize.SMALL_TOOLBAR)
            self.open_button = ob = InformationToolButton(icon="document-open-symbolic")
            ob.set_label(_("Launch this subject"))
            self.delete_button = del_ = InformationToolButton(icon="edit-delete-symbolic")
            del_.set_label(_("Delete this subject"))
            self.pin_button = pin = InformationToolButton(icon="view-pin-symbolic")
            pin.set_label(_("Add Pin"))
            self.note_button = note = InformationToolButton(icon="document-edit-symbolic")
            note.set_label(_("Edit Note"))
            sep = Gtk.SeparatorToolItem()
            for item in (del_, note, pin, sep, ob):
                if item:
                    self.insert(item, 0)

    class _EditNoteWindow(Gtk.Window):

        def __init__(self, parent, obj):
            Gtk.Window.__init__(self)
            self.obj = obj
            self.set_title(_("Edit Note"))
            self.set_transient_for(parent)
            self.set_destroy_with_parent(True)
            self.set_size_request(400, 400)
            self.connect("destroy", self.on_save_note)
            sw = Gtk.ScrolledWindow()
            sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
            textview = Gtk.TextView()
            self.textbuffer = textview.get_buffer()
            self.textbuffer.set_text("" if self.obj.annotation is None else self.obj.annotation)
            sw.add(textview)
            self.add(sw)
            self.show_all()

        def on_save_note(self, *arg):
            self.obj.annotation = self.textbuffer.get_text(self.textbuffer.get_start_iter (),
                                                           self.textbuffer.get_end_iter(),
                                                           False)

    def __init__(self, parent=None):
        super(Gtk.Window, self).__init__()
        if parent: self.set_transient_for(parent)
        self.set_destroy_with_parent(True)
        self.set_size_request(400, 400)
        self.connect("destroy", self.hide_on_delete)
        box1 = Gtk.VBox()
        box2 = Gtk.VBox()
        vbox = Gtk.VBox()
        self.toolbar = self._InformationToolbar()
        self.infopane = InformationBox()
        self.relatedpane = _RelatedPane("Used With")
        scrolledwindow = Gtk.ScrolledWindow()
        box2.set_border_width(5)
        box1.pack_start(self.toolbar, False, False, 0)
        box2.pack_start(self.infopane, False, False, 4)
        scrolledwindow.set_shadow_type(Gtk.ShadowType.IN)
        scrolledwindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        scrolledwindow.add(self.relatedpane)
        vbox.set_property("expand", True)
        vbox.pack_end(scrolledwindow, True, True, 0)
        scrolledwindow.set_size_request(50, 100)
        box2.pack_end(vbox, True, True, 10)
        box1.pack_start(box2, True, True, 0)
        area = self.get_content_area()
        area.add(box1)
        def _launch(w):
            self.obj.launch()
        self.toolbar.open_button.connect("clicked", _launch)
        self.toolbar.delete_button.connect("clicked", self.do_delete_events_with_shared_uri)
        self.toolbar.pin_button.connect("clicked", self.do_toggle_bookmark)
        self.toolbar.note_button.connect("clicked", self.do_edit_note)
        self.connect("size-allocate", self.size_allocate)
        # Remove the close button
        separator = Gtk.SeparatorToolItem()
        separator.set_expand(True)
        separator.set_draw(False)
        self.toolbar.insert(separator, -1)

    def size_allocate(self, widget, allocation):
        if allocation.height < 400:
            self.infopane.display_widget.hide()
        else:
            self.infopane.display_widget.show()

    def do_toggle_bookmark(self, *args):
        uri = str(self.obj.uri)
        if bookmarker.is_bookmarked(uri):
            bookmarker.unbookmark(uri)
        else:
            bookmarker.bookmark(uri)

    def do_edit_note(self, *args):
        window = self._EditNoteWindow(self, self.obj)

    def do_delete_events_with_shared_uri(self, *args):
        CLIENT.find_event_ids_for_template(
            Event.new_for_values(subject_uri=self.obj.uri),
            lambda ids: CLIENT.delete_events(list(map(int, ids))))
        self.hide()

    def set_content_object(self, obj):
        self.obj = obj
        if not isinstance(self.obj, GioFile):
            self.toolbar.note_button.set_sensitive(False)
        def _callback(events):
            self.relatedpane.set_model_from_list(events)
        get_related_events_for_uri(obj.uri, _callback)
        self.infopane.set_content_object(obj)
        self.set_title(_("More Information"))
        self.show()
        self.emit("content-object-set")

    def hide_on_delete(self, widget, *args):
        super(InformationContainer, self).hide_on_delete()
        return True

class MolteplicityInformationContainer(Gtk.Dialog):
    """
    . . . . .
    .Origin .
    . Info  .
    . . . . .
    . . . . .
    .       .
    .       . <--- Similar items (same basename)
    .       .
    . . . . .

    A pane which holds the information pane and grouped items pane
    """
    def __init__(self, parent=None):
        super(Gtk.Window, self).__init__()
        if parent: self.set_transient_for(parent)
        self.set_destroy_with_parent(True)
        self.set_size_request(400, 400)
        self.connect("destroy", self.hide_on_delete)
        box2 = Gtk.VBox()
        vbox = Gtk.VBox()
        vbox.props.expand = True
        self.infopane = InformationBox()
        self.infopane.is_molteplicity = True
        self.relatedpane = _RelatedPane("Grouped items")
        scrolledwindow = Gtk.ScrolledWindow()
        box2.set_border_width(5)
        box2.pack_start(self.infopane, False, False, 4)
        scrolledwindow.set_shadow_type(Gtk.ShadowType.IN)
        scrolledwindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        scrolledwindow.add(self.relatedpane)
        vbox.pack_end(scrolledwindow, True, True, 0)
        scrolledwindow.set_size_request(50, 100)
        box2.pack_end(vbox, True, True, 10)
        area = self.get_content_area()
        area.add(box2)
        self.connect("size-allocate", self.size_allocate)

    def size_allocate(self, widget, allocation):
        if allocation.height < 400:
            self.infopane.display_widget.hide()
        else:
            self.infopane.display_widget.show()

    def set_item_list(self, list_):
        self.infopane.set_content_object(list_[0].content_object)
        self.relatedpane.set_model_from_list(list_)
        self.set_title(_("More Information"))
        self.show()

    def hide_on_delete(self, widget, *args):
        super(MolteplicityInformationContainer, self).hide_on_delete()
        return True


class PreferencesDialog(Gtk.Dialog):

    class _PluginTreeView(Gtk.TreeView):
        def __init__(self):
            Gtk.TreeView.__init__(self)
            self.set_grid_lines(Gtk.TreeViewGridLines.VERTICAL)
            self.set_headers_visible(False)
            acolumn = Gtk.TreeViewColumn("")
            toggle_render = Gtk.CellRendererToggle()
            acolumn.pack_start(toggle_render, False)
            acolumn.add_attribute(toggle_render, "active", 1)
            self.append_column(acolumn)

            bcolumn = Gtk.TreeViewColumn("")
            text_render = Gtk.CellRendererText()
            text_render.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE)
            bcolumn.pack_start(text_render, True)
            bcolumn.add_attribute(text_render, "markup", 0)
            self.append_column(bcolumn)
            self.set_property("rules-hint", True)
            self.connect("row-activated" , self.on_activate)

        def set_state(self, entry, state):
            PluginManager.plugin_settings[entry.key] = state

        def on_activate(self, widget, path, column):
            model = self.get_model()
            model[path][1] = not model[path][1]
            self.set_state(model[path][2], model[path][1])
            bname = os.path.basename(model[path][2].key)
            if model[path][1]:
                self.manager.activate(name=bname)
            else:
                self.manager.deactivate(name=bname)

        def set_items(self, manager):
            entries = manager.plugin_settings["active-plugins"]
            store = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_BOOLEAN, GObject.TYPE_PYOBJECT)
            for entry in entries:
                bname = os.path.basename(entry.key)
                if bname in manager.plugins:
                    # Load the plugin if the plugin is found
                    module = manager.plugins[bname]
                    name = "<b>" + module.__plugin_name__ + "</b>"
                    desc = "\n<small>" + module.__description__ + "</small>"
                    store.append( [name+desc, False if entry.value is None else entry.value.get_bool(), entry])
                else:
                    # Remove the key if no plugin is found
                    entries.remove(entry)
                    manager.plugin_settings["active-plugins"] = entries
            self.manager = manager
            self.set_model(store)

    def __init__(self, parent=None):
        super(PreferencesDialog, self).__init__()
        if parent: self.set_transient_for(parent)
        self.set_destroy_with_parent(True)
        self.set_title(_("Preferences"))
        self.set_size_request(400, 500)
        area = self.get_content_area()
        self.notebook = notebook = Gtk.Notebook()
        self.notebook.props.expand = True
        area.pack_start(notebook, False, True, 0)
        notebook.set_border_width(10)
        #Plugin page
        plugbox = Gtk.VBox()
        plugbox.set_border_width(10)
        self.plug_tree = self._PluginTreeView()
        label = Gtk.Label(label= _("Active Plugins:"))
        label.set_alignment(0, 0.5)
        plugbox.pack_start(label, False, False, 4)
        scroll_win = Gtk.ScrolledWindow()
        scroll_win.set_shadow_type(Gtk.ShadowType.IN)
        scroll_win.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scroll_win.add(self.plug_tree)
        plugbox.add(scroll_win)
        notebook.append_page(plugbox, Gtk.Label(label= _("Plugins")))
        #Configuration page
        vbox = Gtk.VBox()
        vbox.set_border_width(5)
        hbox_tray = Gtk.HBox(False, 12)
        hbox_tray.props.margin = 12
        label = Gtk.Label(label=_("Show icon in system tray"))
        self.check_button = Gtk.CheckButton()
        self.check_button.set_active(settings["tray-icon"])
        self.check_button.connect("toggled", self.on_check_toggled)
        hbox_tray.pack_start(self.check_button, False, False, 0)
        hbox_tray.pack_start(label, False, False, 0)
        vbox.pack_start(hbox_tray, False, False, 0)
        notebook.append_page(vbox, Gtk.Label(label= _("Configuration")))

        self.connect("delete-event", lambda *args: (True, self.hide())[0])
        close_button = Gtk.Button(stock=Gtk.STOCK_CLOSE)
        self.add_action_widget(close_button, Gtk.ResponseType.DELETE_EVENT)
        close_button.connect("clicked", lambda *args: (True, self.hide())[0])

    def on_check_toggled(self, button, *args):
        settings["tray-icon"] = button.get_active()


#Code adapted from Emesene2 source. Get it at: https://github.com/emesene/emesene
#Thanks Emesene's team
class NiceBar(Gtk.EventBox):
    '''A class used to display messages in a non-intrusive bar'''

    def __init__(self, default_background=None, default_foreground=None):

        Gtk.EventBox.__init__(self)

        self.style = self.get_style_context()
        self.style.add_class("nice-bar")

        self.NORMALFOREGROUND = "white"

        (found, warning_color) = self.style.lookup_color("warning_color")

        if found:
            self.ALERTBACKGROUND = warning_color
        else:
            self.ALERTBACKGROUND = Gdk.RGBA.parse("#f57900")

        (found, normal_color) = self.style.lookup_color("success_color")

        if found:
            self.NORMALBACKGROUND = normal_color
        else:
            self.NORMALBACKGROUND = Gdk.RGBA.parse("#33d17a")

        self.props.height_request = 40
        self.message_label = Gtk.Label()
        self.message_label.set_line_wrap(True)
        self.message_label.set_ellipsize(Pango.EllipsizeMode.END)
        self.message_image = Gtk.Image()
        self.message_hbox = Gtk.HBox()
        self.message_hbox.set_border_width(2)
        self.message_hbox.set_halign(Gtk.Align.CENTER)

        if default_background is None:
            default_background = self.NORMALBACKGROUND
        if default_foreground is None:
            default_foreground = self.NORMALFOREGROUND

        self.default_back = default_background
        self.default_fore = default_foreground
        self.empty_queue()
        self.markup = '<span foreground="%s" font-size="large">%s</span>'
        self.override_background_color(Gtk.StateFlags.NORMAL, default_background)

        self.message_hbox.pack_end(self.message_label, False, True, 0)
        self.add(self.message_hbox)

    def new_message(self, message, stock=None, background=None, \
                                                  foreground=None):
        ''' Adds the actual message to the queue and show a new one '''

        if self.actual_message != '':
            self.messages_queue.append([self.actual_message, \
                    self.actual_image, self.actual_background,
                    self.actual_foreground])

        self.display_message(message, stock, background, foreground)

    def remove_message(self, portal):
        ''' Removes the actual message and display the next if any '''
        try:
            message, stock, back, fore = self.messages_queue.pop()
            self.display_message(message, stock, back, fore)
        except IndexError:
            self.hide()

        if portal.nicebar_timeout:
            GObject.source_remove(portal.nicebar_timeout)
            portal.nicebar_timeout = None

        return GLib.SOURCE_REMOVE

    def display_message(self, message, stock=None, background=None, \
                        foreground=None):
        '''
            Displays a message without modifying the queue
            A background, text color and a stock image are optional
        '''

        self.actual_message = message
        self.actual_image = stock
        self.actual_background = background or self.default_back
        self.actual_foreground = foreground or self.default_fore

        if self.message_image.get_parent() is not None:
            self.message_hbox.remove(self.message_image)

        if stock is not None:
            self.message_image = Gtk.Image.new_from_stock(stock, \
                                             Gtk.IconSize.LARGE_TOOLBAR)
            self.message_hbox.pack_start(self.message_image, False, False, 0)

        self.override_background_color(Gtk.StateFlags.NORMAL, self.actual_background)
        self.message_label.set_markup(self.markup % (self.actual_foreground,
                                                       self.actual_message))
        self.show_all()

    def empty_queue(self):
        ''' Delets all messages and hide the bar '''

        self.messages_queue = list()
        self.actual_message = ''
        self.actual_image = None
        self.actual_background = self.default_back
        self.actual_foreground = self.default_fore
        self.hide()

###
if Gst is not None:
    VideoPreviewTooltip = VideoPreviewTooltip()
    AudioPreviewTooltip = AudioPreviewTooltip()
else:
    VideoPreviewTooltip = None
    AudioPreviewTooltip = None
StaticPreviewTooltip = StaticPreviewTooltip()
ContextMenu = ContextMenu()
ContextMenuMolteplicity = ContextMenuMolteplicity()
SearchBox = SearchBox()
