]> sigrok.org Git - sigrok-meter.git/blame - mainwindow.py
Allow changing of the recording time.
[sigrok-meter.git] / mainwindow.py
CommitLineData
48723bbb
JS
1##
2## This file is part of the sigrok-meter project.
3##
4## Copyright (C) 2013 Uwe Hermann <uwe@hermann-uwe.de>
5## Copyright (C) 2014 Jens Steinhauser <jens.steinhauser@gmail.com>
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 2 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, write to the Free Software
19## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
20##
21
2e8c2e6e 22import acquisition
48723bbb 23import datamodel
68348e5a 24import icons
f76b9df8 25import multiplotwidget
48723bbb
JS
26import os.path
27import qtcompat
2abf5a93 28import settings
48723bbb 29import textwrap
f76b9df8
JS
30import time
31import util
48723bbb
JS
32
33QtCore = qtcompat.QtCore
34QtGui = qtcompat.QtGui
f76b9df8 35pyqtgraph = qtcompat.pyqtgraph
48723bbb
JS
36
37class EmptyMessageListView(QtGui.QListView):
38 '''List view that shows a message if the model im empty.'''
39
40 def __init__(self, message, parent=None):
41 super(self.__class__, self).__init__(parent)
42
43 self._message = message
44
45 def paintEvent(self, event):
46 m = self.model()
47 if m and m.rowCount():
48 super(self.__class__, self).paintEvent(event)
49 return
50
51 painter = QtGui.QPainter(self.viewport())
52 painter.drawText(self.rect(), QtCore.Qt.AlignCenter, self._message)
53
54class MainWindow(QtGui.QMainWindow):
55 '''The main window of the application.'''
56
f76b9df8
JS
57 # Update interval of the plots in milliseconds.
58 UPDATEINTERVAL = 100
59
48723bbb
JS
60 def __init__(self, context, drivers):
61 super(self.__class__, self).__init__()
62
2e8c2e6e
JS
63 # Used to coordinate the stopping of the acquisition and
64 # the closing of the window.
65 self._closing = False
66
48723bbb 67 self.context = context
2e8c2e6e 68 self.drivers = drivers
48723bbb
JS
69
70 self.delegate = datamodel.MultimeterDelegate(self, self.font())
71 self.model = datamodel.MeasurementDataModel(self)
48723bbb 72
68348e5a
JS
73 # Maps from 'unit' to the corresponding plot.
74 self._plots = {}
75 # Maps from '(plot, device)' to the corresponding curve.
76 self._curves = {}
77
78 self._setup_ui()
79
80 self._plot_update_timer = QtCore.QTimer()
81 self._plot_update_timer.setInterval(MainWindow.UPDATEINTERVAL)
82 self._plot_update_timer.timeout.connect(self._updatePlots)
48723bbb 83
ac584e86
JS
84 settings.graph.backlog.changed.connect(self.on_setting_graph_backlog_changed)
85
2e8c2e6e
JS
86 QtCore.QTimer.singleShot(0, self._start_acquisition)
87
88 def _start_acquisition(self):
89 self.acquisition = acquisition.Acquisition(self.context)
90 self.acquisition.measured.connect(self.model.update)
91 self.acquisition.stopped.connect(self._stopped)
92
93 try:
94 for (ds, cs) in self.drivers:
95 self.acquisition.add_device(ds, cs)
96 except Exception as e:
97 QtGui.QMessageBox.critical(self, 'Error', str(e))
98 self.close()
99 return
100
68348e5a 101 self.start_stop_acquisition()
48723bbb 102
68348e5a 103 def _setup_ui(self):
48723bbb 104 self.setWindowTitle('sigrok-meter')
480cdb7b 105 # Resizing the listView below will increase this again.
0e810ddf 106 self.resize(350, 10)
48723bbb 107
68348e5a 108 self.setWindowIcon(QtGui.QIcon(':/logo.png'))
48723bbb 109
68348e5a
JS
110 self._setup_graphPage()
111 self._setup_addDevicePage()
112 self._setup_logPage()
113 self._setup_preferencesPage()
48723bbb 114
68348e5a
JS
115 self._pages = [
116 self.graphPage,
117 self.addDevicePage,
118 self.logPage,
119 self.preferencesPage
120 ]
48723bbb 121
68348e5a
JS
122 self.stackedWidget = QtGui.QStackedWidget(self)
123 for page in self._pages:
124 self.stackedWidget.addWidget(page)
48723bbb 125
68348e5a 126 self._setup_sidebar()
f76b9df8 127
68348e5a
JS
128 self.setCentralWidget(QtGui.QWidget())
129 self.centralWidget().setContentsMargins(0, 0, 0, 0)
f76b9df8 130
68348e5a
JS
131 layout = QtGui.QHBoxLayout(self.centralWidget())
132 layout.addWidget(self.sideBar)
133 layout.addWidget(self.stackedWidget)
134 layout.setSpacing(0)
135 layout.setContentsMargins(0, 0, 0, 0)
136
2abf5a93
JS
137 self.resize(settings.mainwindow.size.value())
138 if settings.mainwindow.pos.value():
139 self.move(settings.mainwindow.pos.value())
68348e5a
JS
140
141 def _setup_sidebar(self):
142 self.sideBar = QtGui.QToolBar(self)
143 self.sideBar.setOrientation(QtCore.Qt.Vertical)
144
145 actionGraph = self.sideBar.addAction('Instantaneous Values and Graphs')
146 actionGraph.setCheckable(True)
147 actionGraph.setIcon(icons.graph)
148 actionGraph.triggered.connect(self.showGraphPage)
149
150 #actionAdd = self.sideBar.addAction('Add Device')
151 #actionAdd.setCheckable(True)
152 #actionAdd.setIcon(icons.add)
153 #actionAdd.triggered.connect(self.showAddDevicePage)
154
155 #actionLog = self.sideBar.addAction('Logs')
156 #actionLog.setCheckable(True)
157 #actionLog.setIcon(icons.log)
158 #actionLog.triggered.connect(self.showLogPage)
159
ac584e86
JS
160 actionPreferences = self.sideBar.addAction('Preferences')
161 actionPreferences.setCheckable(True)
162 actionPreferences.setIcon(icons.preferences)
163 actionPreferences.triggered.connect(self.showPreferencesPage)
68348e5a
JS
164
165 # make the buttons at the top exclusive
166 self.actionGroup = QtGui.QActionGroup(self)
167 self.actionGroup.addAction(actionGraph)
168 #self.actionGroup.addAction(actionAdd)
169 #self.actionGroup.addAction(actionLog)
ac584e86 170 self.actionGroup.addAction(actionPreferences)
68348e5a
JS
171
172 # show graph at startup
173 actionGraph.setChecked(True)
174
175 # fill space between buttons on the top and on the bottom
176 fill = QtGui.QWidget(self)
177 fill.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
178 self.sideBar.addWidget(fill)
179
180 self.actionStartStop = self.sideBar.addAction('Start Acquisition')
181 self.actionStartStop.setIcon(icons.start)
182 self.actionStartStop.triggered.connect(self.start_stop_acquisition)
183
184 actionAbout = self.sideBar.addAction('About')
185 actionAbout.setIcon(icons.about)
186 actionAbout.triggered.connect(self.show_about)
f76b9df8 187
68348e5a
JS
188 actionQuit = self.sideBar.addAction('Quit')
189 actionQuit.setIcon(icons.exit)
190 actionQuit.triggered.connect(self.close)
191
192 s = self.style().pixelMetric(QtGui.QStyle.PM_LargeIconSize)
193 self.sideBar.setIconSize(QtCore.QSize(s, s))
194
195 self.sideBar.setStyleSheet('''
196 QToolBar {
197 background-color: white;
198 margin: 0px;
199 border: 0px;
200 border-right: 1px solid black;
201 }
202
203 QToolButton {
204 padding: 10px;
205 border: 0px;
206 border-right: 1px solid black;
207 }
208
209 QToolButton:checked,
210 QToolButton[checkable="false"]:hover {
211 background-color: #c0d0e8;
212 }
213 ''')
214
215 def _setup_graphPage(self):
216 listView = EmptyMessageListView('waiting for data...')
217 listView.setFrameShape(QtGui.QFrame.NoFrame)
218 listView.viewport().setBackgroundRole(QtGui.QPalette.Window)
219 listView.viewport().setAutoFillBackground(True)
220 listView.setMinimumWidth(260)
221 listView.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
222 listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
223 listView.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel)
224 listView.setItemDelegate(self.delegate)
225 listView.setModel(self.model)
226 listView.setUniformItemSizes(True)
227 listView.setMinimumSize(self.delegate.sizeHint())
f76b9df8 228
68348e5a
JS
229 self.plotwidget = multiplotwidget.MultiPlotWidget(self)
230 self.plotwidget.plotHidden.connect(self._on_plotHidden)
f76b9df8 231
68348e5a
JS
232 self.graphPage = QtGui.QSplitter(QtCore.Qt.Horizontal, self)
233 self.graphPage.addWidget(listView)
234 self.graphPage.addWidget(self.plotwidget)
235 self.graphPage.setStretchFactor(0, 0)
236 self.graphPage.setStretchFactor(1, 1)
237
238 def _setup_addDevicePage(self):
239 self.addDevicePage = QtGui.QWidget(self)
240 layout = QtGui.QVBoxLayout(self.addDevicePage)
241 label = QtGui.QLabel('add device page')
242 layout.addWidget(label)
243
244 def _setup_logPage(self):
245 self.logPage = QtGui.QWidget(self)
246 layout = QtGui.QVBoxLayout(self.logPage)
247 label = QtGui.QLabel('log page')
248 layout.addWidget(label)
249
250 def _setup_preferencesPage(self):
251 self.preferencesPage = QtGui.QWidget(self)
ac584e86
JS
252 layout = QtGui.QGridLayout(self.preferencesPage)
253
254 layout.addWidget(QtGui.QLabel('<b>Graph</b>'), 0, 0)
255 layout.addWidget(QtGui.QLabel('Recording time (seconds):'), 1, 0)
256
257 spin = QtGui.QSpinBox(self)
258 spin.setMinimum(10)
259 spin.setMaximum(3600)
260 spin.setSingleStep(10)
261 spin.setValue(settings.graph.backlog.value())
262 spin.valueChanged[int].connect(settings.graph.backlog.setValue)
263 layout.addWidget(spin, 1, 1)
264
265 layout.setRowStretch(layout.rowCount(), 100)
68348e5a
JS
266
267 def showPage(self, page):
268 self.stackedWidget.setCurrentIndex(self._pages.index(page))
269
270 @QtCore.Slot(bool)
271 def showGraphPage(self):
272 self.showPage(self.graphPage)
273
274 @QtCore.Slot(bool)
275 def showAddDevicePage(self):
276 self.showPage(self.addDevicePage)
277
278 @QtCore.Slot(bool)
279 def showLogPage(self):
280 self.showPage(self.logPage)
281
282 @QtCore.Slot(bool)
283 def showPreferencesPage(self):
284 self.showPage(self.preferencesPage)
2e8c2e6e 285
ac584e86
JS
286 @QtCore.Slot(int)
287 def on_setting_graph_backlog_changed(self, bl):
288 for unit in self._plots:
289 plot = self._plots[unit]
290
291 # Remove the limits first, otherwise the range update would
292 # be ignored.
293 plot.view.setLimits(xMin=None, xMax=None)
294
295 # Now change the range, and then use the calculated limits
296 # (also see the comment in '_getPlot()').
297 plot.view.setXRange(-bl, 0, update=True)
298 r = plot.view.viewRange()
299 plot.view.setLimits(xMin=r[0][0], xMax=r[0][1])
300
f76b9df8
JS
301 def _getPlot(self, unit):
302 '''Looks up or creates a new plot for 'unit'.'''
303
304 if unit in self._plots:
305 return self._plots[unit]
306
307 # create a new plot for the unit
308 plot = self.plotwidget.addPlot()
309 plot.yaxis.setLabel(util.quantity_from_unit(unit), units=util.format_unit(unit))
ac584e86 310 plot.view.setXRange(-settings.graph.backlog.value(), 0, update=False)
f76b9df8
JS
311 plot.view.setYRange(-1, 1)
312 plot.view.enableAutoRange(axis=pyqtgraph.ViewBox.YAxis)
313 # lock to the range calculated by the view using additional padding,
314 # looks nicer this way
315 r = plot.view.viewRange()
316 plot.view.setLimits(xMin=r[0][0], xMax=r[0][1])
317
318 self._plots[unit] = plot
319 return plot
320
321 def _getCurve(self, plot, deviceID):
322 '''Looks up or creates a new curve for '(plot, deviceID)'.'''
323
68348e5a 324 key = (plot, deviceID)
f76b9df8
JS
325 if key in self._curves:
326 return self._curves[key]
327
328 # create a new curve
3010b5a0
JS
329 curve = pyqtgraph.PlotDataItem(
330 antialias=True,
331 symbolPen=pyqtgraph.mkPen(QtGui.QColor(QtCore.Qt.black)),
332 symbolBrush=pyqtgraph.mkBrush(QtGui.QColor(QtCore.Qt.black)),
333 symbolSize=1
334 )
f76b9df8
JS
335 plot.view.addItem(curve)
336
337 self._curves[key] = curve
338 return curve
339
d0aa45b4
JS
340 def _updatePlots(self):
341 '''Updates all plots.'''
f76b9df8 342
d0aa45b4
JS
343 # loop over all devices and channels
344 for row in range(self.model.rowCount()):
345 idx = self.model.index(row, 0)
346 deviceID = self.model.data(idx,
347 datamodel.MeasurementDataModel.idRole)
1879265a 348 deviceID = tuple(deviceID) # PySide returns a list.
d0aa45b4
JS
349 traces = self.model.data(idx,
350 datamodel.MeasurementDataModel.tracesRole)
351
352 for unit, trace in traces.items():
353 now = time.time()
354
355 # remove old samples
ac584e86 356 l = now - settings.graph.backlog.value()
d0aa45b4
JS
357 while trace.samples and trace.samples[0][0] < l:
358 trace.samples.pop(0)
359
360 plot = self._getPlot(unit)
361 if not plot.visible:
362 if trace.new:
363 self.plotwidget.showPlot(plot)
364
365 if plot.visible:
366 xdata = [s[0] - now for s in trace.samples]
367 ydata = [s[1] for s in trace.samples]
368
369 color = self.model.data(idx,
370 datamodel.MeasurementDataModel.colorRole)
371
372 curve = self._getCurve(plot, deviceID)
373 curve.setPen(pyqtgraph.mkPen(color=color))
374 curve.setData(xdata, ydata)
375
376 @QtCore.Slot(multiplotwidget.Plot)
377 def _on_plotHidden(self, plot):
378 plotunit = [u for u, p in self._plots.items() if p == plot][0]
379
380 # Mark all traces of all devices/channels with the same unit as the
381 # plot as "old" ('trace.new = False'). As soon as a new sample arrives
382 # on one trace, the plot will be shown again
383 for row in range(self.model.rowCount()):
384 idx = self.model.index(row, 0)
385 traces = self.model.data(idx, datamodel.MeasurementDataModel.tracesRole)
f76b9df8 386
d0aa45b4
JS
387 for traceunit, trace in traces.items():
388 if traceunit == plotunit:
389 trace.new = False
48723bbb 390
2e8c2e6e
JS
391 @QtCore.Slot()
392 def _stopped(self):
393 if self._closing:
68348e5a
JS
394 # The acquisition was stopped by the 'closeEvent()', close the
395 # window again now that the acquisition has stopped.
2e8c2e6e
JS
396 self.close()
397
48723bbb 398 def closeEvent(self, event):
2e8c2e6e 399 if self.acquisition.is_running():
68348e5a 400 # Stop the acquisition before closing the window.
2e8c2e6e 401 self._closing = True
68348e5a 402 self.start_stop_acquisition()
2e8c2e6e
JS
403 event.ignore()
404 else:
2abf5a93
JS
405 settings.mainwindow.size.setValue(self.size())
406 settings.mainwindow.pos.setValue(self.pos())
2e8c2e6e 407 event.accept()
48723bbb 408
68348e5a
JS
409 @QtCore.Slot()
410 def start_stop_acquisition(self):
411 if self.acquisition.is_running():
412 self.acquisition.stop()
413 self._plot_update_timer.stop()
414 self.actionStartStop.setText('Start Acquisition')
415 self.actionStartStop.setIcon(icons.start)
416 else:
417 # before starting (again), remove all old samples and old curves
418 self.model.clear_samples()
419
420 for key in self._curves:
421 plot, _ = key
422 curve = self._curves[key]
423 plot.view.removeItem(curve)
424 self._curves = {}
425
426 self.acquisition.start()
427 self._plot_update_timer.start()
428 self.actionStartStop.setText('Stop Acquisition')
429 self.actionStartStop.setIcon(icons.stop)
430
48723bbb
JS
431 @QtCore.Slot()
432 def show_about(self):
433 text = textwrap.dedent('''\
434 <div align="center">
480cdb7b
UH
435 <b>sigrok-meter 0.1.0</b><br/><br/>
436 Using libsigrok {} (lib version {}).<br/><br/>
48723bbb
JS
437 <a href='http://www.sigrok.org'>
438 http://www.sigrok.org</a><br/>
439 <br/>
480cdb7b
UH
440 License: GNU GPL, version 3 or later<br/>
441 <br/>
48723bbb
JS
442 This program comes with ABSOLUTELY NO WARRANTY;<br/>
443 for details visit
444 <a href='http://www.gnu.org/licenses/gpl.html'>
68348e5a
JS
445 http://www.gnu.org/licenses/gpl.html</a><br/>
446 <br/>
447 Some icons by <a href='https://www.gnome.org'>
448 the GNOME project</a>
48723bbb
JS
449 </div>
450 '''.format(self.context.package_version, self.context.lib_version))
451
452 QtGui.QMessageBox.about(self, 'About sigrok-meter', text)