]> sigrok.org Git - sigrok-meter.git/blob - mainwindow.py
3e07ff7b381fcd21b62a61d204ff8b8f21fd8324
[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 = 10
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
118         # Maps from 'unit' to the corresponding plot.
119         self._plots = {}
120         # Maps from '(plot, device)' to the corresponding curve.
121         self._curves = {}
122
123         self.splitter = QtGui.QSplitter(QtCore.Qt.Horizontal);
124         self.splitter.addWidget(self.listView)
125         self.splitter.addWidget(self.plotwidget)
126         self.splitter.setStretchFactor(0, 0)
127         self.splitter.setStretchFactor(1, 1)
128
129         self.setCentralWidget(self.splitter)
130         self.centralWidget().setContentsMargins(0, 0, 0, 0)
131         self.resize(800, 500)
132
133         self.startTimer(MainWindow.UPDATEINTERVAL)
134
135     def _getPlot(self, unit):
136         '''Looks up or creates a new plot for 'unit'.'''
137
138         if unit in self._plots:
139             return self._plots[unit]
140
141         # create a new plot for the unit
142         plot = self.plotwidget.addPlot()
143         plot.yaxis.setLabel(util.quantity_from_unit(unit), units=util.format_unit(unit))
144         plot.view.setXRange(-MainWindow.BACKLOG, 0, update=False)
145         plot.view.setYRange(-1, 1)
146         plot.view.enableAutoRange(axis=pyqtgraph.ViewBox.YAxis)
147         # lock to the range calculated by the view using additional padding,
148         # looks nicer this way
149         r = plot.view.viewRange()
150         plot.view.setLimits(xMin=r[0][0], xMax=r[0][1])
151
152         self._plots[unit] = plot
153         return plot
154
155     def _getCurve(self, plot, deviceID):
156         '''Looks up or creates a new curve for '(plot, deviceID)'.'''
157
158         key = (id(plot), deviceID)
159         if key in self._curves:
160             return self._curves[key]
161
162         # create a new curve
163         curve = pyqtgraph.PlotDataItem()
164         plot.view.addItem(curve)
165
166         self._curves[key] = curve
167         return curve
168
169     def timerEvent(self, event):
170         '''Periodically updates all graphs.'''
171
172         for row in range(self.model.rowCount()):
173             idx = self.model.index(row, 0)
174             deviceID = self.model.data(idx, datamodel.MeasurementDataModel.idRole)
175             sampledict = self.model.data(idx, datamodel.MeasurementDataModel.samplesRole)
176             for unit in sampledict:
177                 self._updatePlot(deviceID, unit, sampledict[unit])
178
179     def _updatePlot(self, deviceID, unit, samples):
180         '''Updates the curve of device 'deviceID' and 'unit' with 'samples'.'''
181
182         plot = self._getPlot(unit)
183         curve = self._getCurve(plot, deviceID)
184
185         now = time.time()
186
187         # remove old samples
188         l = now - MainWindow.BACKLOG
189         while samples and samples[0][0] < l:
190             samples.pop(0)
191
192         xdata = [s[0] - now for s in samples]
193         ydata = [s[1]       for s in samples]
194
195         curve.setData(xdata, ydata)
196
197     def closeEvent(self, event):
198         self.thread.stop()
199         event.accept()
200
201     @QtCore.Slot()
202     def show_about(self):
203         text = textwrap.dedent('''\
204             <div align="center">
205                 <b>sigrok-meter 0.1.0</b><br/><br/>
206                 Using libsigrok {} (lib version {}).<br/><br/>
207                 <a href='http://www.sigrok.org'>
208                          http://www.sigrok.org</a><br/>
209                 <br/>
210                 License: GNU GPL, version 3 or later<br/>
211                 <br/>
212                 This program comes with ABSOLUTELY NO WARRANTY;<br/>
213                 for details visit
214                 <a href='http://www.gnu.org/licenses/gpl.html'>
215                          http://www.gnu.org/licenses/gpl.html</a>
216             </div>
217         '''.format(self.context.package_version, self.context.lib_version))
218
219         QtGui.QMessageBox.about(self, 'About sigrok-meter', text)
220
221     @QtCore.Slot(str)
222     def error(self, msg):
223         '''Error handler for the sampling thread.'''
224         QtGui.QMessageBox.critical(self, 'Error', msg)
225         self.close()
226
227     @QtCore.Slot(object, int, int)
228     def modelRowsInserted(self, parent, start, end):
229         '''Resize the list view to the size of the content.'''
230         rows = self.model.rowCount()
231         dh = self.delegate.sizeHint().height()
232         self.listView.setMinimumHeight(dh * rows)