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.GetVirtualSizeTuple() 164 self.buffer_image = wx.EmptyBitmap(width, height) 165 memdc = wx.BufferedDC(None, self.buffer_image) 166 memdc.SetBackground(wx.Brush(self.GetBackgroundColour(), wx.PENSTYLE_SOLID)) 167 memdc.Clear() 168 memdc.BeginDrawing() 169 self.monitoring.timer_start() 170 self.renderer.render(memdc) 171 self.monitoring.timer_end() 172 if DEBUG_ENABLED: 173 (width, height) = self.GetSizeTuple() 174 redraw_time = self.monitoring.timer_elapsed_ms 175 self.monitoring.count_category_redraw() 176 memdc.SetTextForeground((255, 0, 0)) 177 memdc.SetFont(Font(10, weight=wx.FONTWEIGHT_BOLD)) 178 memdc.DrawText("Redraw count: %d" % self.monitoring._category_redraw_count, 10, height - 35) 179 memdc.DrawText("Last redraw time: %.3f ms" % redraw_time, 10, height - 20) 180 memdc.EndDrawing() 181 del memdc
182
183 - def _size_to_model(self):
184 (view_width, view_height) = self.GetVirtualSizeTuple() 185 self.model.set_view_size(view_width, view_height)
186
187 - def _create_context_menu(self):
188 def add_item(name, callback, should_be_enabled_fn): 189 item = wx.MenuItem(self.context_menu, wx.ID_ANY, name) 190 self.context_menu.AppendItem(item) 191 self.Bind(wx.EVT_MENU, callback, item) 192 self.context_menu_items.append((item, should_be_enabled_fn)) 193 return item
194 self.context_menu_items = [] 195 self.context_menu = wx.Menu() 196 add_item( 197 _("Edit..."), 198 self._on_menu_edit, 199 lambda hit_info: hit_info.has_category()) 200 add_item( 201 _("Add..."), 202 self._on_menu_add, 203 lambda hit_info: self._has_timeline_view()) 204 add_item( 205 _("Delete"), 206 self._on_menu_delete, 207 lambda hit_info: hit_info.has_category()) 208 self.context_menu.AppendSeparator() 209 add_item( 210 _("Check All"), 211 self._on_menu_check_all, 212 lambda hit_info: self._has_timeline_view()) 213 add_item( 214 _("Check children"), 215 self._on_menu_check_children, 216 lambda hit_info: hit_info.has_category()) 217 add_item( 218 _("Check all children"), 219 self._on_menu_check_all_children, 220 lambda hit_info: hit_info.has_category()) 221 add_item( 222 _("Check all parents"), 223 self._on_menu_check_parents, 224 lambda hit_info: hit_info.has_category()) 225 add_item( 226 _("Check parents for checked children"), 227 self._on_menu_check_parents_for_checked_children, 228 lambda hit_info: self._has_timeline_view()) 229 self.context_menu.AppendSeparator() 230 add_item( 231 _("Uncheck All"), 232 self._on_menu_uncheck_all, 233 lambda hit_info: self._has_timeline_view()) 234 add_item( 235 _("Uncheck children"), 236 self._on_menu_uncheck_children, 237 lambda hit_info: hit_info.has_category()) 238 add_item( 239 _("Uncheck all children"), 240 self._on_menu_uncheck_all_children, 241 lambda hit_info: hit_info.has_category()) 242 add_item( 243 _("Uncheck all parents"), 244 self._on_menu_uncheck_parents, 245 lambda hit_info: hit_info.has_category()) 246 247
248 -class CustomCategoryTreeRenderer(object):
249 250 INNER_PADDING = 2 251 TRIANGLE_SIZE = 8 252
253 - def __init__(self, window, model):
254 self.window = window 255 self.model = model
256
257 - def render(self, dc):
258 self.dc = dc 259 self._render_items(self.model.items) 260 del self.dc
261
262 - def _render_items(self, items):
263 self.dc.SetFont(wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)) 264 for item in items: 265 self._render_item(item)
266
267 - def _render_item(self, item):
268 if item["has_children"]: 269 self._render_arrow(item) 270 self._render_checkbox(item) 271 self._render_name(item) 272 self._render_color_box(item)
273
274 - def _render_arrow(self, item):
275 self.dc.SetBrush(wx.Brush(wx.Colour(100, 100, 100), wx.PENSTYLE_SOLID)) 276 self.dc.SetPen(wx.Pen(wx.Colour(100, 100, 100), 0, wx.PENSTYLE_SOLID)) 277 offset = self.TRIANGLE_SIZE / 2 278 center_x = item["x"] + 2 * self.INNER_PADDING + offset 279 center_y = item["y"] + self.model.ITEM_HEIGHT_PX / 2 - 1 280 if item["expanded"]: 281 open_polygon = [ 282 wx.Point(center_x - offset, center_y - offset), 283 wx.Point(center_x + offset, center_y - offset), 284 wx.Point(center_x, center_y + offset), 285 ] 286 self.dc.DrawPolygon(open_polygon) 287 else: 288 closed_polygon = [ 289 wx.Point(center_x - offset, center_y - offset), 290 wx.Point(center_x - offset, center_y + offset), 291 wx.Point(center_x + offset, center_y), 292 ] 293 self.dc.DrawPolygon(closed_polygon)
294
295 - def _render_name(self, item):
296 x = item["x"] + self.TRIANGLE_SIZE + 4 * self.INNER_PADDING + 20 297 (_, h) = self.dc.GetTextExtent(item["name"]) 298 if item["actually_visible"]: 299 self.dc.SetTextForeground(self.window.GetForegroundColour()) 300 else: 301 self.dc.SetTextForeground((150, 150, 150)) 302 self.dc.DrawText(item["name"], 303 x + self.INNER_PADDING, 304 item["y"] + (self.model.ITEM_HEIGHT_PX - h) / 2)
305
306 - def _render_checkbox(self, item):
307 (w, h) = (17, 17) 308 bouning_rect = wx.Rect(item["x"] + self.model.INDENT_PX, 309 item["y"] + (self.model.ITEM_HEIGHT_PX - h) / 2, 310 w, 311 h) 312 if item["visible"]: 313 flag = wx.CONTROL_CHECKED 314 else: 315 flag = 0 316 renderer = wx.RendererNative.Get() 317 renderer.DrawCheckBox(self.window, self.dc, bouning_rect, flag)
318
319 - def _render_color_box(self, item):
320 color = item.get("color", None) 321 self.dc.SetBrush(wx.Brush(color, wx.PENSTYLE_SOLID)) 322 self.dc.SetPen(wx.Pen(darken_color(color), 1, wx.PENSTYLE_SOLID)) 323 (w, h) = (16, 16) 324 self.dc.DrawRectangle( 325 item["x"] + item["width"] - w - self.INNER_PADDING, 326 item["y"] + self.model.ITEM_HEIGHT_PX / 2 - h / 2, 327 w, 328 h)
329 330
331 -class CustomCategoryTreeModel(Observable):
332 333 ITEM_HEIGHT_PX = 22 334 INDENT_PX = 15 335
336 - def __init__(self):
337 Observable.__init__(self) 338 self.view_width = 0 339 self.view_height = 0 340 self.categories = None 341 self.collapsed_category_ids = [] 342 self.items = []
343
344 - def get_items(self):
345 return self.items
346
347 - def set_view_size(self, view_width, view_height):
348 self.view_width = view_width 349 self.view_height = view_height 350 self._update_items()
351
352 - def set_categories(self, categories):
353 if self.categories: 354 self.categories.unlisten(self._update_items) 355 self.categories = categories 356 if self.categories: 357 self.categories.listen_for_any(self._update_items) 358 self._update_items()
359
360 - def hit(self, x, y):
361 item = self._item_at(y) 362 if item: 363 return HitInfo(self.categories, 364 item["category"], 365 self._hits_arrow(x, item), 366 self._hits_checkbox(x, item)) 367 else: 368 return HitInfo(self.categories, None, False, False)
369
370 - def toggle_expandedness(self, category):
371 if category.get_id() in self.collapsed_category_ids: 372 self.collapsed_category_ids.remove(category.get_id()) 373 self._update_items() 374 else: 375 self.collapsed_category_ids.append(category.get_id()) 376 self._update_items()
377
378 - def _item_at(self, y):
379 index = y // self.ITEM_HEIGHT_PX 380 if index < len(self.items): 381 return self.items[index] 382 else: 383 return None
384
385 - def _hits_arrow(self, x, item):
386 return (x > item["x"] and 387 x < (item["x"] + self.INDENT_PX))
388
389 - def _hits_checkbox(self, x, item):
390 return (x > (item["x"] + self.INDENT_PX) and 391 x < (item["x"] + 2 * self.INDENT_PX))
392
393 - def _update_items(self):
394 self.items = [] 395 self.y = 0 396 self._update_from_tree(self._list_to_tree(self._get_categories())) 397 self._notify(None)
398
399 - def _get_categories(self):
400 if self.categories is None: 401 return [] 402 else: 403 return self.categories.get_all()
404
405 - def _list_to_tree(self, categories, parent=None):
406 top = [category for category in categories if (category._get_parent() == parent)] 407 sorted_top = sorted(top, key=lambda category: category.get_name()) 408 return [(category, self._list_to_tree(categories, category)) for 409 category in sorted_top]
410
411 - def _update_from_tree(self, category_tree, indent_level=0):
412 for (category, child_tree) in category_tree: 413 expanded = category.get_id() not in self.collapsed_category_ids 414 self.items.append({ 415 "id": category.get_id(), 416 "name": category.get_name(), 417 "color": category.get_color(), 418 "visible": self._is_category_visible(category), 419 "x": indent_level * self.INDENT_PX, 420 "y": self.y, 421 "width": self.view_width - indent_level * self.INDENT_PX, 422 "expanded": expanded, 423 "has_children": len(child_tree) > 0, 424 "actually_visible": self._is_event_with_category_visible(category), 425 "category": category, 426 }) 427 self.y += self.ITEM_HEIGHT_PX 428 if expanded: 429 self._update_from_tree(child_tree, indent_level + 1)
430
431 - def _is_category_visible(self, category):
432 return self.categories.is_visible(category)
433
434 - def _is_event_with_category_visible(self, category):
436 437
438 -class HitInfo(object):
439
440 - def __init__(self, categories, category, is_on_arrow, is_on_checkbox):
441 self._categories = categories 442 self._category = category 443 self._is_on_arrow = is_on_arrow 444 self._is_on_checkbox = is_on_checkbox
445
446 - def has_category(self):
447 return self._category is not None
448
449 - def get_category(self):
450 return self._category
451
452 - def get_immediate_children(self):
453 return self._categories.get_immediate_children(self._category)
454
455 - def get_all_children(self):
456 return self._categories.get_all_children(self._category)
457
458 - def get_parents(self):
459 return self._categories.get_parents(self._category)
460
462 return self._categories.get_parents_for_checked_childs()
463
464 - def is_on_arrow(self):
465 return self._is_on_arrow
466
467 - def is_on_checkbox(self):
468 return self._is_on_checkbox
469 470
471 -def edit_category(parent_ctrl, db, cat):
472 dialog = EditCategoryDialog(parent_ctrl, _("Edit Category"), db, cat) 473 dialog.ShowModal() 474 dialog.Destroy()
475 476
477 -def add_category(parent_ctrl, db):
478 dialog = EditCategoryDialog(parent_ctrl, _("Add Category"), db, None) 479 dialog.ShowModal() 480 dialog.Destroy()
481 482
483 -def delete_category(parent_ctrl, db, cat):
484 delete_warning = _("Are you sure you want to " 485 "delete category '%s'?") % cat.name 486 if cat.parent is None: 487 update_warning = _("Events belonging to '%s' will no longer " 488 "belong to a category.") % cat.name 489 else: 490 update_warning = _("Events belonging to '%s' will now belong " 491 "to '%s'.") % (cat.name, cat.parent.name) 492 question = "%s\n\n%s" % (delete_warning, update_warning) 493 if gui_utils._ask_question(question, parent_ctrl) == wx.YES: 494 db.delete_category(cat)
495