]> sigrok.org Git - sigrok-meter.git/blame - mainwindow.py
Fix a bug with color picking.
[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
22import datamodel
f76b9df8 23import multiplotwidget
48723bbb
JS
24import os.path
25import qtcompat
26import samplingthread
27import textwrap
f76b9df8
JS
28import time
29import util
48723bbb
JS
30
31QtCore = qtcompat.QtCore
32QtGui = qtcompat.QtGui
f76b9df8 33pyqtgraph = qtcompat.pyqtgraph
48723bbb
JS
34
35class 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
52class MainWindow(QtGui.QMainWindow):
53 '''The main window of the application.'''
54
f76b9df8 55 # Number of seconds that the plots display.
3010b5a0 56 BACKLOG = 30
f76b9df8
JS
57
58 # Update interval of the plots in milliseconds.
59 UPDATEINTERVAL = 100
60
48723bbb
JS
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')
480cdb7b 79 # Resizing the listView below will increase this again.
0e810ddf 80 self.resize(350, 10)
48723bbb
JS
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
f76b9df8 116 self.plotwidget = multiplotwidget.MultiPlotWidget(self)
d0aa45b4 117 self.plotwidget.plotHidden.connect(self._on_plotHidden)
f76b9df8
JS
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)
48723bbb 131 self.centralWidget().setContentsMargins(0, 0, 0, 0)
f76b9df8
JS
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
3010b5a0
JS
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 )
f76b9df8
JS
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
d0aa45b4 178 self._updatePlots()
f76b9df8 179
d0aa45b4
JS
180 def _updatePlots(self):
181 '''Updates all plots.'''
f76b9df8 182
d0aa45b4
JS
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)
1879265a 188 deviceID = tuple(deviceID) # PySide returns a list.
d0aa45b4
JS
189 traces = self.model.data(idx,
190 datamodel.MeasurementDataModel.tracesRole)
191
192 for unit, trace in traces.items():
193 now = time.time()
194
195 # remove old samples
196 l = now - MainWindow.BACKLOG
197 while trace.samples and trace.samples[0][0] < l:
198 trace.samples.pop(0)
199
200 plot = self._getPlot(unit)
201 if not plot.visible:
202 if trace.new:
203 self.plotwidget.showPlot(plot)
204
205 if plot.visible:
206 xdata = [s[0] - now for s in trace.samples]
207 ydata = [s[1] for s in trace.samples]
208
209 color = self.model.data(idx,
210 datamodel.MeasurementDataModel.colorRole)
211
212 curve = self._getCurve(plot, deviceID)
213 curve.setPen(pyqtgraph.mkPen(color=color))
214 curve.setData(xdata, ydata)
215
216 @QtCore.Slot(multiplotwidget.Plot)
217 def _on_plotHidden(self, plot):
218 plotunit = [u for u, p in self._plots.items() if p == plot][0]
219
220 # Mark all traces of all devices/channels with the same unit as the
221 # plot as "old" ('trace.new = False'). As soon as a new sample arrives
222 # on one trace, the plot will be shown again
223 for row in range(self.model.rowCount()):
224 idx = self.model.index(row, 0)
225 traces = self.model.data(idx, datamodel.MeasurementDataModel.tracesRole)
f76b9df8 226
d0aa45b4
JS
227 for traceunit, trace in traces.items():
228 if traceunit == plotunit:
229 trace.new = False
48723bbb
JS
230
231 def closeEvent(self, event):
232 self.thread.stop()
233 event.accept()
234
235 @QtCore.Slot()
236 def show_about(self):
237 text = textwrap.dedent('''\
238 <div align="center">
480cdb7b
UH
239 <b>sigrok-meter 0.1.0</b><br/><br/>
240 Using libsigrok {} (lib version {}).<br/><br/>
48723bbb
JS
241 <a href='http://www.sigrok.org'>
242 http://www.sigrok.org</a><br/>
243 <br/>
480cdb7b
UH
244 License: GNU GPL, version 3 or later<br/>
245 <br/>
48723bbb
JS
246 This program comes with ABSOLUTELY NO WARRANTY;<br/>
247 for details visit
248 <a href='http://www.gnu.org/licenses/gpl.html'>
249 http://www.gnu.org/licenses/gpl.html</a>
250 </div>
251 '''.format(self.context.package_version, self.context.lib_version))
252
253 QtGui.QMessageBox.about(self, 'About sigrok-meter', text)
254
255 @QtCore.Slot(str)
256 def error(self, msg):
257 '''Error handler for the sampling thread.'''
258 QtGui.QMessageBox.critical(self, 'Error', msg)
259 self.close()
260
261 @QtCore.Slot(object, int, int)
262 def modelRowsInserted(self, parent, start, end):
480cdb7b 263 '''Resize the list view to the size of the content.'''
48723bbb
JS
264 rows = self.model.rowCount()
265 dh = self.delegate.sizeHint().height()
266 self.listView.setMinimumHeight(dh * rows)