Beispiel #1
0
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 &copy; 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()
Beispiel #2
0
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)