Source code for vdat.gui.treeview_model

"""A model to populate a treeview

Widget with custom items.

This was written following this `tutorial
<http://www.hardcoded.net/articles/using_qtreeview_with_qabstractitemmodel>`_

Examples
--------

::

    Simple hierachy

    +- 20150622
        +-- Calibration
            +--- Model-1
            +--- Model-2
            +--- Model-3
    +-- RawScience

>>>  model = ReductionTreeviewModel()
>>>  nights = ReductionNode("Nights", 1, model)
>>>  model.rootnode = nights
>>>  night1 = ReductionNode("20150622", 1, model, parent=nights)
>>>  nights.add_subnode(night1)

>>>  cal    = ReductionNode("Calibration", 1, model, parent=night1)
>>>  night1.add_subnode(cal)

>>>  cal.add_subnode(ReductionNode("Models-1",  1, model, parent=cal))
>>>  cal.add_subnode(ReductionNode("Models-2",  2, model, parent=cal))
>>>  cal.add_subnode(ReductionNode("Models-3",  3, model, parent=cal))

>>>  rawsci = ReductionNode("RawScience", 2, model, parent=night1)
>>>  night1.add_subnode(rawsci)
"""
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import os
import shutil

from PyQt4 import QtCore, QtGui


import vdat.utilities as vutils

from vdat.database import VDATDir
from vdat.database import connect as dbconnect

import vdat.config as vdatconfig
from vdat.gui.fplane import VDATRunControl
from vdat.gui.relay import get_relay


[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 row : int which row the node is on 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 Attributes ---------- name, row, model, parent : as in the parameters column : int column index subnodes : list sub-nodes of the current node """ def __init__(self, name, path, row, model, type_='', selectable=False, checkable=False, parent=None): self.name = name self.path = path self.row = row self.model = model self.type_ = type_ self.selectable = selectable self.checkable = checkable self.checked = False # nothing is checked by default self.parent = parent self.column = 0 self.subnodes = []
[docs] def index(self): """Return the index of the node Returns ------- index : :class:`QModelIndex` instance an index to this node """ return self.model.createIndex(self.row, self.column, self)
[docs] def add_subnode(self, node): """Add a subnode to the current node Parameters ---------- node : :class:`QModelIndex` instance the index of a 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]
[docs]class ReductionTreeviewModel(QtCore.QAbstractItemModel): """A model that stores the tree structure for the treeview widget Parameters ---------- parent : qtobject 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 = {} @property 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
[docs] 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
@property
[docs] 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:`PyQt4.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:`PyQt4.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.) """ 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:`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.) """ 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
[docs] def setData(self, index, value, role): """If the role is :class:`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:`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.) """ 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): 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:`QModelIndex` index to the parent node Returns ------- :class:`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 an index to the parent of the node with index Parameters ---------- index : :class:`QtCore.QModelIndex` instance the index of the node you want the parent of Returns ------- :class:`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 before the given row""" self.beginInsertRows(parent, row, row) parent.internalPointer().add_subnode(node) self.endInsertRows() return True
[docs] def removeRows(self, row, count, parent=QtCore.QModelIndex()): """Remove ``count`` rows starting at ``row``""" self.beginRemoveRows(parent, row, row + count) parent_node = parent.internalPointer() for i in range(count): parent_node.subnodes.pop(row) self.endRemoveRows() return True
[docs]class ReductionQTreeView(QtGui.QTreeView): """Custom tree view widget""" def __init__(self, parent): super(ReductionQTreeView, self).__init__(parent) # 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._pressed) # save the button widget to be able to switch the view self._button_widget = None @property def button_widget(self): """Returns the button widget""" return self._button_widget @button_widget.setter
[docs] def button_widget(self, widget): """set the button widget""" self._button_widget = widget
[docs] def _pressed(self, index): """Action executed when selecting a new """ conf = vdatconfig.get_config('main') node = index.internalPointer() if node: conf.set("redux_dirs", "selected_dir", node.path) # update the central panel signals = get_relay() signals.change_centralPanel.emit(node.type_) # update the button panel self._button_widget.setCurrentWidgetByName(node.type_)
[docs] def option_menu(self, position): """Add an action menu to the tree view Parameters ---------- position : :class:`PyQt4.QtCore.QPoint` instance """ index = self.currentIndex() node = index.internalPointer() menu = QtGui.QMenu(parent=self) with dbconnect(): # check if the selected directory is a clone is_clone = (VDATDir.select() .where((VDATDir.path == node.path) & (VDATDir.is_clone == True))).exists() clone_action = menu.addAction("Clone") if is_clone: remove_action = menu.addAction("Remove") action = menu.exec_(self.viewport().mapToGlobal(position)) if action == clone_action: self._clone_dir(index, node) if is_clone: if action == remove_action: 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:`QModelIndex` instance index of the selected node node : class:`ReductionNode` instance selected node """ parent_path = node.parent.path new_name = self._new_dir_name(node.name, parent_path) if new_name is None: # don't do anything return # insert a new node in the tree view self._insert_row(index, new_name) # copy the data, remove the id and edit the important parts and # reinsert in the database as new entry db_entry = (VDATDir.select() .where(VDATDir.path == node.path)).get() db_entry.id = None db_entry.name = new_name db_entry.make_path() db_entry.is_clone = True db_entry.save(force_insert=True) # copy the directory self._copy_dir(parent_path, node.name, new_name, db_entry)
[docs] def _new_dir_name(self, original_name, parent_path): """Ask the user 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 = (VDATDir.select() .where(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(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 (VDATDir.select() .where((VDATDir.name == new_name) & (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 = QtGui.QInputDialog(parent=self) dialog.setLabelText(label) dialog.setOkButtonText('Clone') dialog.setTextValue(default_text) dialog.setInputMode(QtGui.QInputDialog.TextInput) dialogCode = dialog.exec_() if dialogCode == QtGui.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`` """ 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
[docs] def _insert_row(self, index, new_name): """Clone ``node``, update it and insert it Parameters ---------- index : :class:`QModelIndex` instance index of the selected node new_name : string name of the new entry """ 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), n_rows, model, type_=node.type_, selectable=node.selectable, checkable=node.checkable, parent=parent_node) model.insertRow(new_node, n_rows, parent_index)
[docs] def _remove_dir(self, index, node): """Remove the directory associated to ``node`` and add it to the tree view and to the database Parameters ---------- index : :class:`QModelIndex` instance index of the selected node node : class:`ReductionNode` instance selected node """ # open a dialog asking if you are sure do_remove = self._confirm_remove_dialog(node.name) if not do_remove: return # 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 VDATRunControl.ifu_loop = False shutil.rmtree(node.path) # remove from the database db_entry = VDATDir.get(VDATDir.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 = QtGui.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(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) box.setDefaultButton(QtGui.QMessageBox.Yes) box.setIcon(QtGui.QMessageBox.Information) pressed = box.exec_() return pressed == QtGui.QMessageBox.Yes
[docs]def setup_filebrowser(parent=None): """Setup a treeview widget with which to browse the file structure Parameters ---------- parent : QWidget parent of the tree view Returns ------- :class:`PyQt4.QtGui.QTreeView` instance tree view widget """ filebrowser_treeView = ReductionQTreeView(parent) filebrowser_treeView.setMinimumSize(QtCore.QSize(80, 400)) # Limit the size of this view to stop in taking all the space from # fplane filebrowser_treeView.setMaximumSize(QtCore.QSize(400, 9999)) filebrowser_treeView.setObjectName("filebrowser_treeView") model = create_dirstructure(filebrowser_treeView) # Add it to the panel filebrowser_treeView.setModel(model) filebrowser_treeView.setRootIndex(model.rootnode.index()) filebrowser_treeView.expandAll() # force selecting no directory filebrowser_treeView.setCurrentIndex(filebrowser_treeView.currentIndex()) return filebrowser_treeView
[docs]def create_dirstructure(parent=None): """Create the directory structure in the redux directory Parameters ---------- parent : QWidget parent of the tree view Returns ------- model : :class:`ReductionTreeviewModel` instance """ conf = vdatconfig.get_config('main') redux_dir = os.path.abspath(conf.get("general", 'redux_dir')) # Create the model of the directory structure model = ReductionTreeviewModel(parent=parent) root_node = ReductionNode("Nights", redux_dir, 0, model) model.rootnode = root_node # search the nights nights = set(i.night for i in VDATDir.select(VDATDir.night)) nights = list(nights) nights.sort() for inight, night in enumerate(nights): night_path = os.path.join(redux_dir, night) night_node = ReductionNode(night, night_path, inight, model, parent=root_node) root_node.add_subnode(night_node) # loop through the image types for itype, type_ in enumerate(['zro', 'cal', 'sci']): type_path = os.path.join(night_path, type_) type_node = ReductionNode(type_, type_path, itype, model, parent=night_node) night_node.add_subnode(type_node) is_checkable_type = type_ != 'sci' # find all the shots in the night/type_ # first come the original, then the cloned qshots = (VDATDir.select().where((VDATDir.night == night) & (VDATDir.type_ == type_)) .order_by(VDATDir.is_clone) ) # loop through the shots for ishot, shot in enumerate(qshots): shot_node = ReductionNode(shot.name, shot.path, ishot, model, type_=type_, selectable=True, checkable=is_checkable_type, parent=type_node) type_node.add_subnode(shot_node) return model