Source code for vdat.gui.treeview_model

# Virus Data Analysis Tool: a data reduction GUI for HETDEX/VIRUS data
# Copyright (C) 2015, 2016, 2017  "The HETDEX collaboration"
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""Model and Tree view implementations to show and navigate the directory tree
used by VDAT.
The code has being written following `this tutorial
<http://www.hardcoded.net/articles/using_qtreeview_with_qabstractitemmodel>`_.

To represent the following structure
::

    +- 20150622
        +-- cal
            +-- time_stamp_1
            +-- time_stamp_2
        +-- sci
            +-- object_1
            +-- object_2
        +-- zro
            +-- time_stamp_3
            +-- time_stamp_4

we first create a :class:`ReductionTreeviewModel` instance and create a root
node as a :class:`ReductionNode`:

>>>  model = ReductionTreeviewModel()
>>>  nights = ReductionNode("Nights", '/path/to/redux', model)
>>>  model.rootnode = nights

Then we need to create the node for the night:

>>>  night1 = ReductionNode("20150622", '/path/to/redux/20150622',
...                         model, parent=nights)
>>>  nights.add_subnode(night1)

and below it the nodes for the types. E.g. the calibration node would be:

>>>  cal = ReductionNode("cal", '/path/to/redux/20150622/cal',
...                      model, parent=night1)
>>>  night1.add_subnode(cal)

Finally we can add the final directories with, e.g.:

>>>  cal_1 = ReductionNode('time_stamp_1',  '/path/to/redux/20150622/cal',
...                        model, parent=cal, selectable=True, checkable=True,
...                        tooltip='all relevant info go here')
>>>  cal.add_subnode(cal_1)

The model must then been added to the :class:`ReductionQTreeView` using the
:meth:`ReductionQTreeView.setModel` method as done in
:meth:`ReductionQTreeView.set_model`

"""
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import logging
import os
import pprint
import shutil

from qtpy import QtCore, QtGui, QtWidgets
import six

import vdat.utilities as vutils
import vdat.database as vdb
import vdat.config as vdatconfig
from vdat.gui.menus_actions import RemoveExposuresMenu


[docs]class ReductionNode(object): """A class to store nodes in the custom treeview model. Parameters ---------- name : String a label for the node to appear in the treeview path : string path associated with the name model : :class:`ReductionTreeviewModel` instance the model to attach the node to type_ : string type of the shot in the node selectable : bool, optional where the current node is selectable or not checkable : bool, optional where the current node can have a check box associated parent : :class:`ReductionNode` instance, optional parent of the current node tooltip : if not ``None`` converted to a string with :func:`pprint.pformat` Attributes ---------- name, model, parent : as in the parameters column : int column index subnodes : list sub-nodes of the current node """ def __init__(self, name, path, model, type_='', selectable=False, checkable=False, parent=None, tooltip=None): self.name = name self.path = path self.model = model self.type_ = type_ self.selectable = selectable self.checkable = checkable self.checked = False # nothing is checked by default self.parent = parent self.tooltip = self._stringify(tooltip) self.column = 0 self.subnodes = []
[docs] def _stringify(self, value): """Unless ``None`` or a string, convert ``value`` to a string using pprint. Parameters ---------- value : value to stringify Returns ------- string or None ``None`` if ``value == None``, string otherwise """ if value is not None and not isinstance(value, six.string_types): value = pprint.pformat(value) return value
[docs] def index(self): """Return the index of the node Returns ------- index : :class:`PyQt5.QtCore.QModelIndex` instance the index to this node """ if self.parent: row = self.parent.subnodes.index(self) else: row = 0 return self.model.createIndex(row, self.column, self)
[docs] def add_subnode(self, node): """Add a subnode to the current node Parameters ---------- node : :class:`ReductionNode` instance the subnode to add """ self.subnodes.append(node)
[docs] def subnode(self, row): """Get the child at ``row`` Parameters ---------- row : int index of the child Returns ------- :class:`ReductionNode` instance required child """ return self.subnodes[row]
def __str__(self): """Nice string representation of the node type""" template = "{r}, name: {n}, type: {t}" return template.format(r=self.__class__.__name__, n=self.name, t=self.type_)
[docs]class ReductionTreeviewModel(QtCore.QAbstractItemModel): """A model that stores the tree structure for the treeview widget Parameters ---------- parent : :class:`PyQt5.QtWidgets.QWidget` or derivate parent object of the tree view model column_title : string, optional title of the column """ def __init__(self, parent=None, column_title="Reduction Browser"): super(ReductionTreeviewModel, self).__init__(parent=parent) self._rootnode = None self.column_title = column_title # at most one directory per type can be checked at a time # key: type (str); value: checked node instance self._checked = {} @QtCore.Property(ReductionNode) def rootnode(self): """Get the rootnode of this tree Returns ------- node : :class:`ReductionNode` instance the index of the node you want to be root node """ return self._rootnode @rootnode.setter def rootnode(self, node): """Set the rootnode of this tree Parameters ---------- node : :class:`ReductionNode` instance the index of the node you want to be root node """ self._rootnode = node @QtCore.Property(dict) def checked_nodes(self): """Return the checked nodes. :meth:`setData` makes sure that at most one node per type is selected Returns ------- dictionary key: type (str) of the node ('sci', 'cal') value: corresponding node instance """ return self._checked
[docs] def columnCount(self, parentIndex): """Return number of columns, for us this is always 1 Parameters ---------- parentIndex : :class:`PyQt5.QtCore.QModelIndex` index to the parent node Returns ------- int the number of columns under parent: always 1 """ return 1
[docs] def rowCount(self, parentIndex): """Return the number of subnodes under a parent node Parameters ---------- parentIndex : :class:`PyQt5.QtCore.QModelIndex` index to the parent node Returns ------- nrows : int the number of rows under parent """ if parentIndex.isValid(): node = parentIndex.internalPointer() return len(node.subnodes) else: return 1
[docs] def headerData(self, section, orientation, role): """Return information about the header items Parameters ---------- section : int the section for which this is the header orientation : int flag indicating whether the header is horizontal or vertical role : int flag specifying the type of info requested (i.e. a title for the header, or an icon etc.) Returns ------- string """ if role == QtCore.Qt.DisplayRole and section == 0: return self.column_title
[docs] def data(self, index, role): """Return information about the specified node. Parameters ---------- index : :class:`PyQt5.QtCore.QModelIndex` instance the index of the node you want data for role : int flag specifying the type of info requested (i.e. a title or an icon etc.) Returns ------- string or int depending of the input role: * ``PyQt5.QtCore.Qt.DisplayRole``: return the name of the node associated with the index * ``PyQt5.QtCore.Qt.CheckStateRole``: returns ``PyQt5.QtCore.Qt.Checked``: ``PyQt5.QtCore.Qt.Unchecked`` whether the check-box is checked or not * ``PyQt5.QtCore.Qt.ToolTipRole``: if available, returns the string to put in the tooltip """ if index.isValid(): node = index.internalPointer() if role == QtCore.Qt.DisplayRole: return node.name elif role == QtCore.Qt.CheckStateRole: if node.checkable: if node.checked: return QtCore.Qt.Checked else: return QtCore.Qt.Unchecked else: return elif role == QtCore.Qt.ToolTipRole and node.tooltip: return node.tooltip else: return else: return
[docs] def setData(self, index, value, role): """If the role is :class:`PyQt5.QtCore.Qt.CheckStateRole`, change the check status of the ``index``, making sure that at most one element per checkable node type is selected. Parameters ---------- index : :class:`~PyQt5.QtCore.QModelIndex` instance the index of the node you want data for value : value to set (ignored) role : int flag specifying the type of info requested (i.e. a title or an icon etc.) Returns ------- success : Bool ``True`` if the operation worked """ success = False conf = vdatconfig.get_config('main') if index.isValid(): node = index.internalPointer() # get the node if role == QtCore.Qt.CheckStateRole: try: # try to uncheck already checked nodes, if any old_node = self._checked.pop(node.type_) if old_node is not node: # if the old node is not the same of the new one old_node.checked = False # make sure to update the GUI self.dataChanged.emit(old_node.index(), old_node.index()) conf.remove_option("redux_dirs", node.type_ + "_dir") except KeyError: # no previously checked node: do nothing pass # now toggle the current node node.checked = not node.checked if node.checked: # if checked, add the node to the dictionary self._checked[node.type_] = node conf.set("redux_dirs", node.type_ + "_dir", node.path) # update the GUI self.dataChanged.emit(index, index) success = True return success
[docs] def flags(self, index): '''Set the flag for every index according to the selectable/checkable status of the corresponding node Parameters ---------- index : :class:`~PyQt5.QtCore.QModelIndex` instance the index of the node you want data for Returns ------- int flags for the index as defined `here <https://doc.qt.io/qt-5/qt.html#ItemFlag-enum>`_ ''' if index.isValid(): node = index.internalPointer() if node.selectable and node.checkable: return QtCore.Qt.ItemIsUserCheckable |\ QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled elif node.selectable: return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled else: return QtCore.Qt.NoItemFlags
[docs] def index(self, row, column, parentIndex): """Return the index of the node with row, column and parent Implementation from https://www.mail-archive.com/pyqt@riverbankcomputing.com/msg19414.html Parameters ---------- row, column : int index of the row and the column parentIndex : :class:`PyQt5.QtCore.QModelIndex` index to the parent node Returns ------- :class:`PyQt5.QtCore.QModelIndex` index of the node """ if self.hasIndex(row, column, parentIndex): if parentIndex.isValid(): parent = parentIndex.internalPointer() else: parent = self.rootnode return self.createIndex(row, column, parent.subnode(row)) return QtCore.QModelIndex()
[docs] def parent(self, index): """Return the index of the parent of the input Parameters ---------- index : :class:`PyQt5.QtCore.QModelIndex` instance the index you want the parent of Returns ------- :class:`PyQt5.QtCore.QModelIndex` instance the index of the parent (if the node has a parent) """ if index.isValid(): node = index.internalPointer() if node.parent: return node.parent.index() return QtCore.QModelIndex()
[docs] def insertRow(self, node, row, parent=QtCore.QModelIndex()): """Insert ``node`` in a new row after the given row. Parameters ---------- node : class:`ReductionNode` instance node to insert row: int index of the row after which add the ``node`` index : :class:`PyQt5.QtCore.QModelIndex` instance index of the parent Returns ------- bool ``True`` if the insertion succeed, ``False`` otherwise, whatever exception is raised is logged to the main logger """ try: self.beginInsertRows(parent, row, row) parent.internalPointer().add_subnode(node) self.endInsertRows() return True except Exception: log = logging.getLogger('logger') log.exception("The node '%s' could not be added", node) return False
[docs] def removeRows(self, row, count, parent=QtCore.QModelIndex()): """Remove ``count`` rows starting at ``row`` Parameters ---------- row: int index of the first row to remove count: int number of rows to remove index : :class:`PyQt5.QtCore.QModelIndex` instance index of the parent Returns ------- bool ``True`` if the removal succeed, ``False`` otherwise, whatever exception is raised is logged to the main logger """ try: self.beginRemoveRows(parent, row, row + count) parent_node = parent.internalPointer() for i in range(count): parent_node.subnodes.pop(row) self.endRemoveRows() return True except Exception: log = logging.getLogger('logger') log.exception("The node(s) number [%d, %d) could not be removed", row, row+count) return False
[docs]class ReductionQTreeView(QtWidgets.QTreeView): """Custom tree view widget to display the reduction directories .. list-table:: **Custom signals** :header-rows: 1 * - Name - Signature - Description * - :attr:`sig_selectionChanged` - ``str, str`` - emitted when a use select a directory; the parameters are the full path to the directory and its type (e.g. ``sci``, ``cal``, ``zro``) * - :attr:`sig_exposure_removed` - str - emitted when an exposure is removed; the value is the ``expname`` from the database .. list-table:: **Custom slot** :header-rows: 1 * - Name - Signature - Description * - :meth:`set_model` - - create a model from the database and set it * - :meth:`on_press` - :class:`PyQt5.QtCore.QModelIndex` - put the path of the selected node into the config file and emit :attr:`sig_selectionChanged` * - :meth:`option_menu` - :class:`PyQt5.QtCore.QPoint` - create the actions for the right-click menu * - :meth:`clone_slot` - - slot that triggers the cloning of the selected directory * - :meth:`remove_slot` - - slot that triggers the removal of the selected directory .. list-table:: **Connections between custom signals and/or slots** :header-rows: 1 * - Signal - Slot * - :attr:`customContextMenuRequested` - :meth:`option_menu` * - :attr:`pressed` - :meth:`on_press` * - ``triggered`` signal of the "Clone" action - :meth:`clone_slot` * - ``triggered`` signal of the "Remove" action - :meth:`remove_slot` * - :attr:`sig_exposure_removed` - :attr:`vdat.gui.RemoveExposuresMenu.sig_exposure_removed` Parameters ---------- parent : :class:`PyQt5.QtWidgets.QWidget` or derivate parent object of the tree view model """ sig_selectionChanged = QtCore.Signal(str, str) sig_exposure_removed = QtCore.Signal(str) def __init__(self, parent=None): super(ReductionQTreeView, self).__init__(parent) self.setObjectName("filebrowser_treeView") self.set_model() self._first_shown = True # enable and create context menu (right click menu) # inspired by: # https://wiki.python.org/moin/PyQt/Creating%20a%20context%20menu%20for%20a%20tree%20view self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.option_menu) # activated only when the selection changes: it's emitted after # selectionChanged is called and can be used instead of if the # deselected item is of no interest self.pressed.connect(self.on_press)
[docs] @QtCore.Slot() def set_model(self): '''Create the model and set it. This method is a pyqt slot.''' with vdb.connect(): model = self.create_model() # Add it to the panel self.setModel(model) self.setRootIndex(model.rootnode.index()) self.expandAll() # force selecting no directory self.setCurrentIndex(self.currentIndex())
[docs] def create_model(self): """Create the redux directory structure using the information stored in the database Returns ------- model : :class:`ReductionTreeviewModel` instance """ model = ReductionTreeviewModel(parent=self) redux_dir = vdatconfig.get_config('main', section='general')['redux_dir'] root_node = ReductionNode("Nights", redux_dir, model) model.rootnode = root_node # search the nights nights = [i.night for i in vdb.VDATDir.select(vdb.VDATDir.night).distinct()] if not nights: # no entries in the database return model nights.sort() for inight, night in enumerate(nights): night_path = os.path.join(redux_dir, night) night_node = ReductionNode(night, night_path, model, parent=root_node) root_node.add_subnode(night_node) qnight = vdb.VDATDir.select().where(vdb.VDATDir.night == night) types = [i.type_ for i in qnight.select(vdb.VDATDir.type_).distinct()] types.sort() # loop through the image types for itype, type_ in enumerate(types): type_path = os.path.join(night_path, type_) type_node = ReductionNode(type_, type_path, model, parent=night_node) night_node.add_subnode(type_node) is_checkable_type = type_ in ['cal', 'zro'] # find all the shots in the night/type_ # first come the original, then the cloned qshots = (qnight.where(vdb.VDATDir.type_ == type_) .order_by(vdb.VDATDir.is_clone)) # loop through the shots for ishot, shot in enumerate(qshots): tooltip = shot.data_clean if shot.zero_dir: tooltip.update(zero_dir=shot.zero_dir.path) if shot.cal_dir: tooltip.update(cal_dir=shot.cal_dir.path) shot_node = ReductionNode(shot.name, shot.path, model, type_=type_, selectable=True, checkable=is_checkable_type, parent=type_node, tooltip=tooltip) type_node.add_subnode(shot_node) return model
[docs] def keyPressEvent(self, event): """If enter is pressed, trigger the pressed signal with the currently selected index Parameters ---------- event : :class:`PyQt5.QtGui.QKeyEvent` event happened """ if not event.isAutoRepeat() and event.key() == QtCore.Qt.Key_Enter: self.pressed.emit(self.currentIndex()) else: super(ReductionQTreeView, self).keyPressEvent(event)
[docs] @QtCore.Slot(QtCore.QModelIndex) def on_press(self, index): """Add the path of the underlying node and emit the :attr:`sig_selectionChanged` signal Parameters ---------- index : :class:`PyQt5.QtCore.QModelIndex` instance index of the selected directory """ conf = vdatconfig.get_config('main') node = index.internalPointer() if node: conf.set("redux_dirs", "selected_dir", node.path) self.sig_selectionChanged.emit(node.path, node.type_)
[docs] @QtCore.Slot(QtCore.QPoint) def option_menu(self, position): """Add an action menu to the tree view Parameters ---------- position : :class:`PyQt5.QtCore.QPoint` instance position of the mouse Returns ------- menu : :class:`PyQt5.QtWidgets.QMenu` menu created; mostly for testing purposes """ cursor_index = self.indexAt(position) index = self.currentIndex() if not index.isValid() or cursor_index != index: return node = index.internalPointer() with vdb.connect(): # check if the selected directory is a clone is_clone = (vdb.VDATDir.select() .where((vdb.VDATDir.path == node.path) & (vdb.VDATDir.is_clone == True))).exists() menu = QtWidgets.QMenu(parent=self) # create the clone Action clone_action = QtWidgets.QAction(QtGui.QIcon.fromTheme('edit-copy'), 'Clone', menu) clone_action.triggered.connect(self.clone_slot) menu.addAction(clone_action) # create the clone Action remove_action = QtWidgets.QAction(QtGui.QIcon.fromTheme('edit-delete'), 'Remove', menu) remove_action.triggered.connect(self.remove_slot) if not is_clone: remove_action.setEnabled(False) menu.addAction(remove_action) remove_menu = RemoveExposuresMenu(parent=menu) remove_menu.sig_exposure_removed.connect(self.sig_exposure_removed) if is_clone: remove_menu.path = node.path menu.addMenu(remove_menu) menu.exec_(self.viewport().mapToGlobal(position)) return menu
[docs] @QtCore.Slot() def clone_slot(self): '''Slot triggered when the clone action is clicked. ''' index = self.currentIndex() node = index.internalPointer() self._clone_dir(index, node)
[docs] @QtCore.Slot() def remove_slot(self): '''Slot triggered when the remove action is clicked. ''' index = self.currentIndex() node = index.internalPointer() with vdb.connect(): self._remove_dir(index, node)
[docs] def _clone_dir(self, index, node): """Clone the directory associated to ``node`` and add it to the tree view and to the database Parameters ---------- index : :class:`PyQt5.QtCore.QModelIndex` instance index of the selected node node : class:`ReductionNode` instance selected node """ import vdat.gui.utils as gutils parent_path = node.parent.path with vdb.connect(): new_name = self._new_dir_name(node.name, parent_path) if new_name is None: # don't do anything return gutils.run_wait_func_qthread(self._do_clone_dir, index, node, new_name, parent=self)
[docs] def _do_clone_dir(self, index, node, new_name): with vdb.connect(), vdb.database.atomic() as txn: inserted = False try: # copy the data, remove the id and edit the name, path and # ``is_clone`` attributes; then reinsert in the database as new # entry # copy VDATDir vdat_dir = (vdb.VDATDir.select() .where(vdb.VDATDir.path == node.path)).get() vdat_dir.id = None vdat_dir.name = new_name vdat_dir.make_path() vdat_dir.is_clone = True vdat_dir.save(force_insert=True) # copy VDATExposures vdat_exps = (vdb.VDATExposures.select() .where(vdb.VDATExposures.path == node.path)) for ve in vdat_exps: ve.id = None ve.name = new_name ve.path = vdat_dir.path ve.save(force_insert=True) # insert a new node in the tree view inserted = self._insert_row(index, new_name) if not inserted: txn.rollback() return # copy the directory self._copy_dir(node.parent.path, node.name, new_name, vdat_dir) except Exception: if inserted: # remove the inserted row model = self.model() model.removeRow(model.rowCount(index.parent()), index.parent()) txn.rollback() new_dir = os.path.join(node.parent.path, new_name) if os.path.exists(new_dir): shutil.rmtree(new_dir) log = logging.getLogger('logger') log.exception("The cloning of node '%s' has failed", node)
[docs] def _new_dir_name(self, original_name, parent_path): """Ask the user for the new directory name. Parameters ---------- original_name : string name of the directory we are about to copy parent_path : string path of the parent directory of ``original_name`` Returns ------- new_name : string name of the new directory """ db_entry = (vdb.VDATDir.select() .where(vdb.VDATDir.name % (original_name + "*"))) # create the default new name n_entries = db_entry.count() while True: clone_dir_name = "{0}_{1:d}".format(original_name, n_entries) # if the name already exists, increase n_entries by one and retry if db_entry.where(vdb.VDATDir.name == clone_dir_name).exists(): n_entries += 1 else: break # Open a popup window asking for the directory name label_prefix = "" while True: new_name = self._clone_dialog(clone_dir_name, label_prefix=label_prefix) if new_name is None: break elif not new_name: # the text box was empty label_prefix = "Please provide a name" continue # Check that the input name does not exist if (vdb.VDATDir.select() .where((vdb.VDATDir.name == new_name) & (vdb.VDATDir.path % (parent_path + "*"))) .exists()): label_prefix = "{} already exist, please find a new name." label_prefix = label_prefix.format(new_name) else: break return new_name
[docs] def _clone_dialog(self, default_text, label_prefix=""): """Create a text dialog with the default text. Parameters ---------- default_text : string text set by default in the dialog label_prefix : string if not empty added in the line before the standard label Returns ------- string text from the dialog """ label = "Insert the name for the cloned directory" if label_prefix: label = label_prefix + "\n" + label dialog = QtWidgets.QInputDialog(parent=self) dialog.setLabelText(label) dialog.setOkButtonText('Clone') dialog.setTextValue(default_text) dialog.setInputMode(QtWidgets.QInputDialog.TextInput) dialogCode = dialog.exec_() if dialogCode == QtWidgets.QDialog.Accepted: return dialog.textValue() else: return None
[docs] def _copy_dir(self, parent_path, src, dst, db_entry): """Copy the directory ``src`` to ``dst``. Both are children of ``parent_path``. Also set to true the ``is_clone`` entry in the shot file Parameters ---------- parent_path : string path where the original and new directories live src, dst : string copy ``src`` into ``dst`` db_entry : :class:`vdat.database.models.VDATDir` database entry for the new directory """ src = os.path.join(parent_path, src) dst = os.path.join(parent_path, dst) shutil.copytree(src, dst, symlinks=False) # edit the shot file in the dst directory entries = vutils.read_shot_file(dst) append = False for l in entries: l['is_clone'] = True # copy the following entries from the new database entry for k in ['name', 'path']: l[k] = getattr(db_entry, k) vutils.write_to_shot_file(dst, append=append, **l) append = True # edit the exp file in the dst directory entries = vutils.read_exps_file(dst) append = False for l in entries: # copy the following entries from the new database entry for k in ['name', 'path']: l[k] = getattr(db_entry, k) vutils.write_to_exps_file(dst, append=append, **l) append = True
[docs] def _insert_row(self, index, new_name): """Clone ``node``, update it and insert it Parameters ---------- index : :class:`PyQt5.QtCore.QModelIndex` instance index of the selected node new_name : string name of the new entry Returns ------- bool whether the insertion is successful or not """ node = index.internalPointer() # get the index and the node of the parent parent_index = index.parent() parent_node = parent_index.internalPointer() # get the model model = self.model() # get the number of rows of the parent n_rows = model.rowCount(parent_index) # create the new node and append it new_node = ReductionNode(new_name, os.path.join(os.path.dirname(node.path), new_name), model, type_=node.type_, selectable=node.selectable, checkable=node.checkable, parent=parent_node) return model.insertRow(new_node, n_rows, parent_index)
[docs] def _remove_dir(self, index, node): """Remove the directory associated to ``node`` and remove it from the tree view and from the database. Parameters ---------- index : :class:`PyQt5.QtCore.QModelIndex` instance index of the selected node node : class:`ReductionNode` instance selected node """ import vdat.gui.utils as gutils # open a dialog asking if you are sure do_remove = self._confirm_remove_dialog(node.name) if not do_remove: return with gutils.wait_cursor(): # remove from the gui model = self.model() model.removeRow(index.row(), index.parent()) # remove from the file system # stop making the thumbnails to avoid crashes when removing the # directory # TODO: find a way to make sure that no more pngs are being created # TODO: does the thumbnail creation uses this? shutil.rmtree(node.path) # remove from the database db_entry = vdb.VDATDir.get(vdb.VDATDir.path == node.path) db_entry.delete_instance() for db_entry in (vdb.VDATExposures.select() .where(vdb.VDATExposures.path == node.path)): db_entry.delete_instance()
[docs] def _confirm_remove_dialog(self, dir_name): """Create the dialog to ask if you are sure Parameters ---------- dir_name : string name of the directory Returns ------- bool where the directory can be removed """ box = QtWidgets.QMessageBox(parent=self) box.setText("The directory '{}' will be removed. The removal is not" " reversible".format(dir_name)) box.setInformativeText("Do you want to proceed?") box.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) box.setDefaultButton(QtWidgets.QMessageBox.Yes) box.setIcon(QtWidgets.QMessageBox.Information) pressed = box.exec_() return pressed == QtWidgets.QMessageBox.Yes
[docs] def showEvent(self, event): '''When the :class:`ReductionQTreeView` becomes visible, this method is triggered by Qt. Parameters ---------- event : :class:`PyQt5.QtGui.QShowEvent` the tab is show ''' super(ReductionQTreeView, self).showEvent(event) if self._first_shown and self.verticalScrollBar().isVisible(): self.collapseAll() self._first_shown = False