"""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 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 _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 _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