Package muntjac :: Package ui :: Module tree_table
[hide private]
[frames] | no frames]

Source Code for Module muntjac.ui.tree_table

  1  # Copyright (C) 2012 Vaadin Ltd.  
  2  # Copyright (C) 2012 Richard Lincoln 
  3  #  
  4  # Licensed under the Apache License, Version 2.0 (the "License");  
  5  # you may not use this file except in compliance with the License.  
  6  # You may obtain a copy of the License at  
  7  #  
  8  #     http://www.apache.org/licenses/LICENSE-2.0  
  9  #  
 10  # Unless required by applicable law or agreed to in writing, software  
 11  # distributed under the License is distributed on an "AS IS" BASIS,  
 12  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  
 13  # See the License for the specific language governing permissions and  
 14  # limitations under the License. 
 15   
 16  import logging 
 17   
 18  from muntjac.ui.tree \ 
 19      import ExpandEvent, IExpandListener, CollapseEvent, ICollapseListener, \ 
 20      COLLAPSE_METHOD, EXPAND_METHOD 
 21   
 22  from muntjac.ui.treetable.hierarchical_container_ordered_wrapper \ 
 23      import HierarchicalContainerOrderedWrapper 
 24   
 25  from muntjac.data.container import IOrdered, IHierarchical 
 26  from muntjac.terminal.gwt.client.ui.v_tree_table import VTreeTable 
 27  from muntjac.ui.table import Table 
 28   
 29  from muntjac.data.util.container_hierarchical_wrapper \ 
 30      import ContainerHierarchicalWrapper 
 31   
 32  from muntjac.ui.treetable.collapsible import ICollapsible 
 33  from muntjac.data.util.hierarchical_container import HierarchicalContainer 
 34   
 35   
 36  logger = logging.getLogger(__name__) 
 37   
 38   
39 -class TreeTable(Table, IHierarchical):
40 """TreeTable extends the L{Table} component so that it can also visualize 41 a hierarchy of its Items in a similar manner that {@link Tree} does. The 42 tree hierarchy is always displayed in the first actual column of the 43 TreeTable. 44 45 The TreeTable supports the usual {@link Table} features like lazy loading, 46 so it should be no problem to display lots of items at once. Only required 47 rows and some cache rows are sent to the client. 48 49 TreeTable supports standard L{IHierarchical} container interfaces, but 50 also a more fine tuned version - L{ICollapsible}. A container 51 implementing the L{ICollapsible} interface stores the collapsed/expanded 52 state internally and can this way scale better on the server side than with 53 standard Hierarchical implementations. Developer must however note that 54 L{ICollapsible} containers can not be shared among several users as they 55 share UI state in the container. 56 """ 57
58 - def __init__(self, caption=None, dataSource=None):
59 """Creates a TreeTable instance with optional captions and data source. 60 61 @param caption: 62 the caption for the component 63 @param dataSource: 64 the dataSource that is used to list items in the component 65 """ 66 if dataSource is None: 67 dataSource = HierarchicalContainer() 68 69 super(TreeTable, self).__init__(caption, dataSource) 70 71 72 self._cStrategy = None 73 self._focusedRowId = None 74 self._hierarchyColumnId = None 75 76 # The item id that was expanded or collapsed during this request. Reset 77 # at the end of paint and only used for determining if a partial or 78 # full paint should be done. 79 # 80 # Can safely be reset to null whenever a change occurs that would 81 # prevent a partial update from rendering the correct result, e.g. rows 82 # added or removed during an expand operation. 83 self._toggledItemId = None 84 self._animationsEnabled = None 85 self._clearFocusedRowPending = None
86 87
88 - def getContainerStrategy(self):
89 if self._cStrategy is None: 90 if isinstance(self.getContainerDataSource(), ICollapsible): 91 self._cStrategy = CollapsibleStrategy(self) 92 else: 93 self._cStrategy = HierarchicalStrategy(self) 94 return self._cStrategy
95 96
97 - def paintRowAttributes(self, target, itemId):
98 super(TreeTable, self).paintRowAttributes(target, itemId) 99 depth = self.getContainerStrategy().getDepth(itemId) 100 target.addAttribute('depth', depth) 101 if self.getContainerDataSource().areChildrenAllowed(itemId): 102 target.addAttribute('ca', True) 103 isOpen = self.getContainerStrategy().isNodeOpen(itemId) 104 target.addAttribute('open', isOpen)
105 106
107 - def paintRowIcon(self, target, cells, indexInRowbuffer):
108 # always paint if present (in parent only if row headers visible) 109 if self.getRowHeaderMode() == self.ROW_HEADER_MODE_HIDDEN: 110 cell = cells[self.CELL_ITEMID][indexInRowbuffer] 111 itemIcon = self.getItemIcon(cell) 112 if itemIcon is not None: 113 target.addAttribute('icon', itemIcon) 114 elif cells[self.CELL_ICON][indexInRowbuffer] is not None: 115 cell = cells[self.CELL_ICON][indexInRowbuffer] 116 target.addAttribute('icon', cell)
117 118
119 - def changeVariables(self, source, variables):
120 super(TreeTable, self).changeVariables(source, variables) 121 if 'toggleCollapsed' in variables: 122 obj = variables.get('toggleCollapsed') 123 itemId = self.itemIdMapper.get(obj) 124 self._toggledItemId = itemId 125 self.toggleChildVisibility(itemId) 126 if 'selectCollapsed' in variables: 127 # ensure collapsed is selected unless opened with selection 128 # head 129 if self.isSelectable(): 130 self.select(itemId) 131 elif 'focusParent' in variables: 132 key = variables.get('focusParent') 133 refId = self.itemIdMapper.get(key) 134 itemId = self.getParent(refId) 135 self.focusParent(itemId)
136 137
138 - def focusParent(self, itemId):
139 inView = False 140 inPageId = self.getCurrentPageFirstItemId() 141 142 i = 0 143 while inPageId is not None and i < self.getPageLength(): 144 if inPageId == itemId: 145 inView = True 146 break 147 inPageId = self.nextItemId(inPageId) 148 i += 1 # TODO: check increment 149 150 if not inView: 151 self.setCurrentPageFirstItemId(itemId) 152 153 # Select the row if it is selectable. 154 if self.isSelectable(): 155 if self.isMultiSelect(): 156 self.setValue([itemId]) 157 else: 158 self.setValue(itemId) 159 160 self.setFocusedRow(itemId)
161 162
163 - def setFocusedRow(self, itemId):
164 self._focusedRowId = itemId 165 if self._focusedRowId is None: 166 # Must still inform the client that the focusParent request has 167 # been processed 168 self._clearFocusedRowPending = True 169 self.requestRepaint()
170 171
172 - def paintContent(self, target):
173 # Override methods for partial row updates and additions when 174 # expanding / collapsing nodes. 175 176 if self._focusedRowId is not None: 177 row = self.itemIdMapper.key(self._focusedRowId) 178 target.addAttribute('focusedRow', row) 179 self._focusedRowId = None 180 elif self._clearFocusedRowPending: 181 # Must still inform the client that the focusParent request has 182 # been processed 183 target.addAttribute('clearFocusPending', True) 184 self._clearFocusedRowPending = False 185 186 target.addAttribute('animate', self._animationsEnabled) 187 if self._hierarchyColumnId is not None: 188 visibleColumns2 = self.getVisibleColumns() 189 for i in range(len(visibleColumns2)): 190 obj = visibleColumns2[i] 191 if self._hierarchyColumnId == obj: 192 ahci = VTreeTable.ATTRIBUTE_HIERARCHY_COLUMN_INDEX 193 target.addAttribute(ahci, i) 194 break 195 196 super(TreeTable, self).paintContent(target) 197 self._toggledItemId = None
198 199
200 - def isPartialRowUpdate(self):
201 return self._toggledItemId is not None
202 203
204 - def getFirstAddedItemIndex(self):
205 return self.indexOfId(self._toggledItemId) + 1
206 207
208 - def getAddedRowCount(self):
209 ds = self.getContainerDataSource() 210 return self.countSubNodesRecursively(ds, self._toggledItemId)
211 212
213 - def countSubNodesRecursively(self, hc, itemId):
214 count = 0 215 # we need the number of children for toggledItemId no matter if its 216 # collapsed or expanded. Other items' children are only counted if the 217 # item is expanded. 218 if (self.getContainerStrategy().isNodeOpen(itemId) 219 or (itemId == self._toggledItemId)): 220 children = hc.getChildren(itemId) 221 if children is not None: 222 count += len(children) if children is not None else 0 223 for idd in children: 224 count += self.countSubNodesRecursively(hc, idd) 225 return count
226 227
228 - def getFirstUpdatedItemIndex(self):
229 return self.indexOfId(self._toggledItemId)
230 231
232 - def getUpdatedRowCount(self):
233 return 1
234 235
236 - def shouldHideAddedRows(self):
237 return not self.getContainerStrategy().isNodeOpen(self._toggledItemId)
238 239
240 - def toggleChildVisibility(self, itemId):
241 self.getContainerStrategy().toggleChildVisibility(itemId) 242 # ensure that page still has first item in page, DON'T clear the 243 # caches. 244 idx = self.getCurrentPageFirstItemIndex() 245 self.setCurrentPageFirstItemIndex(idx, False) 246 self.requestRepaint() 247 if self.isCollapsed(itemId): 248 self.fireCollapseEvent(itemId) 249 else: 250 self.fireExpandEvent(itemId)
251 252
253 - def size(self):
254 return len(self.getContainerStrategy())
255 256
257 - def __len__(self):
258 return self.size()
259 260
261 - def getContainerDataSource(self):
262 return super(TreeTable, self).getContainerDataSource()
263 264
265 - def setContainerDataSource(self, newDataSource):
266 self._cStrategy = None 267 if not isinstance(newDataSource, IHierarchical): 268 newDataSource = ContainerHierarchicalWrapper(newDataSource) 269 if not isinstance(newDataSource, IOrdered): 270 newDataSource = HierarchicalContainerOrderedWrapper(newDataSource) 271 super(TreeTable, self).setContainerDataSource(newDataSource)
272 273
274 - def containerItemSetChange(self, event):
275 # Can't do partial repaints if items are added or removed during the 276 # expand/collapse request 277 self._toggledItemId = None 278 self.getContainerStrategy().containerItemSetChange(event) 279 super(TreeTable, self).containerItemSetChange(event)
280 281
282 - def getIdByIndex(self, index):
284 285
286 - def indexOfId(self, itemId):
287 return self.getContainerStrategy().indexOfId(itemId)
288 289
290 - def nextItemId(self, itemId):
291 return self.getContainerStrategy().nextItemId(itemId)
292 293
294 - def lastItemId(self):
295 return self.getContainerStrategy().lastItemId()
296 297
298 - def prevItemId(self, itemId):
299 return self.getContainerStrategy().prevItemId(itemId)
300 301
302 - def isLastId(self, itemId):
303 return self.getContainerStrategy().isLastId(itemId)
304 305
306 - def getItemIds(self):
307 return self.getContainerStrategy().getItemIds()
308 309
310 - def areChildrenAllowed(self, itemId):
311 return self.getContainerDataSource().areChildrenAllowed(itemId)
312 313
314 - def getChildren(self, itemId):
315 return self.getContainerDataSource().getChildren(itemId)
316 317
318 - def getParent(self, itemId=None):
319 if itemId is not None: 320 return self.getContainerDataSource().getParent(itemId) 321 else: 322 super(TreeTable, self).getParent()
323 324
325 - def hasChildren(self, itemId):
326 return self.getContainerDataSource().hasChildren(itemId)
327 328
329 - def isRoot(self, itemId):
330 return self.getContainerDataSource().isRoot(itemId)
331 332
333 - def rootItemIds(self):
334 return self.getContainerDataSource().rootItemIds()
335 336
337 - def setChildrenAllowed(self, itemId, areChildrenAllowed):
340 341
342 - def setParent(self, itemId, newParentId):
343 return self.getContainerDataSource().setParent(itemId, newParentId)
344 345
346 - def setCollapsed(self, itemId, collapsed):
347 """Sets the Item specified by given identifier collapsed or expanded. 348 If the Item is collapsed, its children is not displayed in for the 349 user. 350 351 @param itemId: 352 the identifier of the Item 353 @param collapsed: 354 true if the Item should be collapsed, false if expanded 355 """ 356 if self.isCollapsed(itemId) != collapsed: 357 self.toggleChildVisibility(itemId)
358 359
360 - def isCollapsed(self, itemId):
361 """Checks if Item with given identifier is collapsed in the UI. 362 363 @param itemId: 364 the identifier of the checked Item 365 @return: true if the Item with given id is collapsed 366 @see: L{ICollapsible.isCollapsed} 367 """ 368 return not self.getContainerStrategy().isNodeOpen(itemId)
369 370
371 - def setHierarchyColumn(self, hierarchyColumnId):
372 """Explicitly sets the column in which the TreeTable visualizes the 373 hierarchy. If hierarchyColumnId is not set, the hierarchy is visualized 374 in the first visible column. 375 """ 376 self._hierarchyColumnId = hierarchyColumnId
377 378
379 - def getHierarchyColumnId(self):
380 """@return: the identifier of column into which the hierarchy will be 381 visualized or null if the column is not explicitly defined. 382 """ 383 return self._hierarchyColumnId
384 385
386 - def addListener(self, listener, iface=None):
387 """Adds an expand/collapse listener. 388 389 @param listener: 390 the Listener to be added. 391 """ 392 if (isinstance(listener, ICollapseListener) and 393 (iface is None or issubclass(iface, ICollapseListener))): 394 self.registerListener(CollapseEvent, 395 listener, COLLAPSE_METHOD) 396 397 if (isinstance(listener, IExpandListener) and 398 (iface is None or issubclass(iface, IExpandListener))): 399 self.registerListener(ExpandEvent, 400 listener, EXPAND_METHOD) 401 402 super(TreeTable, self).addListener(listener, iface)
403 404
405 - def addCallback(self, callback, eventType=None, *args):
406 if eventType is None: 407 eventType = callback._eventType 408 409 if issubclass(eventType, CollapseEvent): 410 self.registerCallback(CollapseEvent, callback, None, *args) 411 412 elif issubclass(eventType, ExpandEvent): 413 self.registerCallback(ExpandEvent, callback, None, *args) 414 415 else: 416 super(TreeTable, self).addCallback(callback, eventType, *args)
417 418
419 - def removeListener(self, listener, iface=None):
420 """Removes an expand or collapselistener. 421 422 @param listener: 423 the Listener to be removed. 424 """ 425 if (isinstance(listener, ICollapseListener) and 426 (iface is None or issubclass(iface, ICollapseListener))): 427 self.withdrawListener(CollapseEvent, 428 listener, COLLAPSE_METHOD) 429 430 if (isinstance(listener, IExpandListener) and 431 (iface is None or issubclass(iface, IExpandListener))): 432 self.withdrawListener(ExpandEvent, 433 listener, EXPAND_METHOD) 434 435 super(TreeTable, self).removeListener(listener, iface)
436 437
438 - def removeCallback(self, callback, eventType=None):
439 if eventType is None: 440 eventType = callback._eventType 441 442 if issubclass(eventType, CollapseEvent): 443 self.withdrawCallback(CollapseEvent, callback) 444 445 elif issubclass(eventType, ExpandEvent): 446 self.withdrawCallback(ExpandEvent, callback) 447 448 else: 449 super(TreeTable, self).removeCallback(callback, eventType)
450 451
452 - def fireExpandEvent(self, itemId):
453 """Emits an expand event. 454 455 @param itemId: 456 the item id. 457 """ 458 evt = ExpandEvent(self, itemId) 459 self.fireEvent(evt)
460 461
462 - def fireCollapseEvent(self, itemId):
463 """Emits a collapse event. 464 465 @param itemId: 466 the item id. 467 """ 468 evt = CollapseEvent(self, itemId) 469 self.fireEvent(evt)
470 471
472 - def isAnimationsEnabled(self):
473 """@return true if animations are enabled""" 474 return self._animationsEnabled
475 476
477 - def setAnimationsEnabled(self, animationsEnabled):
478 """Animations can be enabled by passing true to this method. Currently 479 expanding rows slide in from the top and collapsing rows slide out the 480 same way. NOTE! not supported in Internet Explorer 6 or 7. 481 482 @param animationsEnabled 483 true or false whether to enable animations or not. 484 """ 485 self._animationsEnabled = animationsEnabled 486 self.requestRepaint()
487 488
489 -class IContainerStrategy(object):
490
491 - def size(self):
492 raise NotImplementedError
493
494 - def __len__(self):
495 return self.size()
496
497 - def isNodeOpen(self, itemId):
498 raise NotImplementedError
499
500 - def getDepth(self, itemId):
501 raise NotImplementedError
502
503 - def toggleChildVisibility(self, itemId):
504 raise NotImplementedError
505
506 - def getIdByIndex(self, index):
507 raise NotImplementedError
508
509 - def indexOfId(self, idd):
510 raise NotImplementedError
511
512 - def nextItemId(self, itemId):
513 raise NotImplementedError
514
515 - def lastItemId(self):
516 raise NotImplementedError
517
518 - def prevItemId(self, itemId):
519 raise NotImplementedError
520
521 - def isLastId(self, itemId):
522 raise NotImplementedError
523
524 - def getItemIds(self):
525 raise NotImplementedError
526
527 - def containerItemSetChange(self, event):
528 raise NotImplementedError
529 530
531 -class AbstractStrategy(IContainerStrategy):
532
533 - def __init__(self, treetable):
534 self._treetable = treetable
535 536
537 - def getDepth(self, itemId):
538 """Consider adding getDepth to L{ICollapsible}, might help 539 scalability with some container implementations. 540 """ 541 depth = 0 542 hierarchicalContainer = self._treetable.getContainerDataSource() 543 while not hierarchicalContainer.isRoot(itemId): 544 depth += 1 545 itemId = hierarchicalContainer.getParent(itemId) 546 return depth
547 548
549 - def containerItemSetChange(self, event):
550 pass
551 552
553 -class CollapsibleStrategy(AbstractStrategy):
554 """This strategy is used if current container implements L{Collapsible}. 555 556 Open-collapsed logic diverted to container, otherwise use default 557 implementations. 558 """ 559
560 - def c(self):
561 return self._treetable.getContainerDataSource()
562 563
564 - def toggleChildVisibility(self, itemId):
565 self.c().setCollapsed(itemId, not self.c().isCollapsed(itemId))
566 567
568 - def isNodeOpen(self, itemId):
569 return not self.c().isCollapsed(itemId)
570 571
572 - def size(self):
573 super(TreeTable, self._treetable).size()
574 575
576 - def getIdByIndex(self, index):
577 super(TreeTable, self._treetable).getIdByIndex(index)
578 579
580 - def indexOfId(self, idd):
581 super(TreeTable, self._treetable).indexOfId(idd)
582 583
584 - def isLastId(self, itemId):
585 # using the default impl 586 super(TreeTable, self._treetable).isLastId(itemId)
587 588
589 - def lastItemId(self):
590 # using the default impl 591 super(TreeTable, self._treetable).lastItemId()
592 593
594 - def nextItemId(self, itemId):
595 super(TreeTable, self._treetable).nextItemId(itemId)
596 597
598 - def prevItemId(self, itemId):
599 super(TreeTable, self._treetable).prevItemId(itemId)
600 601
602 - def getItemIds(self):
603 super(TreeTable, self._treetable).getItemIds()
604 605
606 -class HierarchicalStrategy(AbstractStrategy):
607 """Strategy for Hierarchical but not Collapsible container like 608 L{HierarchicalContainer}. 609 610 Store collapsed/open states internally, fool Table to use preorder when 611 accessing items from container via Ordered/Indexed methods. 612 """ 613
614 - def __init__(self, treetable):
615 self._openItems = set() 616 self._preOrder = None 617 618 super(HierarchicalStrategy, self).__init__(treetable)
619 620
621 - def isNodeOpen(self, itemId):
622 return itemId in self._openItems
623 624
625 - def size(self):
626 return len(self.getPreOrder())
627 628
629 - def getItemIds(self):
630 return list(self.getPreOrder())
631 632
633 - def isLastId(self, itemId):
634 if itemId is None: 635 return False 636 return itemId == self.lastItemId()
637 638
639 - def lastItemId(self):
640 if len(self.getPreOrder()) > 0: 641 return self.getPreOrder().get(len(self.getPreOrder()) - 1) 642 else: 643 return None
644 645
646 - def nextItemId(self, itemId):
647 try: 648 indexOf = self.getPreOrder().index(itemId) 649 except ValueError: 650 return None 651 652 indexOf += 1 653 654 if indexOf == len(self.getPreOrder()): 655 return None 656 else: 657 return self.getPreOrder().get(indexOf)
658 659
660 - def prevItemId(self, itemId):
661 try: 662 indexOf = self.getPreOrder().index(itemId) 663 except ValueError: 664 indexOf = -1 665 indexOf -= 1 666 if indexOf < 0: 667 return None 668 else: 669 return self.getPreOrder().get(indexOf)
670 671
672 - def toggleChildVisibility(self, itemId):
673 if itemId in self._openItems: 674 self._openItems.remove(itemId) 675 removed = True 676 else: 677 removed = False 678 679 if not removed: 680 self._openItems.add(itemId) 681 logger.debug('Item ' + itemId + ' is now expanded') 682 else: 683 logger.debug('Item ' + itemId + ' is now collapsed') 684 685 self.clearPreorderCache()
686 687
688 - def clearPreorderCache(self):
689 self._preOrder = None # clear preorder cache
690 691
692 - def getPreOrder(self):
693 """Preorder of ids currently visible. 694 """ 695 if self._preOrder is None: 696 self._preOrder = list() 697 dataSource = self._treetable.getContainerDataSource() 698 rootItemIds = dataSource.rootItemIds() 699 for idd in rootItemIds: 700 self._preOrder.add(idd) 701 self.addVisibleChildTree(idd) 702 return self._preOrder
703 704
705 - def addVisibleChildTree(self, idd):
706 if self.isNodeOpen(idd): 707 dataSource = self._treetable.getContainerDataSource() 708 children = dataSource.getChildren(idd) 709 if children is not None: 710 for childId in children: 711 self._preOrder.add(childId) 712 self.addVisibleChildTree(childId)
713 714
715 - def indexOfId(self, idd):
716 try: 717 return self.getPreOrder().index(idd) 718 except ValueError: 719 return -1
720 721
722 - def getIdByIndex(self, index):
723 return self.getPreOrder().get(index)
724 725
726 - def containerItemSetChange(self, event):
727 # preorder becomes invalid on sort, item additions etc. 728 self.clearPreorderCache() 729 super(HierarchicalStrategy, self).containerItemSetChange(event)
730