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