Ejemplo n.º 1
0
    def __init_actions(self):

        self.__context_menu = QMenu(self)

        self.__context_menu.addSection("Mouse Mode")
        mode_action_group = QActionGroup(self)
        self.__move_mode_action = self.__context_menu.addAction("Move")
        self.__move_mode_action.setCheckable(True)
        self.__move_mode_action.setChecked(True)
        mode_action_group.addAction(self.__move_mode_action)
        self.__paint_mode_action = self.__context_menu.addAction("Paint")
        self.__paint_mode_action.setCheckable(True)
        mode_action_group.addAction(self.__paint_mode_action)
        mode_action_group.triggered.connect(
            self.__on_mouse_mode_action_checked)
        self.__on_mouse_mode_action_checked(mode_action_group.checkedAction())

        copy_key_sequence = QKeySequence(QKeySequence.Copy)
        paste_key_sequence = QKeySequence(QKeySequence.Paste)
        undo_key_sequence = QKeySequence(QKeySequence.Undo)

        self.__context_menu.addSection("Pen Width")
        pen_width_action_group = QActionGroup(self)
        for width in (3, 5, 7, 10, 14, 20):
            action = self.__context_menu.addAction(str(width) + " px")
            action.setData(width)
            action.setCheckable(True)
            pen_width_action_group.addAction(action)
            if width == 5:
                action.setChecked(True)
        pen_width_action_group.triggered.connect(
            self.__on_pen_width_action_checked)
        self.__on_pen_width_action_checked(
            pen_width_action_group.checkedAction())

        self.__context_menu.addSeparator()
        copy_action = self.__context_menu.addAction("Copy")
        copy_action.setShortcut(copy_key_sequence)
        copy_action.setShortcutVisibleInContextMenu(True)
        copy_action.triggered.connect(self.__copy)
        paste_action = self.__context_menu.addAction("Paste")
        paste_action.setShortcut(paste_key_sequence)
        paste_action.setShortcutVisibleInContextMenu(True)
        paste_action.triggered.connect(self.__paste)
        reset_scale_action = self.__context_menu.addAction("Actual size")
        reset_scale_action.triggered.connect(self.__reset_scale)
        undo_action = self.__context_menu.addAction("Undo")
        undo_action.setShortcut(undo_key_sequence)
        undo_action.setShortcutVisibleInContextMenu(True)
        undo_action.triggered.connect(self.__image_widget.remove_last_line)
        clear_action = self.__context_menu.addAction("Clear")
        clear_action.triggered.connect(self.__image_widget.clear)

        paste_shortcut = QShortcut(paste_key_sequence, self)
        paste_shortcut.activated.connect(paste_action.trigger)
        copy_shortcut = QShortcut(copy_key_sequence, self)
        copy_shortcut.activated.connect(copy_action.trigger)
        undo_shortcut = QShortcut(undo_key_sequence, self)
        undo_shortcut.activated.connect(undo_action.trigger)
Ejemplo n.º 2
0
class ToolsMenu(QMenu):
    """Menu with file actions."""

    def __init__(self, parent, diagram, title='&Settings'):
        """Initializes the class."""
        super(ToolsMenu, self).__init__(parent)
        self.setTitle(title)
        self._diagram = diagram
        self.parent = parent

        self._selmode_action = QAction('&Selection Mode', self)
        self._selmode_action.setCheckable(True)
        self._selmode_action.setStatusTip('On/off selection mode')
        self._selmode_action.triggered.connect(self._select_mode)
        self.addAction(self._selmode_action)
        self.addSeparator()
        
        self._operation_mode_group = QActionGroup(self, exclusive=True)
        self._operation_mode_group.triggered.connect(self._select_operation)

        self._point_op_action = QAction('&Point Operations', self)
        self._point_op_action.setCheckable(True)
        self._point_op_action.setChecked(True) 
        self._point_op_action.setData(OperatinState.point)
        self._operation_mode_group.addAction(self._point_op_action)
        self._point_op_action.setStatusTip('Set point operation mode')
        self.addAction(self._point_op_action)

        self._line_op_action = QAction('&Line Operations', self)
        self._line_op_action.setCheckable(True)
        self._line_op_action.setData(OperatinState.line)
        self._operation_mode_group.addAction(self._line_op_action)        
        self._line_op_action.setStatusTip('Set line operation mode')
        self.addAction(self._line_op_action)

    def _select_mode(self):
        """set diagram mode"""
        state = self._selmode_action.isChecked()
        self._diagram.set_select_mode(state)
        self._enable_operation(not state)
        
    def _select_operation(self):
        """set diagram operation"""
        action = self._operation_mode_group.checkedAction()
        self._diagram.set_operation_state(action.data())
        
    def _enable_operation(self, enable):
        """enable operations items"""
        self._line_op_action.setEnabled(enable)
        self._point_op_action.setEnabled(enable)
Ejemplo n.º 3
0
class EraserOptionToolBar(QToolBar):

    eraser_size_changed = pyqtSignal(int)
    selection_option_changed = pyqtSignal(QAction)

    def __init__(self, parent=None):
        super(EraserOptionToolBar, self).__init__(parent)

        self._add_action = QAction(self)
        self._eraser_action = QAction(self)

        self._add_action.setIcon(QIcon(":/selection-add.png"))
        self._add_action.setCheckable(True)
        self._add_action.setToolTip("扩展轮廓")
        self._add_action.setData(SelectionOptionToolBar.Add)

        self._eraser_action.setIcon(QIcon(":/selection-subtract.png"))
        self._eraser_action.setCheckable(True)
        self._eraser_action.setChecked(True)
        self._eraser_action.setToolTip("擦除轮廓")
        self._eraser_action.setData(SelectionOptionToolBar.Subtract)

        self._action_group = QActionGroup(self)
        self._action_group.addAction(self._add_action)
        self._action_group.addAction(self._eraser_action)

        self.addAction(self._add_action)
        self.addAction(self._eraser_action)
        self.addSeparator()

        self._spin_box = QSpinBox(self)
        self._spin_box.setMinimum(3)
        self._spin_box.setMaximum(100)
        self.addWidget(self._spin_box)

        self._spin_box.valueChanged.connect(self.eraser_size_changed)
        self._action_group.triggered.connect(self.selection_option_changed)

    def current_eraser_size(self):
        return self._spin_box.value()

    def current_option(self):
        return self._action_group.checkedAction().data()
Ejemplo n.º 4
0
        def get_label_text(prefix:str, cur_key_type: str, tooltip_anchor: str, group: QActionGroup, style: str):
            lbl = '???'
            if self.edit_mode:
                change_mode = f'<td>(<a href="{tooltip_anchor}">use {tooltip_anchor}</a>)</td>'
            else:
                a = group.checkedAction()
                if a:
                    cur_key_type = a.data()
                change_mode = ''

            if cur_key_type == 'privkey':
                lbl = prefix + ' private key'
            elif cur_key_type == 'address':
                lbl = prefix + ' Crown address'
            elif cur_key_type == 'pubkey':
                lbl = prefix + ' public key'
            elif cur_key_type == 'pubkeyhash':
                lbl = prefix + ' public key hash'

            return f'<table style="float:right;{style_to_color(style)}"><tr><td>{lbl}</td>{change_mode}</tr></table>'
Ejemplo n.º 5
0
class CrystfelImage(QObject):
    def __init__(self, imodel, iview, geom, parent=None):
        QObject.__init__(self, parent)
        self.imodel = imodel
        self.dmodel = imodel.model
        self.iview = iview
        self.iview.ui.menuBtn.hide()
        self.iview.ui.roiBtn.hide()

        # Thanks CXIVIEW, Anton & Valerio
        pos = np.array([0.0, 0.5, 1.0])
        color = np.array(
            [[255, 255, 255, 255], [128, 128, 128, 255], [0, 0, 0, 255]],
            dtype=np.ubyte)
        new_color_map = ColorMap(pos, color)
        self.iview.ui.histogram.gradient.setColorMap(new_color_map)

        self.lastrow = -1
        self.curFileName = None
        self.curFile = None
        self.canDraw = self.dmodel.canSaveLst()
        self.checkEvent = "event" in self.dmodel.cols
        self.yxmap = None
        self.slab_shape = None
        self.img_shape = None
        self.geom_coffset = 0
        self.geom_pixsize = None
        self.im_out = None
        self.resolutionLambda = None
        self.pixRadiusToRes = None
        self.hkl = None
        self.hklLookup = None
        self.imodel.dataChanged.connect(self.draw)

        self.iview.view.menu.addSeparator()
        openGeomAct = self.iview.view.menu.addAction("Load CrystFEL Geometry")
        openGeomAct.triggered.connect(self.openGeom)

        peakmenu = self.iview.view.menu.addMenu("Peaks")
        self.peakActionGroup = QActionGroup(self)
        self.peakActionGroup.setExclusive(True)
        self.peakActionGroup.triggered.connect(self.drawPeaks)

        peakNone = peakmenu.addAction("None")
        peakNone.setCheckable(True)
        peakNone.setChecked(True)
        peakNone.setData(0)
        self.peakActionGroup.addAction(peakNone)

        peakCXI = peakmenu.addAction("CXI")
        peakCXI.setCheckable(True)
        peakCXI.setEnabled(self.dmodel.canSaveLst())
        peakCXI.setData(1)
        self.peakActionGroup.addAction(peakCXI)

        peakStream = peakmenu.addAction("Stream")
        peakStream.setCheckable(True)
        peakStream.setEnabled(self.dmodel.hasStreamPeaks())
        peakStream.setData(2)
        self.peakActionGroup.addAction(peakStream)
        self.peakCanvas = ScatterPlotItem()
        self.iview.getView().addItem(self.peakCanvas)

        reflectionmenu = self.iview.view.menu.addMenu("Reflections")
        self.reflectionActionGroup = QActionGroup(self)
        self.reflectionActionGroup.setExclusive(True)
        self.reflectionActionGroup.triggered.connect(self.drawReflections)

        refNone = reflectionmenu.addAction("None")
        refNone.setCheckable(True)
        refNone.setChecked(True)
        refNone.setData(0)
        self.reflectionActionGroup.addAction(refNone)

        refStream = reflectionmenu.addAction("Stream")
        refStream.setCheckable(True)
        refStream.setEnabled(self.dmodel.hasStreamReflections())
        refStream.setData(1)
        self.reflectionActionGroup.addAction(refStream)
        self.reflectionCanvas = ScatterPlotItem()
        self.iview.getView().addItem(self.reflectionCanvas)

        self.drawResRingsAct = self.iview.view.menu.addAction(
            "Resolution Rings")
        self.drawResRingsAct.setCheckable(True)
        self.drawResRingsAct.setChecked(False)
        self.drawResRingsAct.triggered.connect(self.drawResRings)
        self.drawResRingsAct.setEnabled(self.yxmap is not None)
        self.resolutionRingsCanvas = ScatterPlotItem()
        self.iview.getView().addItem(self.resolutionRingsCanvas)
        self.resRingsTextItems = []
        for x in self.dmodel.cfg.viewerResolutionRingsAngstroms:
            self.resRingsTextItems.append(TextItem('', anchor=(0.5, 0.8)))
            self.iview.getView().addItem(self.resRingsTextItems[-1])

        self.drawResolutionLimitAct = self.iview.view.menu.addAction(
            "Resolution Limit Ring")
        self.drawResolutionLimitAct.setCheckable(True)
        self.drawResolutionLimitAct.setChecked(False)
        self.drawResolutionLimitAct.triggered.connect(self.drawResLimitRing)
        self.drawResolutionLimitAct.setEnabled(
            self.yxmap is not None and 'reslim' in self.dmodel.cols)
        self.resolutionLimitCanvas = ScatterPlotItem()
        self.iview.getView().addItem(self.resolutionLimitCanvas)

        if geom is not None:
            self.loadGeom(geom)

        self.toolTipsAct = self.iview.view.menu.addAction(
            "Show Position in Tool Tip")
        self.toolTipsAct.setCheckable(True)
        self.toolTipsAct.setChecked(True)
        self.iview.scene.sigMouseMoved.connect(self.mouseMove)

        self.draw()

    def draw(self):
        if not self.canDraw or self.imodel.currow == self.lastrow:
            return

        # Load the image
        image = self.image()
        if image is None:
            return  # Problem with file, don't try to load anything else

        # Image was loaded, so update what we've currently got drawn
        self.lastrow = self.imodel.currow

        # Apply geometry
        if self.yxmap is None:
            image = np.transpose(image)
        else:
            image = cfel_geom.apply_geometry_from_pixel_maps(
                image, self.yxmap, self.im_out)
        self.iview.setImage(image, autoLevels=False, autoRange=False)

        # CXI View Scaling
        bottom, top = histogram_clip_levels(
            image.ravel(), self.dmodel.cfg.cxiviewHistClipLevelValue)
        self.iview.setLevels(bottom, top)
        self.iview.getHistogramWidget().setHistogramRange(
            min(bottom, self.dmodel.cfg.cxiviewHistMin),
            max(top, self.dmodel.cfg.cxiviewHistMax),
            padding=self.dmodel.cfg.cxiviewHistPadding)

        self.calcResLambda()
        self.drawPeaks()
        self.drawReflections()
        self.drawResRings()
        self.drawResLimitRing()

    def loadGeom(self, filename):
        self.yxmap, self.slab_shape, self.img_shape = cfel_geom.pixel_maps_for_image_view(
            filename)
        self.im_out = np.zeros(self.img_shape, dtype=np.dtype(float))
        self.geom_coffset = cfel_geom.coffset_from_geometry_file(filename)
        self.geom_pixsize = 1 / cfel_geom.res_from_geometry_file(filename)
        self.drawResRingsAct.setEnabled(True)
        self.drawResolutionLimitAct.setEnabled('reslim' in self.dmodel.cols)
        self.lastrow = -1
        self.draw()

    def openGeom(self):
        name = QFileDialog.getOpenFileName(
            self.iview,
            'Select CrystFEL Geomery File (.geom)',
            filter='*.geom')
        if qt5:
            if name:
                self.loadGeom(name[0])
        elif name is not None and len(name):
            self.loadGeom(name)

    def mouseMove(self, pos):
        mapped = self.iview.getView().mapSceneToView(pos)
        txt = ""
        if self.toolTipsAct.isChecked() and self.iview.image is not None \
                                        and mapped.x() >= 0 and mapped.x() < self.iview.image.shape[0] \
                                        and mapped.y() >= 0 and mapped.y() < self.iview.image.shape[1]:
            if self.img_shape is not None:
                x = mapped.x() - self.img_shape[0] / 2
                y = self.img_shape[1] / 2 - mapped.y()
            else:
                x = mapped.x()
                y = mapped.y()
            txt = "x: %i, y: %i" % (x, y)
            if self.iview.image is not None:
                txt += " value: %.2f" % self.iview.image[int(mapped.x()),
                                                         int(mapped.y())]
            if self.pixRadiusToRes is not None:
                txt += " resolution: %.2f" % self.pixRadiusToRes(
                    np.sqrt(x**2 + y**2))
            if self.hklLookup is not None:
                res = self.hklLookup.query([mapped.x(), mapped.y()], k=1)
                if len(res
                       ) > 0 and res[0] < self.dmodel.cfg.viewerReflectionSize:
                    txt += " hkl: %s" % self.hkl[res[1]]
        self.iview.setToolTip(txt)

    # Sub functions called by draw, not meant to be called externally
    def fromMaybeEvent(self, paths):
        r = None
        for path in paths:
            if path in self.curFile:
                if self.checkEvent:
                    e = self.dmodel.data["event"][self.imodel.currow]
                    if e == -1:  # No event
                        r = self.curFile[path]
                    else:
                        r = self.curFile[path][e]
                else:  # No events
                    r = self.curFile[path]
                break
        return r

    def image(self):
        image = None
        ifile = self.dmodel.value("ifile", self.imodel.currow, False)
        if ifile != self.curFileName:
            if self.curFile is not None:
                self.curFile.close()
            self.curFileName = ifile
            self.curFile = h5py.File(ifile, 'r')
        image = self.fromMaybeEvent(self.dmodel.cfg.imageH5paths)
        return image

    def cxipeaks(self):
        # Since this function should ideally only be called internally from draw,
        # after image has been successfully generated, assume we have correct and valid ifile.
        px = self.fromMaybeEvent(self.dmodel.cfg.peakXH5paths)
        if px is None:
            px = []
            py = []
        else:
            if len(px.shape) > 1:
                # Data is two dimension, assume x is first column and y second
                py = px[:, 1]
                px = px[:, 0]
            else:
                py = self.fromMaybeEvent(self.dmodel.cfg.peakYH5paths)
                if py is None:
                    px = []
                    py = []
        return px, py

    def drawPeaks(self):
        px = []
        py = []
        if self.peakActionGroup.checkedAction().data() == 1:  # H5 Peaks
            px, py = self.cxipeaks()
        elif self.peakActionGroup.checkedAction().data() == 2:  # Stream peaks
            px, py = self.dmodel.streamPeaks(self.imodel.currow)

        if self.yxmap is not None and len(px):
            px = np.array(px, dtype=np.dtype(int))
            py = np.array(py, dtype=np.dtype(int))
            slab = py * self.slab_shape[1] + px
            px = self.yxmap[0][slab]
            py = self.yxmap[1][slab]
        self.peakCanvas.setData(px,py,symbol=self.dmodel.cfg.viewerPeakSymbol,\
            size=self.dmodel.cfg.viewerPeakSize,pen=\
            mkPen(self.dmodel.cfg.viewerPeakColor,width=self.dmodel.cfg.viewerPeakPenWidth),\
            brush=(0,0,0,0),pxMode=False)

    def drawReflections(self):
        px = []
        py = []
        self.hkl = None
        self.hklLookup = None
        if self.reflectionActionGroup.checkedAction().data() == 1:  # Stream
            px, py, self.hkl = self.dmodel.streamReflections(
                self.imodel.currow)

        if self.yxmap is not None and len(px):
            px = np.array(px, dtype=np.dtype(int))
            py = np.array(py, dtype=np.dtype(int))
            slab = py * self.slab_shape[1] + px
            px = self.yxmap[0][slab]
            py = self.yxmap[1][slab]
        self.reflectionCanvas.setData(px,py,symbol=self.dmodel.cfg.viewerReflectionSymbol,\
            size=self.dmodel.cfg.viewerReflectionSize,pen=\
            mkPen(self.dmodel.cfg.viewerReflectionColor,width=self.dmodel.cfg.viewerReflectionPenWidth),\
            brush=(0,0,0,0),pxMode=False)
        if self.hkl is not None:
            self.hklLookup = scipy.spatial.cKDTree(np.dstack((px, py))[0])

    def calcResLambda(self):
        self.resolutionLambda = None

        photon_ev = None
        if "phoen" in self.dmodel.cols:
            photon_ev = self.dmodel.data["phoen"][self.imodel.currow]
        if photon_ev is None or photon_ev <= 0:
            photon_ev = self.fromMaybeEvent(
                self.dmodel.cfg.viewerPhotonEvH5Paths)

        clen = None
        if "aclen" in self.dmodel.cols:
            clen = self.dmodel.data["aclen"][
                self.imodel.currow]  # Value is corrected with coffset
        if clen is None:
            clen = self.fromMaybeEvent(
                self.dmodel.cfg.viewerCameraLengthH5Paths)
            if clen is not None:  # Uncorrected, correct here
                clen += self.geom_coffset

        # Need valid photon energy, camera length, and geometry (yxmap)
        if photon_ev is None or photon_ev <= 0 or clen is None or self.yxmap is None:
            return  # Not enough info

        lmbd = scipy.constants.h * scipy.constants.c / (scipy.constants.e *
                                                        photon_ev)
        self.resolutionLambda=lambda r : (2.0/self.geom_pixsize)*(clen)*np.tan(2.0*np.arcsin(lmbd / \
                    (2.0 * r *1e-10)))
        self.pixRadiusToRes = lambda r: 1e10 * lmbd / (2.0 * np.sin(
            0.5 * np.arctan(r * self.geom_pixsize / clen)))

    def drawResRings(self):
        if not self.drawResRingsAct.isChecked(
        ) or self.resolutionLambda is None:
            self.resolutionRingsCanvas.setData([], [])
            for ti in self.resRingsTextItems:
                ti.setText('')
        else:
            ring_sizes = self.resolutionLambda(
                np.array(self.dmodel.cfg.viewerResolutionRingsAngstroms))
            self.resolutionRingsCanvas.setData([self.img_shape[0]/2]*len(ring_sizes), [self.img_shape[1]/2]*len(ring_sizes),symbol='o',\
                size=ring_sizes,pen=mkPen(self.dmodel.cfg.viewerResRingColor, width=self.dmodel.cfg.viewerResRingWidth),\
                brush=(0,0,0,0),pxMode=False)
            for i, ti in enumerate(self.resRingsTextItems):
                ti.setText("%.1f A" %
                           self.dmodel.cfg.viewerResolutionRingsAngstroms[i],
                           color=self.dmodel.cfg.viewerResRingColor)
                ti.setPos(self.img_shape[0] / 2,
                          self.img_shape[1] / 2 + ring_sizes[i] / 2)

    def drawResLimitRing(self):
        if not self.drawResolutionLimitAct.isChecked(
        ) or self.resolutionLambda is None:
            self.resolutionLimitCanvas.setData([], [])
        else:
            r = self.dmodel.data['reslim'][self.imodel.currow]
            if r <= 0:
                self.resolutionLimitCanvas.setData([], [])
            else:
                r = 10 / r
                self.resolutionLimitCanvas.setData([self.img_shape[0]/2], [self.img_shape[1]/2],symbol='o',\
                    size=self.resolutionLambda(r),\
                    pen=mkPen(self.dmodel.cfg.viewerResLimitRingColor, width=self.dmodel.cfg.viewerResLimitRingWidth),\
                    brush=(0,0,0,0),pxMode=False)
Ejemplo n.º 6
0
class ToolsToolBar(QToolBar):

    tools_changed = pyqtSignal(QAction)

    class Tools(Enum):
        EraserTool = 1
        RectangleTool = 2
        PolygonTool = 3
        MagicTool = 4
        BrowserImageTool = 5
        MoveImageTool = 6

    MoveImageTool = Tools.MoveImageTool
    BrowserImageTool = Tools.BrowserImageTool
    EraserTool = Tools.EraserTool
    RectangleTool = Tools.RectangleTool
    PolygonTool = Tools.PolygonTool
    MagicTool = Tools.MagicTool
    del Tools

    selection_option_changed = pyqtSignal(QAction)
    eraser_size_changed = pyqtSignal(int)
    eraser_option_changed = pyqtSignal(QAction)

    def __init__(self, parent=None):
        super(ToolsToolBar, self).__init__(parent)

        self.setObjectName("ToolsTooBar")
        self.setWindowTitle("Tools Toolbar")
        self.setToolButtonStyle(Qt.ToolButtonFollowStyle)
        parent.addToolBar(self)

        self._browser_result_tool = QAction()
        self._eraser_tool = QAction()
        self._rectangle_tool = ActionManager.action(Id("Rectangle"))
        self._polygon_tool = ActionManager.action(Id("Polygon"))
        self._magic_tool = QAction()

        self._browser_result_tool.setIcon(QIcon(":/plugin.png"))
        self._browser_result_tool.setCheckable(True)
        self._browser_result_tool.setToolTip("预览结果")
        self._browser_result_tool.setData(ToolsToolBar.BrowserImageTool)

        self._eraser_tool.setIcon(QIcon(":/stock-tool-eraser.png"))
        self._eraser_tool.setCheckable(True)
        self._eraser_tool.setToolTip("橡皮擦")
        self._eraser_tool.setData(ToolsToolBar.EraserTool)

        self._rectangle_tool.setIcon(QIcon(":/stock-tool-rect-select.png"))
        self._rectangle_tool.setCheckable(True)
        self._rectangle_tool.setChecked(True)
        self._rectangle_tool.setToolTip("矩形选择框")
        self._rectangle_tool.setData(ToolsToolBar.RectangleTool)

        self._polygon_tool.setIcon(QIcon(":/tool-edit-polygons.png"))
        self._polygon_tool.setCheckable(True)
        self._polygon_tool.setToolTip("多边形选择框")
        self._polygon_tool.setData(ToolsToolBar.PolygonTool)

        self._magic_tool.setIcon(QIcon(":/stock-tool-fuzzy-select-22.png"))
        self._magic_tool.setCheckable(True)
        self._magic_tool.setToolTip("魔法棒")
        self._magic_tool.setData(ToolsToolBar.MagicTool)

        self._tools_group = QActionGroup(self)
        self._tools_group.addAction(self._eraser_tool)
        self._tools_group.addAction(self._rectangle_tool)
        self._tools_group.addAction(self._polygon_tool)
        self._tools_group.addAction(self._magic_tool)
        self._tools_group.addAction(self._browser_result_tool)

        self.addAction(self._browser_result_tool)
        self.addAction(self._eraser_tool)
        self.addAction(self._rectangle_tool)
        self.addAction(self._polygon_tool)
        self.addAction(self._magic_tool)

        self._tools_group.triggered.connect(self.checked_action_changed)
        self._tools_group.triggered.connect(self.tools_changed)
        self.__register()

        self._selection_option_toolbar = SelectionOptionToolBar(self)
        self._eraser_option_toolbar = EraserOptionToolBar(self)
        self.parent().addToolBar(self._selection_option_toolbar)
        self.parent().addToolBar(self._eraser_option_toolbar)
        self._eraser_option_toolbar.setHidden(True)

        self._selection_option_toolbar.selection_option_changed.connect(
            self.selection_option_changed)
        self._eraser_option_toolbar.eraser_size_changed.connect(
            self.eraser_size_changed)

    def checked_action_changed(self, action: QAction):
        if action == self._rectangle_tool or action == self._polygon_tool:
            # self._selection_toolbar.setEnabled(True)
            self._eraser_option_toolbar.setHidden(True)
            self._selection_option_toolbar.setHidden(False)
            self._selection_option_toolbar.setEnabled(True)
        elif action == self._eraser_tool:
            self._eraser_option_toolbar.setHidden(False)
            self._selection_option_toolbar.setHidden(True)
            self._eraser_option_toolbar.setEnabled(True)
        else:
            self._selection_option_toolbar.setHidden(True)
            self._eraser_option_toolbar.setHidden(True)

    def selection_toolbar(self):
        return self._selection_option_toolbar

    def eraser_toolbar(self):
        return self._eraser_option_toolbar

    def eraser_toolbar(self):
        return self._eraser_option_toolbar

    def setHidden(self, hidden: bool) -> None:
        if hidden:
            self._eraser_option_toolbar.setHidden(hidden)
            self._selection_option_toolbar.setHidden(hidden)
        else:
            if self._tools_group.checkedAction() == self._rectangle_tool:
                self._selection_option_toolbar.setHidden(False)
            elif self._tools_group.checkedAction() == self._eraser_tool:
                self._eraser_option_toolbar.setHidden(False)
        QToolBar.setHidden(self, hidden)

    def current_tools(self):
        try:
            return self._tools_group.checkedAction().data()
        except Exception as e:
            print(e)
            return SelectionOptionToolBar.Replace

    def __register(self):
        ActionManager.register_action(self._eraser_tool, Id("橡皮擦"))
        ActionManager.register_action(self._rectangle_tool, Id("矩形选择框"))
        ActionManager.register_action(self._eraser_tool, Id("多边形选择框"))
        ActionManager.register_action(self._eraser_tool, Id("魔法棒"))
Ejemplo n.º 7
0
class Gui(QMainWindow, Ui_MainWindow):
    NOTEON = 0x9
    NOTEOFF = 0x8
    MIDICTRL = 11

    GREEN = ("#cell_frame { border: 0px; border-radius: 10px; "
             "background-color: rgb(125,242,0);}")
    BLUE = ("#cell_frame { border: 0px; border-radius: 10px; "
            "background-color: rgb(0, 130, 240);}")
    RED = ("#cell_frame { border: 0px; border-radius: 10px; "
           "background-color: rgb(255, 21, 65);}")
    AMBER = ("#cell_frame { border: 0px; border-radius: 10px; "
             "background-color: rgb(255, 102, 0);}")
    PURPLE = ("#cell_frame { border: 0px; border-radius: 10px; "
              "background-color: rgb(130, 0, 240);}")
    DEFAULT = ("#cell_frame { border: 0px; border-radius: 10px; "
               "background-color: rgb(217, 217, 217);}")

    RECORD_BLINK = ("QPushButton {background-color: rgb(255, 255, 255);}"
                    "QPushButton:pressed {background-color: "
                    "rgb(98, 98, 98);}")

    RECORD_DEFAULT = ("QPushButton {background-color: rgb(0, 0, 0);}"
                      "QPushButton:pressed {background-color: "
                      "rgb(98, 98, 98);}")

    STATE_COLORS = {Clip.STOP: RED,
                    Clip.STARTING: GREEN,
                    Clip.START: GREEN,
                    Clip.STOPPING: RED,
                    Clip.PREPARE_RECORD: AMBER,
                    Clip.RECORDING: AMBER}
    STATE_BLINK = {Clip.STOP: False,
                   Clip.STARTING: True,
                   Clip.START: False,
                   Clip.STOPPING: True,
                   Clip.PREPARE_RECORD: True,
                   Clip.RECORDING: False}

    BLINK_DURATION = 200
    PROGRESS_PERIOD = 300

    ADD_PORT_LABEL = 'Add new Port...'

    updateUi = pyqtSignal()
    readQueueIn = pyqtSignal()
    updatePorts = pyqtSignal()
    songLoad = pyqtSignal()

    def __init__(self, song, jack_client):
        QObject.__init__(self)
        super(Gui, self).__init__()
        self._jack_client = jack_client
        self.setupUi(self)
        self.clip_volume.knobRadius = 3
        self.is_learn_device_mode = False
        self.queue_out, self.queue_in = Queue(), Queue()
        self.updateUi.connect(self.update)
        self.readQueueIn.connect(self.readQueue)
        self.current_vol_block = 0
        self.last_clip = None

        # Load devices
        self.deviceGroup = QActionGroup(self.menuDevice)
        self.devices = []
        device_settings = QSettings('superboucle', 'devices')
        if ((device_settings.contains('devices')
             and device_settings.value('devices'))):
            for raw_device in device_settings.value('devices'):
                self.devices.append(Device(pickle.loads(raw_device)))
        else:
            self.devices.append(Device({'name': 'No Device',}))
        self.updateDevices()
        self.deviceGroup.triggered.connect(self.onDeviceSelect)

        self.settings = QSettings('superboucle', 'session')
        # Qsetting appear to serialize empty lists as @QInvalid
        # which is then read as None :(

        # Load playlist
        self.playlist = self.settings.value('playlist', []) or []
        # Load paths
        self.paths_used = self.settings.value('paths_used', {})

        self.auto_connect = self.settings.value('auto_connect',
                                                'true') == "true"

        # Load song
        self.port_by_name = {}
        self.initUI(song)

        self.actionNew.triggered.connect(self.onActionNew)
        self.actionOpen.triggered.connect(self.onActionOpen)
        self.actionSave.triggered.connect(self.onActionSave)
        self.actionSave_As.triggered.connect(self.onActionSaveAs)
        self.actionAdd_Device.triggered.connect(self.onAddDevice)
        self.actionManage_Devices.triggered.connect(self.onManageDevice)
        self.actionPlaylist_Editor.triggered.connect(self.onPlaylistEditor)
        self.actionScene_Manager.triggered.connect(self.onSceneManager)
        self.actionPort_Manager.triggered.connect(self.onPortManager)
        self.actionFullScreen.triggered.connect(self.onActionFullScreen)
        self.master_volume.valueChanged.connect(self.onMasterVolumeChange)
        self.bpm.valueChanged.connect(self.onBpmChange)
        self.beat_per_bar.valueChanged.connect(self.onBeatPerBarChange)
        self.rewindButton.clicked.connect(self.onRewindClicked)
        self.playButton.clicked.connect(self._jack_client.transport_start)
        self.pauseButton.clicked.connect(self._jack_client.transport_stop)
        self.gotoButton.clicked.connect(self.onGotoClicked)
        self.recordButton.clicked.connect(self.onRecord)
        self.clip_name.textChanged.connect(self.onClipNameChange)
        self.clip_volume.valueChanged.connect(self.onClipVolumeChange)
        self.beat_diviser.valueChanged.connect(self.onBeatDiviserChange)
        self.output.activated.connect(self.onOutputChange)
        self.mute_group.valueChanged.connect(self.onMuteGroupChange)
        self.frame_offset.valueChanged.connect(self.onFrameOffsetChange)
        self.beat_offset.valueChanged.connect(self.onBeatOffsetChange)
        self.revertButton.clicked.connect(self.onRevertClip)
        self.normalizeButton.clicked.connect(self.onNormalizeClip)
        self.exportButton.clicked.connect(self.onExportClip)
        self.deleteButton.clicked.connect(self.onDeleteClipClicked)

        self.blktimer = QTimer()
        self.blktimer.state = False
        self.blktimer.timeout.connect(self.toggleBlinkButton)
        self.blktimer.start(self.BLINK_DURATION)

        self.disptimer = QTimer()
        self.disptimer.start(self.PROGRESS_PERIOD)
        self.disptimer.timeout.connect(self.updateProgress)

        self._jack_client.set_timebase_callback(self.timebase_callback)
        self.show()

    def initUI(self, song):

        # remove old buttons
        self.btn_matrix = [[None for y in range(song.height)]
                           for x in range(song.width)]
        self.state_matrix = [[-1 for y in range(song.height)]
                             for x in range(song.width)]

        for i in reversed(range(self.gridLayout.count())):
            self.gridLayout.itemAt(i).widget().close()
            self.gridLayout.itemAt(i).widget().setParent(None)

        # first pass without removing old ports
        self.updateJackPorts(song, remove_ports=False)
        self.song = song
        # second pass with removing
        self.updateJackPorts(song, remove_ports=True)

        self.frame_clip.setEnabled(False)
        self.output.clear()
        self.output.addItems(song.outputsPorts)
        self.output.addItem(Gui.ADD_PORT_LABEL)
        self.master_volume.setValue(song.volume * 256)
        self.bpm.setValue(song.bpm)
        self.beat_per_bar.setValue(song.beat_per_bar)
        for x in range(song.width):
            for y in range(song.height):
                clip = song.clips_matrix[x][y]
                cell = Cell(self, clip, x, y)
                self.btn_matrix[x][y] = cell
                self.gridLayout.addWidget(cell, y, x)

        # send init command
        for init_cmd in self.device.init_command:
            self.queue_out.put(init_cmd)

        self.setWindowTitle("Super Boucle - {}"
                            .format(song.file_name or "Empty Song"))

        if self.song.initial_scene in self.song.scenes:
            self.song.loadScene(self.song.initial_scene)
        self.update()
        self.songLoad.emit()

        timer = QTimer()
        timer.singleShot(1000,self.send_clip_state_feedback)

    def openSongFromDisk(self, file_name):
        self._jack_client.transport_stop()
        self._jack_client.transport_locate(0)

        self.setEnabled(False)
        message = QMessageBox(self)
        message.setWindowTitle("Loading ....")
        message.setText("Reading Files, please wait ...")
        message.show()
        self.initUI(load_song_from_file(file_name))
        message.close()
        self.setEnabled(True)

    def closeEvent(self, event):
        device_settings = QSettings('superboucle', 'devices')
        device_settings.setValue('devices',
                                 [pickle.dumps(x.mapping)
                                  for x in self.devices])
        self.settings.setValue('playlist', self.playlist)
        self.settings.setValue('paths_used', self.paths_used)
        self.settings.setValue('auto_connect', self.auto_connect)

    def onStartStopClicked(self):
        clip = self.sender().parent().parent().clip
        self.startStop(clip.x, clip.y)

    def startStop(self, x, y):
        clip = self.btn_matrix[x][y].clip
        if clip is None:
            return
        if self.song.is_record:
            self.song.is_record = False
            self.updateRecordBtn()
            # calculate buffer size
            state, position = self._jack_client.transport_query()
            bps = position['beats_per_minute'] / 60
            fps = position['frame_rate']
            size = (1 / bps) * clip.beat_diviser * fps
            self.song.init_record_buffer(clip, 2, size, fps)
            # set frame offset based on jack block size
            clip.frame_offset = self._jack_client.blocksize
            clip.state = Clip.PREPARE_RECORD
            self.recordButton.setStyleSheet(self.RECORD_DEFAULT)
        else:
            self.song.toggle(clip.x, clip.y)
        self.update()

    def onEdit(self):
        self.last_clip = self.sender().parent().parent().clip
        if self.last_clip:
            self.frame_clip.setEnabled(True)
            self.clip_name.setText(self.last_clip.name)
            self.frame_offset.setValue(self.last_clip.frame_offset)
            self.beat_offset.setValue(self.last_clip.beat_offset)
            self.beat_diviser.setValue(self.last_clip.beat_diviser)
            self.output.setCurrentText(self.last_clip.output)
            self.mute_group.setValue(self.last_clip.mute_group)
            self.clip_volume.setValue(self.last_clip.volume * 256)
            state, position = self._jack_client.transport_query()
            fps = position['frame_rate']
            bps = self.bpm.value() / 60
            if self.bpm.value() and fps:
                size_in_beat = (bps / fps) * self.song.length(self.last_clip)
            else:
                size_in_beat = "No BPM info"
            clip_description = ("Size in sample : %s\nSize in beat : %s"
                                % (self.song.length(self.last_clip),
                                   round(size_in_beat, 1)))

            self.clip_description.setText(clip_description)

    def onAddClipClicked(self):
        cell = self.sender().parent().parent()
        if QApplication.keyboardModifiers() == Qt.ControlModifier:
            cell.setClip(cell.openClip())
        else:
            AddClipDialog(self, cell)

    def onRevertClip(self):
        if self.last_clip and self.last_clip.audio_file:
            audio_file = self.last_clip.audio_file
            self.song.data[audio_file] = self.song.data[audio_file][::-1]

    def onNormalizeClip(self):
        if self.last_clip and self.last_clip.audio_file:
            audio_file = self.last_clip.audio_file
            absolute_val = np.absolute(self.song.data[audio_file])
            current_level = np.ndarray.max(absolute_val)
            self.song.data[audio_file][:] *= (1 / current_level)

    def onExportClip(self):
        if self.last_clip and self.last_clip.audio_file:
            audio_file = self.last_clip.audio_file
            file_name, a = self.getSaveFileName(
                'Export Clip : %s' % self.last_clip.name, 'WAVE (*.wav)')

            if file_name:
                file_name = verify_ext(file_name, 'wav')
                sf.write(self.song.data[audio_file], file_name,
                         self.song.samplerate[audio_file],
                         subtype=sf.default_subtype('WAV'),
                         format='WAV')

    def onDeleteClipClicked(self):
        if self.last_clip:
            response = QMessageBox.question(self,
                                            "Delete Clip ?",
                                            ("Are you sure "
                                             "to delete the clip ?"))
            if response == QMessageBox.Yes:
                self.frame_clip.setEnabled(False)
                self.song.removeClip(self.last_clip)
                self.initUI(self.song)

    def onMasterVolumeChange(self):
        self.song.volume = (self.master_volume.value() / 256)

    def onBpmChange(self):
        self.song.bpm = self.bpm.value()

    def onBeatPerBarChange(self):
        self.song.beat_per_bar = self.beat_per_bar.value()

    def onGotoClicked(self):
        state, position = self._jack_client.transport_query()
        new_position = (position['beats_per_bar']
                        * (self.gotoTarget.value() - 1)
                        * position['frame_rate']
                        * (60 / position['beats_per_minute']))
        self._jack_client.transport_locate(int(round(new_position, 0)))

    def onRecord(self):
        self.song.is_record = not self.song.is_record
        self.updateRecordBtn()

    def updateRecordBtn(self):
        if not self.song.is_record:
            self.recordButton.setStyleSheet(self.RECORD_DEFAULT)
        if self.device.record_btn:
            (msg_type, channel, pitch, velocity) = self.device.record_btn
            if self.song.is_record:
                color = self.device.blink_amber_vel
            else:
                color = self.device.black_vel
            self.queue_out.put(((msg_type << 4) + channel, pitch, color))

    def onRewindClicked(self):
        self._jack_client.transport_locate(0)

    def onClipNameChange(self):
        self.last_clip.name = self.clip_name.text()
        cell = self.btn_matrix[self.last_clip.x][self.last_clip.y]
        cell.clip_name.setText(self.last_clip.name)

    def onClipVolumeChange(self):
        self.last_clip.volume = (self.clip_volume.value() / 256)

    def onBeatDiviserChange(self):
        self.last_clip.beat_diviser = self.beat_diviser.value()

    def onOutputChange(self):
        new_port = self.output.currentText()
        if new_port == Gui.ADD_PORT_LABEL:
            AddPortDialog(self)
        else:
            self.last_clip.output = new_port

    def addPort(self, name):
        self.song.outputsPorts.add(name)
        self.updateJackPorts(self.song)
        if self.output.findText(name) == -1:
            self.output.insertItem(self.output.count() - 1, name)
        if self.last_clip:
            self.last_clip.output = name
            self.output.setCurrentText(name)

    def removePort(self, name):
        if name != Clip.DEFAULT_OUTPUT:
            self.song.outputsPorts.remove(name)
            for c in self.song.clips:
                if c.output == name:
                    c.output = Clip.DEFAULT_OUTPUT
            self.updateJackPorts(self.song)
            self.output.removeItem(self.output.findText(name))
            if self.last_clip:
                self.output.setCurrentText(self.last_clip.output)

    def updateJackPorts(self, song, remove_ports=True):
        '''Update jack port based on clip output settings
        update dict containing ports with shortname as key'''

        current_ports = set()
        for port in self._jack_client.outports:
            current_ports.add(port.shortname)

        wanted_ports = set()
        for port_basename in song.outputsPorts:
            for ch in Song.CHANNEL_NAMES:
                port = Song.CHANNEL_NAME_PATTERN.format(port=port_basename,
                                                        channel=ch)
                wanted_ports.add(port)

        # remove unwanted ports
        if remove_ports:
            port_to_remove = []
            for port in self._jack_client.outports:
                if port.shortname not in wanted_ports:
                    current_ports.remove(port.shortname)
                    port_to_remove.append(port)
            for port in port_to_remove:
                port.unregister()

        # create new ports
        for new_port_name in wanted_ports - current_ports:
            self._jack_client.outports.register(new_port_name)

        self.port_by_name = {port.shortname: port
                             for port in self._jack_client.outports}

        self.updatePorts.emit()

    def onMuteGroupChange(self):
        self.last_clip.mute_group = self.mute_group.value()

    def onFrameOffsetChange(self):
        self.last_clip.frame_offset = self.frame_offset.value()

    def onBeatOffsetChange(self):
        self.last_clip.beat_offset = self.beat_offset.value()

    def onActionNew(self):
        NewSongDialog(self)

    def getOpenFileName(self, title, file_type, parent=None,
                        dialog=QFileDialog.getOpenFileName):
        path = self.paths_used.get(file_type, expanduser('~'))
        file_name, a = dialog(parent or self, title, path, file_type)
        if a and file_name:
            if isinstance(file_name, list):
                self.paths_used[file_type] = dirname(file_name[0])
            else:
                self.paths_used[file_type] = dirname(file_name)
        return file_name, a

    def getSaveFileName(self, *args):
        return self.getOpenFileName(*args, dialog=QFileDialog.getSaveFileName)

    def onActionOpen(self):
        file_name, a = self.getOpenFileName('Open Song',
                                            'Super Boucle Song (*.sbs)')
        if a and file_name:
            self.openSongFromDisk(file_name)

    def onActionSave(self):
        if self.song.file_name:
            self.song.save()
        else:
            self.onActionSaveAs()

    def onActionSaveAs(self):
        file_name, a = self.getSaveFileName('Save Song',
                                            'Super Boucle Song (*.sbs)')

        if file_name:
            file_name = verify_ext(file_name, 'sbs')
            self.song.file_name = file_name
            self.song.save()
            print("File saved to : {}".format(self.song.file_name))

    def onAddDevice(self):
        self.learn_device = LearnDialog(self, self.addDevice)
        self.is_learn_device_mode = True

    def onManageDevice(self):
        ManageDialog(self)

    def onPlaylistEditor(self):
        PlaylistDialog(self)

    def onSceneManager(self):
        SceneManager(self)

    def onPortManager(self):
        PortManager(self)

    def onActionFullScreen(self):
        if self.isFullScreen():
            self.showNormal()
        else:
            self.showFullScreen()
        self.show()

    def send_clip_state_feedback(self):
        for x in range(self.song.width):
            for y in range(self.song.height):
                clip = self.song.clips_matrix[x][y]
                state = clip.state if clip else None
                self._update_clip_state(x, y, state)

    def _update_clip_state(self, x, y, state):
        clip = self.song.clips_matrix[x][y]
        if clip:
            self.btn_matrix[x][y].setColor(state)
        try:
            self.queue_out.put(self.device.generateNote(x, y, state))
        except IndexError:
            # print("No cell associated to %s x %s"
            # % (clp.x, clp.y))
            pass
        self.state_matrix[x][y] = state

    def update(self):
        for x in range(len(self.song.clips_matrix)):
            line = self.song.clips_matrix[x]
            for y in range(len(line)):
                clp = line[y]
                if clp is None:
                    state = None
                else:
                    state = clp.state
                if state != self.state_matrix[x][y]:
                    self._update_clip_state(x, y, state)

    def redraw(self):
        self.state_matrix = [[-1 for x in range(self.song.height)]
                             for x in range(self.song.width)]
        self.update()

    def readQueue(self):
        try:
            while True:
                note = self.queue_in.get(block=False)
                if len(note) == 3:
                    status, pitch, vel = struct.unpack('3B', note)
                    channel = status & 0xF
                    msg_type = status >> 4
                    self.processNote(msg_type, channel, pitch, vel)
                    # else:
                    # print("Invalid message length")
        except Empty:
            pass

    def processNote(self, msg_type, channel, pitch, vel):

        btn_id = (msg_type,
                  channel,
                  pitch,
                  vel)
        btn_id_vel = (msg_type, channel, pitch, -1)
        ctrl_key = (msg_type, channel, pitch)

        # master volume
        if ctrl_key == self.device.master_volume_ctrl:
            self.song.master_volume = vel / 127
            (self.master_volume
             .setValue(self.song.master_volume * 256))
        elif self.device.play_btn in [btn_id, btn_id_vel]:
            self._jack_client.transport_start()
        elif self.device.pause_btn in [btn_id, btn_id_vel]:
            self._jack_client.transport_stop()
        elif self.device.rewind_btn in [btn_id, btn_id_vel]:
            self.onRewindClicked()
        elif self.device.goto_btn in [btn_id, btn_id_vel]:
            self.onGotoClicked()
        elif self.device.record_btn in [btn_id, btn_id_vel]:
            self.onRecord()
        elif ctrl_key in self.device.ctrls:
            try:
                ctrl_index = self.device.ctrls.index(ctrl_key)
                clip = (self.song.clips_matrix
                        [ctrl_index]
                        [self.current_vol_block])
                if clip:
                    clip.volume = vel / 127
                    if self.last_clip == clip:
                        self.clip_volume.setValue(self.last_clip.volume * 256)
            except KeyError:
                pass
        elif (btn_id in self.device.scene_buttons
              or btn_id_vel in self.device.scene_buttons):
            try:
                scene_id = self.device.scene_buttons.index(btn_id)
            except ValueError:
                scene_id = self.device.scene_buttons.index(btn_id_vel)

            try:
                self.song.loadSceneId(scene_id)
                self.update()
            except IndexError:
                print('cannot load scene {} - there are only {} scenes.'
                      ''.format(scene_id, len(self.song.scenes)))

        elif (btn_id in self.device.block_buttons
              or btn_id_vel in self.device.block_buttons):
            try:
                self.current_vol_block = (
                    self.device.block_buttons.index(btn_id))
            except ValueError:
                self.current_vol_block = (
                    self.device.block_buttons.index(btn_id_vel))
            for i in range(len(self.device.block_buttons)):
                (a, b_channel, b_pitch, b) = self.device.block_buttons[i]
                if i == self.current_vol_block:
                    color = self.device.red_vel
                else:
                    color = self.device.black_vel
                self.queue_out.put(((self.NOTEON << 4) + b_channel,
                                    b_pitch,
                                    color))
        else:
            x, y = -1, -1
            try:
                x, y = self.device.getXY(btn_id)
            except IndexError:
                pass
            except KeyError:
                try:
                    x, y = self.device.getXY(btn_id_vel)
                except KeyError:
                    pass

            if (x >= 0 and y >= 0):
                self.startStop(x, y)

    def toggleBlinkButton(self):
        for line in self.btn_matrix:
            for btn in line:
                if btn.blink:
                    if self.blktimer.state:
                        btn.setStyleSheet(btn.color)
                    else:
                        btn.setStyleSheet(self.DEFAULT)
        if self.song.is_record:
            if self.blktimer.state:
                self.recordButton.setStyleSheet(self.RECORD_BLINK)
            else:
                self.recordButton.setStyleSheet(self.RECORD_DEFAULT)

        self.blktimer.state = not self.blktimer.state

    def updateProgress(self):
        state, pos = self._jack_client.transport_query()
        if 'bar' in pos:
            bbt = "%d|%d|%03d" % (pos['bar'], pos['beat'], pos['tick'])
        else:
            bbt = "-|-|-"
        seconds = int(pos['frame'] / pos['frame_rate'])
        (minutes, second) = divmod(seconds, 60)
        (hour, minute) = divmod(minutes, 60)
        time = "%d:%02d:%02d" % (hour, minute, second)
        self.bbtLabel.setText("%s\n%s" % (bbt, time))
        for line in self.btn_matrix:
            for btn in line:
                if btn.clip and btn.clip.audio_file:
                    value = ((btn.clip.last_offset
                              / self.song.length(btn.clip))
                             * 97)
                    btn.clip_position.setValue(value)
                    btn.clip_position.repaint()

    def updateDevices(self):
        for action in self.deviceGroup.actions():
            self.deviceGroup.removeAction(action)
            self.menuDevice.removeAction(action)
        for device in self.devices:
            action = QAction(device.name, self.menuDevice)
            action.setCheckable(True)
            action.setData(device)
            self.menuDevice.addAction(action)
            self.deviceGroup.addAction(action)
        action.setChecked(True)
        self.device = device

    def addDevice(self, device):
        self.devices.append(device)
        self.updateDevices()
        self.is_learn_device_mode = False

    def onDeviceSelect(self):
        self.device = self.deviceGroup.checkedAction().data()
        if self.device:
            if self.device.init_command:
                for note in self.device.init_command:
                    self.queue_out.put(note)
            self.redraw()

    def timebase_callback(self, state, nframes, pos, new_pos):
        if pos.frame_rate == 0:
            return None
        pos.valid = 0x10
        pos.bar_start_tick = BAR_START_TICK
        pos.beats_per_bar = self.beat_per_bar.value()
        pos.beat_type = BEAT_TYPE
        pos.ticks_per_beat = TICKS_PER_BEAT
        pos.beats_per_minute = self.bpm.value()
        ticks_per_second = (pos.beats_per_minute *
                            pos.ticks_per_beat) / 60
        ticks = (ticks_per_second * pos.frame) / pos.frame_rate
        (beats, pos.tick) = divmod(int(round(ticks, 0)),
                                   int(round(pos.ticks_per_beat, 0)))
        (bar, beat) = divmod(beats, int(round(pos.beats_per_bar, 0)))
        (pos.bar, pos.beat) = (bar + 1, beat + 1)
        return None
Ejemplo n.º 8
0
class MainWindow(QMainWindow):
    sig_chatmessage_added = pyqtSignal(object)
    sig_avatar_loaded = pyqtSignal(str, object)

    def __init__(self, path_to_logs, trayicon):
        """ systems = list of system-objects creted by dotlan.py
        """
        QMainWindow.__init__(self)
        uic.loadUi(resource_path('vi/ui/MainWindow.ui'), self)
        self.setWindowTitle("Vintel " + VERSION)
        self.setWindowIcon(
            QtGui.QIcon(resource_path("vi/ui/res/logo_small.png")))
        self.path_to_logs = path_to_logs
        self.trayicon = trayicon
        self.trayicon.activated.connect(self.systray_activated)
        c = Cache()
        regionname = c.get_from_cache("regionname")
        if not regionname:
            regionname = "Providence"
        # is it a local map?
        svg = None
        try:
            with open(
                    resource_path("vi/ui/res/mapdata/{0}.svg".format(
                        regionname))) as svg_file:
                svg = svg_file.read()
        except Exception as e:
            pass

        try:
            self.dotlan = dotlan.Map(regionname, svg)
        except dotlan.DotlanException as e:
            QMessageBox.critical(None, "Error getting map", str(e),
                                 QMessageBox.Close)
            sys.exit(1)

        if self.dotlan.outdated_cache_error:
            e = self.dotlan.outdated_cache_error
            diatext = "I tried to get and process the data for the map "\
                "but something went wrong. To proceed I use the data I "\
                "have in my cache. This could be outdated.\nIf this problem "\
                "is permanent, there might be a change in the dotlan data "\
                "and VINTEL must be modified. Check for a newer version "\
                "and inform the maintainer.\n\nWhat went wrong: {0} {1}"\
                .format(type(e), str(e))
            QMessageBox.warning(None, "Using map from my cache", diatext,
                                QMessageBox.Ok)

        jumpbridge_url = c.get_from_cache("jumpbridge_url")
        self.set_jumpbridges(jumpbridge_url)
        self.init_map_position = None  # we read this after first rendering
        # self.systems = self.dotlan.systems
        self.chatentries = []

        self.kos_request_thread = KOSCheckerThread()
        self.kos_request_thread.kos_result.connect(self.show_kos_result)
        self.kos_request_thread.start()

        self.avatar_find_thread = AvatarFindThread()
        self.avatar_find_thread.avatar_update.connect(
            self.update_avatar_on_chatentry)
        self.avatar_find_thread.start()

        self.clipboard = QApplication.clipboard()
        self.clipboard.clear(mode=self.clipboard.Clipboard)
        self.old_clipboard_content = (0, "")
        self.clipboard.changed.connect(self.clipboard_changed)

        self.zoomin.clicked.connect(self.zoomMapIn)
        self.zoomout.clicked.connect(self.zoomMapOut)
        self.actionStatistics.clicked.connect(
            self.dotlan.change_statistics_visibility)
        self.chat_large.clicked.connect(self.chat_larger)
        self.chat_small.clicked.connect(self.chat_smaller)
        self.jumpBridgesButton.clicked.connect(self.change_jumpbridge_view)
        self.sound_button.clicked.connect(self.show_sound_setup)

        self.actionInfo.triggered.connect(self.show_info)
        self.actionShow_Chat_Avatars.triggered.connect(
            self.change_show_avatars)
        self.actionAlways_on_top.triggered.connect(self.change_always_on_top)
        self.choose_chatrooms_button.triggered.connect(
            self.show_chatroom_chooser)
        self.choose_region_button.triggered.connect(self.show_region_chooser)
        self.action_show_chat.triggered.connect(self.change_chat_visibility)
        self.actionSound_Setup.triggered.connect(self.show_sound_setup)

        self.opacity_group = QActionGroup(self.menu)
        for i in (100, 80, 60, 40, 20):
            action = QAction("Opacity {0}%".format(i), None, checkable=True)

            if i == 100:
                action.setChecked(True)

            action.opacity = i / 100.0
            action.triggered.connect(self.change_opacity)

            self.opacity_group.addAction(action)
            self.menuTransparency.addAction(action)

        # map with menu =======================================================
        self.custom_content_page = MainWindowPage()
        self.custom_content_page.sig_link_clicked.connect(
            self.map_link_clicked)
        self.map.setPage(self.custom_content_page)
        self.map.page().set_svg(self.dotlan.svg_clean)

        self.map.contextmenu = TrayContextMenu(self.trayicon)

        def map_contextmenu_event(event):
            self.map.contextmenu.exec_(
                self.mapToGlobal(QPoint(event.x(), event.y())))

        self.map.contextMenuEvent = map_contextmenu_event
        # self.map.connect(self.map, Qt.SIGNAL("linkClicked(const QUrl&)"), self.map_link_clicked)
        # self.map.page().linkClicked.connect(self.map_link_clicked)
        # http://stackoverflow.com/questions/40747827/qwebenginepage-disable-links
        # end map =============================================================

        self.filewatcher_thread = filewatcher.FileWatcher(
            self.path_to_logs, 60 * 60 * 24)
        # self.connect(self.filewatcher_thread, QtCore.SIGNAL("fchange"), self.logfile_changed)
        self.filewatcher_thread.fchange.connect(self.logfile_changed)
        self.filewatcher_thread.start()

        if False:
            self.last_statistics_update = 0
            self.maptimer = QtCore.QTimer(self)
            # self.connect(self.maptimer, QtCore.SIGNAL("timeout()"), self.update_map)
            self.maptimer.timeout.connect(self.update_map)
            self.maptimer.start(1000)

        self.evetimer = QtCore.QTimer(self)
        # self.connect(self.maptimer, QtCore.SIGNAL("timeout()"), self.update_map)
        self.evetimer.timeout.connect(self.update_evetime)
        self.evetimer.start(1000)

        self.trayicon.sig_alarm_distance.connect(self.change_alarm_distance)
        self.trayicon.sig_change_frameless.connect(self.change_frameless)

        self.frameButton.clicked.connect(self.change_frameless)
        self.frameButton.setVisible(False)

        self.btn_night_mode.clicked.connect(self.toggle_nightmode)
        self.btn_night_mode.setCheckable(True)
        # self.btn_night_mode.setChecked(True)

        self.actionFrameless_Window.triggered.connect(self.change_frameless)

        self.is_frameless = None  # we need this because 2 places to change
        self.alarm_distance = 0
        self.actionActivate_Sound.triggered.connect(self.change_sound)

        if not sound.SOUND_AVAILABLE:
            self.change_sound(disable=True)
        else:
            self.change_sound()

        self.jumpbridgedata_button.triggered.connect(
            self.show_jumbridge_chooser)

        # load something from cache =====================================
        self.known_playernames = c.get_from_cache("known_playernames")
        if self.known_playernames:
            self.known_playernames = set(self.known_playernames.split(","))
        else:
            self.known_playernames = set()
        roomnames = c.get_from_cache("roomnames")
        if roomnames:
            roomnames = roomnames.split(",")
        else:
            roomnames = ("TheCitadel", "North Provi Intel")
            c.put_into_cache("roomnames", ",".join(roomnames),
                             60 * 60 * 24 * 365 * 5)

        self.set_sound_volume(
            75)  # default - maybe overwritten by the settings

        try:
            settings = c.get_from_cache("settings")
            if settings:
                settings = pickle.loads(base64.b64decode(settings))

                for setting in settings:
                    try:
                        if not setting[0]:
                            obj = self
                        else:
                            obj = getattr(self, setting[0])

                        getattr(obj, setting[1])(setting[2])
                    except Exception as e:
                        log.error(str(e))
        except Exception as e:
            self.trayicon.showMessage(
                "Can't remember",
                "Something went wrong when I load my last state:\n{0}".format(
                    str(e)), 1)
        # load cache ends ===============================================
        self.actionQuit.triggered.connect(self.close)
        self.trayicon.sig_quit.connect(self.close)

        self.chatparser = ChatParser(self.path_to_logs, roomnames,
                                     self.dotlan.systems)

        version_check_thread = drachenjaeger.NotifyNewVersionThread()
        version_check_thread.newer_version.connect(self.notify_newer_version)
        version_check_thread.run()

    def toggle_nightmode(self, pressed):
        if pressed:
            self.set_style_dark()
        else:
            self.set_style_light()

    def set_style_dark(self):
        self.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
        self.map.page().set_style('dark')

    def set_style_light(self):
        self.setStyleSheet('')
        self.map.page().set_style('light')

    def update_evetime(self):
        self.evetime_label.setText(
            datetime.datetime.utcnow().strftime('Current EVE Time: %X'))

    def notify_newer_version(self, newest_version):
        self.trayicon.showMessage("Newer Version",
                                  ("A newer Version of VINTEL is available.\n"
                                   "Find the URL in the info!"), 1)

    def change_chat_visibility(self, new_value=None):
        if new_value is not None:
            self.action_show_chat.setChecked(new_value)
        self.chatbox.setVisible(self.action_show_chat.isChecked())

    def change_opacity(self, lvl=None):
        if isinstance(lvl, float):
            opacityVal = lvl
        elif self.sender():
            opacityVal = self.sender().opacity
        else:
            opacityVal = 1.0

        if opacityVal:
            for action in self.opacity_group.actions():
                if action.opacity == opacityVal:
                    action.setChecked(True)

        action = self.opacity_group.checkedAction()
        self.setWindowOpacity(action.opacity)

    def change_sound(self, new_value=None, disable=False):
        if disable:
            self.actionActivate_Sound.setChecked(False)
            self.actionActivate_Sound.setEnabled(False)
            self.actionSound_Setup.setEnabled(False)
            self.sound_button.setEnabled(False)
            QMessageBox.warning(
                None, "Sound disabled",
                "I can't find the lib 'pygame' which I use to play sounds, so I have to disable the soundsystem.\n"
                "If you want sound, please install the 'pygame' library.",
                QMessageBox.Ok)
        else:
            if new_value is not None:
                self.actionActivate_Sound.setChecked(new_value)
            sound.sound_active = self.actionActivate_Sound.isChecked()

    def add_message_to_intelchat(self, message):
        scroll_to_bottom = False
        if (self.chatListWidget.verticalScrollBar().value() ==
                self.chatListWidget.verticalScrollBar().maximum()):
            scroll_to_bottom = True

        entry = ChatEntry(message)
        listWidgetItem = QtWidgets.QListWidgetItem(self.chatListWidget)
        listWidgetItem.setSizeHint(entry.sizeHint())
        self.chatListWidget.addItem(listWidgetItem)
        self.chatListWidget.setItemWidget(listWidgetItem, entry)
        if ChatEntry.SHOW_AVATAR:
            # log.debug('requesting "{0}" avatar'.format(entry.message.user))
            self.avatar_find_thread.add_chatentry(entry)
        # else:
        # log.debug('requesting "{0}" avatar disabled in options'.format(entry.message.user))
        self.chatentries.append(entry)
        entry.mark_system.connect(self.mark_system_on_map)
        self.sig_chatmessage_added.emit(entry)
        if scroll_to_bottom:
            self.chatListWidget.scrollToBottom()

    def change_always_on_top(self, new_value=None):
        self.hide()
        if new_value is not None:
            self.actionAlways_on_top.setChecked(new_value)
        always_on_top = self.actionAlways_on_top.isChecked()
        if always_on_top:
            self.setWindowFlags(self.windowFlags()
                                | QtCore.Qt.WindowStaysOnTopHint)
        else:
            self.setWindowFlags(self.windowFlags()
                                & (~QtCore.Qt.WindowStaysOnTopHint))
        self.show()

    def change_frameless(self, new_value=None):
        self.hide()
        if new_value is None:
            if self.is_frameless is None:
                self.is_frameless = False
            new_value = not self.is_frameless
        if new_value:
            self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
            self.menubar.setVisible(False)
            self.frameButton.setVisible(True)
        else:
            self.setWindowFlags(self.windowFlags()
                                & (~QtCore.Qt.FramelessWindowHint))
            self.menubar.setVisible(True)
            self.frameButton.setVisible(False)
        self.change_always_on_top(new_value)
        self.is_frameless = new_value
        self.actionFrameless_Window.setChecked(new_value)
        for cm in TrayContextMenu.INSTANCES:
            cm.frameless_check.setChecked(new_value)
        self.show()

    def change_show_avatars(self, new_value=None):
        if new_value is not None:
            self.actionShow_Chat_Avatars.setChecked(new_value)
        show = self.actionShow_Chat_Avatars.isChecked()
        ChatEntry.SHOW_AVATAR = show
        for entry in self.chatentries:
            entry.avatar_label.setVisible(show)

    def chat_smaller(self):
        new_size = ChatEntry.TEXTSIZE - 1
        ChatEntry.TEXTSIZE = new_size
        for entry in self.chatentries:
            entry.change_fontsize(new_size)

    def chat_larger(self):
        new_size = ChatEntry.TEXTSIZE + 1
        ChatEntry.TEXTSIZE = new_size
        for entry in self.chatentries:
            entry.change_fontsize(new_size)

    def change_alarm_distance(self, distance):
        self.alarm_distance = distance
        for cm in TrayContextMenu.INSTANCES:
            for action in cm.distance_group.actions():
                if action.alarm_distance_value == distance:
                    action.setChecked(True)
        self.trayicon.alarm_distance_value = distance

    def change_jumpbridge_view(self):
        self.dotlan.change_jumpbrigde_visibility()
        self.update_map()

    def clipboard_changed(self, mode):
        if mode == 0 and self.action_kos_clipboard_active.isChecked():
            content = str(self.clipboard.text())
            last_modified, old_content = self.old_clipboard_content
            if content == old_content and time.time() - last_modified < 3:
                parts = content.split("\n")
                for part in parts:
                    if part in self.known_playernames:
                        self.trayicon.setIcon(
                            QtGui.QIcon(
                                resource_path(
                                    "vi/ui/res/logo_small_green.png")))
                        self.kos_request_thread.add_request(
                            parts, "clipboard", True)
                        self.old_clipboard_content = (0, "")
                        break
            else:
                self.old_clipboard_content = (time.time(), content)

    def closeEvent(self, event):
        """ writing the cache before closing the window """
        c = Cache()
        # known playernames
        if self.known_playernames:
            value = ",".join(self.known_playernames)
            c.put_into_cache("known_playernames", value, 60 * 60 * 24 * 365)
        # program state to cache (to read it on next startup)
        settings = (
            (None, "restoreGeometry", self.saveGeometry()),
            (None, "restoreState", self.saveState()),
            (None, "change_opacity",
             self.opacity_group.checkedAction().opacity),
            (None, "change_always_on_top",
             self.actionAlways_on_top.isChecked()),
            ("splitter", "restoreGeometry", self.splitter.saveGeometry()),
            ("splitter", "restoreState", self.splitter.saveState()),
            (None, "change_show_avatars",
             self.actionShow_Chat_Avatars.isChecked()),
            (None, "change_alarm_distance", self.alarm_distance),
            ("action_kos_clipboard_active", "setChecked",
             self.action_kos_clipboard_active.isChecked()),
            (None, "change_sound", self.actionActivate_Sound.isChecked()),
            (None, "change_chat_visibility",
             self.action_show_chat.isChecked()),
            ("map", "setZoomFactor", self.map.zoomFactor()),
            (None, "set_init_map_scrollposition",
             (self.map.page().scrollPosition().x(),
              self.map.page().scrollPosition().y())),
            (None, "set_sound_volume", self.sound_volume),
            (None, "change_frameless",
             self.actionFrameless_Window.isChecked()),
        )
        settings = pickle.dumps(settings, protocol=pickle.HIGHEST_PROTOCOL)
        c.put_into_cache("settings", base64.b64encode(settings),
                         60 * 60 * 24 * 365)
        event.accept()

    def map_link_clicked(self, url):
        systemname = str(url.path().split("/")[-1]).upper()
        system = self.dotlan.systems[str(systemname)]
        sc = SystemChat(self, SystemChat.SYSTEM, system, self.chatentries,
                        self.known_playernames)
        self.sig_chatmessage_added.connect(sc.add_chatentry)
        self.sig_avatar_loaded.connect(sc.new_avatar_available)
        sc.sig_location_set.connect(self.set_location)
        sc.show()

    def mark_system_on_map(self, systemname):
        self.map.page().mark_system(systemname)

        # self.dotlan.systems[str(systemname)].mark()
        # self.update_map()

    def set_location(self, char, new_system):
        self.map.page().mark_player(char, new_system)
        """
        for system in self.dotlan.systems.values():
            system.remove_located_character(char)

        if not new_system == "?" and new_system in self.dotlan.systems:
            self.dotlan.systems[new_system].add_located_character(char)
            self.set_map_content(self.dotlan.svg)
        """

    def set_init_map_scrollposition(self, xy):
        self.init_map_position = QPoint(xy[0], xy[1])

    def show_chatroom_chooser(self):
        chooser = ChatroomsChooser(self)
        chooser.rooms_changed.connect(self.changed_roomnames)
        chooser.show()

    def show_jumbridge_chooser(self):
        cache = Cache()
        url = cache.get_from_cache("jumpbridge_url")
        chooser = JumpBridgeChooser(self, url)
        chooser.set_jb_url.connect(self.set_jumpbridges)
        chooser.show()

    def set_sound_volume(self, value):
        if value < 0:
            value = 0
        elif value > 100:
            value = 100

        self.sound_volume = value
        sound.set_sound_volume(float(value) / 100.0)

    def set_jumpbridges(self, url):
        if url is None:
            url = ""
        try:
            data = []
            if url != "":
                content = requests.get(url).text
                for line in content.split("\n"):
                    parts = line.strip().split()
                    if len(parts) == 3:
                        data.append(parts)
            else:
                data = drachenjaeger.get_jumpbridge_data(
                    self.dotlan.region.lower())
            self.dotlan.set_jumpbridges(data)
            c = Cache()
            c.put_into_cache("jumpbridge_url", url, 60 * 60 * 24 * 365 * 8)
        except Exception as e:
            QMessageBox.warning(None, "Loading jumpbridges failed!",
                                "Error: {0}".format(str(e)), QMessageBox.Ok)

    def show_region_chooser(self):
        chooser = RegionChooser(self)
        chooser.show()

    def show_kos_result(self, state, text, request_type, has_kos):
        if has_kos:
            sound.play_sound("beep")

        self.trayicon.setIcon(
            QtGui.QIcon(resource_path("vi/ui/res/logo_small.png")))

        if state == "ok":
            if request_type == "xxx":  # a xxx request out of the chat
                self.trayicon.showMessage("A xxx KOS-Check", text, 1)
            elif request_type == "clipboard":  # request from clipboard-change
                if len(text) <= 0:
                    text = "Noone KOS"

                self.trayicon.showMessage("Your KOS-Check", text, 1)

            text = text.replace("\n\n", "<br>")
            message = chatparser.chatparser.Message("Vintel KOS-Check", text,
                                                    evegate.current_eve_time(),
                                                    "VINTEL", [],
                                                    states.NOT_CHANGE,
                                                    text.upper(), text)

            self.add_message_to_intelchat(message)
        elif state == "error":
            self.trayicon.showMessage("KOS Failure", text, 3)

    def changed_roomnames(self, new_roomnames):
        cache = Cache()
        cache.put_into_cache("roomnames", u",".join(new_roomnames),
                             60 * 60 * 24 * 365 * 5)
        self.chatparser.rooms = new_roomnames

    def show_info(self):
        info_dialog = QDialog(self)
        uic.loadUi(resource_path("vi/ui/Info.ui"), info_dialog)
        info_dialog.version_label.setText(u"Version: {0}".format(VERSION))
        info_dialog.logo_label.setPixmap(
            QtGui.QPixmap(resource_path("vi/ui/res/logo.png")))
        info_dialog.close_button.clicked.connect(info_dialog.accept)
        info_dialog.show()

    def show_sound_setup(self):
        dialog = QDialog(self)
        uic.loadUi(resource_path("vi/ui/SoundSetup.ui"), dialog)
        dialog.volumeSlider.setValue(self.sound_volume)
        dialog.volumeSlider.valueChanged.connect(self.set_sound_volume)
        dialog.testsound_button.clicked.connect(sound.play_sound)
        dialog.close_button.clicked.connect(dialog.accept)
        dialog.show()

    def systray_activated(self, reason):
        if reason == QtWidgets.QSystemTrayIcon.Trigger:
            if self.isMinimized():
                self.showNormal()
                self.activateWindow()
            elif not self.isActiveWindow():
                self.activateWindow()
            else:
                self.showMinimized()

    def update_avatar_on_chatentry(self, chatentry, avatar_data):
        updated = chatentry.update_avatar(avatar_data)
        if not updated:
            self.avatar_find_thread.add_chatentry(chatentry, clear_cache=True)
        else:
            self.avatar_loaded.emit(chatentry.message.user, avatar_data)

    """
    def update_map(self):
        # log.debug('check')

        def update_statistics_on_map(data):
            if data["result"] == "ok":
                self.dotlan.add_system_statistics(data["statistics"])
            elif data["result"] == "error":
                text = data["text"]
                self.trayicon.showMessage("Loading statstics failed", text, 3)

        if self.last_statistics_update < (time.time() - 5 * 60):
            self.last_statistics_update = time.time()
            statistic_thread = MapStatisticsThread()
            # self.connect(statistic_thread, Qt.SIGNAL("statistic_data_update"), update_statistics_on_map)
            statistic_thread.statistic_data_update.connect(update_statistics_on_map)
            statistic_thread.start()

        self.set_map_content(self.dotlan.svg)

    def set_map_content(self, content):
        if self.init_map_position is None:
            scrollposition = self.map.page().scrollPosition()
            # scrollposition = self.map.scrollPosition()
        else:
            scrollposition = self.init_map_position
            self.init_map_position = None

        if True:
            # array = QByteArray().append(content)
            # self.map.page().setContent(array, mimeType=str('image/svg+xml'))

            if not self.set_map_content_done:
                self.map.page().set_svg(content)
                self.set_map_content_done = True
        else:
            # self.map.setContent(content)
            self.map.page().setHtml(content)

            if isinstance(scrollposition, QtCore.QPointF):
                script_str = "window.scrollTo({0}, {1});".format(
                    scrollposition.toPoint().x(),
                    scrollposition.toPoint().y()
                )
            elif isinstance(scrollposition, QtCore.QPoint):
                script_str = "window.scrollTo({0}, {1});".format(scrollposition.x(), scrollposition.y())
            else:
                script_str = None
                log.warning('unknown object type {0}'.format(type(scrollposition)))
            # script_str = "window.scrollTop(340);"

            # log.debug('current scroll is {0}x{1}'.format(scrollposition.toPoint().x(), scrollposition.toPoint().y()))
            # log.debug(script_str)

            # self.map.page().setScrollPosition(scrollposition)
            # self.map.page().scrollPosition = scrollposition;

            self.map.page().runJavaScript(script_str)
        # time.sleep(1)
        # self.map.page().setLinkDelegationPolicy(QWebEnginePage.DelegateAllLinks)
    """

    def zoomMapIn(self):
        self.map.setZoomFactor(self.map.zoomFactor() + 0.1)

    def zoomMapOut(self):
        self.map.setZoomFactor(self.map.zoomFactor() - 0.1)

    def logfile_changed(self, path, roomname):
        messages = self.chatparser.file_modified(path, roomname)
        for message in messages:
            # if players location changed
            if message.status == states.LOCATION:
                self.known_playernames.add(message.user)
                self.set_location(message.user, message.systems[0])
            # soundtest special
            elif message.status == states.SOUNDTEST and message.user in self.known_playernames:
                words = message.message.split()
                if len(words) > 1:
                    sound.play_sound(words[1])
            # KOS request
            elif message.status == states.KOS_STATUS_REQUEST:
                text = message.message[4:]
                text = text.replace("  ", ",")
                parts = (name.strip() for name in text.split(","))
                self.trayicon.setIcon(
                    QtGui.QIcon(
                        resource_path("vi/ui/res/logo_small_green.png")))
                self.kos_request_thread.add_request(parts, "xxx", False)
            # if it is a 'normal' chat message
            elif message.user not in ("EVE-System", "EVE System"
                                      ) and message.status != states.IGNORE:
                self.add_message_to_intelchat(message)

                if message.systems:
                    for system in message.systems:
                        systemname = system.name
                        # self.dotlan.set_system_status(systemname, message.status)
                        self.map.page().set_system_status(system, message)

                        if message.status in (states.REQUEST, states.ALARM) \
                                and message.user not in self.known_playernames:
                            if message.status == states.ALARM:
                                alarm_distance = self.alarm_distance
                            else:
                                alarm_distance = 0

                            for nsystem, data in system.get_neighbours(
                                    alarm_distance).items():
                                distance = data["distance"]
                                chars = nsystem.get_located_characters()
                                if len(chars
                                       ) > 0 and message.user not in chars:
                                    self.trayicon.show_notification(
                                        message, system.name, ", ".join(chars),
                                        distance)
Ejemplo n.º 9
0
class Window(Ui_MainWindow, QMainWindow):

    quitCountDown = pyqtSignal()

    def __init__(self, ctx, *args, **kwargs):
        """
        Normally initialize the MainWindow and some attributes and slots
        """
        super(Window, self).__init__(*args, **kwargs)
        self.setupUi(self)

        self.ctx = ctx
        # A flag to distinguish between pausing and first running
        # 1: first_run
        self.first_run = 1
        self.profileActions = {}  # A dict contains profiles
        self._profile_group = QActionGroup(self)

        self.init_menu()
        self.init_data()
        self.init_slots()
        self.init_buttons()
        self.init_profiles_for_menu()
        self.init_timer()

    def init_timer(self):
        """Initialize the timer and the TimeController"""
        # The status of continung(1) and pausing(-1)
        self.controlStatus = -1
        self.timer = QTimer()  # Initialize the timer
        self.controller = TimeController(self.ctx)
        self.timer.setInterval(1000)
        self.timer.timeout.connect(self.controller.runner.minus)
        self.quitCountDown.connect(self.controller.quit_)

        self.controller.timeChanged.connect(self.update)
        self.controller.clearOld.connect(self.clearOld)
        self.controller.circleChanged.connect(self.updateLCD)
        self.controller.started.connect(self.timer.start)
        self.controller.finished.connect(self.reset)
        # After a whole counting down period, we say you eat a tomato.
        self.controller.eatTomato.connect(self.eat)
        self.controller.timerStart.connect(self.timer.start)
        self.controller.timerTempStop.connect(self.timer.stop)

    def init_data(self):
        """Display the data in the profile on the main window"""
        # Circle: work-rest period
        # Every 2 work-rest period, there is a long_rest.
        self.reload()
        self.workPB.setValue(0)
        self.restPB.setValue(0)
        self.long_restPB.setValue(0)

    def init_menu(self):
        """Initialize the mojarity of the actions placed in the menu"""
        self.actions = MenuActions(self.ctx)
        self.actionAbout.triggered.connect(self.actions.showAbout)
        self.actionMinimum.triggered.connect(self.showMinimized)
        self.actionQuit.triggered.connect(self.quit_)
        self.actionOptions.triggered.connect(self.actions.showOptions)
        self.actionDonate.triggered.connect(lambda: self.actions.donate(self))

    def init_buttons(self):
        """Initialize the buttons lay out on the main window"""
        self.quitButton.clicked.connect(self.quit_)
        self.optionButton.clicked.connect(self.actions.showOptions)
        self.controlButton.clicked.connect(self.control)
        self.stopButton.clicked.connect(self.stop)
        self.stopButton.setEnabled(False)
        self.counterButton.clicked.connect(self.count)

    def init_slots(self):
        """Initialize the labels, lcdnumbers, statusbar and menubar"""
        # Initializethe slots of labels
        # These three label display the work time, rest time and long rest time.
        # To make the user interface more simple and keep your attention focused
        # on the task. I use the label instead of extra buttons to realize the
        # function of setting the time period quickly.
        # Also, you can find a full-functioned setting panel in the preference
        # menu and you can save these as a profile to quick load it when you use
        # the app next time as well.
        self.workLabel.doubleClicked.connect(self.setWorkTime)
        self.restLabel.doubleClicked.connect(self.setRestTime)
        self.longRestLabel.doubleClicked.connect(self.setLongRestTime)

        # Initialize the slots of circle lcdnumber
        self.circleTimesLCD.doubleClicked.connect(self.setCircleTimes)

        #Initialize the slots of profile_settings_window
        self.ctx.settings_gui.profileRemoved.connect(self.removeProfile)
        self.ctx.settings_gui.newProfileSig.connect(self.addProfileActions)
        self.ctx.settings_gui.updateSig.connect(
            self.refreshSelectedActionAndData)

    def init_profiles_for_menu(self):
        """Initialize the profiles found in the directory profiles and display them in the preferences menu. So you can get a easy access to use your profile quickly"""
        # Init the profile according to the global setting: "lastProfile"
        # Add the found profiles into the preferences menu and add them into
        # the ActionGroup, so you can only select one profile at one time
        self.ctx.profile.reloadObj.reloadSig.connect(self.reload)
        self.loadProfileActions()

    def refreshSelectedActionAndData(self):
        self.reload()
        self.profileActions[self.ctx.profile.name].setChecked(True)

    def addOneProfileAction(self, name):
        # QAction(name, parent)
        action = QAction(name, self)
        action.setCheckable(True)
        # connected with the reload method provided by profile object
        action.triggered.connect(
            lambda: self.ctx.profile.reload(action.text()))
        return action

    def addProfileActions(self, names):
        for name in names:
            action = self.addOneProfileAction(name)
            self.profileActions[name] = action
            self._profile_group.addAction(action)
            self.menuPreferences.addAction(action)

    def loadProfileActions(self):
        self.addProfileActions(self.ctx.profileList)
        self.profileActions[self.ctx.profile.name].setChecked(True)

    def removeProfile(self, name):
        try:
            action = self.profileActions.pop(name)
        except KeyError:
            pass
        else:
            self._profile_group.removeAction(action)
            self.menuPreferences.removeAction(action)
            sip.delete(action)

    def reload(self):
        """
        When reloadSig triggered, this reload method will update the data
        shown on the main window.
        """
        self.workCD.setText(self.ctx.profile["work"])
        self.restCD.setText(self.ctx.profile["rest"])
        self.long_restCD.setText(self.ctx.profile["long_rest"])
        self.circleTimesLCD.display(self.ctx.profile["circle_times"])
        if self.ctx.log4p.query(f"fatal:{self.ctx.profile.name}:"):
            self.controlButton.setEnabled(False)
        else:
            self.controlButton.setEnabled(True)

    def _set(self, name, attr, value):
        if value:
            self.ctx.profile[attr] = value
            if not self.ctx.inspector.check(value, attr):
                self.ctx.log4p.remove(f"fatal:{name}:{attr}")
        if self.ctx.log4p.query(f"fatal:{name}:") is None:
            self.controlButton.setEnabled(True)
        self.ctx.ask_dialog.close()

    def setWorkTime(self):
        f = lambda value: self._set(self.ctx.profile.name, "work", value)
        self.ctx.ask_dialog.replySig.connect(f)
        self.ctx.ask_dialog.ask("work", self.ctx.profile["work"])
        self.workCD.setText(self.ctx.profile["work"])
        self.workPB.setValue(0)
        self.ctx.ask_dialog.replySig.disconnect(f)

    def setRestTime(self):
        f = lambda value: self._set(self.ctx.profile.name, "rest", value)
        self.ctx.ask_dialog.replySig.connect(f)
        self.ctx.ask_dialog.ask("rest", self.ctx.profile["rest"])
        self.restCD.setText(self.ctx.profile["rest"])
        self.restPB.setValue(0)
        self.ctx.ask_dialog.replySig.disconnect(f)

    def setLongRestTime(self):
        f = lambda value: self._set(self.ctx.profile.name, "long_rest", value)
        self.ctx.ask_dialog.replySig.connect(f)
        self.ctx.ask_dialog.ask("long-resting", self.ctx.profile["long_rest"])
        self.long_restCD.setText(self.ctx.profile["long_rest"])
        self.long_restPB.setValue(0)
        self.ctx.ask_dialog.replySig.disconnect(f)

    def setCircleTimes(self):
        f = lambda value: self._set(self.ctx.profile.name, "circle_times",
                                    value)
        self.ctx.ask_dialog.replySig.connect(f)
        self.ctx.ask_dialog.ask("circle_times",
                                self.ctx.profile["circle_times"])
        self.circleTimesLCD.display(self.ctx.profile["circle_times"])
        self.ctx.ask_dialog.replySig.disconnect(f)

    def reset(self):
        self.timer.stop()
        self.controlButton.setIcon(self.ctx.icontinue)
        self.stopButton.setEnabled(False)
        self.init_data()
        self.first_run = 1
        self.controlStatus = -1

    def eat(self):
        self.stopButton.setEnabled(False)
        self.ctx.global_setting["count"] += 1
        self.statusBar.showMessage("You have eaten a tomato just now!", 2000)

    def quit_(self):
        """Quit the app"""
        self.ctx.profile.save()
        self.ctx.global_setting[
            "lastProfile"] = self._profile_group.checkedAction().text()
        self.ctx.saveGlobal()
        self.ctx.app.quit()

    def count(self):
        self.statusBar.showMessage(
            "You have eaten {} tomatoes by now".format(
                self.ctx.global_setting["count"]), 1500)

    def update(self, type_, pc, time_gen):
        pb = type_ + "PB"
        label = type_ + "CD"
        getattr(self, label).setText("".join(time_gen))
        getattr(self, pb).setValue(100 - pc)

    def updateLCD(self):
        num = self.circleTimesLCD.intValue() - 1
        self.circleTimesLCD.display(num)

    def clearOld(self, type_):
        if type_ == "":
            type_ = "work"
        getattr(self, type_ + "PB").setValue(0)
        getattr(self, type_ + "CD").setText('0s')

    def control(self):
        self.controlStatus = -self.controlStatus
        if self.controlStatus == 1:  # pause counting down
            self.controlButton.setIcon(self.ctx.ipause)
            self.statusBar.showMessage("continue", 1000)
            if self.first_run:
                self.first_run = 0
                self.stopButton.setEnabled(True)
                self.statusBar.showMessage("preparing...", 2000)
                # Initialize the controller's counting down related data,
                # in order to keep the controller receiving
                # the newest profile data.
                self.controller.reset()
                self.controller.start()
            else:  # continue counting down
                self.timer.start()
        else:
            self.controlButton.setIcon(self.ctx.icontinue)
            self.statusBar.showMessage("paused")
            self.timer.stop()

    def stop(self):
        """Quit counting down but not quit the app"""
        self.quitCountDown.emit()
        self.statusBar.showMessage("stopped", 1000)
Ejemplo n.º 10
0
class Gui(QMainWindow, Ui_MainWindow):
    NOTEON = 0x9
    NOTEOFF = 0x8
    MIDICTRL = 11

    GREEN = ("#cell_frame { border: 0px; border-radius: 10px; "
             "background-color: rgb(125,242,0);}")
    BLUE = ("#cell_frame { border: 0px; border-radius: 10px; "
            "background-color: rgb(0, 130, 240);}")
    RED = ("#cell_frame { border: 0px; border-radius: 10px; "
           "background-color: rgb(255, 21, 65);}")
    AMBER = ("#cell_frame { border: 0px; border-radius: 10px; "
             "background-color: rgb(255, 102, 0);}")
    PURPLE = ("#cell_frame { border: 0px; border-radius: 10px; "
              "background-color: rgb(130, 0, 240);}")
    DEFAULT = ("#cell_frame { border: 0px; border-radius: 10px; "
               "background-color: rgb(217, 217, 217);}")

    RECORD_BLINK = ("QPushButton {background-color: rgb(255, 255, 255);}"
                    "QPushButton:pressed {background-color: "
                    "rgb(98, 98, 98);}")

    RECORD_DEFAULT = ("QPushButton {background-color: rgb(0, 0, 0);}"
                      "QPushButton:pressed {background-color: "
                      "rgb(98, 98, 98);}")

    STATE_COLORS = {Clip.STOP: RED,
                    Clip.STARTING: GREEN,
                    Clip.START: GREEN,
                    Clip.STOPPING: RED,
                    Clip.PREPARE_RECORD: AMBER,
                    Clip.RECORDING: AMBER}
    STATE_BLINK = {Clip.STOP: False,
                   Clip.STARTING: True,
                   Clip.START: False,
                   Clip.STOPPING: True,
                   Clip.PREPARE_RECORD: True,
                   Clip.RECORDING: False}

    BLINK_DURATION = 200
    PROGRESS_PERIOD = 300

    ADD_PORT_LABEL = 'Add new Port...'

    updateUi = pyqtSignal()
    readQueueIn = pyqtSignal()
    updatePorts = pyqtSignal()
    songLoad = pyqtSignal()

    def __init__(self, song, jack_client):
        QObject.__init__(self)
        super(Gui, self).__init__()
        self._jack_client = jack_client
        self.setupUi(self)
        self.clip_volume.knobRadius = 3
        self.is_learn_device_mode = False
        self.queue_out, self.queue_in = Queue(), Queue()
        self.updateUi.connect(self.update)
        self.readQueueIn.connect(self.readQueue)
        self.current_vol_block = 0
        self.last_clip = None

        # Load devices
        self.deviceGroup = QActionGroup(self.menuDevice)
        self.devices = []
        device_settings = QSettings('superboucle', 'devices')
        if ((device_settings.contains('devices')
             and device_settings.value('devices'))):
            for raw_device in device_settings.value('devices'):
                self.devices.append(Device(pickle.loads(raw_device)))
        else:
            self.devices.append(Device({'name': 'No Device',}))
        self.updateDevices()
        self.deviceGroup.triggered.connect(self.onDeviceSelect)

        self.settings = QSettings('superboucle', 'session')
        # Qsetting appear to serialize empty lists as @QInvalid
        # which is then read as None :(

        # Load playlist
        self.playlist = self.settings.value('playlist', []) or []
        # Load paths
        self.paths_used = self.settings.value('paths_used', {})

        self.auto_connect = self.settings.value('auto_connect',
                                                'true') == "true"

        # Load song
        self.port_by_name = {}
        self.initUI(song)

        self.actionNew.triggered.connect(self.onActionNew)
        self.actionOpen.triggered.connect(self.onActionOpen)
        self.actionSave.triggered.connect(self.onActionSave)
        self.actionSave_As.triggered.connect(self.onActionSaveAs)
        self.actionAdd_Device.triggered.connect(self.onAddDevice)
        self.actionManage_Devices.triggered.connect(self.onManageDevice)
        self.actionPlaylist_Editor.triggered.connect(self.onPlaylistEditor)
        self.actionScene_Manager.triggered.connect(self.onSceneManager)
        self.actionPort_Manager.triggered.connect(self.onPortManager)
        self.actionFullScreen.triggered.connect(self.onActionFullScreen)
        self.master_volume.valueChanged.connect(self.onMasterVolumeChange)
        self.bpm.valueChanged.connect(self.onBpmChange)
        self.beat_per_bar.valueChanged.connect(self.onBeatPerBarChange)
        self.rewindButton.clicked.connect(self.onRewindClicked)
        self.playButton.clicked.connect(self._jack_client.transport_start)
        self.pauseButton.clicked.connect(self._jack_client.transport_stop)
        self.gotoButton.clicked.connect(self.onGotoClicked)
        self.recordButton.clicked.connect(self.onRecord)
        self.clip_name.textChanged.connect(self.onClipNameChange)
        self.clip_volume.valueChanged.connect(self.onClipVolumeChange)
        self.beat_diviser.valueChanged.connect(self.onBeatDiviserChange)
        self.output.activated.connect(self.onOutputChange)
        self.mute_group.valueChanged.connect(self.onMuteGroupChange)
        self.frame_offset.valueChanged.connect(self.onFrameOffsetChange)
        self.beat_offset.valueChanged.connect(self.onBeatOffsetChange)
        self.revertButton.clicked.connect(self.onRevertClip)
        self.normalizeButton.clicked.connect(self.onNormalizeClip)
        self.exportButton.clicked.connect(self.onExportClip)
        self.deleteButton.clicked.connect(self.onDeleteClipClicked)

        self.blktimer = QTimer()
        self.blktimer.state = False
        self.blktimer.timeout.connect(self.toggleBlinkButton)
        self.blktimer.start(self.BLINK_DURATION)

        self.disptimer = QTimer()
        self.disptimer.start(self.PROGRESS_PERIOD)
        self.disptimer.timeout.connect(self.updateProgress)

        self._jack_client.set_timebase_callback(self.timebase_callback)
        self.show()

    def initUI(self, song):

        # remove old buttons
        self.btn_matrix = [[None for y in range(song.height)]
                           for x in range(song.width)]
        self.state_matrix = [[-1 for y in range(song.height)]
                             for x in range(song.width)]

        for i in reversed(range(self.gridLayout.count())):
            self.gridLayout.itemAt(i).widget().close()
            self.gridLayout.itemAt(i).widget().setParent(None)

        # first pass without removing old ports
        self.updateJackPorts(song, remove_ports=False)
        self.song = song
        # second pass with removing
        self.updateJackPorts(song, remove_ports=True)

        self.frame_clip.setEnabled(False)
        self.output.clear()
        self.output.addItems(song.outputsPorts)
        self.output.addItem(Gui.ADD_PORT_LABEL)
        self.master_volume.setValue(song.volume * 256)
        self.bpm.setValue(song.bpm)
        self.beat_per_bar.setValue(song.beat_per_bar)
        for x in range(song.width):
            for y in range(song.height):
                clip = song.clips_matrix[x][y]
                cell = Cell(self, clip, x, y)
                self.btn_matrix[x][y] = cell
                self.gridLayout.addWidget(cell, y, x)

        # send init command
        for init_cmd in self.device.init_command:
            self.queue_out.put(init_cmd)

        self.setWindowTitle("Super Boucle - {}"
                            .format(song.file_name or "Empty Song"))

        if self.song.initial_scene in self.song.scenes:
            self.song.loadScene(self.song.initial_scene)
        self.update()
        self.songLoad.emit()

    def openSongFromDisk(self, file_name):
        self._jack_client.transport_stop()
        self._jack_client.transport_locate(0)

        self.setEnabled(False)
        message = QMessageBox(self)
        message.setWindowTitle("Loading ....")
        message.setText("Reading Files, please wait ...")
        message.show()
        self.initUI(load_song_from_file(file_name))
        message.close()
        self.setEnabled(True)

    def closeEvent(self, event):
        device_settings = QSettings('superboucle', 'devices')
        device_settings.setValue('devices',
                                 [pickle.dumps(x.mapping)
                                  for x in self.devices])
        self.settings.setValue('playlist', self.playlist)
        self.settings.setValue('paths_used', self.paths_used)
        self.settings.setValue('auto_connect', self.auto_connect)

    def onStartStopClicked(self):
        clip = self.sender().parent().parent().clip
        self.startStop(clip.x, clip.y)

    def startStop(self, x, y):
        clip = self.btn_matrix[x][y].clip
        if clip is None:
            return
        if self.song.is_record:
            self.song.is_record = False
            self.updateRecordBtn()
            # calculate buffer size
            state, position = self._jack_client.transport_query()
            bps = position['beats_per_minute'] / 60
            fps = position['frame_rate']
            size = int((1 / bps) * clip.beat_diviser * fps)
            self.song.init_record_buffer(clip, 2, size, fps)
            # set frame offset based on jack block size
            clip.frame_offset = self._jack_client.blocksize
            clip.state = Clip.PREPARE_RECORD
            self.recordButton.setStyleSheet(self.RECORD_DEFAULT)
        else:
            self.song.toggle(clip.x, clip.y)
        self.update()

    def onEdit(self):
        self.last_clip = self.sender().parent().parent().clip
        if self.last_clip:
            self.frame_clip.setEnabled(True)
            self.clip_name.setText(self.last_clip.name)
            self.frame_offset.setValue(self.last_clip.frame_offset)
            self.beat_offset.setValue(self.last_clip.beat_offset)
            self.beat_diviser.setValue(self.last_clip.beat_diviser)
            self.output.setCurrentText(self.last_clip.output)
            self.mute_group.setValue(self.last_clip.mute_group)
            self.clip_volume.setValue(self.last_clip.volume * 256)
            state, position = self._jack_client.transport_query()
            fps = position['frame_rate']
            bps = self.bpm.value() / 60
            if self.bpm.value() and fps:
                size_in_beat = (bps / fps) * self.song.length(self.last_clip)
            else:
                size_in_beat = "No BPM info"
            clip_description = ("Size in sample : %s\nSize in beat : %s"
                                % (self.song.length(self.last_clip),
                                   round(size_in_beat, 1)))

            self.clip_description.setText(clip_description)

    def onAddClipClicked(self):
        cell = self.sender().parent().parent()
        if QApplication.keyboardModifiers() == Qt.ControlModifier:
            cell.setClip(cell.openClip())
        else:
            AddClipDialog(self, cell)

    def onRevertClip(self):
        if self.last_clip and self.last_clip.audio_file:
            audio_file = self.last_clip.audio_file
            self.song.data[audio_file] = self.song.data[audio_file][::-1]

    def onNormalizeClip(self):
        if self.last_clip and self.last_clip.audio_file:
            audio_file = self.last_clip.audio_file
            absolute_val = np.absolute(self.song.data[audio_file])
            current_level = np.ndarray.max(absolute_val)
            self.song.data[audio_file][:] *= (1 / current_level)

    def onExportClip(self):
        if self.last_clip and self.last_clip.audio_file:
            audio_file = self.last_clip.audio_file
            file_name, a = self.getSaveFileName(
                'Export Clip : %s' % self.last_clip.name, 'WAVE (*.wav)')

            if file_name:
                file_name = verify_ext(file_name, 'wav')
                sf.write(self.song.data[audio_file], file_name,
                         self.song.samplerate[audio_file],
                         subtype=sf.default_subtype('WAV'),
                         format='WAV')

    def onDeleteClipClicked(self):
        if self.last_clip:
            response = QMessageBox.question(self,
                                            "Delete Clip ?",
                                            ("Are you sure "
                                             "to delete the clip ?"))
            if response == QMessageBox.Yes:
                self.frame_clip.setEnabled(False)
                self.song.removeClip(self.last_clip)
                self.initUI(self.song)

    def onMasterVolumeChange(self):
        self.song.volume = (self.master_volume.value() / 256)

    def onBpmChange(self):
        self.song.bpm = self.bpm.value()

    def onBeatPerBarChange(self):
        self.song.beat_per_bar = self.beat_per_bar.value()

    def onGotoClicked(self):
        state, position = self._jack_client.transport_query()
        new_position = (position['beats_per_bar']
                        * (self.gotoTarget.value() - 1)
                        * position['frame_rate']
                        * (60 / position['beats_per_minute']))
        self._jack_client.transport_locate(int(round(new_position, 0)))

    def onRecord(self):
        self.song.is_record = not self.song.is_record
        self.updateRecordBtn()

    def updateRecordBtn(self):
        if not self.song.is_record:
            self.recordButton.setStyleSheet(self.RECORD_DEFAULT)
        if self.device.record_btn:
            (msg_type, channel, pitch, velocity) = self.device.record_btn
            if self.song.is_record:
                color = self.device.blink_amber_vel
            else:
                color = self.device.black_vel
            self.queue_out.put(((msg_type << 4) + channel, pitch, color))

    def onRewindClicked(self):
        self._jack_client.transport_locate(0)

    def onClipNameChange(self):
        self.last_clip.name = self.clip_name.text()
        cell = self.btn_matrix[self.last_clip.x][self.last_clip.y]
        cell.clip_name.setText(self.last_clip.name)

    def onClipVolumeChange(self):
        self.last_clip.volume = (self.clip_volume.value() / 256)

    def onBeatDiviserChange(self):
        self.last_clip.beat_diviser = self.beat_diviser.value()

    def onOutputChange(self):
        new_port = self.output.currentText()
        if new_port == Gui.ADD_PORT_LABEL:
            AddPortDialog(self)
        else:
            self.last_clip.output = new_port

    def addPort(self, name):
        self.song.outputsPorts.add(name)
        self.updateJackPorts(self.song)
        if self.output.findText(name) == -1:
            self.output.insertItem(self.output.count() - 1, name)
        if self.last_clip:
            self.last_clip.output = name
            self.output.setCurrentText(name)

    def removePort(self, name):
        if name != Clip.DEFAULT_OUTPUT:
            self.song.outputsPorts.remove(name)
            for c in self.song.clips:
                if c.output == name:
                    c.output = Clip.DEFAULT_OUTPUT
            self.updateJackPorts(self.song)
            self.output.removeItem(self.output.findText(name))
            if self.last_clip:
                self.output.setCurrentText(self.last_clip.output)

    def updateJackPorts(self, song, remove_ports=True):
        '''Update jack port based on clip output settings
        update dict containing ports with shortname as key'''

        current_ports = set()
        for port in self._jack_client.outports:
            current_ports.add(port.shortname)

        wanted_ports = set()
        for port_basename in song.outputsPorts:
            for ch in Song.CHANNEL_NAMES:
                port = Song.CHANNEL_NAME_PATTERN.format(port=port_basename,
                                                        channel=ch)
                wanted_ports.add(port)

        # remove unwanted ports
        if remove_ports:
            port_to_remove = []
            for port in self._jack_client.outports:
                if port.shortname not in wanted_ports:
                    current_ports.remove(port.shortname)
                    port_to_remove.append(port)
            for port in port_to_remove:
                port.unregister()

        # create new ports
        for new_port_name in wanted_ports - current_ports:
            self._jack_client.outports.register(new_port_name)

        self.port_by_name = {port.shortname: port
                             for port in self._jack_client.outports}

        self.updatePorts.emit()

    def onMuteGroupChange(self):
        self.last_clip.mute_group = self.mute_group.value()

    def onFrameOffsetChange(self):
        self.last_clip.frame_offset = self.frame_offset.value()

    def onBeatOffsetChange(self):
        self.last_clip.beat_offset = self.beat_offset.value()

    def onActionNew(self):
        NewSongDialog(self)

    def getOpenFileName(self, title, file_type, parent=None,
                        dialog=QFileDialog.getOpenFileName):
        path = self.paths_used.get(file_type, expanduser('~'))
        file_name, a = dialog(parent or self, title, path, file_type)
        if a and file_name:
            if isinstance(file_name, list):
                self.paths_used[file_type] = dirname(file_name[0])
            else:
                self.paths_used[file_type] = dirname(file_name)
        return file_name, a

    def getSaveFileName(self, *args):
        return self.getOpenFileName(*args, dialog=QFileDialog.getSaveFileName)

    def onActionOpen(self):
        file_name, a = self.getOpenFileName('Open Song',
                                            'Super Boucle Song (*.sbs)')
        if a and file_name:
            self.openSongFromDisk(file_name)

    def onActionSave(self):
        if self.song.file_name:
            self.song.save()
        else:
            self.onActionSaveAs()

    def onActionSaveAs(self):
        file_name, a = self.getSaveFileName('Save Song',
                                            'Super Boucle Song (*.sbs)')

        if file_name:
            file_name = verify_ext(file_name, 'sbs')
            self.song.file_name = file_name
            self.song.save()
            print("File saved to : {}".format(self.song.file_name))

    def onAddDevice(self):
        self.learn_device = LearnDialog(self, self.addDevice)
        self.is_learn_device_mode = True

    def onManageDevice(self):
        ManageDialog(self)

    def onPlaylistEditor(self):
        PlaylistDialog(self)

    def onSceneManager(self):
        SceneManager(self)

    def onPortManager(self):
        PortManager(self)

    def onActionFullScreen(self):
        if self.isFullScreen():
            self.showNormal()
        else:
            self.showFullScreen()
        self.show()

    def update(self):
        for x in range(len(self.song.clips_matrix)):
            line = self.song.clips_matrix[x]
            for y in range(len(line)):
                clp = line[y]
                if clp is None:
                    state = None
                else:
                    state = clp.state
                if state != self.state_matrix[x][y]:
                    if clp:
                        self.btn_matrix[x][y].setColor(state)
                    try:
                        self.queue_out.put(self.device.generateNote(x,
                                                                    y,
                                                                    state))
                    except IndexError:
                        # print("No cell associated to %s x %s"
                        # % (clp.x, clp.y))
                        pass
                self.state_matrix[x][y] = state

    def redraw(self):
        self.state_matrix = [[-1 for x in range(self.song.height)]
                             for x in range(self.song.width)]
        self.update()

    def readQueue(self):
        try:
            while True:
                note = self.queue_in.get(block=False)
                if len(note) == 3:
                    status, pitch, vel = struct.unpack('3B', note)
                    channel = status & 0xF
                    msg_type = status >> 4
                    self.processNote(msg_type, channel, pitch, vel)
                    # else:
                    # print("Invalid message length")
        except Empty:
            pass

    def processNote(self, msg_type, channel, pitch, vel):

        btn_id = (msg_type,
                  channel,
                  pitch,
                  vel)
        btn_id_vel = (msg_type, channel, pitch, -1)
        ctrl_key = (msg_type, channel, pitch)

        # master volume
        if ctrl_key == self.device.master_volume_ctrl:
            self.song.master_volume = vel / 127
            (self.master_volume
             .setValue(self.song.master_volume * 256))
        elif self.device.play_btn in [btn_id, btn_id_vel]:
            self._jack_client.transport_start()
        elif self.device.pause_btn in [btn_id, btn_id_vel]:
            self._jack_client.transport_stop()
        elif self.device.rewind_btn in [btn_id, btn_id_vel]:
            self.onRewindClicked()
        elif self.device.goto_btn in [btn_id, btn_id_vel]:
            self.onGotoClicked()
        elif self.device.record_btn in [btn_id, btn_id_vel]:
            self.onRecord()
        elif ctrl_key in self.device.ctrls:
            try:
                ctrl_index = self.device.ctrls.index(ctrl_key)
                clip = (self.song.clips_matrix
                        [ctrl_index]
                        [self.current_vol_block])
                if clip:
                    clip.volume = vel / 127
                    if self.last_clip == clip:
                        self.clip_volume.setValue(self.last_clip.volume * 256)
            except KeyError:
                pass
        elif (btn_id in self.device.scene_buttons
              or btn_id_vel in self.device.scene_buttons):
            try:
                scene_id = self.device.scene_buttons.index(btn_id)
            except ValueError:
                scene_id = self.device.scene_buttons.index(btn_id_vel)

            try:
                self.song.loadSceneId(scene_id)
                self.update()
            except IndexError:
                print('cannot load scene {} - there are only {} scenes.'
                      ''.format(scene_id, len(self.song.scenes)))

        elif (btn_id in self.device.block_buttons
              or btn_id_vel in self.device.block_buttons):
            try:
                self.current_vol_block = (
                    self.device.block_buttons.index(btn_id))
            except ValueError:
                self.current_vol_block = (
                    self.device.block_buttons.index(btn_id_vel))
            for i in range(len(self.device.block_buttons)):
                (a, b_channel, b_pitch, b) = self.device.block_buttons[i]
                if i == self.current_vol_block:
                    color = self.device.red_vel
                else:
                    color = self.device.black_vel
                self.queue_out.put(((self.NOTEON << 4) + b_channel,
                                    b_pitch,
                                    color))
        else:
            x, y = -1, -1
            try:
                x, y = self.device.getXY(btn_id)
            except IndexError:
                pass
            except KeyError:
                try:
                    x, y = self.device.getXY(btn_id_vel)
                except KeyError:
                    pass

            if (x >= 0 and y >= 0):
                self.startStop(x, y)

    def toggleBlinkButton(self):
        for line in self.btn_matrix:
            for btn in line:
                if btn.blink:
                    if self.blktimer.state:
                        btn.setStyleSheet(btn.color)
                    else:
                        btn.setStyleSheet(self.DEFAULT)
        if self.song.is_record:
            if self.blktimer.state:
                self.recordButton.setStyleSheet(self.RECORD_BLINK)
            else:
                self.recordButton.setStyleSheet(self.RECORD_DEFAULT)

        self.blktimer.state = not self.blktimer.state

    def updateProgress(self):
        state, pos = self._jack_client.transport_query()
        if 'bar' in pos:
            bbt = "%d|%d|%03d" % (pos['bar'], pos['beat'], pos['tick'])
        else:
            bbt = "-|-|-"
        seconds = int(pos['frame'] / pos['frame_rate'])
        (minutes, second) = divmod(seconds, 60)
        (hour, minute) = divmod(minutes, 60)
        time = "%d:%02d:%02d" % (hour, minute, second)
        self.bbtLabel.setText("%s\n%s" % (bbt, time))
        for line in self.btn_matrix:
            for btn in line:
                if btn.clip and btn.clip.audio_file:
                    value = ((btn.clip.last_offset
                              / self.song.length(btn.clip))
                             * 97)
                    btn.clip_position.setValue(value)
                    btn.clip_position.repaint()

    def updateDevices(self):
        for action in self.deviceGroup.actions():
            self.deviceGroup.removeAction(action)
            self.menuDevice.removeAction(action)
        for device in self.devices:
            action = QAction(device.name, self.menuDevice)
            action.setCheckable(True)
            action.setData(device)
            self.menuDevice.addAction(action)
            self.deviceGroup.addAction(action)
        action.setChecked(True)
        self.device = device

    def addDevice(self, device):
        self.devices.append(device)
        self.updateDevices()
        self.is_learn_device_mode = False

    def onDeviceSelect(self):
        self.device = self.deviceGroup.checkedAction().data()
        if self.device:
            if self.device.init_command:
                for note in self.device.init_command:
                    self.queue_out.put(note)
            self.redraw()

    def timebase_callback(self, state, nframes, pos, new_pos):
        if pos.frame_rate == 0:
            return None
        pos.valid = 0x10
        pos.bar_start_tick = BAR_START_TICK
        pos.beats_per_bar = self.beat_per_bar.value()
        pos.beat_type = BEAT_TYPE
        pos.ticks_per_beat = TICKS_PER_BEAT
        pos.beats_per_minute = self.bpm.value()
        ticks_per_second = (pos.beats_per_minute *
                            pos.ticks_per_beat) / 60
        ticks = (ticks_per_second * pos.frame) / pos.frame_rate
        (beats, pos.tick) = divmod(int(round(ticks, 0)),
                                   int(round(pos.ticks_per_beat, 0)))
        (bar, beat) = divmod(beats, int(round(pos.beats_per_bar, 0)))
        (pos.bar, pos.beat) = (bar + 1, beat + 1)
        return None
Ejemplo n.º 11
0
class MainWindow(QtWidgets.QMainWindow):

    chat_message_added = pyqtSignal(object)
    avatar_loaded = pyqtSignal(str, bytes)

    def __init__(self, pathToLogs, pathToGameLogs, trayIcon, backGroundColor,
                 logging):

        QtWidgets.QMainWindow.__init__(self)
        self.cache = Cache()

        if backGroundColor:
            self.setStyleSheet("QWidget { background-color: %s; }" %
                               backGroundColor)
        uic.loadUi(resource_stream(__name__, 'MainWindow.ui'), self)
        self.setWindowTitle("Vintel " + vi.version.VERSION + "{dev}".format(
            dev="-SNAPSHOT" if vi.version.SNAPSHOT else ""))
        self.taskbarIconQuiescent = QIcon(
            resource_filename(__name__, 'res/logo_small.png'))
        self.taskbarIconWorking = QIcon(
            resource_filename(__name__, 'res/logo_small_green.png'))
        self.setWindowIcon(self.taskbarIconQuiescent)
        self.setFocusPolicy(QtCore.Qt.StrongFocus)

        self.pathToLogs = pathToLogs
        self.pathToGameLogs = pathToGameLogs
        self.mapTimer = QTimer(self)
        self.mapTimer.timeout.connect(self.updateMapView)
        self.clipboardTimer = QTimer(self)
        self.oldClipboardContent = ""
        self.trayIcon = trayIcon
        self.trayIcon.activated.connect(self.systemTrayActivated)
        self.clipboard = QtWidgets.QApplication.clipboard()
        self.clipboard.clear(mode=self.clipboard.Clipboard)
        self.alarmDistance = 0
        self.lastStatisticsUpdate = 0
        self.chatEntries = []
        self.frameButton.setVisible(False)
        self.scanIntelForKosRequestsEnabled = True
        self.initialMapPosition = None
        self.mapPositionsDict = {}
        self.deferedScrollPosition = None
        self.logging = logging
        # Load user's toon names
        self.knownPlayerNames = self.cache.getFromCache("known_player_names")
        if self.knownPlayerNames:
            self.knownPlayerNames = set(self.knownPlayerNames.split(","))
        else:
            self.knownPlayerNames = set()
            diagText = "Vintel scans EVE system logs and remembers your characters as they change systems.\n\nSome features (clipboard KOS checking, alarms, etc.) may not work until your character(s) have been registered. Change systems, with each character you want to monitor, while Vintel is running to remedy this."
            QMessageBox.warning(None, "Known Characters not Found", diagText,
                                QMessageBox.Ok)

        # Set up user's intel rooms
        roomnames = self.cache.getFromCache("room_names")
        if roomnames:
            roomnames = roomnames.split(",")
        else:
            roomnames = (u"TheCitadel", u"North Provi Intel",
                         u"North Catch Intel", "North Querious Intel")
            self.cache.putIntoCache("room_names", u",".join(roomnames),
                                    60 * 60 * 24 * 365 * 5)
        self.roomnames = roomnames

        # Disable the sound UI if sound is not available
        if not SoundManager().soundAvailable:
            self.changeSound(disable=True)
        else:
            self.changeSound()

        # Set up Transparency menu - fill in opacity values and make connections
        self.opacityGroup = QActionGroup(self.menu)
        for i in (100, 80, 60, 40, 20):
            action = QAction("Opacity {0}%".format(i), None, checkable=True)
            if i == 100:
                action.setChecked(True)
            action.opacity = i / 100.0
            action.triggered.connect(self.changeOpacity)
            self.opacityGroup.addAction(action)
            self.menuTransparency.addAction(action)

        #
        # Platform specific UI resizing - we size items in the resource files to look correct on the mac,
        # then resize other platforms as needed
        #
        if sys.platform.startswith("win32") or sys.platform.startswith(
                "cygwin"):
            font = self.statisticsButton.font()
            font.setPointSize(8)
            self.statisticsButton.setFont(font)
            self.jumpbridgesButton.setFont(font)
        elif sys.platform.startswith("linux"):
            pass

        self.wireUpUIConnections()
        self.recallCachedSettings()
        self.setupThreads()
        self.setupMap(True)

    def paintEvent(self, event):
        opt = QStyleOption()
        opt.initFrom(self)
        painter = QPainter(self)
        self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)

    def recallCachedSettings(self):
        try:
            self.cache.recallAndApplySettings(self, "settings")
        except Exception as e:
            logging.error(e)
            # todo: add a button to delete the cache / DB
            self.trayIcon.showMessage(
                "Settings error",
                "Something went wrong loading saved state:\n {0}".format(
                    str(e)), 1)

    def wireUpUIConnections(self):
        # Wire up general UI connections
        self.clipboard.changed.connect(self.clipboardChanged)

        self.autoScanIntelAction.triggered.connect(self.changeAutoScanIntel)
        self.kosClipboardActiveAction.triggered.connect(
            self.changeKosCheckClipboard)
        self.zoomInButton.clicked.connect(self.zoomMapIn)
        self.zoomOutButton.clicked.connect(self.zoomMapOut)
        self.statisticsButton.clicked.connect(self.changeStatisticsVisibility)
        self.jumpbridgesButton.clicked.connect(
            self.changeJumpbridgesVisibility)
        self.chatLargeButton.clicked.connect(self.chatLarger)
        self.chatSmallButton.clicked.connect(self.chatSmaller)
        self.infoAction.triggered.connect(self.showInfo)
        self.showChatAvatarsAction.triggered.connect(self.changeShowAvatars)
        self.alwaysOnTopAction.triggered.connect(self.changeAlwaysOnTop)
        self.chooseChatRoomsAction.triggered.connect(self.showChatroomChooser)
        self.catchRegionAction.triggered.connect(
            lambda: self.handleRegionMenuItemSelected(self.catchRegionAction))
        self.providenceRegionAction.triggered.connect(
            lambda: self.handleRegionMenuItemSelected(self.
                                                      providenceRegionAction))
        self.queriousRegionAction.triggered.connect(
            lambda: self.handleRegionMenuItemSelected(self.queriousRegionAction
                                                      ))
        self.providenceCatchRegionAction.triggered.connect(
            lambda: self.handleRegionMenuItemSelected(
                self.providenceCatchRegionAction))
        self.providenceCatchCompactRegionAction.triggered.connect(
            lambda: self.handleRegionMenuItemSelected(
                self.providenceCatchCompactRegionAction))
        self.chooseRegionAction.triggered.connect(self.showRegionChooser)
        self.showChatAction.triggered.connect(self.changeChatVisibility)
        self.soundSetupAction.triggered.connect(self.showSoundSetup)
        self.activateSoundAction.triggered.connect(self.changeSound)
        self.useSpokenNotificationsAction.triggered.connect(
            self.changeUseSpokenNotifications)
        self.trayIcon.alarm_distance.connect(self.changeAlarmDistance)
        self.framelessWindowAction.triggered.connect(self.changeFrameless)
        #self.trayIcon.change_frameless.connect(self.changeFrameless)
        self.frameButton.clicked.connect(self.changeFrameless)
        self.quitAction.triggered.connect(self.close)
        self.trayIcon.quit_sig.connect(self.close)
        self.jumpBridgeDataAction.triggered.connect(self.showJumpBridgeChooser)
        #self.mapView.page().scrollRequested.connect(self.mapPositionChanged)
        self.mapView.page().mapLinkClicked.connect(self.mapLinkClicked)
        self.mapView.loadFinished.connect(self.handleLoadFinished)
        pass

    def handleLoadFinished(self):
        self.loaded = True
        if self.deferedScrollPosition:
            self.mapView.page().runJavaScript(
                'window.scrollTo({}, {});'.format(
                    self.deferedScrollPosition.x(),
                    self.deferedScrollPosition.y()))
            self.deferedScrollPosition = None

    def setMapScrollPosition(self, position):
        if self.loaded:
            self.mapView.page().runJavaScript(
                'window.scrollTo({}, {});'.format(position.x(), position.y()))
        else:
            self.deferedScrollPosition = position

    def getMapScrollPosition(self):
        return self.mapView.page().scrollPosition()

    def setupThreads(self):
        # Set up threads and their connections
        self.avatarFindThread = AvatarFindThread()
        self.avatarFindThread.avatar_update.connect(
            self.updateAvatarOnChatEntry)
        self.avatarFindThread.start()

        self.kosRequestThread = KOSCheckerThread()
        self.kosRequestThread.kos_result.connect(self.showKosResult)
        self.kosRequestThread.start()

        self.filewatcherThread = filewatcher.FileWatcher(self.pathToLogs)
        self.filewatcherThread.data_changed.connect(self.logFileChanged)
        self.filewatcherThread.start()

        self.fileWatcherForGameLogsThread = filewatcher.FileWatcher(
            self.pathToGameLogs)
        self.fileWatcherForGameLogsThread.data_changed.connect(
            self.logFileChanged)
        self.fileWatcherForGameLogsThread.start()

        self.versionCheckThread = amazon_s3.NotifyNewVersionThread()
        self.versionCheckThread.newer_version.connect(self.notifyNewerVersion)
        self.versionCheckThread.start()

        self.statisticsThread = MapStatisticsThread()
        self.statisticsThread.statisticDataUpdate.connect(
            self.updateStatisticsOnMap)
        self.statisticsThread.start()
        # statisticsThread is blocked until first call of requestStatistics

    def setupMap(self, initialize=False):
        self.mapTimer.stop()
        self.filewatcherThread.paused = True
        self.fileWatcherForGameLogsThread.paused = True
        logging.info("Finding map file")
        regionName = self.cache.getFromCache("region_name")
        if not regionName:
            regionName = "Providence"
        svg = None
        try:
            with open(
                    resource_filename(
                        __name__,
                        "res/mapdata/{0}.svg".format(regionName))) as svgFile:
                svg = svgFile.read()
        except Exception as e:
            logging.critical(e)

        try:
            self.dotlan = dotlan.Map(regionName, svg)
        except dotlan.DotlanException as e:
            logging.error(e)
            QMessageBox.critical(None, "Error getting map", six.text_type(e),
                                 QMessageBox.Ok)
            sys.exit(1)

        if self.dotlan.outdatedCacheError:
            e = self.dotlan.outdatedCacheError
            diagText = "Something went wrong getting map data. Proceeding with older cached data. " \
                       "Check for a newer version and inform the maintainer.\n\nError: {0} {1}".format(type(e), six.text_type(e))
            logging.warn(diagText)
            QMessageBox.warning(None, "Using map from cache", diagText,
                                QMessageBox.Ok)

        # Load the jumpbridges
        logging.critical("Load jump bridges")
        self.setJumpbridges(self.cache.getFromCache("jumpbridge_url"))
        self.systems = self.dotlan.systems
        logging.critical("Creating chat parser")
        self.chatparser = ChatParser(self.pathToLogs, self.roomnames,
                                     self.systems, self.logging)

        # Menus - only once
        if initialize:
            logging.critical("Initializing contextual menus")

            # Add a contextual menu to the mapView
            def mapContextMenuEvent(event):
                #if QApplication.activeWindow() or QApplication.focusWidget():
                self.mapView.contextMenu.exec_(
                    self.mapToGlobal(QPoint(event.x(), event.y())))

            self.mapView.contextMenuEvent = mapContextMenuEvent
            self.mapView.contextMenu = self.trayIcon.contextMenu()

            # Also set up our app menus
            if not regionName:
                self.providenceCatchRegionAction.setChecked(True)
            elif regionName.startswith("Providencecatch"):
                self.providenceCatchRegionAction.setChecked(True)
            elif regionName.startswith("Catch"):
                self.catchRegionAction.setChecked(True)
            elif regionName.startswith("Providence"):
                self.providenceRegionAction.setChecked(True)
            elif regionName.startswith("Querious"):
                self.queriousRegionAction.setChecked(True)
            else:
                self.chooseRegionAction.setChecked(True)
        self.jumpbridgesButton.setChecked(False)
        self.statisticsButton.setChecked(False)

        # Update the new map view, then clear old statistics from the map and request new
        logging.critical("Updating the map")
        self.updateMapView()
        self.setInitialMapPositionForRegion(regionName)
        self.mapTimer.start(MAP_UPDATE_INTERVAL_MSECS)
        # Allow the file watcher to run now that all else is set up
        self.filewatcherThread.paused = False
        self.fileWatcherForGameLogsThread.paused = False
        logging.critical("Map setup complete")

    # def eventFilter(self, obj, event):
    #     if event.type() == QtCore.QEvent.WindowDeactivate:
    #         self.enableContextMenu(False)
    #         return True
    #     elif event.type() == QtCore.QEvent.WindowActivate:
    #         self.enableContextMenu(True)
    #         return True
    #     return False

    def startClipboardTimer(self):
        """
            Start a timer to check the keyboard for changes and kos check them,
            first initializing the content so we dont kos check from random content
        """
        self.oldClipboardContent = tuple(six.text_type(self.clipboard.text()))
        self.clipboardTimer.timeout.connect(self.clipboardChanged)
        self.clipboardTimer.start(CLIPBOARD_CHECK_INTERVAL_MSECS)

    def stopClipboardTimer(self):
        if self.clipboardTimer:
            self.clipboardTimer.timeout.connect(self.clipboardChanged)
            self.clipboardTimer.stop()

    def closeEvent(self, event):
        """
            Persisting things to the cache before closing the window
        """
        # Known playernames
        if self.knownPlayerNames:
            value = ",".join(self.knownPlayerNames)
            self.cache.putIntoCache("known_player_names", value,
                                    60 * 60 * 24 * 30)

        # Program state to cache (to read it on next startup)
        settings = ((None, "restoreGeometry", self.saveGeometry()),
                    (None, "restoreState",
                     self.saveState()), ("splitter", "restoreGeometry",
                                         self.splitter.saveGeometry()),
                    ("splitter", "restoreState",
                     self.splitter.saveState()), ("mapView", "setZoomFactor",
                                                  self.mapView.zoomFactor()),
                    (None, "changeChatFontSize", ChatEntryWidget.TEXT_SIZE),
                    (None, "changeOpacity",
                     self.opacityGroup.checkedAction().opacity),
                    (None, "changeAlwaysOnTop",
                     self.alwaysOnTopAction.isChecked()),
                    (None, "changeShowAvatars",
                     self.showChatAvatarsAction.isChecked()),
                    (None, "changeAlarmDistance", self.alarmDistance),
                    (None, "changeSound",
                     self.activateSoundAction.isChecked()),
                    (None, "changeChatVisibility",
                     self.showChatAction.isChecked()),
                    (None, "loadInitialMapPositions",
                     self.mapPositionsDict), (None, "setSoundVolume",
                                              SoundManager().soundVolume),
                    (None, "changeFrameless",
                     self.framelessWindowAction.isChecked()),
                    (None, "changeUseSpokenNotifications",
                     self.useSpokenNotificationsAction.isChecked()),
                    (None, "changeKosCheckClipboard",
                     self.kosClipboardActiveAction.isChecked()),
                    (None, "changeAutoScanIntel",
                     self.scanIntelForKosRequestsEnabled))
        self.cache.putIntoCache("settings", str(settings), 60 * 60 * 24 * 30)

        # Stop the threads
        try:
            SoundManager().quit()
            self.avatarFindThread.quit()
            self.avatarFindThread.wait()
            self.filewatcherThread.quit()
            self.filewatcherThread.wait()
            self.fileWatcherForGameLogsThread.quit()
            self.fileWatcherForGameLogsThread.wait()
            self.kosRequestThread.quit()
            self.kosRequestThread.wait()
            self.versionCheckThread.quit()
            self.versionCheckThread.wait()
            self.statisticsThread.quit()
            self.statisticsThread.wait()
        except Exception:
            pass
        self.trayIcon.hide()
        event.accept()

    def notifyNewerVersion(self, newestVersion):
        self.trayIcon.showMessage("Newer Version", (
            "An update is available for Vintel.\nhttps://github.com/Xanthos-Eve/vintel"
        ), 1)

    def changeChatVisibility(self, newValue=None):
        if newValue is None:
            newValue = self.showChatAction.isChecked()
        self.showChatAction.setChecked(newValue)
        self.chatbox.setVisible(newValue)

    def changeKosCheckClipboard(self, newValue=None):
        if newValue is None:
            newValue = self.kosClipboardActiveAction.isChecked()
        self.kosClipboardActiveAction.setChecked(newValue)
        if newValue:
            self.startClipboardTimer()
        else:
            self.stopClipboardTimer()

    def changeAutoScanIntel(self, newValue=None):
        if newValue is None:
            newValue = self.autoScanIntelAction.isChecked()
        self.autoScanIntelAction.setChecked(newValue)
        self.scanIntelForKosRequestsEnabled = newValue

    def changeUseSpokenNotifications(self, newValue=None):
        if SoundManager().platformSupportsSpeech():
            if newValue is None:
                newValue = self.useSpokenNotificationsAction.isChecked()
            self.useSpokenNotificationsAction.setChecked(newValue)
            SoundManager().setUseSpokenNotifications(newValue)
        else:
            self.useSpokenNotificationsAction.setChecked(False)
            self.useSpokenNotificationsAction.setEnabled(False)

    def changeOpacity(self, newValue=None):
        if newValue is not None:
            for action in self.opacityGroup.actions():
                if action.opacity == newValue:
                    action.setChecked(True)
        action = self.opacityGroup.checkedAction()
        self.setWindowOpacity(action.opacity)

    def changeSound(self, newValue=None, disable=False):
        if disable:
            self.activateSoundAction.setChecked(False)
            self.activateSoundAction.setEnabled(False)
            self.soundSetupAction.setEnabled(False)
            #self.soundButton.setEnabled(False)
            QMessageBox.warning(
                None, "Sound disabled",
                "The lib 'pyglet' which is used to play sounds cannot be found, "
                "so the soundsystem is disabled.\nIf you want sound, please install the 'pyglet' library. This warning will not be shown again.",
                QMessageBox.Ok)
        else:
            if newValue is None:
                newValue = self.activateSoundAction.isChecked()
            self.activateSoundAction.setChecked(newValue)
            SoundManager().soundActive = newValue

    def changeAlwaysOnTop(self, newValue=None):
        if newValue is None:
            newValue = self.alwaysOnTopAction.isChecked()
        self.hide()
        self.alwaysOnTopAction.setChecked(newValue)
        if newValue:
            self.setWindowFlags(self.windowFlags()
                                | QtCore.Qt.WindowStaysOnTopHint)
        else:
            self.setWindowFlags(self.windowFlags()
                                & (~QtCore.Qt.WindowStaysOnTopHint))
        self.show()

    def changeFrameless(self, newValue=None):
        if newValue is None:
            newValue = not self.frameButton.isVisible()
        self.hide()
        if newValue:
            self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
            self.changeAlwaysOnTop(True)
        else:
            self.setWindowFlags(self.windowFlags()
                                & (~QtCore.Qt.FramelessWindowHint))
        self.menubar.setVisible(not newValue)
        self.frameButton.setVisible(newValue)
        self.framelessWindowAction.setChecked(newValue)

        for cm in TrayContextMenu.instances:
            cm.framelessCheck.setChecked(newValue)
        self.show()

    def changeShowAvatars(self, newValue=None):
        if newValue is None:
            newValue = self.showChatAvatarsAction.isChecked()
        self.showChatAvatarsAction.setChecked(newValue)
        ChatEntryWidget.SHOW_AVATAR = newValue
        for entry in self.chatEntries:
            entry.avatarLabel.setVisible(newValue)

    def changeChatFontSize(self, newSize):
        if newSize:
            for entry in self.chatEntries:
                entry.changeFontSize(newSize)
            ChatEntryWidget.TEXT_SIZE = newSize

    def chatSmaller(self):
        newSize = ChatEntryWidget.TEXT_SIZE - 1
        self.changeChatFontSize(newSize)

    def chatLarger(self):
        newSize = ChatEntryWidget.TEXT_SIZE + 1
        self.changeChatFontSize(newSize)

    def changeAlarmDistance(self, distance):
        self.alarmDistance = distance
        for cm in TrayContextMenu.instances:
            for action in cm.distanceGroup.actions():
                if action.alarmDistance == distance:
                    action.setChecked(True)
        self.trayIcon.alarmDistance = distance

    def changeJumpbridgesVisibility(self):
        newValue = self.dotlan.changeJumpbridgesVisibility()
        self.jumpbridgesButton.setChecked(newValue)
        self.updateMapView()

    def changeStatisticsVisibility(self):
        newValue = self.dotlan.changeStatisticsVisibility()
        self.statisticsButton.setChecked(newValue)
        self.updateMapView()
        if newValue:
            self.statisticsThread.requestStatistics()

    def clipboardChanged(self, mode=0):
        if not (mode == 0 and self.kosClipboardActiveAction.isChecked()
                and self.clipboard.mimeData().hasText()):
            return
        content = six.text_type(self.clipboard.text())
        contentTuple = tuple(content)
        # Limit redundant kos checks
        if contentTuple != self.oldClipboardContent:
            parts = tuple(content.split("\n"))
            knownPlayers = self.knownPlayerNames
            for part in parts:
                # Make sure user is in the content (this is a check of the local system in Eve).
                # also, special case for when you have no knonwnPlayers (initial use)
                if not knownPlayers or part in knownPlayers:
                    self.trayIcon.setIcon(self.taskbarIconWorking)
                    self.kosRequestThread.addRequest(parts, "clipboard", True)
                    break
            self.oldClipboardContent = contentTuple

    def mapLinkClicked(self, url):
        systemName = six.text_type(url.path().split("/")[-1]).upper()
        try:
            system = self.systems[str(systemName)]
            sc = SystemChat(self, SystemChat.SYSTEM, system, self.chatEntries,
                            self.knownPlayerNames)
            self.chat_message_added.connect(sc.addChatEntry)
            self.avatar_loaded.connect(sc.newAvatarAvailable)
            sc.signal_location_set.connect(self.setLocation)
            sc.show()
        except Exception as e:
            # We didn't click a system url
            logging.critical("mapLinkClicked->Error::{0}".format(e))
            pass

    def markSystemOnMap(self, systemname):
        self.systems[six.text_type(systemname)].mark()
        self.updateMapView()

    def setLocation(self, char, newSystem):
        for system in self.systems.values():
            system.removeLocatedCharacter(char)
        if not newSystem == "?" and newSystem in self.systems:
            self.systems[newSystem].addLocatedCharacter(char)
            self.setMapContent(self.dotlan.svg)

    def setMapContent(self, content):
        self.loaded = False
        if self.initialMapPosition is None:
            scrollPosition = self.getMapScrollPosition()
        else:
            scrollPosition = self.initialMapPosition

        self.mapView.setHtml(content)
        # Make sure we have positioned the window before we nil the initial position;
        # even though we set it, it may not take effect until the map is fully loaded
        scrollPosition = self.mapView.page().scrollPosition()
        self.scrollPosition = self.setMapScrollPosition(scrollPosition)
        scrollPosition = self.getMapScrollPosition()

        if scrollPosition and (scrollPosition.x() or scrollPosition.y()):
            self.initialMapPosition = None

    def loadInitialMapPositions(self, newDictionary):
        self.mapPositionsDict = newDictionary

    def setInitialMapPositionForRegion(self, regionName):
        try:
            if not regionName:
                regionName = self.cache.getFromCache("region_name")
            if regionName:
                xy = self.mapPositionsDict[regionName]
                self.initialMapPosition = QPoint(xy[0], xy[1])
        except Exception:
            pass

    def mapPositionChanged(self, dx, dy, rectToScroll):
        regionName = self.cache.getFromCache("region_name")
        if regionName:
            scrollPosition = self.mapView.page().mainFrame().scrollPosition()
            self.mapPositionsDict[regionName] = (scrollPosition.x(),
                                                 scrollPosition.y())

    def showChatroomChooser(self):
        chooser = ChatroomsChooser(self)
        chooser.roomsChanged.connect(self.changedRoomnames)
        chooser.show()

    def showJumpBridgeChooser(self):
        url = self.cache.getFromCache("jumpbridge_url")
        chooser = JumpBridgeChooser(self, url)
        chooser.setJumpbridgeURL.connect(self.setJumpbridges)
        chooser.show()

    def setSoundVolume(self, value):
        SoundManager().setSoundVolume(value)

    def setJumpbridges(self, url):
        if url is None:
            url = ""
        try:
            data = []
            if url != "":
                resp = requests.get(url)
                for line in resp.iter_lines(decode_unicode=True):
                    parts = line.strip().split()
                    if len(parts) == 3:
                        data.append(parts)
            else:
                data = amazon_s3.getJumpbridgeData(self.dotlan.region.lower())
            self.dotlan.setJumpbridges(data)
            self.cache.putIntoCache("jumpbridge_url", url,
                                    60 * 60 * 24 * 365 * 8)
        except Exception as e:
            QMessageBox.warning(None, "Loading jumpbridges failed!",
                                "Error: {0}".format(six.text_type(e)),
                                QMessageBox.Ok)

    def handleRegionMenuItemSelected(self, menuAction=None):
        self.catchRegionAction.setChecked(False)
        self.providenceRegionAction.setChecked(False)
        self.queriousRegionAction.setChecked(False)
        self.providenceCatchRegionAction.setChecked(False)
        self.providenceCatchCompactRegionAction.setChecked(False)
        self.chooseRegionAction.setChecked(False)
        if menuAction:
            menuAction.setChecked(True)
            regionName = six.text_type(menuAction.property("regionName"))
            regionName = dotlan.convertRegionName(regionName)
            Cache().putIntoCache("region_name", regionName, 60 * 60 * 24 * 365)
            self.setupMap()

    def showRegionChooser(self):
        def handleRegionChosen():
            self.handleRegionMenuItemSelected(None)
            self.chooseRegionAction.setChecked(True)
            self.setupMap()

        self.chooseRegionAction.setChecked(False)
        chooser = RegionChooser(self)
        chooser.newRegionChosen.connect(handleRegionChosen)
        chooser.show()

    def addMessageToIntelChat(self, message):
        scrollToBottom = False
        if (self.chatListWidget.verticalScrollBar().value() ==
                self.chatListWidget.verticalScrollBar().maximum()):
            scrollToBottom = True
        chatEntryWidget = ChatEntryWidget(message)
        listWidgetItem = QtWidgets.QListWidgetItem(self.chatListWidget)
        listWidgetItem.setSizeHint(chatEntryWidget.sizeHint())
        self.chatListWidget.addItem(listWidgetItem)
        self.chatListWidget.setItemWidget(listWidgetItem, chatEntryWidget)
        self.avatarFindThread.addChatEntry(chatEntryWidget)
        self.chatEntries.append(chatEntryWidget)
        chatEntryWidget.mark_system.connect(self.markSystemOnMap)
        self.chat_message_added.emit(chatEntryWidget)
        self.pruneMessages()
        if scrollToBottom:
            self.chatListWidget.scrollToBottom()

    def pruneMessages(self):
        try:
            now = time.mktime(evegate.currentEveTime().timetuple())
            for row in range(self.chatListWidget.count()):
                chatListWidgetItem = self.chatListWidget.item(0)
                chatEntryWidget = self.chatListWidget.itemWidget(
                    chatListWidgetItem)
                message = chatEntryWidget.message
                if now - time.mktime(
                        message.timestamp.timetuple()) > MESSAGE_EXPIRY_SECS:
                    self.chatEntries.remove(chatEntryWidget)
                    self.chatListWidget.takeItem(0)

                    for widgetInMessage in message.widgets:
                        widgetInMessage.removeItemWidget(chatListWidgetItem)
                else:
                    break
        except Exception as e:
            logging.error(e)

    def showKosResult(self, state, text, requestType, hasKos):
        if not self.scanIntelForKosRequestsEnabled:
            return
        try:
            if hasKos:
                SoundManager().playSound("kos", text)
            if state == "ok":
                if requestType == "xxx":  # An xxx request out of the chat
                    self.trayIcon.showMessage("Player KOS-Check", text, 1)
                elif requestType == "clipboard":  # request from clipboard-change
                    if len(text) <= 0:
                        text = "None KOS"
                    self.trayIcon.showMessage("Your KOS-Check", text, 1)
                text = text.replace("\n\n", "<br>")
                message = chatparser.chatparser.Message(
                    "Vintel KOS-Check", text, evegate.currentEveTime(),
                    "VINTEL", [], states.NOT_CHANGE, text.upper(), text)
                self.addMessageToIntelChat(message)
            elif state == "error":
                self.trayIcon.showMessage("KOS Failure", text, 3)
        except Exception:
            pass
        self.trayIcon.setIcon(self.taskbarIconQuiescent)

    def changedRoomnames(self, newRoomnames):
        self.cache.putIntoCache("room_names", u",".join(newRoomnames),
                                60 * 60 * 24 * 365 * 5)
        self.chatparser.rooms = newRoomnames

    def showInfo(self):
        infoDialog = QtWidgets.QDialog(self)
        uic.loadUi(resource_stream(__name__, "Info.ui"), infoDialog)
        infoDialog.versionLabel.setText(u"Version: {0}".format(
            vi.version.VERSION))
        infoDialog.logoLabel.setPixmap(
            QPixmap(resource_filename(__name__, "res/logo.png")))
        infoDialog.closeButton.clicked.connect(infoDialog.accept)
        infoDialog.show()

    def showSoundSetup(self):
        dialog = QtWidgets.QDialog(self)
        uic.loadUi(resource_stream(__name__, "SoundSetup.ui"), dialog)
        dialog.volumeSlider.setValue(SoundManager().soundVolume)
        dialog.volumeSlider.valueChanged.connect(SoundManager().setSoundVolume)
        dialog.testSoundButton.clicked.connect(SoundManager().playSound)
        dialog.closeButton.clicked.connect(dialog.accept)
        dialog.show()

    def systemTrayActivated(self, reason):
        if reason == QtWidgets.QSystemTrayIcon.Trigger:
            if self.isMinimized():
                self.showNormal()
                self.activateWindow()
            elif not self.isActiveWindow():
                self.activateWindow()
            else:
                self.showMinimized()

    def updateAvatarOnChatEntry(self, chatEntry, avatarData):
        updated = chatEntry.updateAvatar(avatarData)
        if not updated:
            self.avatarFindThread.addChatEntry(chatEntry, clearCache=True)
        else:
            self.avatar_loaded.emit(chatEntry.message.user, avatarData)

    def updateStatisticsOnMap(self, data):
        if not self.statisticsButton.isChecked():
            return
        if data["result"] == "ok":
            self.dotlan.addSystemStatistics(data["statistics"])
        elif data["result"] == "error":
            text = data["text"]
            self.trayIcon.showMessage("Loading statstics failed", text, 3)
            logging.error("updateStatisticsOnMap, error: %s" % text)

    def updateMapView(self):
        logging.debug("Updating map start")
        self.setMapContent(self.dotlan.svg)
        logging.debug("Updating map complete")

    def zoomMapIn(self):
        self.mapView.setZoomFactor(self.mapView.zoomFactor() + 0.1)

    def zoomMapOut(self):
        self.mapView.setZoomFactor(self.mapView.zoomFactor() - 0.1)

    def logFileChanged(self, path):
        messages = self.chatparser.fileModified(path)
        for message in messages:
            # If players location has changed
            if message.status == states.LOCATION:
                self.knownPlayerNames.add(message.user)
                self.setLocation(message.user, message.systems[0])
            elif message.status == states.KOS_STATUS_REQUEST:
                # Do not accept KOS requests from any but monitored intel channels
                # as we don't want to encourage the use of xxx in those channels.
                if not message.room in self.roomnames:
                    text = message.message[4:]
                    text = text.replace("  ", ",")
                    parts = (name.strip() for name in text.split(","))
                    self.trayIcon.setIcon(self.taskbarIconWorking)
                    self.kosRequestThread.addRequest(parts, "xxx", False)
            # Otherwise consider it a 'normal' chat message
            elif message.user not in ("EVE-System", "EVE System"
                                      ) and message.status != states.IGNORE:
                self.addMessageToIntelChat(message)
                # For each system that was mentioned in the message, check for alarm distance to the current system
                # and alarm if within alarm distance.
                systemList = self.dotlan.systems
                if message.systems:
                    for system in message.systems:
                        systemname = system.name
                        systemList[systemname].setStatus(message.status)
                        if message.status in (
                                states.REQUEST, states.ALARM
                        ) and message.user not in self.knownPlayerNames:
                            alarmDistance = self.alarmDistance if message.status == states.ALARM else 0
                            for nSystem, data in system.getNeighbours(
                                    alarmDistance).items():
                                distance = data["distance"]
                                chars = nSystem.getLocatedCharacters()
                                if len(chars
                                       ) > 0 and message.user not in chars:
                                    self.trayIcon.showNotification(
                                        message, system.name, ", ".join(chars),
                                        distance)
                self.setMapContent(self.dotlan.svg)
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        #adjusting main window
        self.resize(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)
        self.move(0, 0)

        #setting scaling factor = 1
        self.zoom = 1

        #initializing and registering centeral widget
        self.centeral_widget = QWidget(self)
        self.setCentralWidget(self.centeral_widget)

        #initializing frame view
        self.frame_panel = DLabel(self.centeral_widget, self)
        # position frame panel
        self.frame_panel.move(FRAME_MARGIN_LEFT, FRAME_MARGIN_TOP)

        # set initial image
        self.frame_panel.setPixmap(QPixmap("initial.png"))

        #fur testing
        self.frame_panel.content = cv2.imread("initial.png")

        # enabling custom context menu
        self.setContextMenuPolicy(Qt.CustomContextMenu)

        #initializing menubar
        self.menubar = QMenuBar(self)

        #adding menu options
        self.file_menu = QMenu(self.menubar)
        self.zoom_menu = QMenu(self.menubar)
        self.marker_menu = QMenu(self.menubar)
        self.bounding_boxes = QMenu(self.menubar)

        #registering menubar
        self.menubar.move(0, 0)
        self.menubar.setDefaultUp(False)
        self.setMenuBar(self.menubar)

        #initializing and registering statusbar
        self.statusbar = QStatusBar(self)
        self.setStatusBar(self.statusbar)

        #add permanent frame lable to statusbar
        self.statusbar.frame_lable = QLabel(self)
        self.statusbar.frame_lable.setText("")
        self.statusbar.frame_lable.resize(100, 50)
        self.statusbar.frame_lable.move(DEFAULT_WINDOW_WIDTH - 100,
                                        DEFAULT_WINDOW_HEIGHT - 37)

        # set initail status tip
        self.statusbar.setStatusTip("Please choose a video file to begin")

        self.initialize_submenus()

        #assign visual texts to window, menus and submenus
        self.setTexts()

        #initailize video path
        self.vid_path = ""
        self.vid = False

        #adding slider for quick seek
        self.slider = QSlider(self.centeral_widget)

        #setting slider geometery
        self.slider.resize(DEFAULT_WINDOW_WIDTH - SLIDER_MARGINS * 2,
                           SLIDER_HEIGHT)
        self.slider.move(
            SLIDER_MARGINS, DEFAULT_WINDOW_HEIGHT - STATUSBAR_HEIGHT -
            SLIDER_HEIGHT - SLIDER_MARGINS)

        #setting orientation
        self.slider.setOrientation(Qt.Horizontal)

        #connecting mouse event
        self.slider.valueChanged.connect(self.moveSliderToClickedPosition)
        self.slider.setDisabled(True)

        # initialize context menu
        self.initContextMenu()

        # initializing mongo client and db
        self.database_client = MongoClient()
        self.db = self.database_client.fish

        # initializing collection
        self.collection = False

    def initContextMenu(self):

        # initialize custom context menu and submenu
        self.context_menu = QMenu(self)
        self.context_sub_menu = QMenu('Class', self)

        # create a delete action
        self.delete_action = QAction('Delete', self)
        self.delete_action.triggered.connect(self.frame_panel.deleteMarker)

        # create get lable action
        self.ask_lable = QAction('Ask Lable', self)
        self.ask_lable.triggered.connect(self.frame_panel.getLable)

        # adding dictionary for reverse search
        self.classes = {}

        # initializing class group
        self.class_group = QActionGroup(self)

        # adding submenu actions

        for item in CONFIGURED_CLASSES:
            self.classes[item] = QAction(item, self)
            self.classes[item].setCheckable(True)
            self.class_group.addAction(self.classes[item])

        # connecting group to handlers and setting default class
        self.class_group.triggered.connect(self.frame_panel.toggleMarkerClass)
        self.classes[CONFIGURED_CLASSES[0]].setChecked(True)

        # add delete action to menu
        self.context_menu.addAction(self.delete_action)

        # add ask lable action to menu
        self.context_menu.addAction(self.ask_lable)

        # add class actions to submenu
        self.context_sub_menu.addActions(self.class_group.actions())

        # regester submenu to menu
        self.context_menu.addMenu(self.context_sub_menu)

    def initialize_submenus(self):

        #intializing open submenu
        self.open_submenu = QAction(self)
        self.open_submenu.setShortcut('Ctrl+F')
        self.open_submenu.setStatusTip("Open")
        self.open_submenu.triggered.connect(self.selectFile)

        # intializing export submenu
        self.export_submenu = QAction(self)
        self.export_submenu.setShortcut('Ctrl+E')
        self.export_submenu.setStatusTip("Export")
        self.export_submenu.triggered.connect(self.exportImages)
        self.export_submenu.setDisabled(True)

        # intializing export submenu
        self.init_submenu = QAction(self)
        self.init_submenu.setShortcut('Ctrl+I')
        self.init_submenu.setStatusTip("Initialize")
        self.init_submenu.triggered.connect(self.initializeWithAlgorithm)
        self.init_submenu.setDisabled(True)

        #initializing options for zoom menu
        self.p50_submenu = QAction(self)
        self.p50_submenu.setStatusTip("50%")
        self.p50_submenu.setCheckable(True)
        self.p50_submenu.val = 0.5

        self.p100_submenu = QAction(self)
        self.p100_submenu.setStatusTip("100%")
        self.p100_submenu.setCheckable(True)
        self.p100_submenu.val = 1.0

        self.p150_submenu = QAction(self)
        self.p150_submenu.setStatusTip("150%")
        self.p150_submenu.setCheckable(True)
        self.p150_submenu.val = 1.5

        self.p200_submenu = QAction(self)
        self.p200_submenu.setStatusTip("200%")
        self.p200_submenu.setCheckable(True)
        self.p200_submenu.val = 2.0

        self.zoom_group = QActionGroup(self)
        self.zoom_group.addAction(self.p50_submenu)
        self.zoom_group.addAction(self.p100_submenu)
        self.zoom_group.addAction(self.p150_submenu)
        self.zoom_group.addAction(self.p200_submenu)
        self.p100_submenu.setChecked(True)
        self.zoom_group.setDisabled(True)
        self.zoom_group.triggered.connect(self.changeZoom)

        #registering file submenus
        self.file_menu.addAction(self.open_submenu)

        # registering bounding box submenus
        self.bounding_boxes.addAction(self.export_submenu)
        self.bounding_boxes.addAction(self.init_submenu)

        #registering zoom submenus
        self.zoom_menu.addActions(self.zoom_group.actions())

        #registering menus to menubar
        self.menubar.addAction(self.file_menu.menuAction())
        self.menubar.addAction(self.zoom_menu.menuAction())
        self.menubar.addAction(self.bounding_boxes.menuAction())

    def setTexts(self):
        self.setWindowTitle("FIBVID Analyser")

        #file menu
        self.file_menu.setTitle("File")

        self.open_submenu.setText("Open")

        self.bounding_boxes.setTitle("Boxes")

        # bouding boxes menu
        self.export_submenu.setText("Export as Images")
        self.init_submenu.setText("Initialize with Model")

        #zoom menu
        self.zoom_menu.setTitle("Zoom")

        self.p50_submenu.setText("50%")
        self.p100_submenu.setText("100%")
        self.p100_submenu.setText("100%")
        self.p150_submenu.setText("150%")
        self.p200_submenu.setText("200%")

    def selectFile(self):

        # get the file browser pop-up
        path, _ = QFileDialog.getOpenFileName(self.statusbar,
                                              'Select Video File', '/home')
        self.vid_path = path
        self.initVideo()

    def exportImages(self):

        cursor_pos = self.vid.get(cv2.CAP_PROP_POS_FRAMES)

        markers = self.collection.find({})
        markers.batch_size(1000000000)

        markers = [marker for marker in markers]

        height, width = self.current_frame.shape[0:2]

        empty = np.zeros((height, width, 3), np.uint8)

        center = (int(width / 2), int(height / 2))

        i = 0

        last = 0

        for marker in markers:

            progress = int(i * 100 / len(markers))

            img = empty.copy()
            cv2.circle(img, center, 100, (255, 255, 255))
            cv2.circle(img, center, progress, (255, 255, 255), -1)

            print(progress)

            self.setFrame(img)
            cv2.waitKey(1)
            last = progress

            box = [marker["start_pos"], marker["end_pos"]]

            rbox = RBox.fromPointBoundingBox(box)

            self.vid.set(cv2.CAP_PROP_POS_FRAMES, marker["frame_no"] - 1)

            rtt, frame = self.vid.read()

            patch = rbox.extractPatchFromImage(frame, square=True)

            name = str(marker["_id"])
            name = "./exports/" + name + ".png"
            cv2.imwrite(name, patch)
            i += 1

        self.initVideo()

    def showProgress(self, progress, fps):

        height, width = self.current_frame.shape[0:2]

        empty = np.zeros((height, width, 3), np.uint8)

        center = (int(width / 2), int(height / 2))

        cv2.circle(empty, center, progress, (255, 255, 255), -1)
        empty[center[1] - 100:center[1] + 100, center[0] - 100:center[0] +
              100] = cv2.bitwise_and(empty[center[1] - 100:center[1] + 100,
                                           center[0] - 100:center[0] + 100],
                                     empty[center[1] - 100:center[1] + 100,
                                           center[0] - 100:center[0] + 100],
                                     mask=self.coin)

        cv2.circle(empty, center, 98, (255, 255, 255))

        font = cv2.FONT_HERSHEY_SIMPLEX
        cv2.putText(empty, "FPS: {}".format(fps), (10, 20), font, 0.5,
                    (255, 255, 255), 1, cv2.LINE_AA)

        self.setFrame(empty)

    def initializeWithAlgorithm(self):

        self.initVideo()
        self.analyser = RuntimeABGSC(vid_path=self.vid_path)

        # read progress image
        coin = cv2.imread("fish_coin_inv.png")
        self.coin = np.uint8(
            cv2.cvtColor(coin, cv2.COLOR_RGB2GRAY) > 150) * 255

        # empty collection
        self.collection.delete_many({})

        # frame cout
        frame_count = self.vid.get(cv2.CAP_PROP_FRAME_COUNT)
        counter = 0

        # disable scroll bar
        self.slider.setDisabled(True)

        # disable video
        self.vid = False

        markers = []

        while (self.analyser.status):

            # while video is not complete
            start = time()
            self.analyser.process(show_output=False)
            end = time()
            # increment counter
            counter += 1

            # calculate fps
            fps = int(1 / (end - start))

            # show progress in display window
            progress = int(counter * 100 / frame_count)
            self.showProgress(progress, fps)

            for box in self.analyser.final_boxes:

                start_pos = (int(box[0][0]), int(box[0][1]))
                end_pos = (int(box[1][0]), int(box[1][1]))

                # add resultant marker to markers of current frame
                marker = Marker(start_pos, end_pos, counter,
                                CONFIGURED_CLASSES[3])
                markers.append(marker.toDictionary())
                #self.frame_panel.markers.append(marker)

        # add markers to database
        self.collection.insert_many(markers)

        self.initVideo()

    def changeZoom(self):

        # get selected option's value
        zoom = self.zoom_group.checkedAction().val

        # reset frame according to new scaling
        self.resetFrame(zoom)

    def resetFrame(self, zoom):

        self.zoom = zoom
        self.frame_panel.zoom = zoom

        #resize frame panel
        self.frame_panel.resize(self.vid_height * zoom, self.vid_width * zoom)

        #resize window if dynamic scaling is enabled
        if DYNAMIC_WINDOW_SCALING:
            self.resize(
                self.vid_height * zoom + FRAME_MARGIN_LEFT +
                FRAME_MARGIN_RIGHT,
                self.vid_width * zoom + FRAME_MARGIN_TOP + FRAME_MARGIN_BOTTOM)

            # reposition slider
            self.slider.move(
                SLIDER_MARGINS,
                self.vid_width * zoom - (SLIDER_HEIGHT + SLIDER_MARGINS))

            # resize slider
            self.slider.resize(self.width() - SLIDER_MARGINS * 2,
                               SLIDER_HEIGHT)

            # reposition frame lable
            self.statusbar.frame_lable.move(self.width() - 100,
                                            self.height() - 37)

        self.currentFrame()

    def initVideo(self):

        # reset zoom
        self.zoom = 1.0

        self.vid = cv2.VideoCapture()
        # load selected video
        rtt = self.vid.open(self.vid_path)

        # clear slider
        self.slider.disconnect()

        # seek slider to start
        self.slider.setValue(0)
        self.slider.valueChanged.connect(self.moveSliderToClickedPosition)

        if rtt == False:
            if self.vid_path == "":
                self.statusbar.setStatusTip("No file selected")
            else:
                self.statusbar.setStatusTip("Error: Format not supported")

            # disalbe zoom options
            self.zoom_group.setDisabled(True)

            # clean video from memory
            self.vid = False

            # disable slider
            self.slider.setDisabled(True)

            return
        else:
            self.statusbar.setStatusTip("Ready")

            # set collection
            self.collection = self.db[self.vid_path]

        # seek to start
        self.vid.set(cv2.CAP_PROP_POS_FRAMES, 0)

        # read first frame
        rtt, frame = self.vid.read()

        # extract height and width of video
        self.vid_width, self.vid_height, self.vid_depth = np.shape(frame)
        self.vid_width *= self.zoom
        self.vid_height *= self.zoom

        self.current_frame = frame.copy()

        # resize frame view according to the video's geometery
        self.frame_panel.resize(self.vid_height, self.vid_width)

        # getting current frame number
        self.frame_no = self.vid.get(cv2.CAP_PROP_POS_FRAMES)

        # adding frame no to status tip
        self.statusbar.frame_lable.setText("Frame: {0}".format(
            "%05.0i" % int(self.frame_no)))

        # reset frame to start
        self.resetFrame(self.zoom)

        #enable zoom options
        self.zoom_group.setDisabled(False)

        #enable slider
        self.slider.setDisabled(False)

        # enable algorithmBasedInitialization
        self.init_submenu.setDisabled(False)
        self.export_submenu.setDisabled(False)

        # plot the current frame on canvas
        self.frame_panel.content = frame.copy()
        self.setImageFrame(frame)

        # seek slider to start
        self.moveSliderToPosition(0)

    def setImageFrame(self, frame):

        # load image with markers
        self.frame_panel.loadMarkers()
        self.frame_panel.reDraw()
        self.frame_panel.dragStatus = "Ended"

    def setFrame(self, frame):
        # convert raw frame to QPixmap and set it on frame_panel
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        self.frame_panel.setPixmap(
            QPixmap.fromImage(
                QImage(frame, self.vid_height * self.zoom,
                       self.vid_width * self.zoom,
                       self.vid_height * self.zoom * self.vid_depth,
                       QImage.Format_RGB888)))

    def nextFrame(self):
        # read next frame of video as Image
        rtt, frame = self.vid.read()

        if rtt:

            self.frame_panel.content = frame.copy()
            self.current_frame = frame.copy()

            # check if there is a next frame and convert cv Image to Pixmap
            frame = cv2.resize(frame,
                               None,
                               fx=self.zoom,
                               fy=self.zoom,
                               interpolation=cv2.INTER_CUBIC)

            # initializing markers on frame_panel
            self.frame_panel.markers = []

            # getting current frame number
            self.frame_no = self.vid.get(cv2.CAP_PROP_POS_FRAMES)

            # clear slider
            self.slider.disconnect()

            # seek slider to start
            self.slider.setValue(self.frame_no * 100 /
                                 self.vid.get(cv2.CAP_PROP_FRAME_COUNT))
            self.slider.valueChanged.connect(self.moveSliderToClickedPosition)

            self.setImageFrame(frame)

            #adding frame no to status tip
            self.statusbar.frame_lable.setText("Frame: {0}".format(
                "%05.0i" % int(self.frame_no)))

    def previousFrame(self):
        # set current frame number to 2 frames backward
        self.vid.set(cv2.CAP_PROP_POS_FRAMES,
                     self.vid.get(cv2.CAP_PROP_POS_FRAMES) - 2)
        # get next frame
        self.nextFrame()

    def currentFrame(self):

        self.frame_panel.content = self.current_frame.copy()

        # check if there is a next frame and convert cv Image to Pixmap
        frame = cv2.resize(self.current_frame.copy(),
                           None,
                           fx=self.zoom,
                           fy=self.zoom,
                           interpolation=cv2.INTER_CUBIC)
        self.setImageFrame(frame)

    def keyPressEvent(self, event):

        # don't do anything if no video is selected
        if self.vid == False:
            return

        # Key press event on window
        if type(event) == QKeyEvent and event.key() == Qt.Key_D:

            # navigate forward if D pressed
            self.nextFrame()
        if type(event) == QKeyEvent and event.key() == Qt.Key_A:

            # navigate backward if A pressed
            self.previousFrame()

        if type(event) == QKeyEvent and event.key() == Qt.Key_Plus:

            # zoom in
            self.resetFrame(self.zoom + 1)

        if type(event) == QKeyEvent and event.key() == Qt.Key_Minus:

            # zoom out if zoom factor is greater than 1
            if (self.zoom > 1.0):
                self.resetFrame(self.zoom - 1)

    def moveSliderToClickedPosition(self):

        # enable slider to move with mouse click

        # get click position relative to slider
        click_position = self.slider.mapFromGlobal(QCursor.pos()).x()
        self.moveSliderToPosition(click_position)

    def moveSliderToPosition(self, position):

        # set position of slider to position by setting its value
        slider_completion_ratio = position / self.slider.width()
        self.slider.setValue(slider_completion_ratio * 100)

        # seek video accordingly
        if self.vid:
            self.vid.set(
                cv2.CAP_PROP_POS_FRAMES,
                self.vid.get(cv2.CAP_PROP_FRAME_COUNT) *
                slider_completion_ratio)

            # refresh frame
            self.nextFrame()
Ejemplo n.º 13
0
class TVMaxeMainWindow(QMainWindow):
    def __init__(self, parent):
        super(QMainWindow, self).__init__(parent)
        uic.loadUi('ui/mainWindow.ui', self)

        self.play_btn.clicked.connect(self.play_btn_clicked)
        self.stop_btn.clicked.connect(self.stop_btn_clicked)
        self.fullscreen_btn.clicked.connect(self.switch_fullscreen_mode)
        self.volume_slider.sliderMoved.connect(self.volume_changed)
        self.tv_channel_list.channelActivated.connect(self.activated_channel)
        self.radio_channel_list.channelActivated.connect(
            self.activated_channel)

        self.video_player.playback_started.connect(self.video_playback_started)
        self.video_player.playback_paused.connect(self.video_playback_paused)
        self.video_player.playback_stopped.connect(self.video_playback_stopped)
        self.video_player.playback_error.connect(self.video_playback_error)
        self.video_player.volume_changed.connect(self.video_volume_changed)
        self.video_player.chromecast_available.connect(
            self.chromecast_available)
        self.video_player.chromecast_connected.connect(
            self.chromecast_connected)

        self.chlist_manager = ChannelListManager()
        self.chlist_manager.channel_added.connect(self.channel_added)
        self.chlist_manager.channellist_available.connect(
            self.channel_list_available)

        self.statusbar.addPermanentWidget(self.bottom_bar, 1)
        self.splitter.setStretchFactor(1, 1)
        self.progress_bar.hide()
        self.progress_label.setText(self.tr("Idle"))
        self.video_player.set_volume(self.volume_slider.value())

        self.channellist_show_actiongroup = QActionGroup(self)
        self.channellist_show_actiongroup.triggered.connect(
            self.show_channel_list)
        chlist_showall_action = QAction(self.tr("All"), self.menu_show_chlist)
        chlist_showall_action.setCheckable(True)
        chlist_showall_action.setChecked(True)
        chlist_showall_action.setActionGroup(self.channellist_show_actiongroup)
        self.menu_show_chlist.addAction(chlist_showall_action)

        os_type = platform.system()
        log.info('Detected OS type: {0}'.format(os_type))
        if os_type == 'Darwin':
            from AppKit import NSWindow, NSUserDefaults
            NSWindow.setAllowsAutomaticWindowTabbing_(False)
            NSUserDefaults.standardUserDefaults().setBool_forKey_(
                False, "NSFullScreenMenuItemEverywhere")
            self.playlist_tab_widget.setDocumentMode(True)

            channel_list_action = self.menubar.actions()[0].menu().actions()[0]
            preferences_list_action = self.menubar.actions()[0].menu().actions(
            )[1]
            self.menubar.removeAction(self.menubar.actions()[0])
            channel_list_action.menu().addAction(preferences_list_action)
            self.menubar.insertAction(self.menubar.actions()[0],
                                      channel_list_action)

        self.load_settings()

        # Set custom icons
        self.play_btn.setIcon(TXIcon('icons/play-button.svg'))
        self.stop_btn.setIcon(TXIcon('icons/stop-button.svg'))
        self.fullscreen_btn.setIcon(TXIcon('icons/fullscreen.svg'))
        self.cast_btn.setIcon(TXIcon('icons/cast.svg'))

        self.cast_label_pixmap.setHidden(True)
        self.cast_label.setHidden(True)
        self.cast_btn.hide()

    def load_settings(self):
        log.debug('Loading settings...')
        app = QApplication.instance()
        settings = app.settings_manager
        try:
            self.restoreGeometry(settings.value("geometry"))
            self.restoreState(settings.value("windowState"))
        except TypeError:
            pass  # do nothing for now

        self.splitter.setSizes(
            settings.value("splitterSizes", [242, self.width() - 242], int))
        self.video_player.set_volume(int(settings.value("player/volume", 50)))
        self.action_hide_channellist.setChecked(
            settings.value("hideChannelList", False, bool))

        self.chlist_manager.download_chlists(settings.get_subscriptions())

        log.debug('Settings loaded')

    def play_btn_clicked(self, checked=False):
        self.video_player.switch_pause()

    def stop_btn_clicked(self, checked=False):
        self.video_player.stop()

    def volume_changed(self, level):
        self.video_player.volume_changed.disconnect(self.video_volume_changed)
        self.video_player.set_volume(level)
        self.video_player.volume_changed.connect(self.video_volume_changed)

    def switch_fullscreen_mode(self, checked=False):
        self.video_player.switch_fullscreen()

    def activated_channel(self, channel, play_index):
        self.play_channel(channel, play_index)

    def channel_list_available(self, chlist):
        if len(self.menu_show_chlist.actions()) == 1:
            self.menu_show_chlist.addSeparator()

        for action in self.menu_show_chlist.actions():
            if isinstance(action, ChannelListShowAction):
                if action.channel_list == chlist:
                    return  # We already have this channel list in menu

        chlist_action = ChannelListShowAction(self.menu_show_chlist, chlist)
        chlist_action.setActionGroup(self.channellist_show_actiongroup)
        self.menu_show_chlist.addAction(chlist_action)

    def show_channel_list(self):
        action = self.channellist_show_actiongroup.checkedAction()
        if isinstance(action, ChannelListShowAction):
            self.tv_channel_list.showChannelList(action.channel_list)
            self.radio_channel_list.showChannelList(action.channel_list)
        else:
            self.tv_channel_list.showAllChannelLists()
            self.radio_channel_list.showAllChannelLists()

    def channel_added(self, channel):
        if channel.type == 'tv':
            self.tv_channel_list.addChannel(channel)
        elif channel.type == 'radio':
            self.radio_channel_list.addChannel(channel)

    def play_channel(self, channel, play_index=0):
        self.video_player.stop()
        self.progress_bar.setMinimum(0)
        self.progress_bar.setMaximum(0)
        self.progress_bar.show()
        self.progress_label.setText(
            self.tr("Now loading: {0} ({1})".format(
                channel.name, channel.streamurls[play_index])))
        self.video_player.play_channel(channel, play_index)

    def video_playback_started(self, channel):
        self.play_btn.setIcon(TXIcon('icons/pause-button.svg'))
        self.progress_bar.hide()
        self.progress_bar.setMaximum(1)
        self.progress_label.setText(
            self.tr("Now playing: {0}".format(channel.name)))

    def video_playback_paused(self, channel):
        self.play_btn.setIcon(TXIcon('icons/play-button.svg'))

    def video_playback_stopped(self, channel):
        self.play_btn.setIcon(TXIcon('icons/play-button.svg'))
        self.progress_label.setText(self.tr("Idle"))
        self.progress_bar.hide()
        self.progress_bar.setMaximum(1)

    def video_playback_error(self, channel):
        if channel.play_index + 1 < len(channel.streamurls):
            self.progress_bar.setMinimum(0)
            self.progress_bar.setMaximum(0)
            self.progress_bar.show()
            self.progress_label.setText(
                self.tr("Retrying: {0} ({1})".format(
                    channel.name, channel.streamurls[channel.play_index + 1])))
            self.video_player.play_channel(channel, channel.play_index + 1)
        else:
            self.play_btn.setIcon(TXIcon('icons/play-button.svg'))
            self.progress_label.setText(
                self.tr("Channel not available: {0}".format(channel.name)))
            self.progress_bar.hide()
            self.progress_bar.setMaximum(1)

    def video_volume_changed(self, value):
        self.volume_slider.setValue(int(value))

    def chromecast_available(self, devices):
        if devices:
            log.debug("Found {0} Chromecast compatible devices".format(
                len(devices)))
            cast_menu = QMenu(self.cast_btn)
            for chromecast in devices:
                action = cast_menu.addAction(chromecast.device.friendly_name)
                action.setProperty("cast-device", chromecast)
            cast_menu.triggered.connect(self.selectChromecast)
            self.cast_btn.setMenu(cast_menu)
            self.cast_btn.show()
        else:
            log.debug("No Chromecast devices found")

    def chromecast_connected(self, device):
        self.progress_label.setText(
            self.tr("Connected to {0}".format(device.device.friendly_name)))

    @pyqtSlot()
    def reloadChannelList(self):
        self.tv_channel_list.clear()
        self.radio_channel_list.clear()
        subscriptions = QApplication.instance(
        ).settings_manager.get_subscriptions()
        self.chlist_manager.clear_cached_chlists(subscriptions)
        self.chlist_manager.download_chlists(subscriptions)

    @pyqtSlot()
    def showDeletedChannels(self):
        if self.action_show_deleted.isChecked():
            self.tv_channel_list.show_deleted = True
            self.radio_channel_list.show_deleted = True
        else:
            self.tv_channel_list.show_deleted = False
            self.radio_channel_list.show_deleted = False

    @pyqtSlot(bool)
    def hideShowChannelList(self, hide):
        if hide:
            self.playlist_tab_widget.hide()
        else:
            self.playlist_tab_widget.show()

    @pyqtSlot()
    def openSettings(self):
        settings_dialog = SettingsDialog(self)
        settings_dialog.exec()

    @pyqtSlot()
    def showAddChannelDialog(self):
        add_channel_dialog = AddChannelDialog(self)
        add_channel_dialog.channel_saved.connect(self.custom_channel_saved)
        add_channel_dialog.exec()

    @pyqtSlot(QAction)
    def selectChromecast(self, action):
        device = action.property("cast-device")
        if self.video_player.chromecast_manager.current_device == device:
            self.video_player.disconnect_chromecast()
            action.setText(device.device.friendly_name)
            self.progress_label.setText(
                self.tr("Disconnected from {0}".format(
                    device.device.friendly_name)))
        elif self.video_player.chromecast_manager.current_device:
            for cast_action in self.cast_btn.menu().actions():
                cast_device = cast_action.property("cast-device")
                cast_action.setText(cast_device.device.friendly_name)
            self.video_player.disconnect_chromecast()
            self.progress_label.setText(
                self.tr("Connecting to {0}".format(
                    device.device.friendly_name)))
            self.video_player.connect_chromecast(device)
            action.setText(
                self.tr("Disconnect: {0}").format(device.device.friendly_name))
        else:
            self.progress_label.setText(
                self.tr("Connecting to {0}".format(
                    device.device.friendly_name)))
            self.video_player.connect_chromecast(device)
            action.setText(
                self.tr("Disconnect: {0}").format(device.device.friendly_name))

    def custom_channel_saved(self, channel):
        if channel.type == 'tv':
            self.tv_channel_list.addChannel(channel)
        else:
            self.radio_channel_list.addChannel(channel)
        self.chlist_manager.save_user_channel(channel)

    # Qt Events

    def closeEvent(self, event):
        app = QApplication.instance()
        settings = app.settings_manager
        settings.setValue("geometry", self.saveGeometry())
        settings.setValue("windowState", self.saveState())
        settings.setValue("hideChannelList",
                          self.action_hide_channellist.isChecked())
        if not self.action_hide_channellist.isChecked():
            settings.setValue("splitterSizes", self.splitter.sizes())
        settings.setValue("player/volume", self.volume_slider.value())

        self.video_player.quit()
        super().closeEvent(event)
Ejemplo n.º 14
0
class MainWindow(QMainWindow, Ui_MainWindow):
    """docstring for MainWindow."""
    def __init__(self, parent=None):
        super(MainWindow, self).__init__()
        self._csvFilePath = ""
        self.serialport = serial.Serial()
        self.receiver_thread = readerThread(self)
        self.receiver_thread.setPort(self.serialport)
        self._localEcho = None
        self._viewMode = None
        self._quickSendOptRow = 1

        self.setupUi(self)
        self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea)
        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
        font = QtGui.QFont()
        font.setFamily(EDITOR_FONT)
        font.setPointSize(9)
        self.txtEdtOutput.setFont(font)
        self.txtEdtInput.setFont(font)
        #self.quickSendTable.setFont(font)
        if UI_FONT is not None:
            font = QtGui.QFont()
            font.setFamily(UI_FONT)
            font.setPointSize(9)
            self.dockWidget_PortConfig.setFont(font)
            self.dockWidget_SendHex.setFont(font)
            self.dockWidget_QuickSend.setFont(font)
        self.setupMenu()
        self.setupFlatUi()
        self.onEnumPorts()

        icon = QtGui.QIcon(":/MyTerm.ico")
        self.setWindowIcon(icon)
        self.actionAbout.setIcon(icon)

        self.defaultStyleWidget = QWidget()
        self.defaultStyleWidget.setWindowIcon(icon)

        icon = QtGui.QIcon(":/qt_logo_16.ico")
        self.actionAbout_Qt.setIcon(icon)

        self._viewGroup = QActionGroup(self)
        self._viewGroup.addAction(self.actionAscii)
        self._viewGroup.addAction(self.actionHex_lowercase)
        self._viewGroup.addAction(self.actionHEX_UPPERCASE)
        self._viewGroup.setExclusive(True)

        # bind events
        self.actionOpen_Cmd_File.triggered.connect(self.openQuickSend)
        self.actionSave_Log.triggered.connect(self.onSaveLog)
        self.actionExit.triggered.connect(self.onExit)

        self.actionOpen.triggered.connect(self.openPort)
        self.actionClose.triggered.connect(self.closePort)

        self.actionPort_Config_Panel.triggered.connect(self.onTogglePrtCfgPnl)
        self.actionQuick_Send_Panel.triggered.connect(self.onToggleQckSndPnl)
        self.actionSend_Hex_Panel.triggered.connect(self.onToggleHexPnl)
        self.dockWidget_PortConfig.visibilityChanged.connect(self.onVisiblePrtCfgPnl)
        self.dockWidget_QuickSend.visibilityChanged.connect(self.onVisibleQckSndPnl)
        self.dockWidget_SendHex.visibilityChanged.connect(self.onVisibleHexPnl)
        self.actionLocal_Echo.triggered.connect(self.onLocalEcho)
        self.actionAlways_On_Top.triggered.connect(self.onAlwaysOnTop)

        self.actionAscii.triggered.connect(self.onViewChanged)
        self.actionHex_lowercase.triggered.connect(self.onViewChanged)
        self.actionHEX_UPPERCASE.triggered.connect(self.onViewChanged)

        self.actionAbout.triggered.connect(self.onAbout)
        self.actionAbout_Qt.triggered.connect(self.onAboutQt)

        self.btnOpen.clicked.connect(self.onOpen)
        self.btnClear.clicked.connect(self.onClear)
        self.btnSaveLog.clicked.connect(self.onSaveLog)
        self.btnEnumPorts.clicked.connect(self.onEnumPorts)
        self.btnSendHex.clicked.connect(self.onSend)

        self.receiver_thread.read.connect(self.onReceive)
        self.receiver_thread.exception.connect(self.onReaderExcept)
        self._signalMapQuickSendOpt = QSignalMapper(self)
        self._signalMapQuickSendOpt.mapped[int].connect(self.onQuickSendOptions)
        self._signalMapQuickSend = QSignalMapper(self)
        self._signalMapQuickSend.mapped[int].connect(self.onQuickSend)

        # initial action
        self.actionHEX_UPPERCASE.setChecked(True)
        self.receiver_thread.setViewMode(VIEWMODE_HEX_UPPERCASE)
        self.initQuickSend()
        self.restoreLayout()
        self.moveScreenCenter()
        self.syncMenu()
        
        if self.isMaximized():
            self.setMaximizeButton("restore")
        else:
            self.setMaximizeButton("maximize")
            
        self.loadSettings()

    def setupMenu(self):
        self.menuMenu = QtWidgets.QMenu()
        self.menuMenu.setTitle("&File")
        self.menuMenu.setObjectName("menuMenu")
        self.menuView = QtWidgets.QMenu(self.menuMenu)
        self.menuView.setTitle("&View")
        self.menuView.setObjectName("menuView")

        self.menuView.addAction(self.actionAscii)
        self.menuView.addAction(self.actionHex_lowercase)
        self.menuView.addAction(self.actionHEX_UPPERCASE)
        self.menuMenu.addAction(self.actionOpen_Cmd_File)
        self.menuMenu.addAction(self.actionSave_Log)
        self.menuMenu.addSeparator()
        self.menuMenu.addAction(self.actionPort_Config_Panel)
        self.menuMenu.addAction(self.actionQuick_Send_Panel)
        self.menuMenu.addAction(self.actionSend_Hex_Panel)
        self.menuMenu.addAction(self.menuView.menuAction())
        self.menuMenu.addAction(self.actionLocal_Echo)
        self.menuMenu.addAction(self.actionAlways_On_Top)
        self.menuMenu.addSeparator()
        self.menuMenu.addAction(self.actionAbout)
        self.menuMenu.addAction(self.actionAbout_Qt)
        self.menuMenu.addSeparator()
        self.menuMenu.addAction(self.actionExit)

        self.sendOptMenu = QtWidgets.QMenu()
        self.actionSend_Hex = QtWidgets.QAction(self)
        self.actionSend_Hex.setText("Send &Hex")
        self.actionSend_Hex.setStatusTip("Send Hex (e.g. 31 32 FF)")

        self.actionSend_Asc = QtWidgets.QAction(self)
        self.actionSend_Asc.setText("Send &Asc")
        self.actionSend_Asc.setStatusTip("Send Asc (e.g. abc123)")

        self.actionSend_TFH = QtWidgets.QAction(self)
        self.actionSend_TFH.setText("Send &Text file as hex")
        self.actionSend_TFH.setStatusTip('Send text file as hex (e.g. strings "31 32 FF" in the file)')

        self.actionSend_TFA = QtWidgets.QAction(self)
        self.actionSend_TFA.setText("Send t&Ext file as asc")
        self.actionSend_TFA.setStatusTip('Send text file as asc (e.g. strings "abc123" in the file)')

        self.actionSend_FB = QtWidgets.QAction(self)
        self.actionSend_FB.setText("Send &Bin/Hex file")
        self.actionSend_FB.setStatusTip("Send a bin file or a hex file")

        self.sendOptMenu.addAction(self.actionSend_Hex)
        self.sendOptMenu.addAction(self.actionSend_Asc)
        self.sendOptMenu.addAction(self.actionSend_TFH)
        self.sendOptMenu.addAction(self.actionSend_TFA)
        self.sendOptMenu.addAction(self.actionSend_FB)

        self.actionSend_Hex.triggered.connect(self.onSetSendHex)
        self.actionSend_Asc.triggered.connect(self.onSetSendAsc)
        self.actionSend_TFH.triggered.connect(self.onSetSendTFH)
        self.actionSend_TFA.triggered.connect(self.onSetSendTFA)
        self.actionSend_FB.triggered.connect(self.onSetSendFB)

    def setupFlatUi(self):
        self._dragPos = self.pos()
        self._isDragging = False
        self.setMouseTracking(True)
        self.setWindowFlags(Qt.FramelessWindowHint)
        self.setStyleSheet("""
            QWidget {
                background-color: %(BackgroundColor)s;
                /*background-image: url(:/background.png);*/
                outline: none;
            }
            QLabel {
                color:%(TextColor)s;
                font-size:12px;
                /*font-family:Century;*/
            }
            
            QComboBox {
                color:%(TextColor)s;
                font-size:12px;
                /*font-family:Century;*/
            }
            QComboBox {
                border: none;
                padding: 1px 1px 1px 3px;
            }
            QComboBox:editable {
                background: white;
            }
            QComboBox:!editable, QComboBox::drop-down:editable {
                background: #62c7e0;
            }
            QComboBox:!editable:hover, QComboBox::drop-down:editable:hover {
                background: #c7eaf3;
            }
            QComboBox:!editable:pressed, QComboBox::drop-down:editable:pressed {
                background: #35b6d7;
            }
            QComboBox:on {
                padding-top: 3px;
                padding-left: 4px;
            }
            QComboBox::drop-down {
                subcontrol-origin: padding;
                subcontrol-position: top right;
                width: 16px;
                border: none;
            }
            QComboBox::down-arrow {
                image: url(:/downarrow.png);
            }
            QComboBox::down-arrow:on {
                image: url(:/uparrow.png);
            }
            
            QGroupBox {
                color:%(TextColor)s;
                font-size:12px;
                /*font-family:Century;*/
                border: 1px solid gray;
                margin-top: 15px;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                subcontrol-position: top left;
                left:5px;
                top:3px;
            }
            
            QCheckBox {
                color:%(TextColor)s;
                spacing: 5px;
                font-size:12px;
                /*font-family:Century;*/
            }
            QCheckBox::indicator:unchecked {
                image: url(:/checkbox_unchecked.png);
            }

            QCheckBox::indicator:unchecked:hover {
                image: url(:/checkbox_unchecked_hover.png);
            }

            QCheckBox::indicator:unchecked:pressed {
                image: url(:/checkbox_unchecked_pressed.png);
            }

            QCheckBox::indicator:checked {
                image: url(:/checkbox_checked.png);
            }

            QCheckBox::indicator:checked:hover {
                image: url(:/checkbox_checked_hover.png);
            }

            QCheckBox::indicator:checked:pressed {
                image: url(:/checkbox_checked_pressed.png);
            }
            
            QScrollBar:horizontal {
                background-color:%(BackgroundColor)s;
                border: none;
                height: 15px;
                margin: 0px 20px 0 20px;
            }
            QScrollBar::handle:horizontal {
                background: %(ScrollBar_Handle)s;
                min-width: 20px;
            }
            QScrollBar::add-line:horizontal {
                image: url(:/rightarrow.png);
                border: none;
                background: %(ScrollBar_Line)s;
                width: 20px;
                subcontrol-position: right;
                subcontrol-origin: margin;
            }
            QScrollBar::sub-line:horizontal {
                image: url(:/leftarrow.png);
                border: none;
                background: %(ScrollBar_Line)s;
                width: 20px;
                subcontrol-position: left;
                subcontrol-origin: margin;
            }
            
            QScrollBar:vertical {
                background-color:%(BackgroundColor)s;
                border: none;
                width: 15px;
                margin: 20px 0px 20px 0px;
            }
            QScrollBar::handle::vertical {
                background: %(ScrollBar_Handle)s;
                min-height: 20px;
            }
            QScrollBar::add-line::vertical {
                image: url(:/downarrow.png);
                border: none;
                background: %(ScrollBar_Line)s;
                height: 20px;
                subcontrol-position: bottom;
                subcontrol-origin: margin;
            }
            QScrollBar::sub-line::vertical {
                image: url(:/uparrow.png);
                border: none;
                background: %(ScrollBar_Line)s;
                height: 20px;
                subcontrol-position: top;
                subcontrol-origin: margin;
            }
            
            QTableView {
                background-color: white;
                /*selection-background-color: #FF92BB;*/
                border: 1px solid %(TableView_Border)s;
                color: %(TextColor)s;
            }
            QTableView::focus {
                /*border: 1px solid #2a7fff;*/
            }
            QTableView QTableCornerButton::section {
                border: none;
                border-right: 1px solid %(TableView_Border)s;
                border-bottom: 1px solid %(TableView_Border)s;
                background-color: %(TableView_Corner)s;
            }
            QTableView QWidget {
                background-color: white;
            }
            QTableView::item:focus {
                border: 1px red;
                background-color: transparent;
                color: %(TextColor)s;
            }
            QHeaderView::section {
                border: none;
                border-right: 1px solid %(TableView_Border)s;
                border-bottom: 1px solid %(TableView_Border)s;
                padding-left: 2px;
                padding-right: 2px;
                color: #444444;
                background-color: %(TableView_Header)s;
            }
            QTextEdit {
                background-color:white;
                color:%(TextColor)s;
                border-top: none;
                border-bottom: none;
                border-left: 2px solid %(BackgroundColor)s;
                border-right: 2px solid %(BackgroundColor)s;
            }
            QTextEdit::focus {
            }
            
            QToolButton, QPushButton {
                background-color:#30a7b8;
                border:none;
                color:#ffffff;
                font-size:12px;
                /*font-family:Century;*/
            }
            QToolButton:hover, QPushButton:hover {
                background-color:#51c0d1;
            }
            QToolButton:pressed, QPushButton:pressed {
                background-color:#3a9ecc;
            }
            
            QMenuBar {
                color: %(TextColor)s;
                height: 24px;
            }
            QMenuBar::item {
                background-color: transparent;
                margin: 8px 0px 0px 0px;
                padding: 1px 8px 1px 8px;
                height: 15px;
            }
            QMenuBar::item:selected {
                background: #51c0d1;
            }
            QMenuBar::item:pressed {
                
            }
            /*
            QMenu {
                color: %(TextColor)s;
                background: #ffffff;
            }
            QMenu {
                margin: 2px;
            }
            QMenu::item {
                padding: 2px 25px 2px 21px;
                border: 1px solid transparent;
            }
            QMenu::item:selected {
                background: #51c0d1;
            }
            QMenu::icon {
                background: transparent;
                border: 2px inset transparent;
            }*/

            QDockWidget {
                font-size:12px;
                /*font-family:Century;*/
                color: %(TextColor)s;
                titlebar-close-icon: none;
                titlebar-normal-icon: none;
            }
            QDockWidget::title {
                margin: 0;
                padding: 2px;
                subcontrol-origin: content;
                subcontrol-position: right top;
                text-align: left;
                background: #67baed;
                
            }
            QDockWidget::float-button {
                max-width: 12px;
                max-height: 12px;
                background-color:transparent;
                border:none;
                image: url(:/restore_inactive.png);
            }
            QDockWidget::float-button:hover {
                background-color:#227582;
                image: url(:/restore_active.png);
            }
            QDockWidget::float-button:pressed {
                padding: 0;
                background-color:#14464e;
                image: url(:/restore_active.png);
            }
            QDockWidget::close-button {
                max-width: 12px;
                max-height: 12px;
                background-color:transparent;
                border:none;
                image: url(:/close_inactive.png);
            }
            QDockWidget::close-button:hover {
                background-color:#ea5e00;
                image: url(:/close_active.png);
            }
            QDockWidget::close-button:pressed {
                background-color:#994005;
                image: url(:/close_active.png);
                padding: 0;
            }
            
        """ % dict(
            BackgroundColor =  '#99d9ea',
            TextColor =        '#202020',
            ScrollBar_Handle = '#61b9e1',
            ScrollBar_Line =   '#7ecfe4',
            TableView_Corner = '#8ae6d2',
            TableView_Header = '#8ae6d2',
            TableView_Border = '#eeeeee'
        ))
        self.dockWidgetContents.setStyleSheet("""
            QPushButton {
                min-height:23px;
            }
        """)
        self.dockWidget_QuickSend.setStyleSheet("""
            QToolButton, QPushButton {
                background-color:#27b798;
                /*font-family:Consolas;*/
                /*font-size:12px;*/
                /*min-width:46px;*/
            }
            QToolButton:hover, QPushButton:hover {
                background-color:#3bd5b4;
            }
            QToolButton:pressed, QPushButton:pressed {
                background-color:#1d8770;
            }
        """)
        self.dockWidgetContents_2.setStyleSheet("""
            QPushButton {
                min-height:23px;
                min-width:50px;
            }
        """)

        w = self.frameGeometry().width()
        self._minBtn = QPushButton(self)
        self._minBtn.setGeometry(w-103,0,28,24)
        self._minBtn.clicked.connect(self.onMinimize)
        self._minBtn.setStyleSheet("""
            QPushButton {
                background-color:transparent;
                border:none;
                outline: none;
                image: url(:/minimize_inactive.png);
            }
            QPushButton:hover {
                background-color:#227582;
                image: url(:/minimize_active.png);
            }
            QPushButton:pressed {
                background-color:#14464e;
                image: url(:/minimize_active.png);
            }
        """)
        
        self._maxBtn = QPushButton(self)
        self._maxBtn.setGeometry(w-74,0,28,24)
        self._maxBtn.clicked.connect(self.onMaximize)
        self.setMaximizeButton("maximize")
        
        self._closeBtn = QPushButton(self)
        self._closeBtn.setGeometry(w-45,0,36,24)
        self._closeBtn.clicked.connect(self.onExit)
        self._closeBtn.setStyleSheet("""
            QPushButton {
                background-color:transparent;
                border:none;
                outline: none;
                image: url(:/close_inactive.png);
            }
            QPushButton:hover {
                background-color:#ea5e00;
                image: url(:/close_active.png);
            }
            QPushButton:pressed {
                background-color:#994005;
                image: url(:/close_active.png);
            }
        """)
        
        self.btnMenu = QtWidgets.QToolButton(self)
        self.btnMenu.setEnabled(True)
        self.btnMenu.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
        self.btnMenu.setIcon(QtGui.QIcon(':/MyTerm.ico'))
        self.btnMenu.setText('Myterm  ')
        self.btnMenu.setGeometry(3,3,80,18)
        self.btnMenu.setMenu(self.menuMenu)
        self.btnMenu.setPopupMode(QtWidgets.QToolButton.InstantPopup)
        
        self.btnRefresh = QtWidgets.QToolButton(self)
        self.btnRefresh.setEnabled(True)
        self.btnRefresh.setIcon(QtGui.QIcon(':/refresh.ico'))
        self.btnRefresh.setGeometry(110,3,18,18)
        self.btnRefresh.clicked.connect(self.onEnumPorts)
        
        self.verticalLayout_1.removeWidget(self.cmbPort)
        self.cmbPort.setParent(self)
        self.cmbPort.setGeometry(128,3,60,18)
        
        self.verticalLayout_1.removeWidget(self.btnOpen)
        self.btnOpen.setParent(self)
        self.btnOpen.setGeometry(210,3,60,18)
        
    def resizeEvent(self, event):
        w = event.size().width()
        self._minBtn.move(w-103,0)
        self._maxBtn.move(w-74,0)
        self._closeBtn.move(w-45,0)

    def onMinimize(self):
        self.showMinimized()
    
    def isMaximized(self):
        return ((self.windowState() == Qt.WindowMaximized))
    
    def onMaximize(self):
        if self.isMaximized():
            self.showNormal()
            self.setMaximizeButton("maximize")
        else:
            self.showMaximized()
            self.setMaximizeButton("restore")
    
    def setMaximizeButton(self, style):
        if "maximize" == style:
            self._maxBtn.setStyleSheet("""
                QPushButton {
                    background-color:transparent;
                    border:none;
                    outline: none;
                    image: url(:/maximize_inactive.png);
                }
                QPushButton:hover {
                    background-color:#227582;
                    image: url(:/maximize_active.png);
                }
                QPushButton:pressed {
                    background-color:#14464e;
                    image: url(:/maximize_active.png);
                }
            """)
        elif "restore" == style:
            self._maxBtn.setStyleSheet("""
                QPushButton {
                    background-color:transparent;
                    border:none;
                    outline: none;
                    image: url(:/restore_inactive.png);
                }
                QPushButton:hover {
                    background-color:#227582;
                    image: url(:/restore_active.png);
                }
                QPushButton:pressed {
                    background-color:#14464e;
                    image: url(:/restore_active.png);
                }
            """)
    
    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self._isDragging = True
            self._dragPos = event.globalPos() - self.pos()
        event.accept()
        
    def mouseMoveEvent(self, event):
        if event.buttons() == Qt.LeftButton and self._isDragging and not self.isMaximized():
            self.move(event.globalPos() - self._dragPos)
        event.accept()

    def mouseReleaseEvent(self, event):
        self._isDragging = False
        event.accept()

    def saveSettings(self):
        root = ET.Element("MyTerm")
        GUISettings = ET.SubElement(root, "GUISettings")

        PortCfg = ET.SubElement(GUISettings, "PortConfig")
        ET.SubElement(PortCfg, "port").text = self.cmbPort.currentText()
        ET.SubElement(PortCfg, "baudrate").text = self.cmbBaudRate.currentText()
        ET.SubElement(PortCfg, "databits").text = self.cmbDataBits.currentText()
        ET.SubElement(PortCfg, "parity").text = self.cmbParity.currentText()
        ET.SubElement(PortCfg, "stopbits").text = self.cmbStopBits.currentText()
        ET.SubElement(PortCfg, "rtscts").text = self.chkRTSCTS.isChecked() and "on" or "off"
        ET.SubElement(PortCfg, "xonxoff").text = self.chkXonXoff.isChecked() and "on" or "off"

        View = ET.SubElement(GUISettings, "View")
        ET.SubElement(View, "LocalEcho").text = self.actionLocal_Echo.isChecked() and "on" or "off"
        ET.SubElement(View, "ReceiveView").text = self._viewGroup.checkedAction().text()

        with open(get_config_path('MyTerm.xml'), 'w') as f:
            f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
            f.write(ET.tostring(root, encoding='utf-8', pretty_print=True).decode("utf-8"))

    def loadSettings(self):
        if os.path.isfile(get_config_path("MyTerm.xml")):
            with open(get_config_path("MyTerm.xml"), 'r') as f:
                tree = safeET.parse(f)

            port = tree.findtext('GUISettings/PortConfig/port', default='')
            if port != '':
                self.cmbPort.setCurrentText(port)

            baudrate = tree.findtext('GUISettings/PortConfig/baudrate', default='38400')
            if baudrate != '':
                self.cmbBaudRate.setCurrentText(baudrate)

            databits = tree.findtext('GUISettings/PortConfig/databits', default='8')
            id = self.cmbDataBits.findText(databits)
            if id >= 0:
                self.cmbDataBits.setCurrentIndex(id)

            parity = tree.findtext('GUISettings/PortConfig/parity', default='None')
            id = self.cmbParity.findText(parity)
            if id >= 0:
                self.cmbParity.setCurrentIndex(id)

            stopbits = tree.findtext('GUISettings/PortConfig/stopbits', default='1')
            id = self.cmbStopBits.findText(stopbits)
            if id >= 0:
                self.cmbStopBits.setCurrentIndex(id)

            rtscts = tree.findtext('GUISettings/PortConfig/rtscts', default='off')
            if 'on' == rtscts:
                self.chkRTSCTS.setChecked(True)
            else:
                self.chkRTSCTS.setChecked(False)

            xonxoff = tree.findtext('GUISettings/PortConfig/xonxoff', default='off')
            if 'on' == xonxoff:
                self.chkXonXoff.setChecked(True)
            else:
                self.chkXonXoff.setChecked(False)

            LocalEcho = tree.findtext('GUISettings/View/LocalEcho', default='off')
            if 'on' == LocalEcho:
                self.actionLocal_Echo.setChecked(True)
                self._localEcho = True
            else:
                self.actionLocal_Echo.setChecked(False)
                self._localEcho = False

            ReceiveView = tree.findtext('GUISettings/View/ReceiveView', default='HEX(UPPERCASE)')
            if 'Ascii' in ReceiveView:
                self.actionAscii.setChecked(True)
                self._viewMode = VIEWMODE_ASCII
            elif 'lowercase' in ReceiveView:
                self.actionHex_lowercase.setChecked(True)
                self._viewMode = VIEWMODE_HEX_LOWERCASE
            elif 'UPPERCASE' in ReceiveView:
                self.actionHEX_UPPERCASE.setChecked(True)
                self._viewMode = VIEWMODE_HEX_UPPERCASE
            self.receiver_thread.setViewMode(self._viewMode)

    def closeEvent(self, event):
        self.saveLayout()
        self.saveQuickSend()
        self.saveSettings()
        event.accept()

    def initQuickSend(self):
        #self.quickSendTable.horizontalHeader().setDefaultSectionSize(40)
        #self.quickSendTable.horizontalHeader().setMinimumSectionSize(25)
        self.quickSendTable.setRowCount(50)
        self.quickSendTable.setColumnCount(3)
        self.quickSendTable.verticalHeader().setSectionsClickable(True)

        for row in range(50):
            self.initQuickSendButton(row)

        if os.path.isfile(get_config_path('QuickSend.csv')):
            self.loadQuickSend(get_config_path('QuickSend.csv'))

        self.quickSendTable.resizeColumnsToContents()

    def initQuickSendButton(self, row, cmd = 'cmd', opt = 'H', dat = ''):
        if self.quickSendTable.cellWidget(row, 0) is None:
            item = QToolButton(self)
            item.setText(cmd)
            item.clicked.connect(self._signalMapQuickSend.map)
            self._signalMapQuickSend.setMapping(item, row)
            self.quickSendTable.setCellWidget(row, 0, item)
        else:
            self.quickSendTable.cellWidget(row, 0).setText(cmd)

        if self.quickSendTable.cellWidget(row, 1) is None:
            item = QToolButton(self)
            item.setText(opt)
            #item.setMaximumSize(QtCore.QSize(16, 16))
            item.clicked.connect(self._signalMapQuickSendOpt.map)
            self._signalMapQuickSendOpt.setMapping(item, row)
            self.quickSendTable.setCellWidget(row, 1, item)
        else:
            self.quickSendTable.cellWidget(row, 1).setText(opt)

        if self.quickSendTable.item(row, 2) is None:
            self.quickSendTable.setItem(row, 2, QTableWidgetItem(dat))
        else:
            self.quickSendTable.item(row, 2).setText(dat)

        self.quickSendTable.setRowHeight(row, 16)

    def onSetSendHex(self):
        item = self.quickSendTable.cellWidget(self._quickSendOptRow, 1)
        item.setText('H')

    def onSetSendAsc(self):
        item = self.quickSendTable.cellWidget(self._quickSendOptRow, 1)
        item.setText('A')

    def onSetSendTFH(self):
        item = self.quickSendTable.cellWidget(self._quickSendOptRow, 1)
        item.setText('FH')

    def onSetSendTFA(self):
        item = self.quickSendTable.cellWidget(self._quickSendOptRow, 1)
        item.setText('FA')

    def onSetSendFB(self):
        item = self.quickSendTable.cellWidget(self._quickSendOptRow, 1)
        item.setText('FB')

    def onQuickSendOptions(self, row):
        self._quickSendOptRow = row
        item = self.quickSendTable.cellWidget(row, 1)
        self.sendOptMenu.popup(item.mapToGlobal(QPoint(item.size().width(), item.size().height())))

    def openQuickSend(self):
        fileName = QFileDialog.getOpenFileName(self, "Select a file",
            os.getcwd(), "CSV Files (*.csv)")[0]
        if fileName:
            self.loadQuickSend(fileName, notifyExcept = True)

    def saveQuickSend(self):
        # scan table
        rows = self.quickSendTable.rowCount()
        #cols = self.quickSendTable.columnCount()

        save_data = [[self.quickSendTable.cellWidget(row, 0).text(),
                      self.quickSendTable.cellWidget(row, 1).text(),
                      self.quickSendTable.item(row, 2) is not None
                      and self.quickSendTable.item(row, 2).text() or ''] for row in range(rows)]

        #import pprint
        #pprint.pprint(save_data, width=120, compact=True)

        # write to file
        with open(get_config_path('QuickSend.csv'), 'w') as csvfile:
            csvwriter = csv.writer(csvfile, delimiter=',', lineterminator='\n')
            csvwriter.writerows(save_data)

    def loadQuickSend(self, path, notifyExcept = False):
        data = []
        set_rows = 0
        set_cols = 0
        try:
            with open(path) as csvfile:
                csvData = csv.reader(csvfile)
                for row in csvData:
                    data.append(row)
                    set_rows = set_rows + 1
                    if len(row) > set_cols:
                        set_cols = len(row)
        except IOError as e:
            print("({})".format(e))
            if notifyExcept:
                QMessageBox.critical(self.defaultStyleWidget, "Open failed",
                    str(e), QMessageBox.Close)
            return

        rows = self.quickSendTable.rowCount()
        cols = self.quickSendTable.columnCount()

        if rows < set_rows:
            rows = set_rows + 10
            self.quickSendTable.setRowCount(rows)

        for row, rowdat in enumerate(data):
            if len(rowdat) >= 3:
                cmd, opt, dat = rowdat[0:3]
                self.initQuickSendButton(row, cmd, opt, dat)
#                self.quickSendTable.cellWidget(row, 0).setText(cmd)
#                self.quickSendTable.cellWidget(row, 1).setText(opt)
#                self.quickSendTable.setItem(row, 2, QTableWidgetItem(dat))

        self.quickSendTable.resizeColumnsToContents()
        #self.quickSendTable.resizeRowsToContents()

    def onQuickSend(self, row):
        if self.quickSendTable.item(row, 2) is not None:
            tablestring = self.quickSendTable.item(row, 2).text()
            format = self.quickSendTable.cellWidget(row, 1).text()
            if 'H' == format:
                self.transmitHex(tablestring)
            elif 'A' == format:
                self.transmitAsc(tablestring)
            elif 'FB' == format:
                try:
                    with open(tablestring, 'rb') as f:
                        bytes = f.read()
                        self.transmitBytearray(bytes)
                except IOError as e:
                    print("({})".format(e))
                    QMessageBox.critical(self.defaultStyleWidget, "Open failed",
                        str(e), QMessageBox.Close)
            else:
                try:
                    with open(tablestring, 'rt') as f:
                        filestring = f.read()
                        if 'FH' == format:
                            self.transmitHex(filestring)
                        elif 'FA' == format:
                            self.transmitAsc(filestring)
                except IOError as e:
                    print("({})".format(e))
                    QMessageBox.critical(self.defaultStyleWidget, "Open failed",
                        str(e), QMessageBox.Close)

    def onSend(self):
        sendstring = self.txtEdtInput.toPlainText()
        self.transmitHex(sendstring)

    def transmitHex(self, hexstring):
        if len(hexstring) > 0:
            hexarray = []
            _hexstring = hexstring.replace(' ', '')
            _hexstring = _hexstring.replace('\r', '')
            _hexstring = _hexstring.replace('\n', '')
            for i in range(0, len(_hexstring), 2):
                word = _hexstring[i:i+2]
                if is_hex(word):
                    hexarray.append(int(word, 16))
                else:
                    QMessageBox.critical(self.defaultStyleWidget, "Error",
                        "'%s' is not hexadecimal." % (word), QMessageBox.Close)
                    return

            self.transmitBytearray(bytearray(hexarray))

    def transmitAsc(self, text):
        if len(text) > 0:
            byteArray = [ord(char) for char in text]
            self.transmitBytearray(bytearray(byteArray))

    def transmitBytearray(self, byteArray):
        if self.serialport.isOpen():
            try:
                self.serialport.write(byteArray)
            except Exception as e:
                QMessageBox.critical(self.defaultStyleWidget,
                    "Exception in transmit", str(e), QMessageBox.Close)
                print("Exception in transmitBytearray(%s)" % text)
            else:
                if self._viewMode == VIEWMODE_ASCII:
                    text = byteArray.decode('unicode_escape')
                elif self._viewMode == VIEWMODE_HEX_LOWERCASE:
                    text = ''.join('%02x ' % t for t in byteArray)
                elif self._viewMode == VIEWMODE_HEX_UPPERCASE:
                    text = ''.join('%02X ' % t for t in byteArray)
                self.appendOutputText("\n%s T->:%s" % (self.timestamp(), text), Qt.blue)

    def onReaderExcept(self, e):
        self.closePort()
        QMessageBox.critical(self.defaultStyleWidget, "Read failed", str(e), QMessageBox.Close)

    def timestamp(self):
        return datetime.datetime.now().time().isoformat()[:-3]

    def onReceive(self, data):
        self.appendOutputText("\n%s R<-:%s" % (self.timestamp(), data))

    def appendOutputText(self, data, color=Qt.black):
        # the qEditText's "append" methon will add a unnecessary newline.
        # self.txtEdtOutput.append(data.decode('utf-8'))

        tc=self.txtEdtOutput.textColor()
        self.txtEdtOutput.moveCursor(QtGui.QTextCursor.End)
        self.txtEdtOutput.setTextColor(QtGui.QColor(color))
        self.txtEdtOutput.insertPlainText(data)
        self.txtEdtOutput.moveCursor(QtGui.QTextCursor.End)
        self.txtEdtOutput.setTextColor(tc)

    def getPort(self):
        return self.cmbPort.currentText()

    def getDataBits(self):
        return {'5':serial.FIVEBITS,
                '6':serial.SIXBITS,
                '7':serial.SEVENBITS, 
                '8':serial.EIGHTBITS}[self.cmbDataBits.currentText()]

    def getParity(self):
        return {'None' :serial.PARITY_NONE,
                'Even' :serial.PARITY_EVEN,
                'Odd'  :serial.PARITY_ODD,
                'Mark' :serial.PARITY_MARK,
                'Space':serial.PARITY_SPACE}[self.cmbParity.currentText()]

    def getStopBits(self):
        return {'1'  :serial.STOPBITS_ONE,
                '1.5':serial.STOPBITS_ONE_POINT_FIVE,
                '2'  :serial.STOPBITS_TWO}[self.cmbStopBits.currentText()]

    def openPort(self):
        if self.serialport.isOpen():
            return

        _port = self.getPort()
        if '' == _port:
            QMessageBox.information(self.defaultStyleWidget, "Invalid parameters", "Port is empty.")
            return

        _baudrate = self.cmbBaudRate.currentText()
        if '' == _baudrate:
            QMessageBox.information(self.defaultStyleWidget, "Invalid parameters", "Baudrate is empty.")
            return

        self.serialport.port     = _port
        self.serialport.baudrate = _baudrate
        self.serialport.bytesize = self.getDataBits()
        self.serialport.stopbits = self.getStopBits()
        self.serialport.parity   = self.getParity()
        self.serialport.rtscts   = self.chkRTSCTS.isChecked()
        self.serialport.xonxoff  = self.chkXonXoff.isChecked()
        # self.serialport.timeout  = THREAD_TIMEOUT
        # self.serialport.writeTimeout = SERIAL_WRITE_TIMEOUT
        try:
            self.serialport.open()
        except Exception as e:
            QMessageBox.critical(self.defaultStyleWidget, 
                "Could not open serial port", str(e), QMessageBox.Close)
        else:
            self._start_reader()
            self.setWindowTitle("%s on %s [%s, %s%s%s%s%s]" % (
                appInfo.title,
                self.serialport.portstr,
                self.serialport.baudrate,
                self.serialport.bytesize,
                self.serialport.parity,
                self.serialport.stopbits,
                self.serialport.rtscts and ' RTS/CTS' or '',
                self.serialport.xonxoff and ' Xon/Xoff' or '',
                )
            )
            pal = self.btnOpen.palette()
            pal.setColor(QtGui.QPalette.Button, QtGui.QColor(0,0xff,0x7f))
            self.btnOpen.setAutoFillBackground(True)
            self.btnOpen.setPalette(pal)
            self.btnOpen.setText('Close')
            self.btnOpen.update()

    def closePort(self):
        if self.serialport.isOpen():
            self._stop_reader()
            self.serialport.close()
            self.setWindowTitle(appInfo.title)
            pal = self.btnOpen.style().standardPalette()
            self.btnOpen.setAutoFillBackground(True)
            self.btnOpen.setPalette(pal)
            self.btnOpen.setText('Open')
            self.btnOpen.update()

    def _start_reader(self):
        """Start reader thread"""
        self.receiver_thread.start()

    def _stop_reader(self):
        """Stop reader thread only, wait for clean exit of thread"""
        self.receiver_thread.join()

    def onTogglePrtCfgPnl(self):
        if self.actionPort_Config_Panel.isChecked():
            self.dockWidget_PortConfig.show()
        else:
            self.dockWidget_PortConfig.hide()

    def onToggleQckSndPnl(self):
        if self.actionQuick_Send_Panel.isChecked():
            self.dockWidget_QuickSend.show()
        else:
            self.dockWidget_QuickSend.hide()

    def onToggleHexPnl(self):
        if self.actionSend_Hex_Panel.isChecked():
            self.dockWidget_SendHex.show()
        else:
            self.dockWidget_SendHex.hide()

    def onVisiblePrtCfgPnl(self, visible):
        self.actionPort_Config_Panel.setChecked(visible)

    def onVisibleQckSndPnl(self, visible):
        self.actionQuick_Send_Panel.setChecked(visible)

    def onVisibleHexPnl(self, visible):
        self.actionSend_Hex_Panel.setChecked(visible)

    def onLocalEcho(self):
        self._localEcho = self.actionLocal_Echo.isChecked()

    def onAlwaysOnTop(self):
        if self.actionAlways_On_Top.isChecked():
            style = self.windowFlags()
            self.setWindowFlags(style|Qt.WindowStaysOnTopHint)
            self.show()
        else:
            style = self.windowFlags()
            self.setWindowFlags(style & ~Qt.WindowStaysOnTopHint)
            self.show()

    def onOpen(self):
        if self.serialport.isOpen():
            self.closePort()
        else:
            self.openPort()

    def onClear(self):
        self.txtEdtOutput.clear()

    def onSaveLog(self):
        fileName = QFileDialog.getSaveFileName(self, "Save as", os.getcwd(),
            "Log files (*.log);;Text files (*.txt);;All files (*.*)")[0]
        if fileName:
            import codecs
            with codecs.open(fileName, 'w', 'utf-8') as f:
                f.write(self.txtEdtOutput.toPlainText())

    def moveScreenCenter(self):
        w = self.frameGeometry().width()
        h = self.frameGeometry().height()
        desktop = QDesktopWidget()
        screenW = desktop.screen().width()
        screenH = desktop.screen().height()
        self.setGeometry((screenW-w)/2, (screenH-h)/2, w, h)

    def onEnumPorts(self):
        self.cmbPort.clear()
        for p in enum_ports():
            self.cmbPort.addItem(p)

    def onAbout(self):
        QMessageBox.about(self.defaultStyleWidget, "About MyTerm", appInfo.aboutme)

    def onAboutQt(self):
        QMessageBox.aboutQt(self.defaultStyleWidget)

    def onExit(self):
        if self.serialport.isOpen():
            self.closePort()
        self.close()

    def restoreLayout(self):
        if os.path.isfile(get_config_path("UILayout.dat")):
            try:
                f=open(get_config_path("UILayout.dat"), 'rb')
                geometry, state=pickle.load(f)
                self.restoreGeometry(geometry)
                self.restoreState(state)
            except Exception as e:
                print("Exception on restoreLayout, {}".format(e))
        else:
            try:
                f=QFile(':/default_layout_qt5.dat')
                f.open(QIODevice.ReadOnly)
                geometry, state=pickle.loads(f.readAll())
                self.restoreGeometry(geometry)
                self.restoreState(state)
            except Exception as e:
                print("Exception on restoreLayout, {}".format(e))

    def saveLayout(self):
        with open(get_config_path("UILayout.dat"), 'wb') as f:
            pickle.dump((self.saveGeometry(), self.saveState()), f)

    def syncMenu(self):
        self.actionPort_Config_Panel.setChecked(not self.dockWidget_PortConfig.isHidden())
        self.actionQuick_Send_Panel.setChecked(not self.dockWidget_QuickSend.isHidden())
        self.actionSend_Hex_Panel.setChecked(not self.dockWidget_SendHex.isHidden())

    def onViewChanged(self):
        checked = self._viewGroup.checkedAction()
        if checked is None:
            self._viewMode = VIEWMODE_HEX_UPPERCASE
            self.actionHEX_UPPERCASE.setChecked(True)
        else:
            if 'Ascii' in checked.text():
                self._viewMode = VIEWMODE_ASCII
            elif 'lowercase' in checked.text():
                self._viewMode = VIEWMODE_HEX_LOWERCASE
            elif 'UPPERCASE' in checked.text():
                self._viewMode = VIEWMODE_HEX_UPPERCASE

        self.receiver_thread.setViewMode(self._viewMode)
Ejemplo n.º 15
0
class MainWindow(QMainWindow, Ui_MainWindow):
    """docstring for MainWindow."""
    def __init__(self, parent=None):
        super(MainWindow, self).__init__()
        self._csvFilePath = ""
        self.serialport = serial.Serial()
        self.receiver_thread = readerThread(self)
        self.receiver_thread.setPort(self.serialport)
        self._localEcho = None
        self._viewMode = None
        self._quickSendOptRow = 1

        self.setupUi(self)
        self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea)
        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
        font = QtGui.QFont()
        font.setFamily(EDITOR_FONT)
        font.setPointSize(9)
        self.txtEdtOutput.setFont(font)
        self.txtEdtInput.setFont(font)
        #self.quickSendTable.setFont(font)
        if UI_FONT is not None:
            font = QtGui.QFont()
            font.setFamily(UI_FONT)
            font.setPointSize(9)
            self.dockWidget_PortConfig.setFont(font)
            self.dockWidget_SendHex.setFont(font)
            self.dockWidget_QuickSend.setFont(font)
        self.setupMenu()
        self.setupFlatUi()
        self.onEnumPorts()

        icon = QtGui.QIcon(":/MyTerm.ico")
        self.setWindowIcon(icon)
        self.actionAbout.setIcon(icon)

        self.defaultStyleWidget = QWidget()
        self.defaultStyleWidget.setWindowIcon(icon)

        icon = QtGui.QIcon(":/qt_logo_16.ico")
        self.actionAbout_Qt.setIcon(icon)

        self._viewGroup = QActionGroup(self)
        self._viewGroup.addAction(self.actionAscii)
        self._viewGroup.addAction(self.actionHex_lowercase)
        self._viewGroup.addAction(self.actionHEX_UPPERCASE)
        self._viewGroup.setExclusive(True)

        # bind events
        self.actionOpen_Cmd_File.triggered.connect(self.openQuickSend)
        self.actionSave_Log.triggered.connect(self.onSaveLog)
        self.actionExit.triggered.connect(self.onExit)

        self.actionOpen.triggered.connect(self.openPort)
        self.actionClose.triggered.connect(self.closePort)

        self.actionPort_Config_Panel.triggered.connect(self.onTogglePrtCfgPnl)
        self.actionQuick_Send_Panel.triggered.connect(self.onToggleQckSndPnl)
        self.actionSend_Hex_Panel.triggered.connect(self.onToggleHexPnl)
        self.dockWidget_PortConfig.visibilityChanged.connect(
            self.onVisiblePrtCfgPnl)
        self.dockWidget_QuickSend.visibilityChanged.connect(
            self.onVisibleQckSndPnl)
        self.dockWidget_SendHex.visibilityChanged.connect(self.onVisibleHexPnl)
        self.actionLocal_Echo.triggered.connect(self.onLocalEcho)
        self.actionAlways_On_Top.triggered.connect(self.onAlwaysOnTop)

        self.actionAscii.triggered.connect(self.onViewChanged)
        self.actionHex_lowercase.triggered.connect(self.onViewChanged)
        self.actionHEX_UPPERCASE.triggered.connect(self.onViewChanged)

        self.actionAbout.triggered.connect(self.onAbout)
        self.actionAbout_Qt.triggered.connect(self.onAboutQt)

        self.btnOpen.clicked.connect(self.onOpen)
        self.btnClear.clicked.connect(self.onClear)
        self.btnSaveLog.clicked.connect(self.onSaveLog)
        self.btnEnumPorts.clicked.connect(self.onEnumPorts)
        self.btnSendHex.clicked.connect(self.onSend)

        self.receiver_thread.read.connect(self.onReceive)
        self.receiver_thread.exception.connect(self.onReaderExcept)
        self._signalMapQuickSendOpt = QSignalMapper(self)
        self._signalMapQuickSendOpt.mapped[int].connect(
            self.onQuickSendOptions)
        self._signalMapQuickSend = QSignalMapper(self)
        self._signalMapQuickSend.mapped[int].connect(self.onQuickSend)

        # initial action
        self.actionHEX_UPPERCASE.setChecked(True)
        self.receiver_thread.setViewMode(VIEWMODE_HEX_UPPERCASE)
        self.initQuickSend()
        self.restoreLayout()
        self.moveScreenCenter()
        self.syncMenu()

        if self.isMaximized():
            self.setMaximizeButton("restore")
        else:
            self.setMaximizeButton("maximize")

        self.loadSettings()

    def setupMenu(self):
        self.menuMenu = QtWidgets.QMenu()
        self.menuMenu.setTitle("&File")
        self.menuMenu.setObjectName("menuMenu")
        self.menuView = QtWidgets.QMenu(self.menuMenu)
        self.menuView.setTitle("&View")
        self.menuView.setObjectName("menuView")

        self.menuView.addAction(self.actionAscii)
        self.menuView.addAction(self.actionHex_lowercase)
        self.menuView.addAction(self.actionHEX_UPPERCASE)
        self.menuMenu.addAction(self.actionOpen_Cmd_File)
        self.menuMenu.addAction(self.actionSave_Log)
        self.menuMenu.addSeparator()
        self.menuMenu.addAction(self.actionPort_Config_Panel)
        self.menuMenu.addAction(self.actionQuick_Send_Panel)
        self.menuMenu.addAction(self.actionSend_Hex_Panel)
        self.menuMenu.addAction(self.menuView.menuAction())
        self.menuMenu.addAction(self.actionLocal_Echo)
        self.menuMenu.addAction(self.actionAlways_On_Top)
        self.menuMenu.addSeparator()
        self.menuMenu.addAction(self.actionAbout)
        self.menuMenu.addAction(self.actionAbout_Qt)
        self.menuMenu.addSeparator()
        self.menuMenu.addAction(self.actionExit)

        self.sendOptMenu = QtWidgets.QMenu()
        self.actionSend_Hex = QtWidgets.QAction(self)
        self.actionSend_Hex.setText("Send &Hex")
        self.actionSend_Hex.setStatusTip("Send Hex (e.g. 31 32 FF)")

        self.actionSend_Asc = QtWidgets.QAction(self)
        self.actionSend_Asc.setText("Send &Asc")
        self.actionSend_Asc.setStatusTip("Send Asc (e.g. abc123)")

        self.actionSend_TFH = QtWidgets.QAction(self)
        self.actionSend_TFH.setText("Send &Text file as hex")
        self.actionSend_TFH.setStatusTip(
            'Send text file as hex (e.g. strings "31 32 FF" in the file)')

        self.actionSend_TFA = QtWidgets.QAction(self)
        self.actionSend_TFA.setText("Send t&Ext file as asc")
        self.actionSend_TFA.setStatusTip(
            'Send text file as asc (e.g. strings "abc123" in the file)')

        self.actionSend_FB = QtWidgets.QAction(self)
        self.actionSend_FB.setText("Send &Bin/Hex file")
        self.actionSend_FB.setStatusTip("Send a bin file or a hex file")

        self.sendOptMenu.addAction(self.actionSend_Hex)
        self.sendOptMenu.addAction(self.actionSend_Asc)
        self.sendOptMenu.addAction(self.actionSend_TFH)
        self.sendOptMenu.addAction(self.actionSend_TFA)
        self.sendOptMenu.addAction(self.actionSend_FB)

        self.actionSend_Hex.triggered.connect(self.onSetSendHex)
        self.actionSend_Asc.triggered.connect(self.onSetSendAsc)
        self.actionSend_TFH.triggered.connect(self.onSetSendTFH)
        self.actionSend_TFA.triggered.connect(self.onSetSendTFA)
        self.actionSend_FB.triggered.connect(self.onSetSendFB)

    def setupFlatUi(self):
        self._dragPos = self.pos()
        self._isDragging = False
        self.setMouseTracking(True)
        self.setWindowFlags(Qt.FramelessWindowHint)
        self.setStyleSheet("""
            QWidget {
                background-color: %(BackgroundColor)s;
                /*background-image: url(:/background.png);*/
                outline: none;
            }
            QLabel {
                color:%(TextColor)s;
                font-size:12px;
                /*font-family:Century;*/
            }
            
            QComboBox {
                color:%(TextColor)s;
                font-size:12px;
                /*font-family:Century;*/
            }
            QComboBox {
                border: none;
                padding: 1px 1px 1px 3px;
            }
            QComboBox:editable {
                background: white;
            }
            QComboBox:!editable, QComboBox::drop-down:editable {
                background: #62c7e0;
            }
            QComboBox:!editable:hover, QComboBox::drop-down:editable:hover {
                background: #c7eaf3;
            }
            QComboBox:!editable:pressed, QComboBox::drop-down:editable:pressed {
                background: #35b6d7;
            }
            QComboBox:on {
                padding-top: 3px;
                padding-left: 4px;
            }
            QComboBox::drop-down {
                subcontrol-origin: padding;
                subcontrol-position: top right;
                width: 16px;
                border: none;
            }
            QComboBox::down-arrow {
                image: url(:/downarrow.png);
            }
            QComboBox::down-arrow:on {
                image: url(:/uparrow.png);
            }
            
            QGroupBox {
                color:%(TextColor)s;
                font-size:12px;
                /*font-family:Century;*/
                border: 1px solid gray;
                margin-top: 15px;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                subcontrol-position: top left;
                left:5px;
                top:3px;
            }
            
            QCheckBox {
                color:%(TextColor)s;
                spacing: 5px;
                font-size:12px;
                /*font-family:Century;*/
            }
            QCheckBox::indicator:unchecked {
                image: url(:/checkbox_unchecked.png);
            }

            QCheckBox::indicator:unchecked:hover {
                image: url(:/checkbox_unchecked_hover.png);
            }

            QCheckBox::indicator:unchecked:pressed {
                image: url(:/checkbox_unchecked_pressed.png);
            }

            QCheckBox::indicator:checked {
                image: url(:/checkbox_checked.png);
            }

            QCheckBox::indicator:checked:hover {
                image: url(:/checkbox_checked_hover.png);
            }

            QCheckBox::indicator:checked:pressed {
                image: url(:/checkbox_checked_pressed.png);
            }
            
            QScrollBar:horizontal {
                background-color:%(BackgroundColor)s;
                border: none;
                height: 15px;
                margin: 0px 20px 0 20px;
            }
            QScrollBar::handle:horizontal {
                background: %(ScrollBar_Handle)s;
                min-width: 20px;
            }
            QScrollBar::add-line:horizontal {
                image: url(:/rightarrow.png);
                border: none;
                background: %(ScrollBar_Line)s;
                width: 20px;
                subcontrol-position: right;
                subcontrol-origin: margin;
            }
            QScrollBar::sub-line:horizontal {
                image: url(:/leftarrow.png);
                border: none;
                background: %(ScrollBar_Line)s;
                width: 20px;
                subcontrol-position: left;
                subcontrol-origin: margin;
            }
            
            QScrollBar:vertical {
                background-color:%(BackgroundColor)s;
                border: none;
                width: 15px;
                margin: 20px 0px 20px 0px;
            }
            QScrollBar::handle::vertical {
                background: %(ScrollBar_Handle)s;
                min-height: 20px;
            }
            QScrollBar::add-line::vertical {
                image: url(:/downarrow.png);
                border: none;
                background: %(ScrollBar_Line)s;
                height: 20px;
                subcontrol-position: bottom;
                subcontrol-origin: margin;
            }
            QScrollBar::sub-line::vertical {
                image: url(:/uparrow.png);
                border: none;
                background: %(ScrollBar_Line)s;
                height: 20px;
                subcontrol-position: top;
                subcontrol-origin: margin;
            }
            
            QTableView {
                background-color: white;
                /*selection-background-color: #FF92BB;*/
                border: 1px solid %(TableView_Border)s;
                color: %(TextColor)s;
            }
            QTableView::focus {
                /*border: 1px solid #2a7fff;*/
            }
            QTableView QTableCornerButton::section {
                border: none;
                border-right: 1px solid %(TableView_Border)s;
                border-bottom: 1px solid %(TableView_Border)s;
                background-color: %(TableView_Corner)s;
            }
            QTableView QWidget {
                background-color: white;
            }
            QTableView::item:focus {
                border: 1px red;
                background-color: transparent;
                color: %(TextColor)s;
            }
            QHeaderView::section {
                border: none;
                border-right: 1px solid %(TableView_Border)s;
                border-bottom: 1px solid %(TableView_Border)s;
                padding-left: 2px;
                padding-right: 2px;
                color: #444444;
                background-color: %(TableView_Header)s;
            }
            QTextEdit {
                background-color:white;
                color:%(TextColor)s;
                border-top: none;
                border-bottom: none;
                border-left: 2px solid %(BackgroundColor)s;
                border-right: 2px solid %(BackgroundColor)s;
            }
            QTextEdit::focus {
            }
            
            QToolButton, QPushButton {
                background-color:#30a7b8;
                border:none;
                color:#ffffff;
                font-size:12px;
                /*font-family:Century;*/
            }
            QToolButton:hover, QPushButton:hover {
                background-color:#51c0d1;
            }
            QToolButton:pressed, QPushButton:pressed {
                background-color:#3a9ecc;
            }
            
            QMenuBar {
                color: %(TextColor)s;
                height: 24px;
            }
            QMenuBar::item {
                background-color: transparent;
                margin: 8px 0px 0px 0px;
                padding: 1px 8px 1px 8px;
                height: 15px;
            }
            QMenuBar::item:selected {
                background: #51c0d1;
            }
            QMenuBar::item:pressed {
                
            }
            /*
            QMenu {
                color: %(TextColor)s;
                background: #ffffff;
            }
            QMenu {
                margin: 2px;
            }
            QMenu::item {
                padding: 2px 25px 2px 21px;
                border: 1px solid transparent;
            }
            QMenu::item:selected {
                background: #51c0d1;
            }
            QMenu::icon {
                background: transparent;
                border: 2px inset transparent;
            }*/

            QDockWidget {
                font-size:12px;
                /*font-family:Century;*/
                color: %(TextColor)s;
                titlebar-close-icon: none;
                titlebar-normal-icon: none;
            }
            QDockWidget::title {
                margin: 0;
                padding: 2px;
                subcontrol-origin: content;
                subcontrol-position: right top;
                text-align: left;
                background: #67baed;
                
            }
            QDockWidget::float-button {
                max-width: 12px;
                max-height: 12px;
                background-color:transparent;
                border:none;
                image: url(:/restore_inactive.png);
            }
            QDockWidget::float-button:hover {
                background-color:#227582;
                image: url(:/restore_active.png);
            }
            QDockWidget::float-button:pressed {
                padding: 0;
                background-color:#14464e;
                image: url(:/restore_active.png);
            }
            QDockWidget::close-button {
                max-width: 12px;
                max-height: 12px;
                background-color:transparent;
                border:none;
                image: url(:/close_inactive.png);
            }
            QDockWidget::close-button:hover {
                background-color:#ea5e00;
                image: url(:/close_active.png);
            }
            QDockWidget::close-button:pressed {
                background-color:#994005;
                image: url(:/close_active.png);
                padding: 0;
            }
            
        """ % dict(BackgroundColor='#99d9ea',
                   TextColor='#202020',
                   ScrollBar_Handle='#61b9e1',
                   ScrollBar_Line='#7ecfe4',
                   TableView_Corner='#8ae6d2',
                   TableView_Header='#8ae6d2',
                   TableView_Border='#eeeeee'))
        self.dockWidgetContents.setStyleSheet("""
            QPushButton {
                min-height:23px;
            }
        """)
        self.dockWidget_QuickSend.setStyleSheet("""
            QToolButton, QPushButton {
                background-color:#27b798;
                /*font-family:Consolas;*/
                /*font-size:12px;*/
                /*min-width:46px;*/
            }
            QToolButton:hover, QPushButton:hover {
                background-color:#3bd5b4;
            }
            QToolButton:pressed, QPushButton:pressed {
                background-color:#1d8770;
            }
        """)
        self.dockWidgetContents_2.setStyleSheet("""
            QPushButton {
                min-height:23px;
                min-width:50px;
            }
        """)

        w = self.frameGeometry().width()
        self._minBtn = QPushButton(self)
        self._minBtn.setGeometry(w - 103, 0, 28, 24)
        self._minBtn.clicked.connect(self.onMinimize)
        self._minBtn.setStyleSheet("""
            QPushButton {
                background-color:transparent;
                border:none;
                outline: none;
                image: url(:/minimize_inactive.png);
            }
            QPushButton:hover {
                background-color:#227582;
                image: url(:/minimize_active.png);
            }
            QPushButton:pressed {
                background-color:#14464e;
                image: url(:/minimize_active.png);
            }
        """)

        self._maxBtn = QPushButton(self)
        self._maxBtn.setGeometry(w - 74, 0, 28, 24)
        self._maxBtn.clicked.connect(self.onMaximize)
        self.setMaximizeButton("maximize")

        self._closeBtn = QPushButton(self)
        self._closeBtn.setGeometry(w - 45, 0, 36, 24)
        self._closeBtn.clicked.connect(self.onExit)
        self._closeBtn.setStyleSheet("""
            QPushButton {
                background-color:transparent;
                border:none;
                outline: none;
                image: url(:/close_inactive.png);
            }
            QPushButton:hover {
                background-color:#ea5e00;
                image: url(:/close_active.png);
            }
            QPushButton:pressed {
                background-color:#994005;
                image: url(:/close_active.png);
            }
        """)

        self.btnMenu = QtWidgets.QToolButton(self)
        self.btnMenu.setEnabled(True)
        self.btnMenu.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
        self.btnMenu.setIcon(QtGui.QIcon(':/MyTerm.ico'))
        self.btnMenu.setText('Myterm  ')
        self.btnMenu.setGeometry(3, 3, 80, 18)
        self.btnMenu.setMenu(self.menuMenu)
        self.btnMenu.setPopupMode(QtWidgets.QToolButton.InstantPopup)

        self.btnRefresh = QtWidgets.QToolButton(self)
        self.btnRefresh.setEnabled(True)
        self.btnRefresh.setIcon(QtGui.QIcon(':/refresh.ico'))
        self.btnRefresh.setGeometry(110, 3, 18, 18)
        self.btnRefresh.clicked.connect(self.onEnumPorts)

        self.verticalLayout_1.removeWidget(self.cmbPort)
        self.cmbPort.setParent(self)
        self.cmbPort.setGeometry(128, 3, 60, 18)

        self.verticalLayout_1.removeWidget(self.btnOpen)
        self.btnOpen.setParent(self)
        self.btnOpen.setGeometry(210, 3, 60, 18)

    def resizeEvent(self, event):
        w = event.size().width()
        self._minBtn.move(w - 103, 0)
        self._maxBtn.move(w - 74, 0)
        self._closeBtn.move(w - 45, 0)

    def onMinimize(self):
        self.showMinimized()

    def isMaximized(self):
        return ((self.windowState() == Qt.WindowMaximized))

    def onMaximize(self):
        if self.isMaximized():
            self.showNormal()
            self.setMaximizeButton("maximize")
        else:
            self.showMaximized()
            self.setMaximizeButton("restore")

    def setMaximizeButton(self, style):
        if "maximize" == style:
            self._maxBtn.setStyleSheet("""
                QPushButton {
                    background-color:transparent;
                    border:none;
                    outline: none;
                    image: url(:/maximize_inactive.png);
                }
                QPushButton:hover {
                    background-color:#227582;
                    image: url(:/maximize_active.png);
                }
                QPushButton:pressed {
                    background-color:#14464e;
                    image: url(:/maximize_active.png);
                }
            """)
        elif "restore" == style:
            self._maxBtn.setStyleSheet("""
                QPushButton {
                    background-color:transparent;
                    border:none;
                    outline: none;
                    image: url(:/restore_inactive.png);
                }
                QPushButton:hover {
                    background-color:#227582;
                    image: url(:/restore_active.png);
                }
                QPushButton:pressed {
                    background-color:#14464e;
                    image: url(:/restore_active.png);
                }
            """)

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self._isDragging = True
            self._dragPos = event.globalPos() - self.pos()
        event.accept()

    def mouseMoveEvent(self, event):
        if event.buttons(
        ) == Qt.LeftButton and self._isDragging and not self.isMaximized():
            self.move(event.globalPos() - self._dragPos)
        event.accept()

    def mouseReleaseEvent(self, event):
        self._isDragging = False
        event.accept()

    def saveSettings(self):
        root = ET.Element("MyTerm")
        GUISettings = ET.SubElement(root, "GUISettings")

        PortCfg = ET.SubElement(GUISettings, "PortConfig")
        ET.SubElement(PortCfg, "port").text = self.cmbPort.currentText()
        ET.SubElement(PortCfg,
                      "baudrate").text = self.cmbBaudRate.currentText()
        ET.SubElement(PortCfg,
                      "databits").text = self.cmbDataBits.currentText()
        ET.SubElement(PortCfg, "parity").text = self.cmbParity.currentText()
        ET.SubElement(PortCfg,
                      "stopbits").text = self.cmbStopBits.currentText()
        ET.SubElement(
            PortCfg,
            "rtscts").text = self.chkRTSCTS.isChecked() and "on" or "off"
        ET.SubElement(
            PortCfg,
            "xonxoff").text = self.chkXonXoff.isChecked() and "on" or "off"

        View = ET.SubElement(GUISettings, "View")
        ET.SubElement(
            View, "LocalEcho"
        ).text = self.actionLocal_Echo.isChecked() and "on" or "off"
        ET.SubElement(
            View, "ReceiveView").text = self._viewGroup.checkedAction().text()

        with open(get_config_path('MyTerm.xml'), 'w') as f:
            f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
            f.write(
                ET.tostring(root, encoding='utf-8',
                            pretty_print=True).decode("utf-8"))

    def loadSettings(self):
        if os.path.isfile(get_config_path("MyTerm.xml")):
            with open(get_config_path("MyTerm.xml"), 'r') as f:
                tree = safeET.parse(f)

            port = tree.findtext('GUISettings/PortConfig/port', default='')
            if port != '':
                self.cmbPort.setCurrentText(port)

            baudrate = tree.findtext('GUISettings/PortConfig/baudrate',
                                     default='38400')
            if baudrate != '':
                self.cmbBaudRate.setCurrentText(baudrate)

            databits = tree.findtext('GUISettings/PortConfig/databits',
                                     default='8')
            id = self.cmbDataBits.findText(databits)
            if id >= 0:
                self.cmbDataBits.setCurrentIndex(id)

            parity = tree.findtext('GUISettings/PortConfig/parity',
                                   default='None')
            id = self.cmbParity.findText(parity)
            if id >= 0:
                self.cmbParity.setCurrentIndex(id)

            stopbits = tree.findtext('GUISettings/PortConfig/stopbits',
                                     default='1')
            id = self.cmbStopBits.findText(stopbits)
            if id >= 0:
                self.cmbStopBits.setCurrentIndex(id)

            rtscts = tree.findtext('GUISettings/PortConfig/rtscts',
                                   default='off')
            if 'on' == rtscts:
                self.chkRTSCTS.setChecked(True)
            else:
                self.chkRTSCTS.setChecked(False)

            xonxoff = tree.findtext('GUISettings/PortConfig/xonxoff',
                                    default='off')
            if 'on' == xonxoff:
                self.chkXonXoff.setChecked(True)
            else:
                self.chkXonXoff.setChecked(False)

            LocalEcho = tree.findtext('GUISettings/View/LocalEcho',
                                      default='off')
            if 'on' == LocalEcho:
                self.actionLocal_Echo.setChecked(True)
                self._localEcho = True
            else:
                self.actionLocal_Echo.setChecked(False)
                self._localEcho = False

            ReceiveView = tree.findtext('GUISettings/View/ReceiveView',
                                        default='HEX(UPPERCASE)')
            if 'Ascii' in ReceiveView:
                self.actionAscii.setChecked(True)
                self._viewMode = VIEWMODE_ASCII
            elif 'lowercase' in ReceiveView:
                self.actionHex_lowercase.setChecked(True)
                self._viewMode = VIEWMODE_HEX_LOWERCASE
            elif 'UPPERCASE' in ReceiveView:
                self.actionHEX_UPPERCASE.setChecked(True)
                self._viewMode = VIEWMODE_HEX_UPPERCASE
            self.receiver_thread.setViewMode(self._viewMode)

    def closeEvent(self, event):
        self.saveLayout()
        self.saveQuickSend()
        self.saveSettings()
        event.accept()

    def initQuickSend(self):
        #self.quickSendTable.horizontalHeader().setDefaultSectionSize(40)
        #self.quickSendTable.horizontalHeader().setMinimumSectionSize(25)
        self.quickSendTable.setRowCount(50)
        self.quickSendTable.setColumnCount(3)
        self.quickSendTable.verticalHeader().setSectionsClickable(True)

        for row in range(50):
            self.initQuickSendButton(row)

        if os.path.isfile(get_config_path('QuickSend.csv')):
            self.loadQuickSend(get_config_path('QuickSend.csv'))

        self.quickSendTable.resizeColumnsToContents()

    def initQuickSendButton(self, row, cmd='cmd', opt='H', dat=''):
        if self.quickSendTable.cellWidget(row, 0) is None:
            item = QToolButton(self)
            item.setText(cmd)
            item.clicked.connect(self._signalMapQuickSend.map)
            self._signalMapQuickSend.setMapping(item, row)
            self.quickSendTable.setCellWidget(row, 0, item)
        else:
            self.quickSendTable.cellWidget(row, 0).setText(cmd)

        if self.quickSendTable.cellWidget(row, 1) is None:
            item = QToolButton(self)
            item.setText(opt)
            #item.setMaximumSize(QtCore.QSize(16, 16))
            item.clicked.connect(self._signalMapQuickSendOpt.map)
            self._signalMapQuickSendOpt.setMapping(item, row)
            self.quickSendTable.setCellWidget(row, 1, item)
        else:
            self.quickSendTable.cellWidget(row, 1).setText(opt)

        if self.quickSendTable.item(row, 2) is None:
            self.quickSendTable.setItem(row, 2, QTableWidgetItem(dat))
        else:
            self.quickSendTable.item(row, 2).setText(dat)

        self.quickSendTable.setRowHeight(row, 16)

    def onSetSendHex(self):
        item = self.quickSendTable.cellWidget(self._quickSendOptRow, 1)
        item.setText('H')

    def onSetSendAsc(self):
        item = self.quickSendTable.cellWidget(self._quickSendOptRow, 1)
        item.setText('A')

    def onSetSendTFH(self):
        item = self.quickSendTable.cellWidget(self._quickSendOptRow, 1)
        item.setText('FH')

    def onSetSendTFA(self):
        item = self.quickSendTable.cellWidget(self._quickSendOptRow, 1)
        item.setText('FA')

    def onSetSendFB(self):
        item = self.quickSendTable.cellWidget(self._quickSendOptRow, 1)
        item.setText('FB')

    def onQuickSendOptions(self, row):
        self._quickSendOptRow = row
        item = self.quickSendTable.cellWidget(row, 1)
        self.sendOptMenu.popup(
            item.mapToGlobal(QPoint(item.size().width(),
                                    item.size().height())))

    def openQuickSend(self):
        fileName = QFileDialog.getOpenFileName(self, "Select a file",
                                               os.getcwd(),
                                               "CSV Files (*.csv)")[0]
        if fileName:
            self.loadQuickSend(fileName, notifyExcept=True)

    def saveQuickSend(self):
        # scan table
        rows = self.quickSendTable.rowCount()
        #cols = self.quickSendTable.columnCount()

        save_data = [[
            self.quickSendTable.cellWidget(row, 0).text(),
            self.quickSendTable.cellWidget(row, 1).text(),
            self.quickSendTable.item(row, 2) is not None
            and self.quickSendTable.item(row, 2).text() or ''
        ] for row in range(rows)]

        #import pprint
        #pprint.pprint(save_data, width=120, compact=True)

        # write to file
        with open(get_config_path('QuickSend.csv'), 'w') as csvfile:
            csvwriter = csv.writer(csvfile, delimiter=',', lineterminator='\n')
            csvwriter.writerows(save_data)

    def loadQuickSend(self, path, notifyExcept=False):
        data = []
        set_rows = 0
        set_cols = 0
        try:
            with open(path) as csvfile:
                csvData = csv.reader(csvfile)
                for row in csvData:
                    data.append(row)
                    set_rows = set_rows + 1
                    if len(row) > set_cols:
                        set_cols = len(row)
        except IOError as e:
            print("({})".format(e))
            if notifyExcept:
                QMessageBox.critical(self.defaultStyleWidget, "Open failed",
                                     str(e), QMessageBox.Close)
            return

        rows = self.quickSendTable.rowCount()
        cols = self.quickSendTable.columnCount()

        if rows < set_rows:
            rows = set_rows + 10
            self.quickSendTable.setRowCount(rows)

        for row, rowdat in enumerate(data):
            if len(rowdat) >= 3:
                cmd, opt, dat = rowdat[0:3]
                self.initQuickSendButton(row, cmd, opt, dat)


#                self.quickSendTable.cellWidget(row, 0).setText(cmd)
#                self.quickSendTable.cellWidget(row, 1).setText(opt)
#                self.quickSendTable.setItem(row, 2, QTableWidgetItem(dat))

        self.quickSendTable.resizeColumnsToContents()
        #self.quickSendTable.resizeRowsToContents()

    def onQuickSend(self, row):
        if self.quickSendTable.item(row, 2) is not None:
            tablestring = self.quickSendTable.item(row, 2).text()
            format = self.quickSendTable.cellWidget(row, 1).text()
            if 'H' == format:
                self.transmitHex(tablestring)
            elif 'A' == format:
                self.transmitAsc(tablestring)
            elif 'FB' == format:
                try:
                    with open(tablestring, 'rb') as f:
                        bytes = f.read()
                        self.transmitBytearray(bytes)
                except IOError as e:
                    print("({})".format(e))
                    QMessageBox.critical(self.defaultStyleWidget,
                                         "Open failed", str(e),
                                         QMessageBox.Close)
            else:
                try:
                    with open(tablestring, 'rt') as f:
                        filestring = f.read()
                        if 'FH' == format:
                            self.transmitHex(filestring)
                        elif 'FA' == format:
                            self.transmitAsc(filestring)
                except IOError as e:
                    print("({})".format(e))
                    QMessageBox.critical(self.defaultStyleWidget,
                                         "Open failed", str(e),
                                         QMessageBox.Close)

    def onSend(self):
        sendstring = self.txtEdtInput.toPlainText()
        self.transmitHex(sendstring)

    def transmitHex(self, hexstring):
        if len(hexstring) > 0:
            hexarray = []
            _hexstring = hexstring.replace(' ', '')
            _hexstring = _hexstring.replace('\r', '')
            _hexstring = _hexstring.replace('\n', '')
            for i in range(0, len(_hexstring), 2):
                word = _hexstring[i:i + 2]
                if is_hex(word):
                    hexarray.append(int(word, 16))
                else:
                    QMessageBox.critical(self.defaultStyleWidget, "Error",
                                         "'%s' is not hexadecimal." % (word),
                                         QMessageBox.Close)
                    return

            self.transmitBytearray(bytearray(hexarray))

    def transmitAsc(self, text):
        if len(text) > 0:
            byteArray = [ord(char) for char in text]
            self.transmitBytearray(bytearray(byteArray))

    def transmitBytearray(self, byteArray):
        if self.serialport.isOpen():
            try:
                self.serialport.write(byteArray)
            except Exception as e:
                QMessageBox.critical(self.defaultStyleWidget,
                                     "Exception in transmit", str(e),
                                     QMessageBox.Close)
                print("Exception in transmitBytearray(%s)" % text)
            else:
                if self._viewMode == VIEWMODE_ASCII:
                    text = byteArray.decode('unicode_escape')
                elif self._viewMode == VIEWMODE_HEX_LOWERCASE:
                    text = ''.join('%02x ' % t for t in byteArray)
                elif self._viewMode == VIEWMODE_HEX_UPPERCASE:
                    text = ''.join('%02X ' % t for t in byteArray)
                self.appendOutputText("\n%s T->:%s" % (self.timestamp(), text),
                                      Qt.blue)

    def onReaderExcept(self, e):
        self.closePort()
        QMessageBox.critical(self.defaultStyleWidget, "Read failed", str(e),
                             QMessageBox.Close)

    def timestamp(self):
        return datetime.datetime.now().time().isoformat()[:-3]

    def onReceive(self, data):
        self.appendOutputText("\n%s R<-:%s" % (self.timestamp(), data))

    def appendOutputText(self, data, color=Qt.black):
        # the qEditText's "append" methon will add a unnecessary newline.
        # self.txtEdtOutput.append(data.decode('utf-8'))

        tc = self.txtEdtOutput.textColor()
        self.txtEdtOutput.moveCursor(QtGui.QTextCursor.End)
        self.txtEdtOutput.setTextColor(QtGui.QColor(color))
        self.txtEdtOutput.insertPlainText(data)
        self.txtEdtOutput.moveCursor(QtGui.QTextCursor.End)
        self.txtEdtOutput.setTextColor(tc)

    def getPort(self):
        return self.cmbPort.currentText()

    def getDataBits(self):
        return {
            '5': serial.FIVEBITS,
            '6': serial.SIXBITS,
            '7': serial.SEVENBITS,
            '8': serial.EIGHTBITS
        }[self.cmbDataBits.currentText()]

    def getParity(self):
        return {
            'None': serial.PARITY_NONE,
            'Even': serial.PARITY_EVEN,
            'Odd': serial.PARITY_ODD,
            'Mark': serial.PARITY_MARK,
            'Space': serial.PARITY_SPACE
        }[self.cmbParity.currentText()]

    def getStopBits(self):
        return {
            '1': serial.STOPBITS_ONE,
            '1.5': serial.STOPBITS_ONE_POINT_FIVE,
            '2': serial.STOPBITS_TWO
        }[self.cmbStopBits.currentText()]

    def openPort(self):
        if self.serialport.isOpen():
            return

        _port = self.getPort()
        if '' == _port:
            QMessageBox.information(self.defaultStyleWidget,
                                    "Invalid parameters", "Port is empty.")
            return

        _baudrate = self.cmbBaudRate.currentText()
        if '' == _baudrate:
            QMessageBox.information(self.defaultStyleWidget,
                                    "Invalid parameters", "Baudrate is empty.")
            return

        self.serialport.port = _port
        self.serialport.baudrate = _baudrate
        self.serialport.bytesize = self.getDataBits()
        self.serialport.stopbits = self.getStopBits()
        self.serialport.parity = self.getParity()
        self.serialport.rtscts = self.chkRTSCTS.isChecked()
        self.serialport.xonxoff = self.chkXonXoff.isChecked()
        # self.serialport.timeout  = THREAD_TIMEOUT
        # self.serialport.writeTimeout = SERIAL_WRITE_TIMEOUT
        try:
            self.serialport.open()
        except Exception as e:
            QMessageBox.critical(self.defaultStyleWidget,
                                 "Could not open serial port", str(e),
                                 QMessageBox.Close)
        else:
            self._start_reader()
            self.setWindowTitle("%s on %s [%s, %s%s%s%s%s]" % (
                appInfo.title,
                self.serialport.portstr,
                self.serialport.baudrate,
                self.serialport.bytesize,
                self.serialport.parity,
                self.serialport.stopbits,
                self.serialport.rtscts and ' RTS/CTS' or '',
                self.serialport.xonxoff and ' Xon/Xoff' or '',
            ))
            pal = self.btnOpen.palette()
            pal.setColor(QtGui.QPalette.Button, QtGui.QColor(0, 0xff, 0x7f))
            self.btnOpen.setAutoFillBackground(True)
            self.btnOpen.setPalette(pal)
            self.btnOpen.setText('Close')
            self.btnOpen.update()

    def closePort(self):
        if self.serialport.isOpen():
            self._stop_reader()
            self.serialport.close()
            self.setWindowTitle(appInfo.title)
            pal = self.btnOpen.style().standardPalette()
            self.btnOpen.setAutoFillBackground(True)
            self.btnOpen.setPalette(pal)
            self.btnOpen.setText('Open')
            self.btnOpen.update()

    def _start_reader(self):
        """Start reader thread"""
        self.receiver_thread.start()

    def _stop_reader(self):
        """Stop reader thread only, wait for clean exit of thread"""
        self.receiver_thread.join()

    def onTogglePrtCfgPnl(self):
        if self.actionPort_Config_Panel.isChecked():
            self.dockWidget_PortConfig.show()
        else:
            self.dockWidget_PortConfig.hide()

    def onToggleQckSndPnl(self):
        if self.actionQuick_Send_Panel.isChecked():
            self.dockWidget_QuickSend.show()
        else:
            self.dockWidget_QuickSend.hide()

    def onToggleHexPnl(self):
        if self.actionSend_Hex_Panel.isChecked():
            self.dockWidget_SendHex.show()
        else:
            self.dockWidget_SendHex.hide()

    def onVisiblePrtCfgPnl(self, visible):
        self.actionPort_Config_Panel.setChecked(visible)

    def onVisibleQckSndPnl(self, visible):
        self.actionQuick_Send_Panel.setChecked(visible)

    def onVisibleHexPnl(self, visible):
        self.actionSend_Hex_Panel.setChecked(visible)

    def onLocalEcho(self):
        self._localEcho = self.actionLocal_Echo.isChecked()

    def onAlwaysOnTop(self):
        if self.actionAlways_On_Top.isChecked():
            style = self.windowFlags()
            self.setWindowFlags(style | Qt.WindowStaysOnTopHint)
            self.show()
        else:
            style = self.windowFlags()
            self.setWindowFlags(style & ~Qt.WindowStaysOnTopHint)
            self.show()

    def onOpen(self):
        if self.serialport.isOpen():
            self.closePort()
        else:
            self.openPort()

    def onClear(self):
        self.txtEdtOutput.clear()

    def onSaveLog(self):
        fileName = QFileDialog.getSaveFileName(
            self, "Save as", os.getcwd(),
            "Log files (*.log);;Text files (*.txt);;All files (*.*)")[0]
        if fileName:
            import codecs
            with codecs.open(fileName, 'w', 'utf-8') as f:
                f.write(self.txtEdtOutput.toPlainText())

    def moveScreenCenter(self):
        w = self.frameGeometry().width()
        h = self.frameGeometry().height()
        desktop = QDesktopWidget()
        screenW = desktop.screen().width()
        screenH = desktop.screen().height()
        self.setGeometry((screenW - w) / 2, (screenH - h) / 2, w, h)

    def onEnumPorts(self):
        self.cmbPort.clear()
        for p in enum_ports():
            self.cmbPort.addItem(p)

    def onAbout(self):
        QMessageBox.about(self.defaultStyleWidget, "About MyTerm",
                          appInfo.aboutme)

    def onAboutQt(self):
        QMessageBox.aboutQt(self.defaultStyleWidget)

    def onExit(self):
        if self.serialport.isOpen():
            self.closePort()
        self.close()

    def restoreLayout(self):
        if os.path.isfile(get_config_path("UILayout.dat")):
            try:
                f = open(get_config_path("UILayout.dat"), 'rb')
                geometry, state = pickle.load(f)
                self.restoreGeometry(geometry)
                self.restoreState(state)
            except Exception as e:
                print("Exception on restoreLayout, {}".format(e))
        else:
            try:
                f = QFile(':/default_layout_qt5.dat')
                f.open(QIODevice.ReadOnly)
                geometry, state = pickle.loads(f.readAll())
                self.restoreGeometry(geometry)
                self.restoreState(state)
            except Exception as e:
                print("Exception on restoreLayout, {}".format(e))

    def saveLayout(self):
        with open(get_config_path("UILayout.dat"), 'wb') as f:
            pickle.dump((self.saveGeometry(), self.saveState()), f)

    def syncMenu(self):
        self.actionPort_Config_Panel.setChecked(
            not self.dockWidget_PortConfig.isHidden())
        self.actionQuick_Send_Panel.setChecked(
            not self.dockWidget_QuickSend.isHidden())
        self.actionSend_Hex_Panel.setChecked(
            not self.dockWidget_SendHex.isHidden())

    def onViewChanged(self):
        checked = self._viewGroup.checkedAction()
        if checked is None:
            self._viewMode = VIEWMODE_HEX_UPPERCASE
            self.actionHEX_UPPERCASE.setChecked(True)
        else:
            if 'Ascii' in checked.text():
                self._viewMode = VIEWMODE_ASCII
            elif 'lowercase' in checked.text():
                self._viewMode = VIEWMODE_HEX_LOWERCASE
            elif 'UPPERCASE' in checked.text():
                self._viewMode = VIEWMODE_HEX_UPPERCASE

        self.receiver_thread.setViewMode(self._viewMode)
Ejemplo n.º 16
0
    def _populate_menubars(self) -> None:
        """Fill in the menubars."""
        # ----------
        # Game menu
        # ----------
        # New game (F2)
        new_game_act = self._game_menu.addAction("New game", self._ctrlr.new_game)
        new_game_act.setShortcut("F2")

        # Replay game (F3)
        replay_act = self._game_menu.addAction("Replay", self._ctrlr.restart_game)
        replay_act.setShortcut("F3")

        # Create board
        def switch_create_mode(checked: bool):
            mode = UIMode.CREATE if checked else UIMode.GAME
            self._state.ui_mode = mode
            self._ctrlr.switch_mode(mode)

        self._create_menu_action = create_act = QAction(
            "Create board",
            self,
            checkable=True,
            checked=self._state.ui_mode is UIMode.CREATE,
        )
        self._game_menu.addAction(create_act)
        create_act.triggered.connect(switch_create_mode)

        # Save board
        self._game_menu.addAction("Save board", self._open_save_board_modal)

        # Load board
        self._game_menu.addAction("Load board", self._open_load_board_modal)

        self._game_menu.addSeparator()

        # Current info (F4)
        info_act = self._game_menu.addAction(
            "Current game info", self._open_current_info_modal
        )
        info_act.setShortcut("F4")

        # Solver
        # - Probabilities (F5)
        # - Auto flag (Ctrl+F)
        # - Auto click (Ctrl+Enter)

        # Highscores (F6)
        highscores_act = self._game_menu.addAction(
            "Highscores", self.open_highscores_window
        )
        highscores_act.setShortcut("F6")

        # Stats (F7)

        self._game_menu.addSeparator()

        # Difficulty radiobuttons
        # - Beginner (b)
        # - Intermediate (i)
        # - Expert (e)
        # - Master (m)
        # - Custom (c)
        diff_group = QActionGroup(self)
        diff_group.setExclusive(True)
        for diff in Difficulty:
            diff_act = QAction(diff.name.capitalize(), diff_group, checkable=True)
            self._diff_menu_actions[diff] = diff_act
            self._game_menu.addAction(diff_act)
            diff_act.id = diff
            if diff is self._state.difficulty:
                diff_act.setChecked(True)
            diff_act.triggered.connect(
                lambda _: self._change_difficulty(diff_group.checkedAction().id)
            )
            diff_act.setShortcut(diff.value)

        self._game_menu.addSeparator()

        # Play highscore
        # self._game_menu.addAction("Play highscore", self._open_play_highscore_modal)

        # Zoom
        self._game_menu.addAction("Button size", self._open_zoom_modal)

        # Styles
        # - Buttons
        # - Images
        # - Numbers
        def get_change_style_func(grp, style):
            def change_style():
                self._state.styles[grp] = style
                self._mf_widget.update_style(grp, style)

            return change_style

        styles_menu = QMenu("Styles", self)
        self._game_menu.addMenu(styles_menu)
        for img_group in [CellImageType.BUTTONS]:
            img_group_name = img_group.name.capitalize()
            submenu = QMenu(img_group_name, self)
            styles_menu.addMenu(submenu)
            group = QActionGroup(self)
            group.setExclusive(True)
            for folder in (utils.IMG_DIR / img_group_name).glob("*"):
                style = folder.name
                style_act = QAction(style, self, checkable=True)
                if style == self._state.styles[img_group]:
                    style_act.setChecked(True)
                group.addAction(style_act)
                style_act.triggered.connect(get_change_style_func(img_group, style))
                submenu.addAction(style_act)

        # Advanced options
        self._game_menu.addAction("Advanced options", self._open_advanced_opts_modal)

        self._game_menu.addSeparator()

        # Exit (F4)
        self._game_menu.addAction("Exit", self.close, shortcut="Alt+F4")

        # ----------
        # Options menu
        # ----------
        # First-click success
        def toggle_first_success():
            new_val = not self._state.pending_first_success
            self._state.first_success = new_val
            self._ctrlr.set_first_success(new_val)

        first_act = QAction(
            "Safe start", self, checkable=True, checked=self._state.first_success
        )
        self._opts_menu.addAction(first_act)
        first_act.triggered.connect(toggle_first_success)

        # Drag select
        drag_act = self._opts_menu.addAction(
            "Drag select",
            lambda: setattr(
                self._state, "drag_select", not self._state.pending_drag_select
            ),
        )
        drag_act.setCheckable(True)
        drag_act.setChecked(self._state.drag_select)

        # Max mines per cell option
        def get_change_per_cell_func(n):
            def change_per_cell():
                self._state.per_cell = n
                self._ctrlr.set_per_cell(n)

            return change_per_cell

        per_cell_menu = self._opts_menu.addMenu("Max per cell")
        per_cell_group = QActionGroup(self)
        per_cell_group.setExclusive(True)
        for i in range(1, 4):
            action = QAction(str(i), self, checkable=True)
            per_cell_menu.addAction(action)
            per_cell_group.addAction(action)
            if self._state.per_cell == i:
                action.setChecked(True)
            action.triggered.connect(get_change_per_cell_func(i))

        # ----------
        # Help menu
        # ----------
        rules_act = QAction("Rules", self)
        self._help_menu.addAction(rules_act)
        rules_act.triggered.connect(
            lambda: self._open_text_popup("Rules", FILES_DIR / "rules.txt")
        )

        tips_act = QAction("Tips", self)
        self._help_menu.addAction(tips_act)
        tips_act.triggered.connect(
            lambda: self._open_text_popup("Tips", FILES_DIR / "tips.txt")
        )

        retrieve_act = QAction("Retrieve highscores", self)
        retrieve_act.triggered.connect(self._open_retrieve_highscores_modal)
        self._help_menu.addAction(retrieve_act)

        self._help_menu.addSeparator()

        about_act = QAction("About", self)
        self._help_menu.addAction(about_act)
        about_act.triggered.connect(
            lambda: self._open_text_popup("About", FILES_DIR / "about.txt")
        )
        about_act.setShortcut("F1")
Ejemplo n.º 17
0
Archivo: main.py Proyecto: jpli/tafor
class MainWindow(QMainWindow, Ui_main.Ui_MainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.setupUi(self)

        # 时钟计时器
        self.clockTimer = QTimer()
        self.clockTimer.timeout.connect(self.singer)
        self.clockTimer.start(1 * 1000)

        self.workerTimer = QTimer()
        self.workerTimer.timeout.connect(self.worker)
        self.workerTimer.start(60 * 1000)

        self.setup()
        self.bindSignal()
        self.updateGui()
        self.worker()
        self.painter()

    def setup(self):
        self.setWindowIcon(QIcon(':/logo.png'))

        # 初始化剪贴板
        self.clip = QApplication.clipboard()

        # 闹钟提示框
        self.remindBox = RemindMessageBox(self)

        # 初始化窗口
        self.settingDialog = SettingDialog(self)

        self.tafSender = TafSender(self)
        self.trendSender = TrendSender(self)
        self.sigmetSender = SigmetSender(self)
        self.reSender = ReSender(self)

        self.tafEditor = TafEditor(self, self.tafSender)
        self.trendEditor = TrendEditor(self, self.trendSender)
        self.sigmetEditor = SigmetEditor(self, self.sigmetSender)

        if boolean(conf.value('General/Serious')):
            self.taskBrowser = TaskBrowser(self)
            self.taskTafSender = TaskTafSender(self)
            self.taskTafEditor = TaskTafEditor(self, self.taskTafSender)

        self.setRecent()
        self.setTable()
        self.setContractMenu()  # 设置切换联系人菜单
        self.setSysTray()
        self.setStatus()
        self.setThread()
        self.setSound()

    def bindSignal(self):
        context.taf.warningSignal.connect(self.dialer)
        context.taf.clockSignal.connect(self.remindTaf)

        # 连接菜单信号
        self.tafAction.triggered.connect(self.tafEditor.show)
        self.trendAction.triggered.connect(self.trendEditor.show)
        self.sigmetAction.triggered.connect(self.sigmetEditor.show)

        # 连接设置对话框的槽
        self.settingAction.triggered.connect(self.settingDialog.exec_)
        self.settingAction.setIcon(QIcon(':/setting.png'))

        self.openDocsAction.triggered.connect(self.openDocs)
        self.reportIssueAction.triggered.connect(self.reportIssue)
        self.checkUpgradeAction.triggered.connect(
            self.checkUpgradeThread.start)

        # 连接关于信息的槽
        self.aboutAction.triggered.connect(self.about)

        # 连接切换联系人的槽
        self.contractsActionGroup.triggered.connect(self.changeContract)
        self.contractsActionGroup.triggered.connect(self.settingDialog.load)

    def setRecent(self):
        self.clock = Clock(self, self.tipsLayout)
        self.tipsLayout.addSpacerItem(
            QSpacerItem(10, 10, QSizePolicy.Expanding, QSizePolicy.Minimum))
        self.currentTaf = CurrentTaf(self, self.tipsLayout)
        self.recentFt = RecentMessage(self, self.recentLayout, 'FT')
        self.recentFc = RecentMessage(self, self.recentLayout, 'FC')
        self.recentSigmet = RecentMessage(self, self.recentLayout, 'WS')
        self.recentTrend = RecentMessage(self, self.recentLayout, 'TREND')

    def setTable(self):
        self.tafTable = TafTable(self, self.tafLayout)
        self.metarTable = MetarTable(self, self.metarLayout)
        self.sigmetTable = SigmetTable(self, self.sigmetLayout)

    def setContractMenu(self):
        self.contractsActionGroup = QActionGroup(self)
        self.contractsActionGroup.addAction(self.contractNo)

        contacts = db.query(User).all()

        for person in contacts:
            setattr(self, 'contract' + str(person.id), QAction(self))
            target = getattr(self, 'contract' + str(person.id))
            target.setText(person.name)
            target.setCheckable(True)

            self.contractsActionGroup.addAction(target)
            self.contractsMenu.addAction(target)

        self.updateContractMenu()

    def setSysTray(self):
        # 设置系统托盘
        self.tray = QSystemTrayIcon(self)
        self.tray.setIcon(QIcon(':/logo.png'))
        self.tray.show()

        # 连接系统托盘的槽
        self.tray.activated.connect(self.restoreWindow)

        self.trayMenu = QMenu(self)

        self.trayMenu.addAction(self.contractsMenu.menuAction())
        self.trayMenu.addAction(self.settingAction)
        self.trayMenu.addAction(self.aboutAction)
        self.trayMenu.addSeparator()
        self.trayMenu.addAction(self.quitAction)

        self.tray.setContextMenu(self.trayMenu)

        title = QCoreApplication.translate(
            'MainWindow', 'Terminal Aerodrome Forecast Encoding Software')
        message = '{} v {}'.format(title, __version__)
        self.tray.setToolTip(message)

    def setStatus(self):
        self.webApiStatus = WebAPIStatus(self, self.statusBar)
        self.callServiceStatus = CallServiceStatus(self,
                                                   self.statusBar,
                                                   last=True)

        # self.statusBar.setStyleSheet('QStatusBar::item{border: 0px}')

    def setThread(self):
        self.workThread = WorkThread(self)
        self.workThread.finished.connect(self.updateMessage)

        self.callThread = CallThread(self)

        self.firInfoThread = FirInfoThread()

        self.checkUpgradeThread = CheckUpgradeThread(self)
        self.checkUpgradeThread.doneSignal.connect(self.checkUpgrade)

    def setSound(self):
        self.ringSound = Sound('ring.wav',
                               conf.value('Monitor/RemindTAFVolume'))
        self.notificationSound = Sound('notification.wav', 100)
        self.alarmSound = Sound('alarm.wav',
                                conf.value('Monitor/WarnTAFVolume'))
        self.trendSound = Sound('trend.wav',
                                conf.value('Monitor/RemindTrendVolume'))
        self.sigmetSound = Sound('sigmet.wav',
                                 conf.value('Monitor/RemindSIGMETVolume'))

        self.settingDialog.warnTafVolume.valueChanged.connect(
            lambda vol: self.alarmSound.play(volume=vol, loop=False))
        self.settingDialog.remindTafVolume.valueChanged.connect(
            lambda vol: self.ringSound.play(volume=vol, loop=False))
        self.settingDialog.remindTrendVolume.valueChanged.connect(
            lambda vol: self.trendSound.play(volume=vol, loop=False))
        self.settingDialog.remindSigmetVolume.valueChanged.connect(
            lambda vol: self.sigmetSound.play(volume=vol, loop=False))

    def changeContract(self):
        target = self.contractsActionGroup.checkedAction()

        if self.contractNo == target:
            conf.setValue('Monitor/SelectedMobile', '')
            logger.info('关闭电话提醒')
        else:
            name = target.text()
            person = db.query(User).filter_by(name=name).first()
            mobile = person.mobile if person else ''

            conf.setValue('Monitor/SelectedMobile', mobile)
            logger.info('切换联系人 %s %s' % (name, mobile))

    def event(self, event):
        if event.type() == QEvent.WindowStateChange and self.isMinimized():
            # 此时窗口已经最小化,
            # 从任务栏中移除窗口
            self.setWindowFlags(self.windowFlags() & Qt.Tool)
            self.tray.show()
            return True
        else:
            return super(self.__class__, self).event(event)

    def keyPressEvent(self, event):
        if boolean(conf.value('General/Serious')):
            if event.modifiers() == (Qt.ShiftModifier | Qt.ControlModifier):
                if event.key() == Qt.Key_P:
                    self.taskTafEditor.show()
                if event.key() == Qt.Key_T:
                    self.taskBrowser.show()

    def closeEvent(self, event):
        if event.spontaneous():
            event.ignore()
            self.hide()
        else:
            self.tafSender.setAttribute(Qt.WA_DeleteOnClose)
            self.trendSender.setAttribute(Qt.WA_DeleteOnClose)
            self.sigmetSender.setAttribute(Qt.WA_DeleteOnClose)
            self.reSender.setAttribute(Qt.WA_DeleteOnClose)
            self.tafEditor.setAttribute(Qt.WA_DeleteOnClose)
            self.trendEditor.setAttribute(Qt.WA_DeleteOnClose)
            self.sigmetEditor.setAttribute(Qt.WA_DeleteOnClose)
            self.settingDialog.setAttribute(Qt.WA_DeleteOnClose)

            self.tafSender.close()
            self.trendSender.close()
            self.sigmetSender.close()
            self.reSender.close()
            self.tafEditor.close()
            self.trendEditor.close()
            self.sigmetEditor.close()
            self.settingDialog.close()

            if boolean(conf.value('General/Serious')):
                self.taskTafSender.setAttribute(Qt.WA_DeleteOnClose)
                self.taskTafEditor.setAttribute(Qt.WA_DeleteOnClose)
                self.taskBrowser.setAttribute(Qt.WA_DeleteOnClose)
                self.taskTafSender.close()
                self.taskTafEditor.close()
                self.taskBrowser.close()

            self.tray.hide()
            event.accept()

    def restoreWindow(self, reason):
        if reason == QSystemTrayIcon.Trigger:
            self.showNormal()

    def singer(self):
        warnSwitch = self.warnTafAction.isChecked()
        trendSwitch = boolean(conf.value('Monitor/RemindTrend'))

        # 管理趋势声音
        utc = datetime.datetime.utcnow()
        if trendSwitch and utc.minute in (58, 59):
            self.trendSound.play()
        else:
            self.trendSound.stop()

        # 管理报文告警声音
        if warnSwitch and context.taf.isWarning():
            self.alarmSound.play()
        else:
            self.alarmSound.stop()

    def worker(self):
        self.workThread.start()

    def painter(self):
        utc = datetime.datetime.utcnow()
        nextTime = utc.replace(microsecond=0, second=0,
                               minute=0) + datetime.timedelta(hours=1,
                                                              minutes=10)
        delta = nextTime - utc
        QTimer.singleShot(delta.total_seconds() * 1000, self.painter)
        if conf.value('Monitor/FirApiURL'):
            self.firInfoThread.start()

    def dialer(self, test=False):
        callSwitch = conf.value('Monitor/SelectedMobile')

        if callSwitch and context.taf.isWarning() or test:
            self.callThread.start()

    def remindTaf(self, tt):
        remindSwitch = boolean(conf.value('Monitor/RemindTAF'))
        if not remindSwitch:
            return None

        state = context.taf.state()
        clock = state[tt]['clock']
        period = state[tt]['period']
        sent = state[tt]['sent']
        warnning = state[tt]['warnning']

        if clock and not warnning and not sent:
            current = tt + period[2:]
            text = QCoreApplication.translate(
                'MainWindow', 'Time to post {}').format(current)
            self.ringSound.play()
            self.remindBox.setText(text)
            ret = self.remindBox.exec_()
            if not ret:
                QTimer.singleShot(1000 * 60 * 5, lambda: self.remindTaf(tt))

            self.ringSound.stop()

    def remindSigmet(self):
        remindSwitch = boolean(conf.value('Monitor/RemindSIGMET'))
        if not remindSwitch:
            return None

        text = QCoreApplication.translate('MainWindow',
                                          'Time to post {}').format('SIGMET')
        self.sigmetSound.play()
        self.remindBox.setText(text)
        ret = self.remindBox.exec_()
        if not ret:
            QTimer.singleShot(1000 * 60 * 5, self.remindSigmet)

        self.sigmetSound.stop()

    def updateMessage(self):
        listen = Listen(parent=self)
        [listen(i) for i in ('FC', 'FT', 'SA', 'SP')]

        self.updateGui()

    def updateGui(self):
        self.updateTable()
        self.updateRecent()
        self.updateContractMenu()

        logger.debug('Update GUI')

    def updateContractMenu(self):
        mobile = conf.value('Monitor/SelectedMobile')
        person = db.query(User).filter_by(mobile=mobile).first()
        if person:
            action = getattr(self, 'contract' + str(person.id), None)
            if action:
                action.setChecked(True)
        else:
            self.contractNo.setChecked(True)

    def updateRecent(self):
        self.currentTaf.updateGui()
        self.recentFt.updateGui()
        self.recentFc.updateGui()
        self.recentSigmet.updateGui()
        self.recentTrend.updateGui()

    def updateTable(self):
        self.tafTable.updateGui()
        self.metarTable.updateGui()
        self.sigmetTable.updateGui()

    def about(self):
        title = QCoreApplication.translate(
            'MainWindow', 'Terminal Aerodrome Forecast Encoding Software')
        head = '<b>{}</b> v <a href="https://github.com/up1and/tafor">{}</a>'.format(
            title, __version__)
        description = QCoreApplication.translate(
            'MainWindow',
            '''The software is used to encode and post terminal aerodrome forecast, trend forecast, 
                    significant meteorological information, monitor the message, return the alarm by sound or telephone'''
        )
        tail = QCoreApplication.translate(
            'MainWindow',
            '''The project is under GPL-2.0 License, Pull Request and Issue are welcome'''
        )
        copyright = '<br/>© UP1AND 2018'
        text = '<p>'.join([head, description, tail, copyright])

        self.showNormal()
        QMessageBox.about(self, title, text)

    def openDocs(self):
        devDocs = os.path.join(BASEDIR, '../docs/_build/html/index.html')
        releaseDocs = os.path.join(BASEDIR, 'docs/_build/html/index.html')

        if os.path.exists(devDocs):
            url = QUrl.fromLocalFile(devDocs)
        elif os.path.exists(releaseDocs):
            url = QUrl.fromLocalFile(releaseDocs)
        else:
            url = QUrl('https://tafor.readthedocs.io')

        QDesktopServices.openUrl(url)

    def reportIssue(self):
        QDesktopServices.openUrl(
            QUrl('https://github.com/up1and/tafor/issues'))

    def checkUpgrade(self, data):
        hasNewVersion = checkVersion(data.get('tag_name', __version__),
                                     __version__)
        if not hasNewVersion:
            return False

        download = 'https://github.com/up1and/tafor/releases'
        title = QCoreApplication.translate('MainWindow', 'Check for Updates')
        text = QCoreApplication.translate(
            'MainWindow',
            'New version found {}, do you want to download now?').format(
                data.get('tag_name'))
        ret = QMessageBox.question(self, title, text)
        if ret == QMessageBox.Yes:
            QDesktopServices.openUrl(QUrl(download))
Ejemplo n.º 18
0
class SelectionOptionToolBar(QToolBar):
    class SelectionOption(Enum):
        Replace = 1
        Add = 2
        Subtract = 3
        Intersect = 4

    Replace = SelectionOption.Replace
    Add = SelectionOption.Add
    Subtract = SelectionOption.Subtract
    Intersect = SelectionOption.Intersect
    del SelectionOption

    selection_option_changed = pyqtSignal(QAction)

    def __init__(self, parent=None):
        super(SelectionOptionToolBar, self).__init__(parent)

        self.setObjectName("ToolsTooBar")
        self.setWindowTitle("Tools Toolbar")
        self.setToolButtonStyle(Qt.ToolButtonFollowStyle)

        self._replace = QAction(self)
        self._add = QAction(self)
        self._subtract = QAction(self)
        self._intersect = QAction(self)
        self._action_group = QActionGroup(self)

        self._replace.setIcon(QIcon(":/selection-replace.png"))
        self._replace.setCheckable(True)
        self._replace.setChecked(True)
        self._replace.setToolTip("新建选区")
        self._replace.setData(SelectionOptionToolBar.Replace)

        self._add.setIcon(QIcon(":/selection-add.png"))
        self._add.setCheckable(True)
        self._add.setToolTip("添加到选区")
        self._add.setData(SelectionOptionToolBar.Add)

        self._subtract.setIcon(QIcon(":/selection-subtract.png"))
        self._subtract.setCheckable(True)
        self._subtract.setToolTip("从选区中减去")
        self._subtract.setData(SelectionOptionToolBar.Subtract)

        self._intersect.setIcon(QIcon(":/selection-intersect.png"))
        self._intersect.setCheckable(True)
        self._intersect.setToolTip("与选区相交")
        self._intersect.setData(SelectionOptionToolBar.Intersect)

        self._action_group.addAction(self._replace)
        self._action_group.addAction(self._add)
        self._action_group.addAction(self._subtract)
        self._action_group.addAction(self._intersect)

        self.addAction(self._replace)
        self.addAction(self._add)
        self.addAction(self._subtract)
        self.addAction(self._intersect)

        self.__register()
        self._action_group.triggered.connect(self.selection_option_changed)

    def current_selection_option(self):
        return self._action_group.checkedAction().data()

    def __register(self):
        ActionManager.register_action(self._replace, Id("ReplaceSelection"))
        ActionManager.register_action(self._add, Id("AddSelection"))
        ActionManager.register_action(self._subtract, Id("SubtractSelection"))
        ActionManager.register_action(self._intersect,
                                      Id("IntersectSelection"))
Ejemplo n.º 19
0
    def init_menu(self):
        layout_load_act = QAction(tr("MenuFile", "Load saved layout..."), self)
        layout_load_act.setShortcut("Ctrl+O")
        layout_load_act.triggered.connect(self.on_layout_load)

        layout_save_act = QAction(tr("MenuFile", "Save current layout..."),
                                  self)
        layout_save_act.setShortcut("Ctrl+S")
        layout_save_act.triggered.connect(self.on_layout_save)

        sideload_json_act = QAction(tr("MenuFile", "Sideload VIA JSON..."),
                                    self)
        sideload_json_act.triggered.connect(self.on_sideload_json)

        download_via_stack_act = QAction(
            tr("MenuFile", "Download VIA definitions"), self)
        download_via_stack_act.triggered.connect(self.load_via_stack_json)

        load_dummy_act = QAction(tr("MenuFile", "Load dummy JSON..."), self)
        load_dummy_act.triggered.connect(self.on_load_dummy)

        exit_act = QAction(tr("MenuFile", "Exit"), self)
        exit_act.setShortcut("Ctrl+Q")
        exit_act.triggered.connect(qApp.exit)

        file_menu = self.menuBar().addMenu(tr("Menu", "File"))
        file_menu.addAction(layout_load_act)
        file_menu.addAction(layout_save_act)
        file_menu.addSeparator()
        file_menu.addAction(sideload_json_act)
        file_menu.addAction(download_via_stack_act)
        file_menu.addAction(load_dummy_act)
        file_menu.addSeparator()
        file_menu.addAction(exit_act)

        keyboard_unlock_act = QAction(tr("MenuSecurity", "Unlock"), self)
        keyboard_unlock_act.triggered.connect(self.unlock_keyboard)

        keyboard_lock_act = QAction(tr("MenuSecurity", "Lock"), self)
        keyboard_lock_act.triggered.connect(self.lock_keyboard)

        keyboard_reset_act = QAction(
            tr("MenuSecurity", "Reboot to bootloader"), self)
        keyboard_reset_act.triggered.connect(self.reboot_to_bootloader)

        keyboard_layout_menu = self.menuBar().addMenu(
            tr("Menu", "Keyboard layout"))
        keymap_group = QActionGroup(self)
        selected_keymap = self.settings.value("keymap")
        for idx, keymap in enumerate(KEYMAPS):
            act = QAction(tr("KeyboardLayout", keymap[0]), self)
            act.triggered.connect(
                lambda checked, x=idx: self.change_keyboard_layout(x))
            act.setCheckable(True)
            if selected_keymap == keymap[0]:
                self.change_keyboard_layout(idx)
                act.setChecked(True)
            keymap_group.addAction(act)
            keyboard_layout_menu.addAction(act)
        # check "QWERTY" if nothing else is selected
        if keymap_group.checkedAction() is None:
            keymap_group.actions()[0].setChecked(True)

        self.security_menu = self.menuBar().addMenu(tr("Menu", "Security"))
        self.security_menu.addAction(keyboard_unlock_act)
        self.security_menu.addAction(keyboard_lock_act)
        self.security_menu.addSeparator()
        self.security_menu.addAction(keyboard_reset_act)

        self.theme_menu = self.menuBar().addMenu(tr("Menu", "Theme"))
        theme_group = QActionGroup(self)
        selected_theme = self.get_theme()
        for name, _ in [("System", None)] + themes.themes:
            act = QAction(tr("MenuTheme", name), self)
            act.triggered.connect(lambda x, name=name: self.set_theme(name))
            act.setCheckable(True)
            act.setChecked(selected_theme == name)
            theme_group.addAction(act)
            self.theme_menu.addAction(act)
        # check "System" if nothing else is selected
        if theme_group.checkedAction() is None:
            theme_group.actions()[0].setChecked(True)
Ejemplo n.º 20
0
class Gui(QMainWindow, Ui_MainWindow):

    NOTEON = 0x9
    NOTEOFF = 0x8
    MIDICTRL = 11

    GREEN = ("#cell_frame { border: 0px; border-radius: 10px; "
             "background-color: rgb(125,242,0);}")
    BLUE = ("#cell_frame { border: 0px; border-radius: 10px; "
            "background-color: rgb(0, 130, 240);}")
    RED = ("#cell_frame { border: 0px; border-radius: 10px; "
           "background-color: rgb(255, 21, 65);}")
    AMBER = ("#cell_frame { border: 0px; border-radius: 10px; "
             "background-color: rgb(255, 102, 0);}")
    PURPLE = ("#cell_frame { border: 0px; border-radius: 10px; "
              "background-color: rgb(130, 0, 240);}")
    DEFAULT = ("#cell_frame { border: 0px; border-radius: 10px; "
               "background-color: rgb(217, 217, 217);}")

    RECORD_BLINK = ("QPushButton {background-color: rgb(255, 255, 255);}"
                    "QPushButton:pressed {background-color: "
                    "rgb(98, 98, 98);}")

    RECORD_DEFAULT = ("QPushButton {background-color: rgb(0, 0, 0);}"
                      "QPushButton:pressed {background-color: "
                      "rgb(98, 98, 98);}")

    STATE_COLORS = {Clip.STOP: RED,
                    Clip.STARTING: GREEN,
                    Clip.START: GREEN,
                    Clip.STOPPING: RED,
                    Clip.PREPARE_RECORD: AMBER,
                    Clip.RECORDING: AMBER}
    STATE_BLINK = {Clip.STOP: False,
                   Clip.STARTING: True,
                   Clip.START: False,
                   Clip.STOPPING: True,
                   Clip.PREPARE_RECORD: True,
                   Clip.RECORDING: False}

    BLINK_DURATION = 200
    PROGRESS_PERIOD = 300

    updateUi = pyqtSignal()
    readQueueIn = pyqtSignal()

    def __init__(self, song, jack_client):
        QObject.__init__(self)
        super(Gui, self).__init__()
        self._jack_client = jack_client
        self.setupUi(self)
        self.clip_volume.knobRadius = 3
        self.is_learn_device_mode = False
        self.queue_out, self.queue_in = Queue(), Queue()
        self.updateUi.connect(self.update)
        self.readQueueIn.connect(self.readQueue)
        self.current_vol_block = 0

        # Load devices
        self.deviceGroup = QActionGroup(self.menuDevice)
        self.devices = []
        settings = QSettings('superboucle', 'devices')
        if settings.contains('devices') and settings.value('devices'):
            for raw_device in settings.value('devices'):
                self.devices.append(Device(pickle.loads(raw_device)))
        else:
            self.devices.append(Device({'name': 'No Device', }))
        self.updateDevices()
        self.deviceGroup.triggered.connect(self.onDeviceSelect)

        # Load song
        self.initUI(song)

        self.actionNew.triggered.connect(self.onActionNew)
        self.actionOpen.triggered.connect(self.onActionOpen)
        self.actionSave.triggered.connect(self.onActionSave)
        self.actionSave_As.triggered.connect(self.onActionSaveAs)
        self.actionAdd_Device.triggered.connect(self.onAddDevice)
        self.actionManage_Devices.triggered.connect(self.onManageDevice)
        self.actionFullScreen.triggered.connect(self.onActionFullScreen)
        self.master_volume.valueChanged.connect(self.onMasterVolumeChange)
        self.rewindButton.clicked.connect(self.onRewindClicked)
        self.playButton.clicked.connect(self._jack_client.transport_start)
        self.pauseButton.clicked.connect(self._jack_client.transport_stop)
        self.gotoButton.clicked.connect(self.onGotoClicked)
        self.recordButton.clicked.connect(self.onRecord)
        self.clip_name.textChanged.connect(self.onClipNameChange)
        self.clip_volume.valueChanged.connect(self.onClipVolumeChange)
        self.beat_diviser.valueChanged.connect(self.onBeatDiviserChange)
        self.frame_offset.valueChanged.connect(self.onFrameOffsetChange)
        self.beat_offset.valueChanged.connect(self.onBeatOffsetChange)
        self.revertButton.clicked.connect(self.onRevertClip)
        self.normalizeButton.clicked.connect(self.onNormalizeClip)
        self.exportButton.clicked.connect(self.onExportClip)
        self.deleteButton.clicked.connect(self.onDeleteClipClicked)

        self.blktimer = QTimer()
        self.blktimer.state = False
        self.blktimer.timeout.connect(self.toogleBlinkButton)
        self.blktimer.start(self.BLINK_DURATION)

        self.disptimer = QTimer()
        self.disptimer.start(self.PROGRESS_PERIOD)
        self.disptimer.timeout.connect(self.updateProgress)

        self._jack_client.set_timebase_callback(self.timebase_callback)

        self.show()

    def initUI(self, song):

        # remove old buttons
        self.btn_matrix = [[None for y in range(song.height)]
                           for x in range(song.width)]
        self.state_matrix = [[-1 for y in range(song.height)]
                             for x in range(song.width)]
        for i in reversed(range(self.gridLayout.count())):
            self.gridLayout.itemAt(i).widget().close()
            self.gridLayout.itemAt(i).widget().setParent(None)

        self.song = song
        self.frame_clip.setEnabled(False)
        self.master_volume.setValue(song.volume*256)
        self.bpm.setValue(song.bpm)
        self.beat_per_bar.setValue(song.beat_per_bar)
        for x in range(song.width):
            for y in range(song.height):
                clip = song.clips_matrix[x][y]
                cell = Cell(self, clip, x, y)
                self.btn_matrix[x][y] = cell
                self.gridLayout.addWidget(cell, y, x)

        # send init command
        for init_cmd in self.device.init_command:
            self.queue_out.put(init_cmd)

        self.update()

    def closeEvent(self, event):
        settings = QSettings('superboucle', 'devices')
        settings.setValue('devices',
                          [pickle.dumps(x.mapping) for x in self.devices])

    def onStartStopClicked(self):
        clip = self.sender().parent().parent().clip
        self.startStop(clip.x, clip.y)

    def startStop(self, x, y):
        clip = self.btn_matrix[x][y].clip
        if clip is None:
            return
        if self.song.is_record:
            self.song.is_record = False
            self.updateRecordBtn()
            # calculate buffer size
            state, position = self._jack_client.transport_query()
            bps = position['beats_per_minute'] / 60
            fps = position['frame_rate']
            size = (1 / bps) * clip.beat_diviser * fps
            self.song.init_record_buffer(clip, 2, size, fps)
            # set frame offset based on jack block size
            clip.frame_offset = self._jack_client.blocksize
            clip.state = Clip.PREPARE_RECORD
            self.recordButton.setStyleSheet(self.RECORD_DEFAULT)
        else:
            self.song.toogle(clip.x, clip.y)
        self.update()

    def onEdit(self):
        self.last_clip = self.sender().parent().parent().clip
        if self.last_clip:
            self.frame_clip.setEnabled(True)
            self.clip_name.setText(self.last_clip.name)
            self.frame_offset.setValue(self.last_clip.frame_offset)
            self.beat_offset.setValue(self.last_clip.beat_offset)
            self.beat_diviser.setValue(self.last_clip.beat_diviser)
            self.clip_volume.setValue(self.last_clip.volume*256)
            state, position = self._jack_client.transport_query()
            fps = position['frame_rate']
            bps = self.bpm.value() / 60
            if self.bpm.value() and fps:
                size_in_beat = (bps/fps)*self.song.length(self.last_clip)
            else:
                size_in_beat = "No BPM info"
            clip_description = ("Size in sample : %s\nSize in beat : %s"
                                % (self.song.length(self.last_clip),
                                   round(size_in_beat, 1)))

            self.clip_description.setText(clip_description)

    def onAddClipClicked(self):
        AddClipDialog(self, self.sender().parent().parent())

    def onRevertClip(self):
        if self.last_clip and self.last_clip.audio_file:
            audio_file = self.last_clip.audio_file
            self.song.data[audio_file] = self.song.data[audio_file][::-1]

    def onNormalizeClip(self):
        if self.last_clip and self.last_clip.audio_file:
            audio_file = self.last_clip.audio_file
            absolute_val = np.absolute(self.song.data[audio_file])
            current_level = np.ndarray.max(absolute_val)
            self.song.data[audio_file][:] *= (1 / current_level)

    def onExportClip(self):
        if self.last_clip and self.last_clip.audio_file:
            audio_file = self.last_clip.audio_file
            file_name, a = (
                QFileDialog.getSaveFileName(self,
                                            'Export Clip : %s'
                                            % self.last_clip.name,
                                            expanduser('~'),
                                            'WAVE (*.wav)'))

            if file_name:
                file_name = verify_ext(file_name, 'wav')
                sf.write(self.song.data[audio_file], file_name,
                         self.song.samplerate[audio_file],
                         subtype=sf.default_subtype('WAV'),
                         format='WAV')

    def onDeleteClipClicked(self):
        if self.last_clip:
            response = QMessageBox.question(self,
                                            "Delete Clip ?",
                                            ("Are you sure "
                                             "to delete the clip ?"))
            if response == QMessageBox.Yes:
                self.frame_clip.setEnabled(False)
                self.song.removeClip(self.last_clip)
                self.initUI(self.song)

    def onMasterVolumeChange(self):
        self.song.volume = (self.master_volume.value() / 256)

    def onStartClicked(self):
        pass
        self._jack_client.transport_start

    def onGotoClicked(self):
        state, position = self._jack_client.transport_query()
        new_position = (position['beats_per_bar']
                        * (self.gotoTarget.value() - 1)
                        * position['frame_rate']
                        * (60 / position['beats_per_minute']))
        self._jack_client.transport_locate(int(round(new_position, 0)))

    def onRecord(self):
        self.song.is_record = not self.song.is_record
        self.updateRecordBtn()

    def updateRecordBtn(self):
        if not self.song.is_record:
            self.recordButton.setStyleSheet(self.RECORD_DEFAULT)
        if self.device.record_btn:
            (msg_type, channel, pitch, velocity) = self.device.record_btn
            if self.song.is_record:
                color = self.device.blink_amber_vel
            else:
                color = self.device.black_vel
            self.queue_out.put(((msg_type << 4) + channel, pitch, color))

    def onRewindClicked(self):
        self._jack_client.transport_locate(0)

    def onClipNameChange(self):
        self.last_clip.name = self.clip_name.text()
        tframe = self.btn_matrix[self.last_clip.x][self.last_clip.y]
        tframe.clip_name.setText(self.last_clip.name)

    def onClipVolumeChange(self):
        self.last_clip.volume = (self.clip_volume.value() / 256)

    def onBeatDiviserChange(self):
        self.last_clip.beat_diviser = self.beat_diviser.value()

    def onFrameOffsetChange(self):
        self.last_clip.frame_offset = self.frame_offset.value()

    def onBeatOffsetChange(self):
        self.last_clip.beat_offset = self.beat_offset.value()

    def onActionNew(self):
        NewSongDialog(self)

    def onActionOpen(self):
        file_name, a = (
            QFileDialog.getOpenFileName(self,
                                        'Open file',
                                        expanduser('~'),
                                        'Super Boucle Song (*.sbs)'))
        if file_name:
            self.setEnabled(False)
            message = QMessageBox(self)
            message.setWindowTitle("Loading ....")
            message.setText("Reading Files, please wait ...")
            message.show()
            self.initUI(load_song_from_file(file_name))
            message.close()
            self.setEnabled(True)

    def onActionSave(self):
        if self.song.file_name:
            self.song.save()
        else:
            self.onActionSaveAs()

    def onActionSaveAs(self):
        file_name, a = (
            QFileDialog.getSaveFileName(self,
                                        'Save As',
                                        expanduser('~'),
                                        'Super Boucle Song (*.sbs)'))

        if file_name:
            file_name = verify_ext(file_name, 'sbs')
            self.song.file_name = file_name
            self.song.save()
            print("File saved to : {}".format(self.song.file_name))

    def onAddDevice(self):
        self.learn_device = LearnDialog(self, self.addDevice)
        self.is_learn_device_mode = True

    def onManageDevice(self):
        ManageDialog(self)

    def onActionFullScreen(self):
        if self.isFullScreen():
            self.showNormal()
        else:
            self.showFullScreen()
        self.show()

    def update(self):
        for x in range(len(self.song.clips_matrix)):
            line = self.song.clips_matrix[x]
            for y in range(len(line)):
                clp = line[y]
                if clp is None:
                    state = None
                else:
                    state = clp.state
                if state != self.state_matrix[x][y]:
                    if clp:
                        self.setCellColor(x,
                                          y,
                                          self.STATE_COLORS[state],
                                          self.STATE_BLINK[state])
                    try:
                        self.queue_out.put(self.device.generateNote(x,
                                                                    y,
                                                                    state))
                    except IndexError:
                        # print("No cell associated to %s x %s"
                        # % (clp.x, clp.y))
                        pass
                self.state_matrix[x][y] = state

    def redraw(self):
        self.state_matrix = [[-1 for x in range(self.song.height)]
                             for x in range(self.song.width)]
        self.update()

    def readQueue(self):
        try:
            while True:
                note = self.queue_in.get(block=False)
                if len(note) == 3:
                    status, pitch, vel = struct.unpack('3B', note)
                    channel = status & 0xF
                    msg_type = status >> 4
                    self.processNote(msg_type, channel, pitch, vel)
                # else:
                # print("Invalid message length")
        except Empty:
            pass

    def processNote(self, msg_type, channel, pitch, vel):

        btn_id = (msg_type,
                  channel,
                  pitch,
                  vel)
        btn_id_vel = (msg_type, channel, pitch, -1)
        ctrl_key = (msg_type, channel, pitch)

        # master volume
        if ctrl_key == self.device.master_volume_ctrl:
            self.song.master_volume = vel / 127
            (self.master_volume
             .setValue(self.song.master_volume * 256))
        elif self.device.play_btn in [btn_id, btn_id_vel]:
            self._jack_client.transport_start()
        elif self.device.pause_btn in [btn_id, btn_id_vel]:
            self._jack_client.transport_stop()
        elif self.device.rewind_btn in [btn_id, btn_id_vel]:
            self.onRewindClicked()
        elif self.device.goto_btn in [btn_id, btn_id_vel]:
            self.onGotoClicked()
        elif self.device.record_btn in [btn_id, btn_id_vel]:
            self.onRecord()
        elif ctrl_key in self.device.ctrls:
            try:
                ctrl_index = self.device.ctrls.index(ctrl_key)
                clip = (self.song.clips_matrix
                        [ctrl_index]
                        [self.current_vol_block])
                if clip:
                    clip.volume = vel / 127
                    if self.last_clip == clip:
                        self.clip_volume.setValue(self.last_clip.volume * 256)
            except KeyError:
                pass
        elif (btn_id in self.device.block_buttons
              or btn_id_vel in self.device.block_buttons):
            try:
                self.current_vol_block = (
                    self.device.block_buttons.index(btn_id))
            except ValueError:
                self.current_vol_block = (
                    self.device.block_buttons.index(btn_id_vel))
            for i in range(len(self.device.block_buttons)):
                (a, b_channel, b_pitch, b) = self.device.block_buttons[i]
                if i == self.current_vol_block:
                    color = self.device.red_vel
                else:
                    color = self.device.black_vel
                self.queue_out.put(((self.NOTEON << 4) + b_channel,
                                    b_pitch,
                                    color))
        else:
            x, y = -1, -1
            try:
                x, y = self.device.getXY(btn_id)
            except IndexError:
                pass
            except KeyError:
                try:
                    x, y = self.device.getXY(btn_id_vel)
                except KeyError:
                    pass

            if (x >= 0 and y >= 0):
                self.startStop(x, y)

    def setCellColor(self, x, y, color, blink=False):
        self.btn_matrix[x][y].setStyleSheet(color)
        self.btn_matrix[x][y].blink = blink
        self.btn_matrix[x][y].color = color

    def toogleBlinkButton(self):
        for line in self.btn_matrix:
            for btn in line:
                if btn.blink:
                    if self.blktimer.state:
                        btn.setStyleSheet(btn.color)
                    else:
                        btn.setStyleSheet(self.DEFAULT)
        if self.song.is_record:
            if self.blktimer.state:
                self.recordButton.setStyleSheet(self.RECORD_BLINK)
            else:
                self.recordButton.setStyleSheet(self.RECORD_DEFAULT)

        self.blktimer.state = not self.blktimer.state

    def updateProgress(self):
        state, pos = self._jack_client.transport_query()
        if 'bar' in pos:
            bbt = "%d|%d|%03d" % (pos['bar'], pos['beat'], pos['tick'])
        else:
            bbt = "-|-|-"
        seconds = int(pos['frame'] / pos['frame_rate'])
        (minutes, second) = divmod(seconds, 60)
        (hour, minute) = divmod(minutes, 60)
        time = "%d:%02d:%02d" % (hour, minute, second)
        self.bbtLabel.setText("%s\n%s" % (bbt, time))
        for line in self.btn_matrix:
            for btn in line:
                if btn.clip and btn.clip.audio_file:
                    value = ((btn.clip.last_offset
                              / self.song.length(btn.clip))
                             * 97)
                    btn.clip_position.setValue(value)
                    btn.clip_position.repaint()

    def updateDevices(self):
        for action in self.deviceGroup.actions():
            self.deviceGroup.removeAction(action)
            self.menuDevice.removeAction(action)
        for device in self.devices:
            action = QAction(device.name, self.menuDevice)
            action.setCheckable(True)
            action.setData(device)
            self.menuDevice.addAction(action)
            self.deviceGroup.addAction(action)
        action.setChecked(True)
        self.device = device

    def addDevice(self, device):
        self.devices.append(device)
        self.updateDevices()
        self.is_learn_device_mode = False

    def onDeviceSelect(self):
        self.device = self.deviceGroup.checkedAction().data()
        if self.device:
            if self.device.init_command:
                for note in self.device.init_command:
                    self.queue_out.put(note)
            self.redraw()

    def timebase_callback(self, state, nframes, pos, new_pos):
        pos.valid = 0x10
        pos.bar_start_tick = BAR_START_TICK
        pos.beats_per_bar = self.beat_per_bar.value()
        pos.beat_type = BEAT_TYPE
        pos.ticks_per_beat = TICKS_PER_BEAT
        pos.beats_per_minute = self.bpm.value()
        ticks = frame2bbt(pos.frame,
                          pos.ticks_per_beat,
                          pos.beats_per_minute,
                          pos.frame_rate)
        (beats, pos.tick) = divmod(int(round(ticks, 0)),
                                   int(round(pos.ticks_per_beat, 0)))
        (bar, beat) = divmod(beats, int(round(pos.beats_per_bar, 0)))
        (pos.bar, pos.beat) = (bar + 1, beat + 1)
        return None