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
99 - def _on_menu_edit(self, e):
100 self._edit_category()
101
102 - def _on_menu_add(self, e):
103 add_category(self, self.db)
104
105 - def _on_menu_delete(self, e):
106 hit_category = self.last_hit_info.get_category() 107 if hit_category: 108 delete_category(self, self.db, hit_category)
109
110 - def _on_menu_check_all(self, e):
111 self.view_properties.set_categories_visible( 112 self.db.get_categories())
113
114 - def _on_menu_check_children(self, e):
115 self.view_properties.set_categories_visible( 116 self.last_hit_info.get_immediate_children())
117
118 - def _on_menu_check_all_children(self, e):
119 self.view_properties.set_categories_visible( 120 self.last_hit_info.get_all_children())
121
122 - def _on_menu_check_parents(self, e):
123 self.view_properties.set_categories_visible( 124 self.last_hit_info.get_parents())
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
130 - def _on_menu_uncheck_all(self, e):
131 self.view_properties.set_categories_visible( 132 self.db.get_categories(), False)
133
134 - def _on_menu_uncheck_children(self, e):
135 self.view_properties.set_categories_visible( 136 self.last_hit_info.get_immediate_children(), False)
137
138 - def _on_menu_uncheck_all_children(self, e):
139 self.view_properties.set_categories_visible( 140 self.last_hit_info.get_all_children(), False)
141
142 - def _on_menu_uncheck_parents(self, e):
143 self.view_properties.set_categories_visible( 144 self.last_hit_info.get_parents(), False)
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
185 - def _create_context_menu(self):
186 def add_item(name, callback, should_be_enabled_fn): 187 item = wx.MenuItem(self.context_menu, wx.ID_ANY, name) 188 self.context_menu.Append(item) 189 self.Bind(wx.EVT_MENU, callback, item) 190 self.context_menu_items.append((item, should_be_enabled_fn)) 191 return item
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):
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
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