Source code for timelinelib.wxgui.components.categorytree

# 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 wx

from timelinelib.canvas.data.exceptions import TimelineIOError
from timelinelib.canvas.drawing.utils import darken_color
from timelinelib.db.utils import safe_locking
from timelinelib.monitoring import Monitoring
from timelinelib.general.observer import Observable
from timelinelib.repositories.categories import CategoriesFacade
from timelinelib.wxgui.components.font import Font
from timelinelib.wxgui.dialogs.editcategory.view import EditCategoryDialog
import timelinelib.wxgui.utils as gui_utils


[docs]class CustomCategoryTree(wx.ScrolledWindow):
[docs] def __init__(self, sidebar, config, name=None, size=(100, 100)): self.monitoring = Monitoring() wx.ScrolledWindow.__init__(self, sidebar, size=size) self._sidebar = sidebar self._config = config self._create_context_menu() self.Bind(wx.EVT_PAINT, self._on_paint) self.Bind(wx.EVT_SIZE, self._on_size) self.Bind(wx.EVT_LEFT_DOWN, self._on_left_down) self.Bind(wx.EVT_RIGHT_DOWN, self._on_right_down) self.Bind(wx.EVT_LEFT_DCLICK, self._on_left_doubleclick) self.model = CustomCategoryTreeModel() self.model.listen_for_any(self._redraw) self.renderer = CustomCategoryTreeRenderer(self, self.model, sidebar.config) self.set_no_timeline_view() self._size_to_model() self._draw_bitmap()
[docs] def redraw(self): self._redraw()
[docs] def set_no_timeline_view(self): self.db = None self.view_properties = None self.model.set_categories(None)
[docs] def set_timeline_view(self, db, view_properties): self.db = db self.view_properties = view_properties self.model.set_categories(CategoriesFacade(db, view_properties))
[docs] def check_categories(self, categories): self.view_properties.set_categories_visible(categories)
[docs] def uncheck_categories(self, categories): self.view_properties.set_categories_visible(categories, False)
def _has_timeline_view(self): return self.db is not None and self.view_properties is not None def _on_paint(self, event): wx.BufferedPaintDC(self, self.buffer_image, wx.BUFFER_VIRTUAL_AREA) def _on_size(self, event): self._size_to_model() def _on_left_doubleclick(self, event): def edit_function(): self._edit_category() safe_locking(self._sidebar, edit_function) def _on_left_down(self, event): self.SetFocus() self._store_hit_info(event) hit_category = self.last_hit_info.get_category() if self.last_hit_info.is_on_arrow(): self.model.toggle_expandedness(hit_category) elif self.last_hit_info.is_on_checkbox(): self.view_properties.toggle_category_visibility(hit_category) def _on_right_down(self, event): def edit_function(): self._store_hit_info(event) for (menu_item, should_be_enabled_fn) in self.context_menu_items: menu_item.Enable(should_be_enabled_fn(self.last_hit_info)) self.PopupMenu(self.context_menu) safe_locking(self._sidebar, edit_function) def _on_menu_edit(self, e): self._edit_category() def _on_menu_add(self, e): add_category(self, self.db) def _on_menu_delete(self, e): hit_category = self.last_hit_info.get_category() if hit_category: delete_category(self, self.db, hit_category) def _on_menu_check_all(self, e): self.view_properties.set_categories_visible( self.db.get_categories()) def _on_menu_check_children(self, e): self.view_properties.set_categories_visible( self.last_hit_info.get_immediate_children()) def _on_menu_check_all_children(self, e): self.view_properties.set_categories_visible( self.last_hit_info.get_all_children()) def _on_menu_check_parents(self, e): self.view_properties.set_categories_visible( self.last_hit_info.get_parents()) def _on_menu_check_parents_for_checked_children(self, e): self.view_properties.set_categories_visible( self.last_hit_info.get_parents_for_checked_childs()) def _on_menu_uncheck_all(self, e): self.view_properties.set_categories_visible( self.db.get_categories(), False) def _on_menu_uncheck_children(self, e): self.view_properties.set_categories_visible( self.last_hit_info.get_immediate_children(), False) def _on_menu_uncheck_all_children(self, e): self.view_properties.set_categories_visible( self.last_hit_info.get_all_children(), False) def _on_menu_uncheck_parents(self, e): self.view_properties.set_categories_visible( self.last_hit_info.get_parents(), False) def _edit_category(self): hit_category = self.last_hit_info.get_category() if hit_category: edit_category(self, self.db, hit_category) def _store_hit_info(self, event): (x, y) = self.CalcUnscrolledPosition(event.GetX(), event.GetY()) self.last_hit_info = self.model.hit(x, y) def _redraw(self): self.SetVirtualSize((-1, self.model.ITEM_HEIGHT_PX * len(self.model.items))) self.SetScrollRate(0, self.model.ITEM_HEIGHT_PX // 2) self._draw_bitmap() self.Refresh() self.Update() def _draw_bitmap(self): width, height = self.GetVirtualSize() self.buffer_image = wx.Bitmap(width, height) memdc = wx.BufferedDC(None, self.buffer_image) memdc.SetBackground(wx.Brush(self.GetBackgroundColour(), wx.BRUSHSTYLE_SOLID)) memdc.Clear() self.monitoring.timer_start() self.renderer.render(memdc) self.monitoring.timer_end() if self._config.debug_enabled: (width, height) = self.GetSize() redraw_time = self.monitoring.timer_elapsed_ms self.monitoring.count_category_redraw() memdc.SetTextForeground((255, 0, 0)) memdc.SetFont(Font(10, weight=wx.FONTWEIGHT_BOLD)) memdc.DrawText("Redraw count: %d" % self.monitoring.category_redraw_count, 10, height - 35) memdc.DrawText("Last redraw time: %.3f ms" % redraw_time, 10, height - 20) del memdc def _size_to_model(self): (view_width, view_height) = self.GetVirtualSize() self.model.set_view_size(view_width, view_height) def _create_context_menu(self): def add_item(name, callback, should_be_enabled_fn): item = wx.MenuItem(self.context_menu, wx.ID_ANY, name) self.context_menu.Append(item) self.Bind(wx.EVT_MENU, callback, item) self.context_menu_items.append((item, should_be_enabled_fn)) return item self.context_menu_items = [] self.context_menu = wx.Menu() add_item( _("Edit..."), self._on_menu_edit, lambda hit_info: hit_info.has_category()) add_item( _("Add..."), self._on_menu_add, lambda hit_info: self._has_timeline_view()) add_item( _("Delete"), self._on_menu_delete, lambda hit_info: hit_info.has_category()) self.context_menu.AppendSeparator() add_item( _("Check All"), self._on_menu_check_all, lambda hit_info: self._has_timeline_view()) add_item( _("Check children"), self._on_menu_check_children, lambda hit_info: hit_info.has_category()) add_item( _("Check all children"), self._on_menu_check_all_children, lambda hit_info: hit_info.has_category()) add_item( _("Check all parents"), self._on_menu_check_parents, lambda hit_info: hit_info.has_category()) add_item( _("Check parents for checked children"), self._on_menu_check_parents_for_checked_children, lambda hit_info: self._has_timeline_view()) self.context_menu.AppendSeparator() add_item( _("Uncheck All"), self._on_menu_uncheck_all, lambda hit_info: self._has_timeline_view()) add_item( _("Uncheck children"), self._on_menu_uncheck_children, lambda hit_info: hit_info.has_category()) add_item( _("Uncheck all children"), self._on_menu_uncheck_all_children, lambda hit_info: hit_info.has_category()) add_item( _("Uncheck all parents"), self._on_menu_uncheck_parents, lambda hit_info: hit_info.has_category())
[docs]class CustomCategoryTreeRenderer: INNER_PADDING = 2 TRIANGLE_SIZE = 8
[docs] def __init__(self, window, model, config): self.window = window self.model = model self._config = config
[docs] def render(self, dc): self.dc = dc self._render_items(self.model.items) del self.dc
def _render_items(self, items): self.dc.SetFont(wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)) for item in items: self._render_item(item) def _render_item(self, item): if item["has_children"]: self._render_arrow(item) self._render_checkbox(item) self._render_name(item) if not self._config.use_sidebar_text_coloring: self._render_color_box(item) def _render_arrow(self, item): self.dc.SetBrush(wx.Brush(wx.Colour(100, 100, 100), wx.BRUSHSTYLE_SOLID)) self.dc.SetPen(wx.Pen(wx.Colour(100, 100, 100), 0, wx.PENSTYLE_SOLID)) offset = self.TRIANGLE_SIZE // 2 center_x = item["x"] + 2 * self.INNER_PADDING + offset center_y = item["y"] + self.model.ITEM_HEIGHT_PX // 2 - 1 if item["expanded"]: open_polygon = [ wx.Point(center_x - offset, center_y - offset), wx.Point(center_x + offset, center_y - offset), wx.Point(center_x, center_y + offset), ] self.dc.DrawPolygon(open_polygon) else: closed_polygon = [ wx.Point(center_x - offset, center_y - offset), wx.Point(center_x - offset, center_y + offset), wx.Point(center_x + offset, center_y), ] self.dc.DrawPolygon(closed_polygon) def _render_name(self, item): x = item["x"] + self.TRIANGLE_SIZE + 4 * self.INNER_PADDING + 20 (_, h) = self.dc.GetTextExtent(item["name"]) if self._config.use_sidebar_text_coloring: self.dc.SetTextForeground(item.get("color", None)) else: if item["actually_visible"]: self.dc.SetTextForeground(self.window.GetForegroundColour()) else: self.dc.SetTextForeground((150, 150, 150)) self.dc.DrawText(item["name"], x + self.INNER_PADDING, item["y"] + (self.model.ITEM_HEIGHT_PX - h) // 2) def _render_checkbox(self, item): color = item.get("color", None) (w, h) = (17, 17) bounding_rect = wx.Rect(item["x"] + self.model.INDENT_PX, item["y"] + (self.model.ITEM_HEIGHT_PX - h) // 2, w, h) if item["visible"]: flag = wx.CONTROL_CHECKED else: flag = 0 renderer = wx.RendererNative.Get() renderer.DrawCheckBox(self.window, self.dc, bounding_rect, flag) def _render_color_box(self, item): color = item.get("color", None) self.dc.SetBrush(wx.Brush(color, wx.BRUSHSTYLE_SOLID)) self.dc.SetPen(wx.Pen(darken_color(color), 1, wx.PENSTYLE_SOLID)) (w, h) = (16, 16) self.dc.DrawRectangle( item["x"] + item["width"] - w - self.INNER_PADDING, item["y"] + self.model.ITEM_HEIGHT_PX // 2 - h // 2, w, h)
[docs]class CustomCategoryTreeModel(Observable): ITEM_HEIGHT_PX = 22 INDENT_PX = 15
[docs] def __init__(self): Observable.__init__(self) self.view_width = 0 self.view_height = 0 self.categories = None self.collapsed_category_ids = [] self.items = []
[docs] def get_items(self): return self.items
[docs] def set_view_size(self, view_width, view_height): self.view_width = view_width self.view_height = view_height self._update_items()
[docs] def set_categories(self, categories): if self.categories: self.categories.unlisten(self._update_items) self.categories = categories if self.categories: self.categories.listen_for_any(self._update_items) self._update_items()
[docs] def hit(self, x, y): item = self._item_at(y) if item: return HitInfo(self.categories, item["category"], self._hits_arrow(x, item), self._hits_checkbox(x, item)) else: return HitInfo(self.categories, None, False, False)
[docs] def toggle_expandedness(self, category): if category.get_id() in self.collapsed_category_ids: self.collapsed_category_ids.remove(category.get_id()) self._update_items() else: self.collapsed_category_ids.append(category.get_id()) self._update_items()
def _item_at(self, y): index = y // self.ITEM_HEIGHT_PX if index < len(self.items): return self.items[index] else: return None def _hits_arrow(self, x, item): return (x > item["x"] and x < (item["x"] + self.INDENT_PX)) def _hits_checkbox(self, x, item): return (x > (item["x"] + self.INDENT_PX) and x < (item["x"] + 2 * self.INDENT_PX)) def _update_items(self): self.items = [] self.y = 0 self._update_from_tree(self._list_to_tree(self._get_categories())) self._notify(None) def _get_categories(self): if self.categories is None: return [] else: return self.categories.get_all() def _list_to_tree(self, categories, parent=None): top = [category for category in categories if (category._get_parent() == parent)] sorted_top = sorted(top, key=lambda category: category.get_name()) return [(category, self._list_to_tree(categories, category)) for category in sorted_top] def _update_from_tree(self, category_tree, indent_level=0): for (category, child_tree) in category_tree: expanded = category.get_id() not in self.collapsed_category_ids self.items.append({ "id": category.get_id(), "name": category.get_name(), "color": category.get_color(), "visible": self._is_category_visible(category), "x": indent_level * self.INDENT_PX, "y": self.y, "width": self.view_width - indent_level * self.INDENT_PX, "expanded": expanded, "has_children": len(child_tree) > 0, "actually_visible": self._is_event_with_category_visible(category), "category": category, }) self.y += self.ITEM_HEIGHT_PX if expanded: self._update_from_tree(child_tree, indent_level + 1) def _is_category_visible(self, category): return self.categories.is_visible(category) def _is_event_with_category_visible(self, category): return self.categories.is_event_with_category_visible(category)
[docs]class HitInfo:
[docs] def __init__(self, categories, category, is_on_arrow, is_on_checkbox): self._categories = categories self._category = category self._is_on_arrow = is_on_arrow self._is_on_checkbox = is_on_checkbox
[docs] def has_category(self): return self._category is not None
[docs] def get_category(self): return self._category
[docs] def get_immediate_children(self): return self._categories.get_immediate_children(self._category)
[docs] def get_all_children(self): return self._categories.get_all_children(self._category)
[docs] def get_parents(self): return self._categories.get_parents(self._category)
[docs] def get_parents_for_checked_childs(self): return self._categories.get_parents_for_checked_childs()
[docs] def is_on_arrow(self): return self._is_on_arrow
[docs] def is_on_checkbox(self): return self._is_on_checkbox
[docs]def edit_category(parent_ctrl, db, cat): dialog = EditCategoryDialog(parent_ctrl, _("Edit Category"), db, cat) dialog.ShowModal() dialog.Destroy()
[docs]def add_category(parent_ctrl, db): dialog = EditCategoryDialog(parent_ctrl, _("Add Category"), db, None) dialog.ShowModal() dialog.Destroy()
[docs]def delete_category(parent_ctrl, db, cat): delete_warning = _("Are you sure you want to " "delete category '%s'?") % cat.name if cat.parent is None: update_warning = _("Events belonging to '%s' will no longer " "belong to a category.") % cat.name else: update_warning = _("Events belonging to '%s' will now belong " "to '%s'.") % (cat.name, cat.parent.name) question = "%s\n\n%s" % (delete_warning, update_warning) if gui_utils._ask_question(question, parent_ctrl) == wx.YES: db.delete_category(cat)