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