# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Rickard Lindberg, Roger Lindberg
#
# This file is part of Timeline.
#
# Timeline 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.
#
# Timeline 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 Timeline. If not, see <http://www.gnu.org/licenses/>.
import math
from timelinelib.canvas.data.base import ItemBase
from timelinelib.canvas.data.immutable import ImmutableEvent
from timelinelib.canvas.data.item import TimelineItem
from timelinelib.canvas.drawing.drawers import get_progress_color
from timelinelib.canvas.drawing.utils import darken_color
DEFAULT_COLOR = (200, 200, 200)
EXPORTABLE_FIELDS = (_("Text"), _("Description"), _("Labels"), _("Start"), _("End"), _("Category"),
_("Fuzzy Start"), _("Fuzzy End"), _("Locked"), _("Ends Today"), _("Hyperlink"),
_("Progress"), _("Progress Color"), _("Done Color"), _("Alert"),
_("Is Container"), _("Is Subevent"))
[docs]class Event(ItemBase, TimelineItem):
"""
An Event is the basic data type for representing information on the timeline.
It must specify where on the timeline it should be placed (when that event happened).
This can be either a specific point in time or a period.
Derived classes:
* :doc:`Milestone <timelinelib_canvas_data_milestone>`.
* :doc:`Container <timelinelib_canvas_data_container>`.
* :doc:`Subevent <timelinelib_canvas_data_subevent>`.
"""
[docs] def __init__(self, db=None, id_=None, immutable_value=ImmutableEvent()):
ItemBase.__init__(self, db, id_, immutable_value)
self._category = None
self._categories = []
self._container = None
self._milestone = False
self._horizontal_mouse_position_factor = None
@property
def horizontal_mouse_position_factor(self):
"""
This factor describes the relation between the lengt of an event and the
length from the start of the event up to the cursor position in the event.
For point events this factor is None.
The factor is used for calculation of the status-bar label that describes
the time duration up to the cursor position.
"""
return self._horizontal_mouse_position_factor
@horizontal_mouse_position_factor.setter
def horizontal_mouse_position_factor(self, value):
self._horizontal_mouse_position_factor = value
[docs] def duplicate(self, target_db=None):
duplicate = ItemBase.duplicate(self, target_db=target_db)
if duplicate.db is self.db:
duplicate.category = self.category
duplicate.sort_order = None
return duplicate
[docs] def save(self):
self._update_category_id()
self._update_category_ids()
self._update_container_id()
self._update_sort_order()
with self._db.transaction("Save event") as t:
t.save_event(self._immutable_value, self.ensure_id())
return self
[docs] def reload(self):
return self._db.find_event_with_id(self.id)
def _update_category_id(self):
if self.category is None:
self._immutable_value = self._immutable_value.update(
category_id=None
)
elif self.category.id is None:
raise Exception("Unknown category")
else:
self._immutable_value = self._immutable_value.update(
category_id=self.category.id
)
def _update_category_ids(self):
if self._categories == list():
self._immutable_value = self._immutable_value.update(
category_ids={}
)
else:
ids = [c.id for c in self._categories]
# Using a dictionary because we don't have any ImmutableList class
dic = {k: None for k in ids}
self._immutable_value = self._immutable_value.update(
category_ids=dic
)
def _update_container_id(self):
if self.container is None:
self._immutable_value = self._immutable_value.update(
container_id=None
)
elif self.container.id is None:
raise Exception("Unknown container")
else:
self._immutable_value = self._immutable_value.update(
container_id=self.container.id
)
def _update_sort_order(self):
if self.sort_order is None:
self.sort_order = 1 + self.db.get_max_sort_order()
[docs] def delete(self):
with self._db.transaction("Delete event") as t:
t.delete_event(self.id)
self.id = None
[docs] def __eq__(self, other):
return (isinstance(other, Event) and
self.get_fuzzy() == other.get_fuzzy() and
self.fuzzy_start == other.fuzzy_start and
self.fuzzy_end == other.fuzzy_end and
self.get_locked() == other.get_locked() and
self.get_ends_today() == other.get_ends_today() and
self.get_id() == other.get_id() and
self.get_time_period().start_time == other.get_time_period().start_time and
(self.get_time_period().end_time == other.get_time_period().end_time or self.get_ends_today()) and
self.text == other.text and
self.category == other.category and
self.get_description() == other.get_description() and
self.get_labels() == other.get_labels() and
self.get_hyperlink() == other.get_hyperlink() and
self.get_progress() == other.get_progress() and
self.get_alert() == other.get_alert() and
self.get_icon() == other.get_icon() and
self.get_default_color() == other.get_default_color())
[docs] def __ne__(self, other):
return not (self == other)
[docs] def __lt__(self, other):
raise NotImplementedError("I don't believe this is in use.")
[docs] def __gt__(self, other):
raise NotImplementedError("I don't believe this is in use.")
[docs] def __le__(self, other):
raise NotImplementedError("I don't believe this is in use.")
[docs] def __ge__(self, other):
raise NotImplementedError("I don't believe this is in use.")
[docs] def __repr__(self):
return "%s<id=%r, text=%r, time_period=%r, ...>" % (
self.__class__.__name__,
self.get_id(),
self.text,
self.get_time_period()
)
[docs] def set_end_time(self, time):
self.set_time_period(self.get_time_period().set_end_time(time))
[docs] def get_text(self):
return self._immutable_value.text
[docs] def set_text(self, text):
self._immutable_value = self._immutable_value.update(text=text.strip())
return self
text = property(get_text, set_text)
[docs] def get_category(self):
return self._category
[docs] def get_category_name(self):
if self.category:
return self.category.get_name()
else:
return None
[docs] def set_category(self, category):
self._category = category
return self
category = property(get_category, set_category)
[docs] def get_categories(self):
return self._categories
[docs] def set_categories(self, categories):
if categories:
if self.category:
if self.category in categories:
categories.remove(self.category)
else:
self.category = categories[0]
categories = categories[1:]
self._categories = categories
[docs] def get_container(self):
return self._container
[docs] def set_container(self, container):
if self._container is not None:
self._container.unregister_subevent(self)
self._container = container
if self._container is not None:
self._container.register_subevent(self)
return self
container = property(get_container, set_container)
[docs] def get_fuzzy(self):
return self._immutable_value.fuzzy
[docs] def set_fuzzy(self, fuzzy):
self._immutable_value = self._immutable_value.update(fuzzy=fuzzy)
self._immutable_value = self._immutable_value.update(fuzzy_start=fuzzy)
self._immutable_value = self._immutable_value.update(fuzzy_end=fuzzy)
return self
fuzzy = property(get_fuzzy, set_fuzzy)
[docs] def get_fuzzy_start(self):
return self._immutable_value.fuzzy_start
[docs] def set_fuzzy_start(self, fuzzy_start):
self._immutable_value = self._immutable_value.update(fuzzy_start=fuzzy_start)
return self
fuzzy_start = property(get_fuzzy_start, set_fuzzy_start)
[docs] def get_fuzzy_end(self):
return self._immutable_value.fuzzy_end
[docs] def set_fuzzy_end(self, fuzzy_end):
self._immutable_value = self._immutable_value.update(fuzzy_end=fuzzy_end)
return self
fuzzy_end = property(get_fuzzy_end, set_fuzzy_end)
[docs] def get_locked(self):
return self._immutable_value.locked
[docs] def set_locked(self, locked):
self._immutable_value = self._immutable_value.update(locked=locked)
return self
locked = property(get_locked, set_locked)
[docs] def get_ends_today(self):
return self._immutable_value.ends_today
[docs] def set_ends_today(self, ends_today):
if not self.locked:
self._immutable_value = self._immutable_value.update(ends_today=ends_today)
return self
ends_today = property(get_ends_today, set_ends_today)
[docs] def get_description(self):
return self._immutable_value.description
[docs] def set_description(self, description):
self._immutable_value = self._immutable_value.update(description=description)
return self
description = property(get_description, set_description)
[docs] def get_labels(self):
return self._immutable_value.labels
[docs] def set_labels(self, value):
self._immutable_value = self._immutable_value.update(labels=value)
return self
labels = property(get_labels, set_labels)
[docs] def get_icon(self):
return self._immutable_value.icon
[docs] def set_icon(self, icon):
self._immutable_value = self._immutable_value.update(icon=icon)
return self
icon = property(get_icon, set_icon)
[docs] def has_edge_icons(self):
return self.get_fuzzy() or self.fuzzy_start or self. fuzzy_end or self.get_locked()
[docs] def get_hyperlink(self):
return self._immutable_value.hyperlink
[docs] def set_hyperlink(self, hyperlink):
self._immutable_value = self._immutable_value.update(hyperlink=hyperlink)
return self
hyperlink = property(get_hyperlink, set_hyperlink)
[docs] def get_alert(self):
return self._immutable_value.alert
[docs] def set_alert(self, alert):
self._immutable_value = self._immutable_value.update(alert=alert)
return self
alert = property(get_alert, set_alert)
[docs] def get_progress(self):
return self._immutable_value.progress
[docs] def set_progress(self, progress):
self._immutable_value = self._immutable_value.update(progress=progress)
return self
progress = property(get_progress, set_progress)
[docs] def get_sort_order(self):
return self._immutable_value.sort_order
[docs] def set_sort_order(self, sort_order):
self._immutable_value = self._immutable_value.update(sort_order=sort_order)
return self
sort_order = property(get_sort_order, set_sort_order)
[docs] def get_default_color(self):
color = self._immutable_value.default_color
if color is None:
color = DEFAULT_COLOR
return color
[docs] def set_default_color(self, color):
self._immutable_value = self._immutable_value.update(default_color=color)
return self
default_color = property(get_default_color, set_default_color)
[docs] def get_color(self):
try:
return self.category.color
except AttributeError:
return self.get_default_color()
[docs] def get_border_color(self):
return darken_color(self.get_color())
[docs] def get_done_color(self):
if self.category:
return self.category.get_done_color()
else:
return get_progress_color(DEFAULT_COLOR)
[docs] def get_progress_color(self):
category = self.category
if category:
if self.get_progress() == 100:
return category.get_done_color()
else:
return category.get_progress_color()
else:
return get_progress_color(DEFAULT_COLOR)
[docs] def update(self, start_time, end_time, text, category=None, fuzzy=None,
locked=None, ends_today=None, fuzzy_start=None, fuzzy_end=None):
"""Change the event data."""
self.update_period(start_time, end_time)
self.text = text.strip()
self.category = category
if ends_today is not None:
if not self.locked:
self.ends_today = ends_today
if fuzzy is not None:
self.fuzzy = fuzzy
if fuzzy_start is not None:
self.fuzzy_start = fuzzy_start
if fuzzy_end is not None:
self.fuzzy_end = fuzzy_end
if locked is not None:
self.locked = locked
return self
[docs] def get_data(self, event_id, default=None):
"""
Return data with the given id or None if no data with that id exists.
See set_data for information how ids map to data.
"""
if event_id == "description":
return self.description or default
elif event_id == "labels":
return self.labels or default
elif event_id == "icon":
return self.icon
elif event_id == "hyperlink":
return self.hyperlink
elif event_id == "alert":
return self.alert
elif event_id == "progress":
return self.progress
elif event_id == "default_color":
if "default_color" in self._immutable_value:
return self._immutable_value.default_color
else:
return None
else:
raise Exception(f"should not happen: {event_id}")
[docs] def set_data(self, event_id, data):
"""
Set data with the given id.
Here is how ids map to data:
description - string
icon - wx.Bitmap
"""
if event_id == "description":
self.description = data
elif event_id == "labels":
self.labels = data
elif event_id == "icon":
self.icon = data
elif event_id == "hyperlink":
self.hyperlink = data
elif event_id == "alert":
self.alert = data
elif event_id == "progress":
self.progress = data
elif event_id == "default_color":
self.default_color = data
else:
raise Exception("should not happen")
[docs] def get_whole_data(self):
data = {}
for event_id in DATA_FIELDS:
data[event_id] = self.get_data(event_id)
return data
[docs] def set_whole_data(self, data):
for event_id in DATA_FIELDS:
self.set_data(event_id, data.get(event_id, None))
data = property(get_whole_data, set_whole_data)
[docs] def has_data(self):
"""Return True if the event has associated data, or False if not."""
for event_id in DATA_FIELDS:
if self.get_data(event_id) is not None:
return True
return False
[docs] def has_balloon_data(self):
"""Return True if the event has associated data to be displayed in a balloon."""
return (self.get_data("description") is not None or
self.get_data("icon") is not None)
[docs] def get_label(self, time_type):
"""Returns a unicode label describing the event."""
event_label = "%s (%s)" % (
self.text,
time_type.format_period(self.get_time_period()),
)
duration_info = self._get_duration_label(time_type)
if duration_info != "":
return f'{event_label} {duration_info} {self._get_cursor_pos_label(time_type)}'
else:
return event_label
def _get_duration_label(self, time_type):
info = time_type.format_delta(self.time_span())
if info == "0":
info = ""
if info == "":
duration_label = ""
else:
label = _("Duration")
duration_label = f'{label}: {info}'
return duration_label
def _get_cursor_pos_label(self, time_type):
if self._horizontal_mouse_position_factor is not None:
duration = time_type.format_delta(self.time_span() * self._horizontal_mouse_position_factor)
label = _("To Cursor")
return f'({label}: {duration})'
else:
return ""
[docs] def is_container(self):
return False
[docs] def is_subevent(self):
return False
[docs] def is_milestone(self):
return False
[docs] def get_exportable_fields(self):
return EXPORTABLE_FIELDS
[docs] def set_milestone(self, value):
self._milestone = value
[docs] def get_milestone(self):
return self._milestone
DATA_FIELDS = [
"description",
"labels",
"icon",
"hyperlink",
"alert",
"progress",
"default_color",
]