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)
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)
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()
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>'
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)
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("魔法棒"))
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
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)
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)
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
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()
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)
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)
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)
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")
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))
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"))
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)
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