1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4# Copyright (C) 2005-2007 Carabos Coop. V. All rights reserved 5# Copyright (C) 2008-2019 Vicent Mas. All rights reserved 6# 7# This program is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program. If not, see <http://www.gnu.org/licenses/>. 19# 20# Author: Vicent Mas - vmas@vitables.org 21 22""" 23This module defines a view for the tree of databases model. 24 25This view is used to display the tree of open databases. Each top level 26node of the tree contains the object tree of a `PyTables`/`HDF5` database. 27""" 28 29__docformat__ = 'restructuredtext' 30 31from qtpy import QtCore 32from qtpy import QtGui 33from qtpy import QtWidgets 34 35from vitables.h5db.nodeitemdelegate import NodeItemDelegate 36 37 38translate = QtWidgets.QApplication.translate 39 40 41class DBsTreeView(QtWidgets.QTreeView): 42 """ 43 A view for the databases tree model. 44 45 :Parameters: 46 - `vtapp`: the VTAPP instance 47 - `model`: the model for this view 48 """ 49 50 51 dbsTreeViewCreated = QtCore.Signal(QtWidgets.QTreeView) 52 53 def __init__(self, vtapp, vtgui, model, parent=None): 54 """Create the view. 55 """ 56 57 super(DBsTreeView, self).__init__(parent) 58 59 # The model 60 self.setModel(model) 61 self.dbt_model = model 62 self.smodel = self.selectionModel() 63 64 self.vtapp = vtapp 65 self.vtgui = vtgui 66 67 # The custom delegate used for editing items 68 self.setItemDelegate(NodeItemDelegate(vtgui, self)) 69 self.setObjectName('dbs_tree_view') 70 71 # The frame specification 72 self.frame_style = {'shape': self.frameShape(), 73 'shadow': self.frameShadow(), 74 'lwidth': self.lineWidth(), 75 'foreground': self.palette().color(QtGui.QPalette.Active, 76 QtGui.QPalette.WindowText)} 77 78 # Setup drag and drop 79 self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) 80 self.setDragEnabled(True) 81 self.setAcceptDrops(True) 82 self.setDropIndicatorShown(True) 83 84 # Misc. setup 85 self.setRootIsDecorated(True) 86 self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) 87 # Whether selections are done in terms of single items, rows or columns 88 self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectItems) 89 # Whether the user can select one or many items 90 # Changed from SingleSelection to ExtendedSelection in commit 403a4c3 91 # but I don't know why. I revert it to SingleSelection or a 92 # tables.ClosedNodeError is randomly raised when a group is moved to a 93 # different file 94 self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) 95 self.setWhatsThis(translate('DBsTreeView', 96 """<qt> 97 <h3>The Tree of databases</h3> 98 For every open database this widget shows the object tree, 99 a graphical representation<br>of the data hierarchy stored 100 in the database.</qt>""", 101 'WhatsThis help for the tree pane')) 102 103 # Connect signals to slots 104 self.customContextMenuRequested.connect(self.createCustomContextMenu) 105 self.activated.connect(self.activateNode) 106 self.expanded.connect(self.updateExpandedGroup) 107 self.collapsed.connect(self.updateCollapsedGroup) 108 self.dbt_model.layoutChanged.connect(self.updateColumnWidth) 109 110 111 def updateColumnWidth(self): 112 """Make sure that a horizontal scrollbar is shown as needed. 113 114 This is a subtle method. As the tree view has only 1 column its 115 width and the width of the viewport are always the same so the 116 horizontal scrollbar is never shown. As the contents width 117 changes every time the layout changes (rows are inserted or 118 deleted) by resizing column to contents when it happens we 119 ensure that the column and the viewport will have different 120 width and the scrollbar will indeed be added as needed. 121 """ 122 self.resizeColumnToContents(0) 123 124 125 def activateNode(self, index): 126 """Expand an item via `Enter`/`Return` key or mouse double click. 127 128 When the user activates a collapsed item (by pressing `Enter`, `Return` 129 or by double clicking the item) then it is expanded. If the user 130 activates the node by double clicking on it while the `Shift` key is 131 pressed, the item is edited (if editing is enabled). 132 133 Lazy population of the model is partially implemented in this 134 method. Expanded items are updated so that children items are added if 135 needed. This fact improves enormously the performance when files 136 whit a large number of nodes are opened. 137 138 This method is a slot connected to the activated(QModelIndex) signal 139 in the ctor. 140 141 :Parameter index: the index of the activated item 142 """ 143 144 modifiers = QtWidgets.QApplication.keyboardModifiers() 145 if (modifiers & QtCore.Qt.ShiftModifier) or \ 146 (modifiers & QtCore.Qt.ControlModifier): 147 return 148 node = self.dbt_model.nodeFromIndex(index) 149 if node.node_kind.count('group'): 150 if not self.isExpanded(index): 151 self.expand(index) 152 elif node.has_view: 153 ## Activate already-open window. 154 # 155 wrkspc = self.vtgui.workspace 156 pcurrent = QtCore.QPersistentModelIndex(index) 157 for window in wrkspc .subWindowList(): 158 if pcurrent == window.pindex: 159 wrkspc.setActiveSubWindow(window) 160 else: 161 self.vtapp.nodeOpen(index) 162 163 164 def updateCollapsedGroup(self, index): 165 """After collapsing a group update its icon. 166 167 This method is a slot connected to the `collapsed(QModelIndex)` signal 168 in the ctor. 169 170 :Parameter index: the index of the collapsed group 171 """ 172 173 node = self.dbt_model.nodeFromIndex(index) 174 if node.node_kind == 'group': 175 self.dbt_model.setData(index, node.closed_folder, 176 QtCore.Qt.DecorationRole) 177 self.smodel.clearSelection() 178 self.smodel.setCurrentIndex(index, 179 QtCore.QItemSelectionModel.SelectCurrent) 180 self.update(index) 181 182 183 def updateExpandedGroup(self, index): 184 """After a group expansion, update the icon and the displayed children. 185 186 Lazy population of the model is partially implemented in this 187 method. Expanded items are updated so that children items are added if 188 needed. This fact reduces enormously the opening times for files 189 whit a large number of nodes and also saves memory. 190 191 This method is a slot connected to the `expanded(QModelIndex)` signal 192 in the ctor. 193 194 :Parameter index: the index of the expanded item 195 """ 196 197 node = self.dbt_model.nodeFromIndex(index) 198 node_kind = node.node_kind 199 if node_kind == 'group': 200 self.dbt_model.setData(index, node.open_folder, 201 QtCore.Qt.DecorationRole) 202 if node_kind in ['group', 'root group']: 203 if not node.updated: 204 self.dbt_model.lazyAddChildren(index) 205 node.updated = True 206 self.smodel.clearSelection() 207 self.smodel.setCurrentIndex(index, 208 QtCore.QItemSelectionModel.SelectCurrent) 209 210 211 def createCustomContextMenu(self, pos): 212 """ 213 A context menu for the tree of databases view. 214 215 When an item of the tree view is right clicked, a context popup 216 menu is displayed. The content of the popup depends on the 217 clicked element. 218 219 :Parameter pos: the local position at which the menu will popup 220 """ 221 222 index = self.indexAt(pos) 223 if not index.isValid(): 224 kind = 'view' 225 else: 226 node = self.dbt_model.nodeFromIndex(index) 227 kind = node.node_kind 228 pos = self.mapToGlobal(pos) 229 self.vtgui.popupContextMenu(kind, pos) 230 231 232 def selectNode(self, index): 233 """Select the given index. 234 235 :Parameter `index`: the model index being selected 236 """ 237 238 self.smodel.clearSelection() 239 self.smodel.setCurrentIndex(index, 240 QtCore.QItemSelectionModel.SelectCurrent) 241 242 243 def mouseDoubleClickEvent(self, event): 244 """Specialised handler for mouse double click events. 245 246 When a node is double clicked in the tree of databases pane: 247 248 - if the node can be renamed and the `Shift` key is pressed then 249 rename the node 250 - if the node is a leaf with no view and the `Shift` key is not pressed 251 then open the node 252 - if the node is a collpased group and the `Shift` key is not pressed 253 then expand the group 254 255 :Parameter event: the event being processed 256 257 """ 258 259 modifier = event.modifiers() 260 current = self.currentIndex() 261 if modifier == QtCore.Qt.ShiftModifier: 262 if current.flags() & QtCore.Qt.ItemIsEditable: 263 self.edit(current) 264 else: 265 self.activateNode(current) 266 267 268 def dragMoveEvent(self, event): 269 """ 270 Event handler for `QDragMoveEvent` events. 271 272 Dragging icons from the Desktop/Files Manager into the tree view is 273 supported. 274 275 :Parameter event: the event being processed. 276 """ 277 278 # The dragged object (one or more files) has a MIME type (namely 279 # text/uri-list) and some associated data (the list of URLs of the 280 # dragged files). All that information can be accessed via the event 281 # MimeData() method. 282 # The widget should examine that information and accept the drop if 283 # appropriate. 284 mime_data = event.mimeData() 285 if mime_data.hasFormat('text/uri-list'): 286 event.setDropAction(QtCore.Qt.CopyAction) 287 event.acceptProposedAction() 288 else: 289 return QtWidgets.QTreeView.dragMoveEvent(self, event) 290 291 292 def dropEvent(self, event): 293 """ 294 Event handler for `QDropEvent` events. 295 296 This event is sent when a drag and drop action is completed. In our case 297 if an icon is dropped on the tree view then the icon URL is converted to 298 a path (which we assume to be an `HDF5` file path) and ``ViTables`` 299 tries to open it. 300 301 :Parameter event: the event being processed. 302 """ 303 304 # The dropped object (one or more files) has a MIME type (namely 305 # text/uri-list) and some associated data (the list of URLs of the 306 # dropped files). All that information can be accessed via the event 307 # MimeData() method. 308 # The widget should examine that information and launch the required 309 # actions. 310 mime_data = event.mimeData() 311 if mime_data.hasFormat('text/uri-list'): 312 if self.dbt_model.dropMimeData( 313 mime_data, QtCore.Qt.CopyAction, -1, -1, self.currentIndex()): 314 event.setDropAction(QtCore.Qt.CopyAction) 315 event.accept() 316 self.setFocus(True) 317 else: 318 QtWidgets.QTreeView.dropEvent(self, event) 319 320 321 def focusInEvent(self, event): 322 """Specialised handler for focus events. 323 324 Repaint differently the databases tree view frame when it gets the 325 keyboard focus so that users can realize easily about this focus 326 change. 327 328 :Parameter event: the event being processed 329 """ 330 331 self.setLineWidth(2) 332 self.setFrameStyle(QtWidgets.QFrame.Panel|QtWidgets.QFrame.Plain) 333 pal = self.palette() 334 pal.setColor(QtGui.QPalette.Active, QtGui.QPalette.WindowText, 335 QtCore.Qt.darkBlue) 336 QtWidgets.QTreeView.focusInEvent(self, event) 337 338 339 def focusOutEvent(self, event): 340 """Specialised handler for focus events. 341 342 Repaint differently the databases tree view frame when it looses the 343 keyboard focus so that users can realize easily about this focus 344 change. 345 346 :Parameter event: the event being processed 347 """ 348 349 self.setLineWidth(self.frame_style['lwidth']) 350 self.setFrameShape(self.frame_style['shape']) 351 self.setFrameShadow(self.frame_style['shadow']) 352 pal = self.palette() 353 pal.setColor(QtGui.QPalette.Active, QtGui.QPalette.WindowText, 354 self.frame_style['foreground']) 355 QtWidgets.QTreeView.focusOutEvent(self, event) 356 357 358if __name__ == '__main__': 359 import sys 360 APP = QtWidgets.QApplication(sys.argv) 361 TREE = DBsTreeView() 362 TREE.show() 363 APP.exec_() 364