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