]> sigrok.org Git - sigrok-meter.git/blob - mainwindow.py
Don't plot values that confuse the graph widget.
[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 datamodel
23 import multiplotwidget
24 import os.path
25 import qtcompat
26 import samplingthread
27 import textwrap
28 import time
29 import util
30
31 QtCore = qtcompat.QtCore
32 QtGui = qtcompat.QtGui
33 pyqtgraph = qtcompat.pyqtgraph
34
35 class EmptyMessageListView(QtGui.QListView):
36     '''List view that shows a message if the model im empty.'''
37
38     def __init__(self, message, parent=None):
39         super(self.__class__, self).__init__(parent)
40
41         self._message = message
42
43     def paintEvent(self, event):
44         m = self.model()
45         if m and m.rowCount():
46             super(self.__class__, self).paintEvent(event)
47             return
48
49         painter = QtGui.QPainter(self.viewport())
50         painter.drawText(self.rect(), QtCore.Qt.AlignCenter, self._message)
51
52 class MainWindow(QtGui.QMainWindow):
53     '''The main window of the application.'''
54
55     # Number of seconds that the plots display.
56     BACKLOG = 30
57
58     # Update interval of the plots in milliseconds.
59     UPDATEINTERVAL = 100
60
61     def __init__(self, context, drivers):
62         super(self.__class__, self).__init__()
63
64         self.context = context
65
66         self.delegate = datamodel.MultimeterDelegate(self, self.font())
67         self.model = datamodel.MeasurementDataModel(self)
68         self.model.rowsInserted.connect(self.modelRowsInserted)
69
70         self.setup_ui()
71
72         self.thread = samplingthread.SamplingThread(self.context, drivers)
73         self.thread.measured.connect(self.model.update)
74         self.thread.error.connect(self.error)
75         self.thread.start()
76
77     def setup_ui(self):
78         self.setWindowTitle('sigrok-meter')
79         # Resizing the listView below will increase this again.
80         self.resize(350, 10)
81
82         p = os.path.abspath(os.path.dirname(__file__))
83         p = os.path.join(p, 'sigrok-logo-notext.png')
84         self.setWindowIcon(QtGui.QIcon(p))
85
86         actionQuit = QtGui.QAction(self)
87         actionQuit.setText('&Quit')
88         actionQuit.setIcon(QtGui.QIcon.fromTheme('application-exit'))
89         actionQuit.setShortcut('Ctrl+Q')
90         actionQuit.triggered.connect(self.close)
91
92         actionAbout = QtGui.QAction(self)
93         actionAbout.setText('&About')
94         actionAbout.setIcon(QtGui.QIcon.fromTheme('help-about'))
95         actionAbout.triggered.connect(self.show_about)
96
97         menubar = self.menuBar()
98         menuFile = menubar.addMenu('&File')
99         menuFile.addAction(actionQuit)
100         menuHelp = menubar.addMenu('&Help')
101         menuHelp.addAction(actionAbout)
102
103         self.listView = EmptyMessageListView('waiting for data...')
104         self.listView.setFrameShape(QtGui.QFrame.NoFrame)
105         self.listView.viewport().setBackgroundRole(QtGui.QPalette.Window)
106         self.listView.viewport().setAutoFillBackground(True)
107         self.listView.setMinimumWidth(260)
108         self.listView.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
109         self.listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
110         self.listView.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel)
111         self.listView.setItemDelegate(self.delegate)
112         self.listView.setModel(self.model)
113         self.listView.setUniformItemSizes(True)
114         self.listView.setMinimumSize(self.delegate.sizeHint())
115
116         self.plotwidget = multiplotwidget.MultiPlotWidget(self)
117         self.plotwidget.plotHidden.connect(self._on_plotHidden)
118
119         # Maps from 'unit' to the corresponding plot.
120         self._plots = {}
121         # Maps from '(plot, device)' to the corresponding curve.
122         self._curves = {}
123
124         self.splitter = QtGui.QSplitter(QtCore.Qt.Horizontal);
125         self.splitter.addWidget(self.listView)
126         self.splitter.addWidget(self.plotwidget)
127         self.splitter.setStretchFactor(0, 0)
128         self.splitter.setStretchFactor(1, 1)
129
130         self.setCentralWidget(self.splitter)
131         self.centralWidget().setContentsMargins(0, 0, 0, 0)
132         self.resize(800, 500)
133
134         self.startTimer(MainWindow.UPDATEINTERVAL)
135
136     def _getPlot(self, unit):
137         '''Looks up or creates a new plot for 'unit'.'''
138
139         if unit in self._plots:
140             return self._plots[unit]
141
142         # create a new plot for the unit
143         plot = self.plotwidget.addPlot()
144         plot.yaxis.setLabel(util.quantity_from_unit(unit), units=util.format_unit(unit))
145         plot.view.setXRange(-MainWindow.BACKLOG, 0, update=False)
146         plot.view.setYRange(-1, 1)
147         plot.view.enableAutoRange(axis=pyqtgraph.ViewBox.YAxis)
148         # lock to the range calculated by the view using additional padding,
149         # looks nicer this way
150         r = plot.view.viewRange()
151         plot.view.setLimits(xMin=r[0][0], xMax=r[0][1])
152
153         self._plots[unit] = plot
154         return plot
155
156     def _getCurve(self, plot, deviceID):
157         '''Looks up or creates a new curve for '(plot, deviceID)'.'''
158
159         key = (id(plot), deviceID)
160         if key in self._curves:
161             return self._curves[key]
162
163         # create a new curve
164         curve = pyqtgraph.PlotDataItem(
165             antialias=True,
166             symbolPen=pyqtgraph.mkPen(QtGui.QColor(QtCore.Qt.black)),
167             symbolBrush=pyqtgraph.mkBrush(QtGui.QColor(QtCore.Qt.black)),
168             symbolSize=1
169         )
170         plot.view.addItem(curve)
171
172         self._curves[key] = curve
173         return curve
174
175     def timerEvent(self, event):
176         '''Periodically updates all graphs.'''
177
178         self._updatePlots()
179
180     def _updatePlots(self):
181         '''Updates all plots.'''
182
183         # loop over all devices and channels
184         for row in range(self.model.rowCount()):
185             idx = self.model.index(row, 0)
186             deviceID = self.model.data(idx,
187                             datamodel.MeasurementDataModel.idRole)
188             traces = self.model.data(idx,
189                             datamodel.MeasurementDataModel.tracesRole)
190
191             for unit, trace in traces.items():
192                 now = time.time()
193
194                 # remove old samples
195                 l = now - MainWindow.BACKLOG
196                 while trace.samples and trace.samples[0][0] < l:
197                     trace.samples.pop(0)
198
199                 plot = self._getPlot(unit)
200                 if not plot.visible:
201                     if trace.new:
202                         self.plotwidget.showPlot(plot)
203
204                 if plot.visible:
205                     xdata = [s[0] - now for s in trace.samples]
206                     ydata = [s[1]       for s in trace.samples]
207
208                     color = self.model.data(idx,
209                                 datamodel.MeasurementDataModel.colorRole)
210
211                     curve = self._getCurve(plot, deviceID)
212                     curve.setPen(pyqtgraph.mkPen(color=color))
213                     curve.setData(xdata, ydata)
214
215     @QtCore.Slot(multiplotwidget.Plot)
216     def _on_plotHidden(self, plot):
217         plotunit = [u for u, p in self._plots.items() if p == plot][0]
218
219         # Mark all traces of all devices/channels with the same unit as the
220         # plot as "old" ('trace.new = False'). As soon as a new sample arrives
221         # on one trace, the plot will be shown again
222         for row in range(self.model.rowCount()):
223             idx = self.model.index(row, 0)
224             traces = self.model.data(idx, datamodel.MeasurementDataModel.tracesRole)
225
226             for traceunit, trace in traces.items():
227                 if traceunit == plotunit:
228                     trace.new = False
229
230     def closeEvent(self, event):
231         self.thread.stop()
232         event.accept()
233
234     @QtCore.Slot()
235     def show_about(self):
236         text = textwrap.dedent('''\
237             <div align="center">
238                 <b>sigrok-meter 0.1.0</b><br/><br/>
239                 Using libsigrok {} (lib version {}).<br/><br/>
240                 <a href='http://www.sigrok.org'>
241                          http://www.sigrok.org</a><br/>
242                 <br/>
243                 License: GNU GPL, version 3 or later<br/>
244                 <br/>
245                 This program comes with ABSOLUTELY NO WARRANTY;<br/>
246                 for details visit
247                 <a href='http://www.gnu.org/licenses/gpl.html'>
248                          http://www.gnu.org/licenses/gpl.html</a>
249             </div>
250         '''.format(self.context.package_version, self.context.lib_version))
251
252         QtGui.QMessageBox.about(self, 'About sigrok-meter', text)
253
254     @QtCore.Slot(str)
255     def error(self, msg):
256         '''Error handler for the sampling thread.'''
257         QtGui.QMessageBox.critical(self, 'Error', msg)
258         self.close()
259
260     @QtCore.Slot(object, int, int)
261     def modelRowsInserted(self, parent, start, end):
262         '''Resize the list view to the size of the content.'''
263         rows = self.model.rowCount()
264         dh = self.delegate.sizeHint().height()
265         self.listView.setMinimumHeight(dh * rows)