]> sigrok.org Git - sigrok-meter.git/blame - mainwindow.py
Save and restore settings.
[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
2abf5a93 28import settings
48723bbb 29import textwrap
f76b9df8
JS
30import time
31import util
48723bbb
JS
32
33QtCore = qtcompat.QtCore
34QtGui = qtcompat.QtGui
f76b9df8 35pyqtgraph = qtcompat.pyqtgraph
48723bbb
JS
36
37class 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
54class MainWindow(QtGui.QMainWindow):
55 '''The main window of the application.'''
56
f76b9df8 57 # Number of seconds that the plots display.
3010b5a0 58 BACKLOG = 30
f76b9df8
JS
59
60 # Update interval of the plots in milliseconds.
61 UPDATEINTERVAL = 100
62
48723bbb
JS
63 def __init__(self, context, drivers):
64 super(self.__class__, self).__init__()
65
2e8c2e6e
JS
66 # Used to coordinate the stopping of the acquisition and
67 # the closing of the window.
68 self._closing = False
69
48723bbb 70 self.context = context
2e8c2e6e 71 self.drivers = drivers
48723bbb
JS
72
73 self.delegate = datamodel.MultimeterDelegate(self, self.font())
74 self.model = datamodel.MeasurementDataModel(self)
48723bbb 75
68348e5a
JS
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)
48723bbb 86
2e8c2e6e
JS
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
68348e5a 102 self.start_stop_acquisition()
48723bbb 103
68348e5a 104 def _setup_ui(self):
48723bbb 105 self.setWindowTitle('sigrok-meter')
480cdb7b 106 # Resizing the listView below will increase this again.
0e810ddf 107 self.resize(350, 10)
48723bbb 108
68348e5a 109 self.setWindowIcon(QtGui.QIcon(':/logo.png'))
48723bbb 110
68348e5a
JS
111 self._setup_graphPage()
112 self._setup_addDevicePage()
113 self._setup_logPage()
114 self._setup_preferencesPage()
48723bbb 115
68348e5a
JS
116 self._pages = [
117 self.graphPage,
118 self.addDevicePage,
119 self.logPage,
120 self.preferencesPage
121 ]
48723bbb 122
68348e5a
JS
123 self.stackedWidget = QtGui.QStackedWidget(self)
124 for page in self._pages:
125 self.stackedWidget.addWidget(page)
48723bbb 126
68348e5a 127 self._setup_sidebar()
f76b9df8 128
68348e5a
JS
129 self.setCentralWidget(QtGui.QWidget())
130 self.centralWidget().setContentsMargins(0, 0, 0, 0)
f76b9df8 131
68348e5a
JS
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
2abf5a93
JS
138 self.resize(settings.mainwindow.size.value())
139 if settings.mainwindow.pos.value():
140 self.move(settings.mainwindow.pos.value())
68348e5a
JS
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)
f76b9df8 188
68348e5a
JS
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())
f76b9df8 229
68348e5a
JS
230 self.plotwidget = multiplotwidget.MultiPlotWidget(self)
231 self.plotwidget.plotHidden.connect(self._on_plotHidden)
f76b9df8 232
68348e5a
JS
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)
2e8c2e6e 275
f76b9df8
JS
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
68348e5a 299 key = (plot, deviceID)
f76b9df8
JS
300 if key in self._curves:
301 return self._curves[key]
302
303 # create a new curve
3010b5a0
JS
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 )
f76b9df8
JS
310 plot.view.addItem(curve)
311
312 self._curves[key] = curve
313 return curve
314
d0aa45b4
JS
315 def _updatePlots(self):
316 '''Updates all plots.'''
f76b9df8 317
d0aa45b4
JS
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)
1879265a 323 deviceID = tuple(deviceID) # PySide returns a list.
d0aa45b4
JS
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)
f76b9df8 361
d0aa45b4
JS
362 for traceunit, trace in traces.items():
363 if traceunit == plotunit:
364 trace.new = False
48723bbb 365
2e8c2e6e
JS
366 @QtCore.Slot()
367 def _stopped(self):
368 if self._closing:
68348e5a
JS
369 # The acquisition was stopped by the 'closeEvent()', close the
370 # window again now that the acquisition has stopped.
2e8c2e6e
JS
371 self.close()
372
48723bbb 373 def closeEvent(self, event):
2e8c2e6e 374 if self.acquisition.is_running():
68348e5a 375 # Stop the acquisition before closing the window.
2e8c2e6e 376 self._closing = True
68348e5a 377 self.start_stop_acquisition()
2e8c2e6e
JS
378 event.ignore()
379 else:
2abf5a93
JS
380 settings.mainwindow.size.setValue(self.size())
381 settings.mainwindow.pos.setValue(self.pos())
2e8c2e6e 382 event.accept()
48723bbb 383
68348e5a
JS
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
48723bbb
JS
406 @QtCore.Slot()
407 def show_about(self):
408 text = textwrap.dedent('''\
409 <div align="center">
480cdb7b
UH
410 <b>sigrok-meter 0.1.0</b><br/><br/>
411 Using libsigrok {} (lib version {}).<br/><br/>
48723bbb
JS
412 <a href='http://www.sigrok.org'>
413 http://www.sigrok.org</a><br/>
414 <br/>
480cdb7b
UH
415 License: GNU GPL, version 3 or later<br/>
416 <br/>
48723bbb
JS
417 This program comes with ABSOLUTELY NO WARRANTY;<br/>
418 for details visit
419 <a href='http://www.gnu.org/licenses/gpl.html'>
68348e5a
JS
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>
48723bbb
JS
424 </div>
425 '''.format(self.context.package_version, self.context.lib_version))
426
427 QtGui.QMessageBox.about(self, 'About sigrok-meter', text)