Coverage for larch/qtlib/view.py: 0%
174 statements
« prev ^ index » next coverage.py v7.6.0, created at 2024-10-16 21:04 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2024-10-16 21:04 +0000
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3"""This module provides base classes to implement models for
4spectroscopy data.
5"""
6from __future__ import absolute_import, division
7import os
9from silx.gui import qt
10import silx.io
12from .items import (ExperimentItem,
13 GroupItem,
14 FileItem,
15 ScanItem)
17from larch.utils import get_cwd
18from larch.utils.logging import getLogger
19_logger = getLogger('larch.qtlib.view')
22class HorizontalHeaderView(qt.QHeaderView):
24 def __init__(self, parent=None):
25 super(HorizontalHeaderView, self).__init__(qt.Qt.Horizontal, parent)
27 # Some properties
28 self.setStretchLastSection(True)
29 # self.setSectionsMovable(True)
31 # Context menu
32 self.setContextMenuPolicy(qt.Qt.CustomContextMenu)
33 self.customContextMenuRequested.connect(self.showContextMenu)
35 def showContextMenu(self, position):
36 menu = qt.QMenu('Horizontal Header View Menu', self)
38 section = self.logicalIndexAt(position)
40 action = qt.QAction('Add', self, triggered=self.append)
41 menu.addAction(action)
43 action = qt.QAction('Remove', self,
44 triggered=lambda: self.remove(section))
45 menu.addAction(action)
47 menu.exec_(self.mapToGlobal(position))
49 def append(self):
50 pass
52 def remove(self, section):
53 model = self.model()
54 if not model.header[section].removable:
55 _logger.info('The selected column cannot be removed')
56 return
57 model.setHeaderData(section, orientation=qt.Qt.Horizontal, value=None)
58 view = self.parent()
59 view.setItemsDelegates()
62class TreeView(qt.QTreeView):
64 def __init__(self, parent=None):
65 super(TreeView, self).__init__(parent)
67 # Header
68 headerView = HorizontalHeaderView()
69 self.setHeader(headerView)
71 # Context menu
72 self.setContextMenuPolicy(qt.Qt.CustomContextMenu)
73 self.customContextMenuRequested.connect(self.showContextMenu)
75 # Selection mode
76 self.setSelectionMode(qt.QAbstractItemView.ExtendedSelection)
78 def setModel(self, model):
79 super(TreeView, self).setModel(model)
80 self.setItemsDelegates()
82 def setItemsDelegates(self):
83 if self.model() is None:
84 return
86 header = self.model().header
87 for i, _ in enumerate(header):
88 delegate = header.delegate(i)
89 if delegate is not None:
90 self.setItemDelegateForColumn(i, delegate(parent=self))
92 def showContextMenu(self, position):
93 menu = qt.QMenu('Tree View Menu', self)
95 action = qt.QAction(
96 'Add Experiment', self, triggered=self.addExperiment)
97 menu.addAction(action)
99 # Get the index under the cursor.
100 index = self.indexAt(position)
101 item = self.model().itemFromIndex(index)
103 if isinstance(item, ExperimentItem) or isinstance(item, GroupItem):
104 action = qt.QAction('Add Group', self, triggered=self.addGroup)
105 menu.addAction(action)
107 action = qt.QAction('Load Files', self, triggered=self.loadFiles)
108 menu.addAction(action)
110 # If there are selected indexes, they can be removed or checked.
111 if self.selectedIndexes():
112 menu.addSeparator()
113 action = qt.QAction(
114 'Toggle Selected', self, triggered=self.toggleSelected)
115 menu.addAction(action)
116 action = qt.QAction(
117 'Remove Selected', self, triggered=self.removeSelected)
118 menu.addAction(action)
120 if isinstance(item, ScanItem) and index.column() > 0:
121 menu.addSeparator()
122 action = qt.QAction(
123 'Copy Value to Selected', self,
124 triggered=lambda: self.copyValueToSelected(index))
125 menu.addAction(action)
127 action = qt.QAction(
128 'Copy Value to Toggled', self,
129 triggered=lambda: self.copyValueToToggled(index))
130 menu.addAction(action)
132 menu.exec_(self.mapToGlobal(position))
134 def loadFiles(self):
135 paths, _ = qt.QFileDialog.getOpenFileNames(
136 self, 'Select Files to Load', get_cwd(),
137 'Data Files (*.spec *.hdf5);; All Files (*)')
139 if not paths:
140 return
142 parent = self.selectionModel().selectedRows().pop()
143 parentItem = self.model().itemFromIndex(parent)
144 for path in paths:
145 self.addFile(path, parentItem)
147 def addExperiment(self, name=None):
148 rootItem = self.model().rootItem
149 row = rootItem.childCount()
150 if name is None or not name:
151 name = 'Experiment{}'.format(row)
152 item = ExperimentItem(name=name, parentItem=rootItem)
153 self.model().appendRow(item)
154 return item
156 def addGroup(self, name=None, parentItem=None):
157 """Add a generic GroupItem at a given parentItem"""
158 # Add the file to the last added item.
159 if parentItem is None:
160 parent = self.selectionModel().selectedRows().pop()
161 parentItem = self.model().itemFromIndex(parent)
162 row = parentItem.childCount()
164 if name is None or not name:
165 name = 'Group{}'.format(row)
167 item = GroupItem(name, parentItem)
168 self.model().appendRow(item)
170 def addFile(self, path=None, parentItem=None):
171 if path is None:
172 return
174 # Add the file to the last added experiment item.
175 if parentItem is None:
176 parentItem = self.model().rootItem.lastChild()
178 try:
179 data = silx.io.open(path)
180 except OSError as e:
181 _logger.warning(e)
182 return
184 # Create a tree item for the file and add it to the experiment item.
185 name, _ = os.path.splitext(os.path.basename(path))
186 item = FileItem(name, parentItem)
187 self.model().appendRow(item)
189 # Create a tree item for each scan. The parent item is now the
190 # previous file item.
191 # TODO: Make this more "intelligent" by using the command to
192 # set better defaults for x, signal, etc.
193 parentItem = item
194 for scan in data:
195 item = ScanItem(name=scan, parentItem=parentItem, data=data[scan])
196 self.model().appendRow(item)
198 def selectedItems(self):
199 indexes = self.selectionModel().selectedRows()
200 items = [self.model().itemFromIndex(index) for index in indexes]
201 return items
203 def scanItems(self):
204 for index in self.model().visitModel():
205 item = self.model().itemFromIndex(index)
206 if isinstance(item, ScanItem):
207 yield item
209 def toggleSelected(self):
210 for item in self.selectedItems():
211 index = self.model().indexFromItem(item)
212 try:
213 if item.isChecked:
214 self.model().setData(
215 index, qt.Qt.Unchecked, qt.Qt.CheckStateRole)
216 else:
217 self.model().setData(
218 index, qt.Qt.Checked, qt.Qt.CheckStateRole)
219 except AttributeError:
220 pass
222 def removeSelected(self):
223 items = self.selectedItems()
224 parentItems = dict()
225 for item in items:
226 parentItem = item.parentItem
227 remove = True
228 while parentItem is not self.model().rootItem:
229 if parentItem in items:
230 remove = False
231 break
232 parentItem = parentItem.parentItem
233 # If an ancestors is selected for removal, pass the item.
234 if not remove:
235 continue
237 # Get the parent item for the current item.
238 parentItem = item.parentItem
239 if parentItem not in parentItems:
240 parentItems[parentItem] = list()
241 # Create a list with the positions of the children that are
242 # going to be removed.
243 parentItems[parentItem].append(item.childPosition())
245 # Remove the rows from the parent.
246 for parentItem in parentItems:
247 rows = parentItems[parentItem]
248 parent = self.model().indexFromItem(parentItem)
249 for row in reversed(sorted(rows)):
250 self.model().removeRow(row, parent)
252 def copyValueToToggled(self, indexAt):
253 """Copy the value under the cursor to the toggled items."""
254 indexes = self.model().visitModel(columns=True)
255 for index in indexes:
256 item = self.model().itemFromIndex(index)
257 if not item.isChecked or index == indexAt:
258 continue
259 elif index.column() == indexAt.column():
260 value = self.model().data(indexAt)
261 self.model().setData(index, value)
263 def copyValueToSelected(self, indexAt):
264 """Copy the value under the cursor to the selected indexes."""
265 indexes = self.selectionModel().selectedIndexes()
266 for index in indexes:
267 if index == indexAt:
268 continue
269 elif index.column() == indexAt.column():
270 value = self.model().data(indexAt)
271 self.model().setData(index, value)