]> sigrok.org Git - sigrok-meter.git/blob - mainwindow.py
Allow changing of the recording time.
[sigrok-meter.git] / mainwindow.py
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
22 import acquisition
23 import datamodel
24 import icons
25 import multiplotwidget
26 import os.path
27 import qtcompat
28 import settings
29 import textwrap
30 import time
31 import util
32
33 QtCore = qtcompat.QtCore
34 QtGui = qtcompat.QtGui
35 pyqtgraph = qtcompat.pyqtgraph
36
37 class 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
54 class MainWindow(QtGui.QMainWindow):
55     '''The main window of the application.'''
56
57     # Update interval of the plots in milliseconds.
58     UPDATEINTERVAL = 100
59
60     def __init__(self, context, drivers):
61         super(self.__class__, self).__init__()
62
63         # Used to coordinate the stopping of the acquisition and
64         # the closing of the window.
65         self._closing = False
66
67         self.context = context
68         self.drivers = drivers
69
70         self.delegate = datamodel.MultimeterDelegate(self, self.font())
71         self.model = datamodel.MeasurementDataModel(self)
72
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)
83
84         settings.graph.backlog.changed.connect(self.on_setting_graph_backlog_changed)
85
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
101         self.start_stop_acquisition()
102
103     def _setup_ui(self):
104         self.setWindowTitle('sigrok-meter')
105         # Resizing the listView below will increase this again.
106         self.resize(350, 10)
107
108         self.setWindowIcon(QtGui.QIcon(':/logo.png'))
109
110         self._setup_graphPage()
111         self._setup_addDevicePage()
112         self._setup_logPage()
113         self._setup_preferencesPage()
114
115         self._pages = [
116             self.graphPage,
117             self.addDevicePage,
118             self.logPage,
119             self.preferencesPage
120         ]
121
122         self.stackedWidget = QtGui.QStackedWidget(self)
123         for page in self._pages:
124             self.stackedWidget.addWidget(page)
125
126         self._setup_sidebar()
127
128         self.setCentralWidget(QtGui.QWidget())
129         self.centralWidget().setContentsMargins(0, 0, 0, 0)
130
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
137         self.resize(settings.mainwindow.size.value())
138         if settings.mainwindow.pos.value():
139             self.move(settings.mainwindow.pos.value())
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
160         actionPreferences = self.sideBar.addAction('Preferences')
161         actionPreferences.setCheckable(True)
162         actionPreferences.setIcon(icons.preferences)
163         actionPreferences.triggered.connect(self.showPreferencesPage)
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)
170         self.actionGroup.addAction(actionPreferences)
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)
187
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())
228
229         self.plotwidget = multiplotwidget.MultiPlotWidget(self)
230         self.plotwidget.plotHidden.connect(self._on_plotHidden)
231
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)
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)
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)
285
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
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))
310         plot.view.setXRange(-settings.graph.backlog.value(), 0, update=False)
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
324         key = (plot, deviceID)
325         if key in self._curves:
326             return self._curves[key]
327
328         # create a new curve
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         )
335         plot.view.addItem(curve)
336
337         self._curves[key] = curve
338         return curve
339
340     def _updatePlots(self):
341         '''Updates all plots.'''
342
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)
348             deviceID = tuple(deviceID) # PySide returns a list.
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
356                 l = now - settings.graph.backlog.value()
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)
386
387             for traceunit, trace in traces.items():
388                 if traceunit == plotunit:
389                     trace.new = False
390
391     @QtCore.Slot()
392     def _stopped(self):
393         if self._closing:
394             # The acquisition was stopped by the 'closeEvent()', close the
395             # window again now that the acquisition has stopped.
396             self.close()
397
398     def closeEvent(self, event):
399         if self.acquisition.is_running():
400             # Stop the acquisition before closing the window.
401             self._closing = True
402             self.start_stop_acquisition()
403             event.ignore()
404         else:
405             settings.mainwindow.size.setValue(self.size())
406             settings.mainwindow.pos.setValue(self.pos())
407             event.accept()
408
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
431     @QtCore.Slot()
432     def show_about(self):
433         text = textwrap.dedent('''\
434             <div align="center">
435                 <b>sigrok-meter 0.1.0</b><br/><br/>
436                 Using libsigrok {} (lib version {}).<br/><br/>
437                 <a href='http://www.sigrok.org'>
438                          http://www.sigrok.org</a><br/>
439                 <br/>
440                 License: GNU GPL, version 3 or later<br/>
441                 <br/>
442                 This program comes with ABSOLUTELY NO WARRANTY;<br/>
443                 for details visit
444                 <a href='http://www.gnu.org/licenses/gpl.html'>
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>
449             </div>
450         '''.format(self.context.package_version, self.context.lib_version))
451
452         QtGui.QMessageBox.about(self, 'About sigrok-meter', text)