1#!/usr/bin/env python3 2 3#****************************************************************************** 4# treeview.py, provides a class for the indented tree view 5# 6# TreeLine, an information storage program 7# Copyright (C) 2018, Douglas W. Bell 8# 9# This is free software; you can redistribute it and/or modify it under the 10# terms of the GNU General Public License, either Version 2 or any later 11# version. This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY. See the included LICENSE file for details. 13#****************************************************************************** 14 15import re 16import unicodedata 17from PyQt5.QtCore import QEvent, QPoint, QPointF, Qt, pyqtSignal 18from PyQt5.QtGui import (QContextMenuEvent, QKeySequence, QMouseEvent, 19 QTextDocument) 20from PyQt5.QtWidgets import (QAbstractItemView, QApplication, QHeaderView, 21 QLabel, QListWidget, QListWidgetItem, QMenu, 22 QStyledItemDelegate, QTreeView) 23import treeselection 24import treenode 25import miscdialogs 26import globalref 27 28 29class TreeView(QTreeView): 30 """Class override for the indented tree view. 31 32 Sets view defaults and links with document for content. 33 """ 34 skippedMouseSelect = pyqtSignal(treenode.TreeNode) 35 shortcutEntered = pyqtSignal(QKeySequence) 36 def __init__(self, model, allActions, parent=None): 37 """Initialize the tree view. 38 39 Arguments: 40 model -- the initial model for view data 41 allActions -- a dictionary of control actions for popup menus 42 parent -- the parent main window 43 """ 44 super().__init__(parent) 45 self.resetModel(model) 46 self.allActions = allActions 47 self.incremSearchMode = False 48 self.incremSearchString = '' 49 self.noMouseSelectMode = False 50 self.mouseFocusNoEditMode = False 51 self.prevSelSpot = None # temp, to check for edit at mouse release 52 self.setSelectionMode(QAbstractItemView.ExtendedSelection) 53 self.header().setSectionResizeMode(0, QHeaderView.ResizeToContents) 54 self.header().setStretchLastSection(False) 55 self.setHeaderHidden(True) 56 self.setItemDelegate(TreeEditDelegate(self)) 57 # use mouse event for editing to avoid with multiple select 58 self.setEditTriggers(QAbstractItemView.NoEditTriggers) 59 self.updateTreeGenOptions() 60 self.setDragDropMode(QAbstractItemView.DragDrop) 61 self.setDefaultDropAction(Qt.MoveAction) 62 self.setDropIndicatorShown(True) 63 self.setUniformRowHeights(True) 64 65 def resetModel(self, model): 66 """Change the model assigned to this view. 67 68 Also assigns a new selection model. 69 Arguments: 70 model -- the new model to assign 71 """ 72 self.setModel(model) 73 self.setSelectionModel(treeselection.TreeSelection(model, self)) 74 75 def updateTreeGenOptions(self): 76 """Set the tree to match the current general options. 77 """ 78 dragAvail = globalref.genOptions['DragTree'] 79 self.setDragEnabled(dragAvail) 80 self.setAcceptDrops(dragAvail) 81 self.setIndentation(globalref.genOptions['IndentOffset'] * 82 self.fontInfo().pixelSize()) 83 84 def isSpotExpanded(self, spot): 85 """Return True if the given spot is expanded (showing children). 86 87 Arguments: 88 spot -- the spot to check 89 """ 90 return self.isExpanded(spot.index(self.model())) 91 92 def expandSpot(self, spot): 93 """Expand a spot in this view. 94 95 Arguments: 96 spot -- the spot to expand 97 """ 98 self.expand(spot.index(self.model())) 99 100 def collapseSpot(self, spot): 101 """Collapse a spot in this view. 102 103 Arguments: 104 spot -- the spot to collapse 105 """ 106 self.collapse(spot.index(self.model())) 107 108 def expandBranch(self, parentSpot): 109 """Expand all spots in the given branch. 110 111 Collapses parentSpot first to avoid extreme slowness. 112 Arguments: 113 parentSpot -- the top spot in the branch 114 """ 115 self.collapse(parentSpot.index(self.model())) 116 for spot in parentSpot.spotDescendantOnlyGen(): 117 if spot.nodeRef.childList: 118 self.expand(spot.index(self.model())) 119 self.expand(parentSpot.index(self.model())) 120 121 def collapseBranch(self, parentSpot): 122 """Collapse all spots in the given branch. 123 124 Arguments: 125 parentSpot -- the top spot in the branch 126 """ 127 for spot in parentSpot.spotDescendantGen(): 128 if spot.nodeRef.childList: 129 self.collapse(spot.index(self.model())) 130 131 def savedExpandState(self, spots): 132 """Return a list of tuples of spots and expanded state (True/False). 133 134 Arguments: 135 spots -- an iterable of spots to save 136 """ 137 return [(spot, self.isSpotExpanded(spot)) for spot in spots] 138 139 def restoreExpandState(self, expandState): 140 """Expand or collapse based on saved tuples. 141 142 Arguments: 143 expandState -- a list of tuples of spots and expanded state 144 """ 145 for spot, expanded in expandState: 146 try: 147 if expanded: 148 self.expandSpot(spot) 149 else: 150 self.collapseSpot(spot) 151 except ValueError: 152 pass 153 154 def spotAtTop(self): 155 """If view is scrolled, return the spot at the top of the view. 156 157 If not scrolled, return None. 158 """ 159 if self.verticalScrollBar().value() > 0: 160 return self.indexAt(QPoint(0, 0)).internalPointer() 161 return None 162 163 def scrollToSpot(self, spot): 164 """Scroll the view to move the spot to the top position. 165 166 Arguments: 167 spot -- the spot to move to the top 168 """ 169 self.scrollTo(spot.index(self.model()), 170 QAbstractItemView.PositionAtTop) 171 172 def scrollTo(self, index, hint=QAbstractItemView.EnsureVisible): 173 """Scroll the view to make node at index visible. 174 175 Overriden to stop autoScroll from horizontally jumping when selecting 176 nodes. 177 Arguments: 178 index -- the node to be made visible 179 hint -- where the visible item should be 180 """ 181 horizPos = self.horizontalScrollBar().value() 182 super().scrollTo(index, hint) 183 self.horizontalScrollBar().setValue(horizPos) 184 185 def endEditing(self): 186 """Stop the editing of any item being renamed. 187 """ 188 delegate = self.itemDelegate() 189 if delegate.editor: 190 delegate.commitData.emit(delegate.editor) 191 self.closePersistentEditor(self.selectionModel().currentIndex()) 192 193 def incremSearchStart(self): 194 """Start an incremental title search. 195 """ 196 self.incremSearchMode = True 197 self.incremSearchString = '' 198 globalref.mainControl.currentStatusBar().showMessage(_('Search for:')) 199 200 def incremSearchRun(self): 201 """Perform an incremental title search. 202 """ 203 msg = _('Search for: {0}').format(self.incremSearchString) 204 globalref.mainControl.currentStatusBar().showMessage(msg) 205 if (self.incremSearchString and not 206 self.selectionModel().selectTitleMatch(self.incremSearchString, 207 True, True)): 208 msg = _('Search for: {0} (not found)').format(self. 209 incremSearchString) 210 globalref.mainControl.currentStatusBar().showMessage(msg) 211 212 def incremSearchNext(self): 213 """Go to the next match in an incremental title search. 214 """ 215 if self.incremSearchString: 216 if self.selectionModel().selectTitleMatch(self.incremSearchString): 217 msg = _('Next: {0}').format(self.incremSearchString) 218 else: 219 msg = _('Next: {0} (not found)').format(self. 220 incremSearchString) 221 globalref.mainControl.currentStatusBar().showMessage(msg) 222 223 def incremSearchPrev(self): 224 """Go to the previous match in an incremental title search. 225 """ 226 if self.incremSearchString: 227 if self.selectionModel().selectTitleMatch(self.incremSearchString, 228 False): 229 msg = _('Next: {0}').format(self.incremSearchString) 230 else: 231 msg = _('Next: {0} (not found)').format(self. 232 incremSearchString) 233 globalref.mainControl.currentStatusBar().showMessage(msg) 234 235 def incremSearchStop(self): 236 """End an incremental title search. 237 """ 238 self.incremSearchMode = False 239 self.incremSearchString = '' 240 globalref.mainControl.currentStatusBar().clearMessage() 241 242 def showTypeMenu(self, menu): 243 """Show a popup menu for setting the item type. 244 """ 245 index = self.selectionModel().currentIndex() 246 self.scrollTo(index) 247 rect = self.visualRect(index) 248 pt = self.mapToGlobal(QPoint(rect.center().x(), rect.bottom())) 249 menu.popup(pt) 250 251 def contextMenu(self): 252 """Return the context menu, creating it if necessary. 253 """ 254 menu = QMenu(self) 255 menu.addAction(self.allActions['EditCut']) 256 menu.addAction(self.allActions['EditCopy']) 257 menu.addAction(self.allActions['EditPaste']) 258 menu.addAction(self.allActions['NodeRename']) 259 menu.addSeparator() 260 menu.addAction(self.allActions['NodeInsertBefore']) 261 menu.addAction(self.allActions['NodeInsertAfter']) 262 menu.addAction(self.allActions['NodeAddChild']) 263 menu.addSeparator() 264 menu.addAction(self.allActions['NodeDelete']) 265 menu.addAction(self.allActions['NodeIndent']) 266 menu.addAction(self.allActions['NodeUnindent']) 267 menu.addSeparator() 268 menu.addAction(self.allActions['NodeMoveUp']) 269 menu.addAction(self.allActions['NodeMoveDown']) 270 menu.addSeparator() 271 menu.addMenu(self.allActions['DataNodeType'].parent()) 272 menu.addSeparator() 273 menu.addAction(self.allActions['ViewExpandBranch']) 274 menu.addAction(self.allActions['ViewCollapseBranch']) 275 return menu 276 277 def contextMenuEvent(self, event): 278 """Show popup context menu on mouse click or menu key. 279 280 Arguments: 281 event -- the context menu event 282 """ 283 if event.reason() == QContextMenuEvent.Mouse: 284 clickedSpot = self.indexAt(event.pos()).internalPointer() 285 if not clickedSpot: 286 event.ignore() 287 return 288 if clickedSpot not in self.selectionModel().selectedSpots(): 289 self.selectionModel().selectSpots([clickedSpot]) 290 pos = event.globalPos() 291 else: # shown for menu key or other reason 292 selectList = self.selectionModel().selectedSpots() 293 if not selectList: 294 event.ignore() 295 return 296 currentSpot = self.selectionModel().currentSpot() 297 if currentSpot in selectList: 298 selectList.insert(0, currentSpot) 299 position = None 300 for spot in selectList: 301 rect = self.visualRect(spot.index(self.model())) 302 pt = QPoint(rect.center().x(), rect.bottom()) 303 if self.rect().contains(pt): 304 position = pt 305 break 306 if not position: 307 self.scrollTo(selectList[0].index(self.model())) 308 rect = self.visualRect(selectList[0].index(self.model())) 309 position = QPoint(rect.center().x(), rect.bottom()) 310 pos = self.mapToGlobal(position) 311 self.contextMenu().popup(pos) 312 event.accept() 313 314 def dropEvent(self, event): 315 """Event handler for view drop actions. 316 317 Selects parent node at destination. 318 Arguments: 319 event -- the drop event 320 """ 321 clickedSpot = self.indexAt(event.pos()).internalPointer() 322 # clear selection to avoid invalid multiple selection bug 323 self.selectionModel().selectSpots([], False) 324 if clickedSpot: 325 super().dropEvent(event) 326 self.selectionModel().selectSpots([clickedSpot], False) 327 self.scheduleDelayedItemsLayout() # reqd before expand 328 self.expandSpot(clickedSpot) 329 else: 330 super().dropEvent(event) 331 self.selectionModel().selectSpots([]) 332 self.scheduleDelayedItemsLayout() 333 if event.isAccepted(): 334 self.model().treeModified.emit(True, True) 335 336 def toggleNoMouseSelectMode(self, active=True): 337 """Set noMouseSelectMode to active or inactive. 338 339 noMouseSelectMode will not change selection on mouse click, 340 it will just signal the clicked node for use in links, etc. 341 Arguments: 342 active -- if True, activate noMouseSelectMode 343 """ 344 self.noMouseSelectMode = active 345 346 def clearHover(self): 347 """Post a mouse move event to clear the mouse hover indication. 348 349 Needed to avoid crash when deleting nodes with hovered child nodes. 350 """ 351 event = QMouseEvent(QEvent.MouseMove, 352 QPointF(0.0, self.viewport().width()), 353 Qt.NoButton, Qt.NoButton, Qt.NoModifier) 354 QApplication.postEvent(self.viewport(), event) 355 QApplication.processEvents() 356 357 def mousePressEvent(self, event): 358 """Skip unselecting click if in noMouseSelectMode. 359 360 If in noMouseSelectMode, signal which node is under the mouse. 361 Arguments: 362 event -- the mouse click event 363 """ 364 if self.incremSearchMode: 365 self.incremSearchStop() 366 self.prevSelSpot = None 367 clickedIndex = self.indexAt(event.pos()) 368 clickedSpot = clickedIndex.internalPointer() 369 selectModel = self.selectionModel() 370 if self.noMouseSelectMode: 371 if clickedSpot and event.button() == Qt.LeftButton: 372 self.skippedMouseSelect.emit(clickedSpot.nodeRef) 373 event.ignore() 374 return 375 if (event.button() == Qt.LeftButton and 376 not self.mouseFocusNoEditMode and 377 selectModel.selectedCount() == 1 and 378 selectModel.currentSpot() == selectModel.selectedSpots()[0] and 379 event.pos().x() > self.visualRect(clickedIndex).left() and 380 globalref.genOptions['ClickRename']): 381 # set for edit if single select and not an expand/collapse click 382 self.prevSelSpot = selectModel.selectedSpots()[0] 383 self.mouseFocusNoEditMode = False 384 super().mousePressEvent(event) 385 386 def mouseReleaseEvent(self, event): 387 """Initiate editing if clicking on a single selected node. 388 389 Arguments: 390 event -- the mouse click event 391 """ 392 clickedIndex = self.indexAt(event.pos()) 393 clickedSpot = clickedIndex.internalPointer() 394 if (event.button() == Qt.LeftButton and 395 self.prevSelSpot and clickedSpot == self.prevSelSpot): 396 self.edit(clickedIndex) 397 event.ignore() 398 return 399 self.prevSelSpot = None 400 super().mouseReleaseEvent(event) 401 402 def keyPressEvent(self, event): 403 """Record characters if in incremental search mode. 404 405 Arguments: 406 event -- the key event 407 """ 408 if self.incremSearchMode: 409 if event.key() in (Qt.Key_Return, Qt.Key_Enter, Qt.Key_Escape): 410 self.incremSearchStop() 411 elif event.key() == Qt.Key_Backspace and self.incremSearchString: 412 self.incremSearchString = self.incremSearchString[:-1] 413 self.incremSearchRun() 414 elif event.text() and unicodedata.category(event.text()) != 'Cc': 415 # unicode category excludes control characters 416 self.incremSearchString += event.text() 417 self.incremSearchRun() 418 event.accept() 419 elif (event.key() in (Qt.Key_Return, Qt.Key_Enter) and 420 not self.itemDelegate().editor): 421 # enter key selects current item if not selected 422 selectModel = self.selectionModel() 423 if selectModel.currentSpot() not in selectModel.selectedSpots(): 424 selectModel.selectSpots([selectModel.currentSpot()]) 425 event.accept() 426 else: 427 super().keyPressEvent(event) 428 else: 429 super().keyPressEvent(event) 430 431 def focusInEvent(self, event): 432 """Avoid editing a tree item with a get-focus click. 433 434 Arguments: 435 event -- the focus in event 436 """ 437 if event.reason() == Qt.MouseFocusReason: 438 self.mouseFocusNoEditMode = True 439 super().focusInEvent(event) 440 441 def focusOutEvent(self, event): 442 """Stop incremental search on focus loss. 443 444 Arguments: 445 event -- the focus out event 446 """ 447 if self.incremSearchMode: 448 self.incremSearchStop() 449 super().focusOutEvent(event) 450 451 452class TreeEditDelegate(QStyledItemDelegate): 453 """Class override for editing tree items to capture shortcut keys. 454 """ 455 def __init__(self, parent=None): 456 """Initialize the delegate class. 457 458 Arguments: 459 parent -- the parent view 460 """ 461 super().__init__(parent) 462 self.editor = None 463 464 def createEditor(self, parent, styleOption, modelIndex): 465 """Return a new text editor for an item. 466 467 Arguments: 468 parent -- the parent widget for the editor 469 styleOption -- the data for styles and geometry 470 modelIndex -- the index of the item to be edited 471 """ 472 self.editor = super().createEditor(parent, styleOption, modelIndex) 473 return self.editor 474 475 def destroyEditor(self, editor, index): 476 """Reset editor storage after editing ends. 477 478 Arguments: 479 editor -- the editor that is ending 480 index -- the index of the edited item 481 """ 482 self.editor = None 483 super().destroyEditor(editor, index) 484 485 def eventFilter(self, editor, event): 486 """Override to handle shortcut control keys. 487 488 Arguments: 489 editor -- the editor that Qt installed a filter on 490 event -- the key press event 491 """ 492 if (event.type() == QEvent.KeyPress and 493 event.modifiers() == Qt.ControlModifier and 494 Qt.Key_A <= event.key() <= Qt.Key_Z): 495 key = QKeySequence(event.modifiers() | event.key()) 496 self.parent().shortcutEntered.emit(key) 497 return True 498 return super().eventFilter(editor, event) 499 500 501class TreeFilterViewItem(QListWidgetItem): 502 """Item container for the flat list of filtered nodes. 503 """ 504 def __init__(self, spot, viewParent=None): 505 """Initialize the list view item. 506 507 Arguments: 508 spot -- the spot to reference for content 509 viewParent -- the parent list view 510 """ 511 super().__init__(viewParent) 512 self.spot = spot 513 self.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEditable | 514 Qt.ItemIsEnabled) 515 self.update() 516 517 def update(self): 518 """Update title and icon from the stored node. 519 """ 520 node = self.spot.nodeRef 521 self.setText(node.title()) 522 if globalref.genOptions['ShowTreeIcons']: 523 self.setIcon(globalref.treeIcons.getIcon(node.formatRef.iconName, 524 True)) 525 526 527class TreeFilterView(QListWidget): 528 """View to show flat list of filtered nodes. 529 """ 530 skippedMouseSelect = pyqtSignal(treenode.TreeNode) 531 shortcutEntered = pyqtSignal(QKeySequence) 532 def __init__(self, treeViewRef, allActions, parent=None): 533 """Initialize the list view. 534 535 Arguments: 536 treeViewRef -- a ref to the tree view for data 537 allActions -- a dictionary of control actions for popup menus 538 parent -- the parent main window 539 """ 540 super().__init__(parent) 541 self.structure = treeViewRef.model().treeStructure 542 self.selectionModel = treeViewRef.selectionModel() 543 self.treeModel = treeViewRef.model() 544 self.allActions = allActions 545 self.menu = None 546 self.noMouseSelectMode = False 547 self.mouseFocusNoEditMode = False 548 self.prevSelSpot = None # temp, to check for edit at mouse release 549 self.drivingSelectionChange = False 550 self.conditionalFilter = None 551 self.messageLabel = None 552 self.filterWhat = miscdialogs.FindScope.fullData 553 self.filterHow = miscdialogs.FindType.keyWords 554 self.filterStr = '' 555 self.setSelectionMode(QAbstractItemView.ExtendedSelection) 556 self.setItemDelegate(TreeEditDelegate(self)) 557 # use mouse event for editing to avoid with multiple select 558 self.setEditTriggers(QAbstractItemView.NoEditTriggers) 559 self.itemSelectionChanged.connect(self.updateSelectionModel) 560 self.itemChanged.connect(self.changeTitle) 561 treeFont = QTextDocument().defaultFont() 562 treeFontName = globalref.miscOptions['TreeFont'] 563 if treeFontName: 564 treeFont.fromString(treeFontName) 565 self.setFont(treeFont) 566 567 def updateItem(self, node): 568 """Update the item corresponding to the given node. 569 570 Arguments: 571 node -- the node to be updated 572 """ 573 for row in range(self.count()): 574 if self.item(row).spot.nodeRef == node: 575 self.blockSignals(True) 576 self.item(row).update() 577 self.blockSignals(False) 578 return 579 580 def updateContents(self): 581 """Update filtered contents from current structure and filter criteria. 582 """ 583 if self.conditionalFilter: 584 self.conditionalUpdate() 585 return 586 QApplication.setOverrideCursor(Qt.WaitCursor) 587 if self.filterHow == miscdialogs.FindType.regExp: 588 criteria = [re.compile(self.filterStr)] 589 useRegExpFilter = True 590 elif self.filterHow == miscdialogs.FindType.fullWords: 591 criteria = [] 592 for word in self.filterStr.lower().split(): 593 criteria.append(re.compile(r'(?i)\b{}\b'. 594 format(re.escape(word)))) 595 useRegExpFilter = True 596 elif self.filterHow == miscdialogs.FindType.keyWords: 597 criteria = self.filterStr.lower().split() 598 useRegExpFilter = False 599 else: # full phrase 600 criteria = [self.filterStr.lower().strip()] 601 useRegExpFilter = False 602 titlesOnly = self.filterWhat == miscdialogs.FindScope.titlesOnly 603 self.blockSignals(True) 604 self.clear() 605 if useRegExpFilter: 606 for rootSpot in self.structure.rootSpots(): 607 for spot in rootSpot.spotDescendantGen(): 608 if spot.nodeRef.regExpSearch(criteria, titlesOnly): 609 item = TreeFilterViewItem(spot, self) 610 else: 611 for rootSpot in self.structure.rootSpots(): 612 for spot in rootSpot.spotDescendantGen(): 613 if spot.nodeRef.wordSearch(criteria, titlesOnly): 614 item = TreeFilterViewItem(spot, self) 615 self.blockSignals(False) 616 self.selectItems(self.selectionModel.selectedSpots(), True) 617 if self.count() and not self.selectedItems(): 618 self.item(0).setSelected(True) 619 if not self.messageLabel: 620 self.messageLabel = QLabel() 621 globalref.mainControl.currentStatusBar().addWidget(self. 622 messageLabel) 623 message = _('Filtering by "{0}", found {1} nodes').format(self. 624 filterStr, 625 self.count()) 626 self.messageLabel.setText(message) 627 QApplication.restoreOverrideCursor() 628 629 def conditionalUpdate(self): 630 """Update filtered contents from structure and conditional criteria. 631 """ 632 QApplication.setOverrideCursor(Qt.WaitCursor) 633 self.blockSignals(True) 634 self.clear() 635 for rootSpot in self.structure.rootSpots(): 636 for spot in rootSpot.spotDescendantGen(): 637 if self.conditionalFilter.evaluate(spot.nodeRef): 638 item = TreeFilterViewItem(spot, self) 639 self.blockSignals(False) 640 self.selectItems(self.selectionModel.selectedSpots(), True) 641 if self.count() and not self.selectedItems(): 642 self.item(0).setSelected(True) 643 if not self.messageLabel: 644 self.messageLabel = QLabel() 645 globalref.mainControl.currentStatusBar().addWidget(self. 646 messageLabel) 647 message = _('Conditional filtering, found {0} nodes').format(self. 648 count()) 649 self.messageLabel.setText(message) 650 QApplication.restoreOverrideCursor() 651 652 def selectItems(self, spots, signalModel=False): 653 """Select items matching given nodes if in filtered view. 654 655 Arguments: 656 spots -- the spot list to select 657 signalModel -- signal to update the tree selection model if True 658 """ 659 selectSpots = set(spots) 660 if not signalModel: 661 self.blockSignals(True) 662 for item in self.selectedItems(): 663 item.setSelected(False) 664 for row in range(self.count()): 665 if self.item(row).spot in selectSpots: 666 self.item(row).setSelected(True) 667 self.setCurrentItem(self.item(row)) 668 self.blockSignals(False) 669 670 def updateFromSelectionModel(self): 671 """Select items selected in the tree selection model. 672 673 Called from a signal that the tree selection model is changing. 674 """ 675 if self.count() and not self.drivingSelectionChange: 676 self.selectItems(self.selectionModel.selectedSpots()) 677 678 def updateSelectionModel(self): 679 """Change the selection model based on a filter list selection signal. 680 """ 681 self.drivingSelectionChange = True 682 self.selectionModel.selectSpots([item.spot for item in 683 self.selectedItems()]) 684 self.drivingSelectionChange = False 685 686 def changeTitle(self, item): 687 """Update the node title in the model based on an edit signal. 688 689 Reset to the node text if invalid. 690 Arguments: 691 item -- the filter view item that changed 692 """ 693 if not self.treeModel.setData(item.spot.index(self.treeModel), 694 item.text()): 695 self.blockSignals(True) 696 item.setText(item.node.title()) 697 self.blockSignals(False) 698 699 def nextPrevSpot(self, spot, forward=True): 700 """Return the next or previous spot in this filter list view. 701 702 Wraps around ends. Return None if view doesn't have spot. 703 Arguments: 704 spot -- the starting spot 705 forward -- next if True, previous if False 706 """ 707 for row in range(self.count()): 708 if self.item(row).spot == spot: 709 if forward: 710 row += 1 711 if row >= self.count(): 712 row = 0 713 else: 714 row -= 1 715 if row < 0: 716 row = self.count() - 1 717 return self.item(row).spot 718 return None 719 720 def contextMenu(self): 721 """Return the context menu, creating it if necessary. 722 """ 723 if not self.menu: 724 self.menu = QMenu(self) 725 self.menu.addAction(self.allActions['EditCut']) 726 self.menu.addAction(self.allActions['EditCopy']) 727 self.menu.addAction(self.allActions['NodeRename']) 728 self.menu.addSeparator() 729 self.menu.addAction(self.allActions['NodeDelete']) 730 self.menu.addSeparator() 731 self.menu.addMenu(self.allActions['DataNodeType'].parent()) 732 return self.menu 733 734 def contextMenuEvent(self, event): 735 """Show popup context menu on mouse click or menu key. 736 737 Arguments: 738 event -- the context menu event 739 """ 740 if event.reason() == QContextMenuEvent.Mouse: 741 clickedItem = self.itemAt(event.pos()) 742 if not clickedItem: 743 event.ignore() 744 return 745 if clickedItem.spot not in self.selectionModel.selectedSpots(): 746 self.selectionModel.selectSpots([clickedItem.spot]) 747 pos = event.globalPos() 748 else: # shown for menu key or other reason 749 selectList = self.selectedItems() 750 if not selectList: 751 event.ignore() 752 return 753 currentItem = self.currentItem() 754 if currentItem in selectList: 755 selectList.insert(0, currentItem) 756 posList = [] 757 for item in selectList: 758 rect = self.visualItemRect(item) 759 pt = QPoint(rect.center().x(), rect.bottom()) 760 if self.rect().contains(pt): 761 posList.append(pt) 762 if not posList: 763 self.scrollTo(self.indexFromItem(selectList[0])) 764 rect = self.visualItemRect(selectList[0]) 765 posList = [QPoint(rect.center().x(), rect.bottom())] 766 pos = self.mapToGlobal(posList[0]) 767 self.contextMenu().popup(pos) 768 event.accept() 769 770 def toggleNoMouseSelectMode(self, active=True): 771 """Set noMouseSelectMode to active or inactive. 772 773 noMouseSelectMode will not change selection on mouse click, 774 it will just signal the clicked node for use in links, etc. 775 Arguments: 776 active -- if True, activate noMouseSelectMode 777 """ 778 self.noMouseSelectMode = active 779 780 def mousePressEvent(self, event): 781 """Skip unselecting click on blank spaces. 782 783 Arguments: 784 event -- the mouse click event 785 """ 786 self.prevSelSpot = None 787 clickedItem = self.itemAt(event.pos()) 788 if not clickedItem: 789 event.ignore() 790 return 791 if self.noMouseSelectMode: 792 if event.button() == Qt.LeftButton: 793 self.skippedMouseSelect.emit(clickedItem.spot.nodeRef) 794 event.ignore() 795 return 796 if (event.button() == Qt.LeftButton and 797 not self.mouseFocusNoEditMode and 798 self.selectionModel.selectedCount() == 1 and 799 globalref.genOptions['ClickRename']): 800 self.prevSelSpot = self.selectionModel.selectedSpots()[0] 801 self.mouseFocusNoEditMode = False 802 super().mousePressEvent(event) 803 804 def mouseReleaseEvent(self, event): 805 """Initiate editing if clicking on a single selected node. 806 807 Arguments: 808 event -- the mouse click event 809 """ 810 clickedItem = self.itemAt(event.pos()) 811 if (event.button() == Qt.LeftButton and clickedItem and 812 self.prevSelSpot and clickedItem.spot == self.prevSelSpot): 813 self.editItem(clickedItem) 814 event.ignore() 815 return 816 self.prevSelSpot = None 817 super().mouseReleaseEvent(event) 818 819 def focusInEvent(self, event): 820 """Avoid editing a tree item with a get-focus click. 821 822 Arguments: 823 event -- the focus in event 824 """ 825 if event.reason() == Qt.MouseFocusReason: 826 self.mouseFocusNoEditMode = True 827 super().focusInEvent(event) 828