Gnumed.timelinelib.wxgui.components.categorytree

Package Gnumed :: Package timelinelib :: Package wxgui :: Package components :: Module categorytree |
[frames] | no frames]

# Source Code for Module Gnumed.timelinelib.wxgui.components.categorytree

      1  # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
      2  #
      3  # This file is part of Timeline.
      4  #
      5  # Timeline is free software: you can redistribute it and/or modify
      6  # it under the terms of the GNU General Public License as published by
      7  # the Free Software Foundation, either version 3 of the License, or
      8  # (at your option) any later version.
      9  #
     10  # Timeline is distributed in the hope that it will be useful,
     11  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13  # GNU General Public License for more details.
     14  #
     15  # You should have received a copy of the GNU General Public License
     16  # along with Timeline.  If not, see <http://www.gnu.org/licenses/>.
     17
     18
     19  import wx
     20
     21  from timelinelib.canvas.data.exceptions import TimelineIOError
     22  from timelinelib.canvas.drawing.utils import darken_color
     23  from timelinelib.db.utils import safe_locking
     24  from timelinelib import DEBUG_ENABLED
     25  from timelinelib.monitoring import Monitoring
     26  from timelinelib.general.observer import Observable
     27  from timelinelib.repositories.categories import CategoriesFacade
     28  from timelinelib.wxgui.components.font import Font
     29  from timelinelib.wxgui.dialogs.editcategory.view import EditCategoryDialog
     30  import timelinelib.wxgui.utils as gui_utils
     31
     32
     33 -class CustomCategoryTree(wx.ScrolledWindow):
     34
     35 -    def __init__(self, parent, name=None, size=(100, 100)):
     36          self.monitoring = Monitoring()
     37          wx.ScrolledWindow.__init__(self, parent, size=size)
     38          self.parent = parent
     39          self._create_context_menu()
     40          self.Bind(wx.EVT_PAINT, self._on_paint)
     41          self.Bind(wx.EVT_SIZE, self._on_size)
     42          self.Bind(wx.EVT_LEFT_DOWN, self._on_left_down)
     43          self.Bind(wx.EVT_RIGHT_DOWN, self._on_right_down)
     44          self.Bind(wx.EVT_LEFT_DCLICK, self._on_left_doubleclick)
     45          self.model = CustomCategoryTreeModel()
     46          self.model.listen_for_any(self._redraw)
     47          self.renderer = CustomCategoryTreeRenderer(self, self.model)
     48          self.set_no_timeline_view()
     49          self._size_to_model()
     50          self._draw_bitmap()
     51
     52 -    def set_no_timeline_view(self):
     53          self.db = None
     54          self.view_properties = None
     55          self.model.set_categories(None)
     56
     57 -    def set_timeline_view(self, db, view_properties):
     58          self.db = db
     59          self.view_properties = view_properties
     60          self.model.set_categories(CategoriesFacade(db, view_properties))
     61
     62 -    def check_categories(self, categories):
     63          self.view_properties.set_categories_visible(categories)
     64
     65 -    def uncheck_categories(self, categories):
     66          self.view_properties.set_categories_visible(categories, False)
     67
     68 -    def _has_timeline_view(self):
     69          return self.db is not None and self.view_properties is not None
     70
     71 -    def _on_paint(self, event):
     72          wx.BufferedPaintDC(self, self.buffer_image, wx.BUFFER_VIRTUAL_AREA)
     73
     74 -    def _on_size(self, event):
     75          self._size_to_model()
     76
     77 -    def _on_left_doubleclick(self, event):
     78          def edit_function():
     79              self._edit_category()
     80          safe_locking(self.parent, edit_function)
     81
     82 -    def _on_left_down(self, event):
     83          self.SetFocus()
     84          self._store_hit_info(event)
     85          hit_category = self.last_hit_info.get_category()
     86          if self.last_hit_info.is_on_arrow():
     87              self.model.toggle_expandedness(hit_category)
     88          elif self.last_hit_info.is_on_checkbox():
     89              self.view_properties.toggle_category_visibility(hit_category)
     90
     91 -    def _on_right_down(self, event):
     92          def edit_function():
     93              self._store_hit_info(event)
     94              for (menu_item, should_be_enabled_fn) in self.context_menu_items:
     95                  menu_item.Enable(should_be_enabled_fn(self.last_hit_info))
     96              self.PopupMenu(self.context_menu)
     97          safe_locking(self.parent, edit_function)
     98
    101
    104
    109
    113
    117
    121
    125
    126 -    def _on_menu_check_parents_for_checked_children(self, e):
    127          self.view_properties.set_categories_visible(
    128              self.last_hit_info.get_parents_for_checked_childs())
    129
    133
    137
    141
    145
    146 -    def _edit_category(self):
    147          hit_category = self.last_hit_info.get_category()
    148          if hit_category:
    149              edit_category(self, self.db, hit_category)
    150
    151 -    def _store_hit_info(self, event):
    152          (x, y) = self.CalcUnscrolledPosition(event.GetX(), event.GetY())
    153          self.last_hit_info = self.model.hit(x, y)
    154
    155 -    def _redraw(self):
    156          self.SetVirtualSize((-1, self.model.ITEM_HEIGHT_PX * len(self.model.items)))
    157          self.SetScrollRate(0, self.model.ITEM_HEIGHT_PX // 2)
    158          self._draw_bitmap()
    159          self.Refresh()
    160          self.Update()
    161
    162 -    def _draw_bitmap(self):
    163          width, height = self.GetVirtualSize()
    164          self.buffer_image = wx.Bitmap(width, height)
    165          memdc = wx.BufferedDC(None, self.buffer_image)
    166          memdc.SetBackground(wx.Brush(self.GetBackgroundColour(), wx.BRUSHSTYLE_SOLID))
    167          memdc.Clear()
    168          self.monitoring.timer_start()
    169          self.renderer.render(memdc)
    170          self.monitoring.timer_end()
    171          if DEBUG_ENABLED:
    172              (width, height) = self.GetSize()
    173              redraw_time = self.monitoring.timer_elapsed_ms
    174              self.monitoring.count_category_redraw()
    175              memdc.SetTextForeground((255, 0, 0))
    176              memdc.SetFont(Font(10, weight=wx.FONTWEIGHT_BOLD))
    177              memdc.DrawText("Redraw count: %d" % self.monitoring._category_redraw_count, 10, height - 35)
    178              memdc.DrawText("Last redraw time: %.3f ms" % redraw_time, 10, height - 20)
    179          del memdc
    180
    181 -    def _size_to_model(self):
    182          (view_width, view_height) = self.GetVirtualSize()
    183          self.model.set_view_size(view_width, view_height)
    184
    192          self.context_menu_items = []
    193          self.context_menu = wx.Menu()
    194          add_item(
    195              _("Edit..."),
    196              self._on_menu_edit,
    197              lambda hit_info: hit_info.has_category())
    198          add_item(
    199              _("Add..."),
    200              self._on_menu_add,
    201              lambda hit_info: self._has_timeline_view())
    202          add_item(
    203              _("Delete"),
    204              self._on_menu_delete,
    205              lambda hit_info: hit_info.has_category())
    206          self.context_menu.AppendSeparator()
    207          add_item(
    208              _("Check All"),
    209              self._on_menu_check_all,
    210              lambda hit_info: self._has_timeline_view())
    211          add_item(
    212              _("Check children"),
    213              self._on_menu_check_children,
    214              lambda hit_info: hit_info.has_category())
    215          add_item(
    216              _("Check all children"),
    217              self._on_menu_check_all_children,
    218              lambda hit_info: hit_info.has_category())
    219          add_item(
    220              _("Check all parents"),
    221              self._on_menu_check_parents,
    222              lambda hit_info: hit_info.has_category())
    223          add_item(
    224              _("Check parents for checked children"),
    225              self._on_menu_check_parents_for_checked_children,
    226              lambda hit_info: self._has_timeline_view())
    227          self.context_menu.AppendSeparator()
    228          add_item(
    229              _("Uncheck All"),
    230              self._on_menu_uncheck_all,
    231              lambda hit_info: self._has_timeline_view())
    232          add_item(
    233              _("Uncheck children"),
    234              self._on_menu_uncheck_children,
    235              lambda hit_info: hit_info.has_category())
    236          add_item(
    237              _("Uncheck all children"),
    238              self._on_menu_uncheck_all_children,
    239              lambda hit_info: hit_info.has_category())
    240          add_item(
    241              _("Uncheck all parents"),
    242              self._on_menu_uncheck_parents,
    243              lambda hit_info: hit_info.has_category())
    244
    245
    246 -class CustomCategoryTreeRenderer(object):
    247
    248      INNER_PADDING = 2
    249      TRIANGLE_SIZE = 8
    250
    251 -    def __init__(self, window, model):
    252          self.window = window
    253          self.model = model
    254
    255 -    def render(self, dc):
    256          self.dc = dc
    257          self._render_items(self.model.items)
    258          del self.dc
    259
    260 -    def _render_items(self, items):
    261          self.dc.SetFont(wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT))
    262          for item in items:
    263              self._render_item(item)
    264
    265 -    def _render_item(self, item):
    266          if item["has_children"]:
    267              self._render_arrow(item)
    268          self._render_checkbox(item)
    269          self._render_name(item)
    270          self._render_color_box(item)
    271
    272 -    def _render_arrow(self, item):
    273          self.dc.SetBrush(wx.Brush(wx.Colour(100, 100, 100), wx.BRUSHSTYLE_SOLID))
    274          self.dc.SetPen(wx.Pen(wx.Colour(100, 100, 100), 0, wx.PENSTYLE_SOLID))
    275          offset = self.TRIANGLE_SIZE // 2
    276          center_x = item["x"] + 2 * self.INNER_PADDING + offset
    277          center_y = item["y"] + self.model.ITEM_HEIGHT_PX // 2 - 1
    278          if item["expanded"]:
    279              open_polygon = [
    280                  wx.Point(center_x - offset, center_y - offset),
    281                  wx.Point(center_x + offset, center_y - offset),
    282                  wx.Point(center_x, center_y + offset),
    283              ]
    284              self.dc.DrawPolygon(open_polygon)
    285          else:
    286              closed_polygon = [
    287                  wx.Point(center_x - offset, center_y - offset),
    288                  wx.Point(center_x - offset, center_y + offset),
    289                  wx.Point(center_x + offset, center_y),
    290              ]
    291              self.dc.DrawPolygon(closed_polygon)
    292
    293 -    def _render_name(self, item):
    294          x = item["x"] + self.TRIANGLE_SIZE + 4 * self.INNER_PADDING + 20
    295          (_, h) = self.dc.GetTextExtent(item["name"])
    296          if item["actually_visible"]:
    297              self.dc.SetTextForeground(self.window.GetForegroundColour())
    298          else:
    299              self.dc.SetTextForeground((150, 150, 150))
    300          self.dc.DrawText(item["name"],
    301                           x + self.INNER_PADDING,
    302                           item["y"] + (self.model.ITEM_HEIGHT_PX - h) // 2)
    303
    304 -    def _render_checkbox(self, item):
    305          (w, h) = (17, 17)
    306          bouning_rect = wx.Rect(item["x"] + self.model.INDENT_PX,
    307                                 item["y"] + (self.model.ITEM_HEIGHT_PX - h) // 2,
    308                                 w,
    309                                 h)
    310          if item["visible"]:
    311              flag = wx.CONTROL_CHECKED
    312          else:
    313              flag = 0
    314          renderer = wx.RendererNative.Get()
    315          renderer.DrawCheckBox(self.window, self.dc, bouning_rect, flag)
    316
    317 -    def _render_color_box(self, item):
    318          color = item.get("color", None)
    319          self.dc.SetBrush(wx.Brush(color, wx.BRUSHSTYLE_SOLID))
    320          self.dc.SetPen(wx.Pen(darken_color(color), 1, wx.PENSTYLE_SOLID))
    321          (w, h) = (16, 16)
    322          self.dc.DrawRectangle(
    323              item["x"] + item["width"] - w - self.INNER_PADDING,
    324              item["y"] + self.model.ITEM_HEIGHT_PX // 2 - h // 2,
    325              w,
    326              h)
    327
    328
    329 -class CustomCategoryTreeModel(Observable):
    330
    331      ITEM_HEIGHT_PX = 22
    332      INDENT_PX = 15
    333
    334 -    def __init__(self):
    335          Observable.__init__(self)
    336          self.view_width = 0
    337          self.view_height = 0
    338          self.categories = None
    339          self.collapsed_category_ids = []
    340          self.items = []
    341
    342 -    def get_items(self):
    343          return self.items
    344
    345 -    def set_view_size(self, view_width, view_height):
    346          self.view_width = view_width
    347          self.view_height = view_height
    348          self._update_items()
    349
    350 -    def set_categories(self, categories):
    351          if self.categories:
    352              self.categories.unlisten(self._update_items)
    353          self.categories = categories
    354          if self.categories:
    355              self.categories.listen_for_any(self._update_items)
    356          self._update_items()
    357
    358 -    def hit(self, x, y):
    359          item = self._item_at(y)
    360          if item:
    361              return HitInfo(self.categories,
    362                             item["category"],
    363                             self._hits_arrow(x, item),
    364                             self._hits_checkbox(x, item))
    365          else:
    366              return HitInfo(self.categories, None, False, False)
    367
    368 -    def toggle_expandedness(self, category):
    369          if category.get_id() in self.collapsed_category_ids:
    370              self.collapsed_category_ids.remove(category.get_id())
    371              self._update_items()
    372          else:
    373              self.collapsed_category_ids.append(category.get_id())
    374              self._update_items()
    375
    376 -    def _item_at(self, y):
    377          index = y // self.ITEM_HEIGHT_PX
    378          if index < len(self.items):
    379              return self.items[index]
    380          else:
    381              return None
    382
    383 -    def _hits_arrow(self, x, item):
    384          return (x > item["x"] and
    385                  x < (item["x"] + self.INDENT_PX))
    386
    387 -    def _hits_checkbox(self, x, item):
    388          return (x > (item["x"] + self.INDENT_PX) and
    389                  x < (item["x"] + 2 * self.INDENT_PX))
    390
    391 -    def _update_items(self):
    392          self.items = []
    393          self.y = 0
    394          self._update_from_tree(self._list_to_tree(self._get_categories()))
    395          self._notify(None)
    396
    397 -    def _get_categories(self):
    398          if self.categories is None:
    399              return []
    400          else:
    401              return self.categories.get_all()
    402
    403 -    def _list_to_tree(self, categories, parent=None):
    404          top = [category for category in categories if (category._get_parent() == parent)]
    405          sorted_top = sorted(top, key=lambda category: category.get_name())
    406          return [(category, self._list_to_tree(categories, category)) for
    407                  category in sorted_top]
    408
    409 -    def _update_from_tree(self, category_tree, indent_level=0):
    410          for (category, child_tree) in category_tree:
    411              expanded = category.get_id() not in self.collapsed_category_ids
    412              self.items.append({
    413                  "id": category.get_id(),
    414                  "name": category.get_name(),
    415                  "color": category.get_color(),
    416                  "visible": self._is_category_visible(category),
    417                  "x": indent_level * self.INDENT_PX,
    418                  "y": self.y,
    419                  "width": self.view_width - indent_level * self.INDENT_PX,
    420                  "expanded": expanded,
    421                  "has_children": len(child_tree) > 0,
    422                  "actually_visible": self._is_event_with_category_visible(category),
    423                  "category": category,
    424              })
    425              self.y += self.ITEM_HEIGHT_PX
    426              if expanded:
    427                  self._update_from_tree(child_tree, indent_level + 1)
    428
    429 -    def _is_category_visible(self, category):
    430          return self.categories.is_visible(category)
    431
    432 -    def _is_event_with_category_visible(self, category):
    433          return self.categories.is_event_with_category_visible(category)
    434
    435
    436 -class HitInfo(object):
    437
    438 -    def __init__(self, categories, category, is_on_arrow, is_on_checkbox):
    439          self._categories = categories
    440          self._category = category
    441          self._is_on_arrow = is_on_arrow
    442          self._is_on_checkbox = is_on_checkbox
    443
    444 -    def has_category(self):
    445          return self._category is not None
    446
    447 -    def get_category(self):
    448          return self._category
    449
    450 -    def get_immediate_children(self):
    451          return self._categories.get_immediate_children(self._category)
    452
    453 -    def get_all_children(self):
    454          return self._categories.get_all_children(self._category)
    455
    456 -    def get_parents(self):
    457          return self._categories.get_parents(self._category)
    458
    459 -    def get_parents_for_checked_childs(self):
    460          return self._categories.get_parents_for_checked_childs()
    461
    462 -    def is_on_arrow(self):
    463          return self._is_on_arrow
    464
    465 -    def is_on_checkbox(self):
    466          return self._is_on_checkbox
    467
    468
    469 -def edit_category(parent_ctrl, db, cat):
    470      dialog = EditCategoryDialog(parent_ctrl, _("Edit Category"), db, cat)
    471      dialog.ShowModal()
    472      dialog.Destroy()
    473
    474
    475 -def add_category(parent_ctrl, db):
    476      dialog = EditCategoryDialog(parent_ctrl, _("Add Category"), db, None)
    477      dialog.ShowModal()
    478      dialog.Destroy()
    479
    480
    481 -def delete_category(parent_ctrl, db, cat):
    482      delete_warning = _("Are you sure you want to "
    483                         "delete category '%s'?") % cat.name
    484      if cat.parent is None:
    485          update_warning = _("Events belonging to '%s' will no longer "
    486                             "belong to a category.") % cat.name
    487      else:
    488          update_warning = _("Events belonging to '%s' will now belong "
    489                             "to '%s'.") % (cat.name, cat.parent.name)
    490      question = "%s\n\n%s" % (delete_warning, update_warning)
    491      if gui_utils._ask_question(question, parent_ctrl) == wx.YES:
    492          db.delete_category(cat)
    493