class MainWindow(QFrame): smx_file = core.SmxFile() hotplug = core.HotPlug() devices = [] target = None worker = None def __init__(self): super().__init__() self.setWindowTitle('i.MX Smart-Boot Tool') self.setMinimumSize(600, 400) self.center() # create main box layout = QVBoxLayout() # -------------------------------------------------------------------------------------------------------------- # Device selection drop-box with scan button # -------------------------------------------------------------------------------------------------------------- box = QHBoxLayout() self.deviceBox = QComboBox() box.addWidget(self.deviceBox) self.scanButton = QPushButton(" Scan") self.scanButton.setFixedWidth(80) self.scanButton.setIcon(QIcon.fromTheme("view-refresh")) self.scanButton.clicked.connect(self.on_scan_button_clicked) box.addWidget(self.scanButton) layout.addLayout(box) # -------------------------------------------------------------------------------------------------------------- # SMX File inbox with open button # -------------------------------------------------------------------------------------------------------------- box = QHBoxLayout() self.smxEdit = QLineEdit() self.smxEdit.setReadOnly(True) box.addWidget(self.smxEdit) self.openButton = QPushButton(" Open") self.openButton.setFixedWidth(80) self.openButton.setIcon(QIcon.fromTheme("document-open")) self.openButton.clicked.connect(self.on_open_button_clicked) box.addWidget(self.openButton) layout.addLayout(box) # -------------------------------------------------------------------------------------------------------------- # Body # -------------------------------------------------------------------------------------------------------------- self.splitter = QSplitter() self.splitter.setHandleWidth(5) self.splitter.setMidLineWidth(0) self.splitter.setOrientation(Qt.Vertical) self.splitter.setOpaqueResize(True) self.splitter.setChildrenCollapsible(False) self.scriptsList = QListWidget(self.splitter) self.scriptsList.setSizeAdjustPolicy( QAbstractScrollArea.AdjustToContents) self.scriptsList.setMinimumHeight(60) self.textEdit = QTextEdit(self.splitter) self.textEdit.setReadOnly(True) self.textEdit.setMinimumHeight(100) layout.addWidget(self.splitter) # Progress Bar self.pgTask = QProgressBar() self.pgTask.setRange(0, PGRANGE) self.pgTask.setFixedHeight(16) self.pgTask.setTextVisible(False) layout.addWidget(self.pgTask) # -------------------------------------------------------------------------------------------------------------- # Buttons # -------------------------------------------------------------------------------------------------------------- box = QHBoxLayout() box.setContentsMargins(-1, 5, -1, -1) # About Button self.aboutButton = QPushButton(" About") self.aboutButton.setMinimumSize(100, 40) self.aboutButton.setIcon(QIcon.fromTheme("help-contents")) self.aboutButton.clicked.connect(self.on_about_button_clicked) box.addWidget(self.aboutButton) # Device Info Button self.devInfoButton = QPushButton(" DevInfo") self.devInfoButton.setEnabled(False) self.devInfoButton.setMinimumSize(100, 40) self.devInfoButton.setIcon(QIcon.fromTheme("help-about")) self.devInfoButton.clicked.connect(self.on_info_button_clicked) box.addWidget(self.devInfoButton) # Spacer box.addItem( QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) # Start Button self.startButton = QPushButton(" Start") self.startButton.setEnabled(False) self.startButton.setMinimumSize(100, 40) self.startButton.setIcon(QIcon.fromTheme("media-playback-start")) self.startButton.clicked.connect(self.on_start_button_clicked) box.addWidget(self.startButton) # Start Button self.exitButton = QPushButton(" Exit") self.exitButton.setMinimumSize(100, 40) self.exitButton.setIcon(QIcon.fromTheme("application-exit")) self.exitButton.clicked.connect(self.on_exit_button_clicked) box.addWidget(self.exitButton) layout.addLayout(box) self.setLayout(layout) # USB hot-plug (Linux only) # self.hotplug.attach(self.scan_usb) # self.hotplug.start() # TODO: Fix USB hot-plug self.scan_usb() def center(self): # center point of screen cp = QDesktopWidget().availableGeometry().center() # move rectangle's center point to screen's center point qr = self.frameGeometry() qr.moveCenter(cp) # top left of rectangle becomes top left of window centering it self.move(qr.topLeft()) #################################################################################################################### # Helper methods #################################################################################################################### def scan_usb(self, obj=None): self.devices = imx.sdp.scan_usb(self.target) self.deviceBox.clear() if self.devices: for dev in self.devices: self.deviceBox.addItem(dev.usbd.info()) self.deviceBox.setCurrentIndex(0) self.deviceBox.setEnabled(True) self.devInfoButton.setEnabled(True) self.startButton.setEnabled(False if self.target is None else True) else: self.deviceBox.setEnabled(False) self.devInfoButton.setEnabled(False) self.startButton.setEnabled(False) def Logger(self, msg, clear): if clear: self.textEdit.clear() if msg: self.textEdit.append(msg) def ProgressBar(self, value): self.pgTask.setValue(min(value, PGRANGE)) def ShowMesageBox(self, title, message, icon=QMessageBox.Warning): alert = QMessageBox() alert.setWindowTitle(title) alert.setText(message) alert.setIcon(icon) alert.exec_() #################################################################################################################### # Buttons callback methods #################################################################################################################### def on_scan_button_clicked(self): self.scan_usb() def on_open_button_clicked(self): options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog fileName, _ = QFileDialog.getOpenFileName( self, "Choose a SmartBoot script file", BASEDIR, "i.MX SmartBoot Files (*.smx)", options=options) if fileName: self.smxEdit.clear() self.scriptsList.clear() try: self.smx_file.open(fileName, True) except Exception as e: self.ShowMesageBox("SMX File Open Error", str(e), QMessageBox.Warning) self.target = None self.startButton.setEnabled(False) else: self.target = self.smx_file.platform self.smxEdit.setText(fileName) for i, item in enumerate(self.smx_file.scripts): self.scriptsList.addItem("{}. {} ({})".format( i, item.name, item.description)) self.scriptsList.setCurrentRow(0) self.scriptsList.adjustSize() # update usb device list self.scan_usb() def on_about_button_clicked(self): text = "<b>i.MX SmartBoot Tool</b> v {}".format(core.__version__) text += "<p>{}".format(core.DESCRIPTION) text += "<p>Copyright © 2018 Martin Olejar." text += "<p>License: {}".format(core.__license__) text += "<p>Sources: <a href='https://github.com/molejar/imxsb'>https://github.com/molejar/imxsb</a>" QMessageBox.about(self, "About", text) def on_info_button_clicked(self): device = self.devices[self.deviceBox.currentIndex()] device.open() self.textEdit.clear() self.textEdit.append("Device: {} \n".format(device.device_name)) device.close() def on_start_button_clicked(self): if self.startButton.text().endswith("Start"): try: device = self.devices[self.deviceBox.currentIndex()] script = self.smx_file.get_script( self.scriptsList.currentRow()) except Exception as e: self.ShowMesageBox("Script Load Error", str(e), QMessageBox.Warning) else: # Start Worker self.worker = Worker(device, script) self.worker.logger.connect(self.Logger) self.worker.finish.connect(self.on_finish) self.worker.prgbar.connect(self.ProgressBar) self.worker.daemon = True self.worker.start() self.startButton.setText(" Stop") self.startButton.setIcon( QIcon.fromTheme("media-playback-stop")) self.scanButton.setEnabled(False) self.openButton.setEnabled(False) self.deviceBox.setEnabled(False) self.scriptsList.setEnabled(False) self.devInfoButton.setEnabled(False) else: # Stop Worker self.worker.stop() def on_finish(self, msg, done): self.textEdit.append(msg) self.startButton.setText(" Start") self.startButton.setIcon(QIcon.fromTheme("media-playback-start")) self.scanButton.setEnabled(True) self.scriptsList.setEnabled(True) self.openButton.setEnabled(True) if done: self.deviceBox.clear() self.startButton.setEnabled(False) else: self.scan_usb() def on_exit_button_clicked(self): self.close()
class LinkedSplits(QWidget): ''' Composite that holds a central chart plus a set of (derived) subcharts (usually computed from the original data) arranged in a splitter for resizing. A single internal references to the data is maintained for each chart and can be updated externally. ''' def __init__( self, godwidget: GodWidget, ) -> None: super().__init__() # self.signals_visible: bool = False self.cursor: Cursor = None # crosshair graphics self.godwidget = godwidget self.chart: ChartPlotWidget = None # main (ohlc) chart self.subplots: dict[tuple[str, ...], ChartPlotWidget] = {} self.godwidget = godwidget # placeholder for last appended ``PlotItem``'s bottom axis. self.xaxis_chart = None self.splitter = QSplitter(QtCore.Qt.Vertical) self.splitter.setMidLineWidth(0) self.splitter.setHandleWidth(2) self.layout = QVBoxLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.splitter) # chart-local graphics state that can be passed to # a ``graphic_update_cycle()`` call by any task wishing to # update the UI for a given "chart instance". self.display_state: Optional[DisplayState] = None self._symbol: Symbol = None def graphics_cycle(self, **kwargs) -> None: from . import _display ds = self.display_state if ds: return _display.graphics_update_cycle(ds, **kwargs) @property def symbol(self) -> Symbol: return self._symbol def set_split_sizes( self, prop: Optional[float] = None, ) -> None: '''Set the proportion of space allocated for linked subcharts. ''' ln = len(self.subplots) # proportion allocated to consumer subcharts if not prop: prop = 3/8*5/8 # if ln < 2: # prop = 3/8*5/8 # elif ln >= 2: # prop = 3/8 major = 1 - prop min_h_ind = int((self.height() * prop) / ln) sizes = [int(self.height() * major)] sizes.extend([min_h_ind] * ln) self.splitter.setSizes(sizes) def focus(self) -> None: if self.chart is not None: self.chart.focus() def unfocus(self) -> None: if self.chart is not None: self.chart.clearFocus() def plot_ohlc_main( self, symbol: Symbol, array: np.ndarray, sidepane: FieldsForm, style: str = 'bar', ) -> ChartPlotWidget: ''' Start up and show main (price) chart and all linked subcharts. The data input struct array must include OHLC fields. ''' # add crosshairs self.cursor = Cursor( linkedsplits=self, digits=symbol.tick_size_digits, ) # NOTE: atm the first (and only) OHLC price chart for the symbol # is given a special reference but in the future there shouldn't # be no distinction since we will have multiple symbols per # view as part of "aggregate feeds". self.chart = self.add_plot( name=symbol.key, array=array, style=style, _is_main=True, sidepane=sidepane, ) # add crosshair graphic self.chart.addItem(self.cursor) # axis placement if ( _xaxis_at == 'bottom' and 'bottom' in self.chart.plotItem.axes ): self.chart.hideAxis('bottom') # style? self.chart.setFrameStyle( QFrame.StyledPanel | QFrame.Plain ) return self.chart def add_plot( self, name: str, array: np.ndarray, array_key: Optional[str] = None, style: str = 'line', _is_main: bool = False, sidepane: Optional[QWidget] = None, **cpw_kwargs, ) -> ChartPlotWidget: ''' Add (sub)plots to chart widget by key. ''' if self.chart is None and not _is_main: raise RuntimeError( "A main plot must be created first with `.plot_ohlc_main()`") # use "indicator axis" by default # TODO: we gotta possibly assign this back # to the last subplot on removal of some last subplot xaxis = DynamicDateAxis( orientation='bottom', linkedsplits=self ) axes = { 'right': PriceAxis(linkedsplits=self, orientation='right'), 'left': PriceAxis(linkedsplits=self, orientation='left'), 'bottom': xaxis, } qframe = ChartnPane( sidepane=sidepane, parent=self.splitter, ) cpw = ChartPlotWidget( # this name will be used to register the primary # graphics curve managed by the subchart name=name, data_key=array_key or name, array=array, parent=qframe, linkedsplits=self, axisItems=axes, **cpw_kwargs, ) cpw.hideAxis('left') cpw.hideAxis('bottom') if self.xaxis_chart: self.xaxis_chart.hideAxis('bottom') # presuming we only want it at the true bottom of all charts. # XXX: uses new api from our ``pyqtgraph`` fork. # https://github.com/pikers/pyqtgraph/tree/plotitemoverlay_onto_pg_master # _ = self.xaxis_chart.removeAxis('bottom', unlink=False) # assert 'bottom' not in self.xaxis_chart.plotItem.axes self.xaxis_chart = cpw cpw.showAxis('bottom') if self.xaxis_chart is None: self.xaxis_chart = cpw qframe.chart = cpw qframe.hbox.addWidget(cpw) # so we can look this up and add back to the splitter # on a symbol switch cpw.qframe = qframe assert cpw.parent() == qframe # add sidepane **after** chart; place it on axis side qframe.hbox.addWidget( sidepane, alignment=Qt.AlignTop ) cpw.sidepane = sidepane cpw.plotItem.vb.linkedsplits = self cpw.setFrameStyle( QtWidgets.QFrame.StyledPanel # | QtWidgets.QFrame.Plain ) # don't show the little "autoscale" A label. cpw.hideButtons() # XXX: gives us outline on backside of y-axis cpw.getPlotItem().setContentsMargins(*CHART_MARGINS) # link chart x-axis to main chart # this is 1/2 of where the `Link` in ``LinkedSplit`` # comes from ;) cpw.setXLink(self.chart) add_label = False anchor_at = ('top', 'left') # draw curve graphics if style == 'bar': graphics, data_key = cpw.draw_ohlc( name, array, array_key=array_key ) self.cursor.contents_labels.add_label( cpw, name, anchor_at=('top', 'left'), update_func=ContentsLabel.update_from_ohlc, ) elif style == 'line': add_label = True graphics, data_key = cpw.draw_curve( name, array, array_key=array_key, color='default_light', ) elif style == 'step': add_label = True graphics, data_key = cpw.draw_curve( name, array, array_key=array_key, step_mode=True, color='davies', fill_color='davies', ) else: raise ValueError(f"Chart style {style} is currently unsupported") if not _is_main: # track by name self.subplots[name] = cpw self.splitter.addWidget(qframe) # scale split regions self.set_split_sizes() else: assert style == 'bar', 'main chart must be OHLC' # add to cross-hair's known plots # NOTE: add **AFTER** creating the underlying ``PlotItem``s # since we require that global (linked charts wide) axes have # been created! self.cursor.add_plot(cpw) if self.cursor and style != 'bar': self.cursor.add_curve_cursor(cpw, graphics) if add_label: self.cursor.contents_labels.add_label( cpw, data_key, anchor_at=anchor_at, ) self.resize_sidepanes() return cpw def resize_sidepanes( self, ) -> None: ''' Size all sidepanes based on the OHLC "main" plot and its sidepane width. ''' main_chart = self.chart if main_chart: sp_w = main_chart.sidepane.width() for name, cpw in self.subplots.items(): cpw.sidepane.setMinimumWidth(sp_w) cpw.sidepane.setMaximumWidth(sp_w)