def addShowActions(self): """Adds a submenu giving access to the (other) opened viewer documents""" mds = self._actionCollection.viewer_document_select docs = mds.viewdocs() document_actions = {} multi_docs = len(docs) > 1 if self._panel.widget().currentViewdoc(): current_doc_filename = self._panel.widget().currentViewdoc().filename() m = self._menu sm = QMenu(m) sm.setTitle(_("Show...")) sm.setEnabled(multi_docs) ag = QActionGroup(m) ag.triggered.connect(self._panel.slotShowViewdoc) for d in docs: action = QAction(sm) action.setText(d.name()) action._document_filename = d.filename() # TODO: Tooltips aren't shown by Qt (it seems) action.setToolTip(d.filename()) action.setCheckable(True) action.setChecked(d.filename() == current_doc_filename) ag.addAction(action) sm.addAction(action) m.addSeparator() m.addMenu(sm)
def setup_menu(): u""" Add a submenu to a view menu. Add a submenu that lists the available extra classes to the view menu, creating that menu when neccessary """ if extra_classes_list: try: mw.addon_view_menu except AttributeError: mw.addon_view_menu = QMenu(_(u"&View"), mw) mw.form.menubar.insertMenu( mw.form.menuTools.menuAction(), mw.addon_view_menu) mw.extra_class_submenu = QMenu(u"Mode (e&xtra class)", mw) mw.addon_view_menu.addMenu(mw.extra_class_submenu) action_group = QActionGroup(mw, exclusive=True) no_class_action = action_group.addAction( QAction('(none/standard)', mw, checkable=True)) no_class_action.setChecked(True) mw.extra_class_submenu.addAction(no_class_action) mw.connect(no_class_action, SIGNAL("triggered()"), lambda: set_extra_class(None)) for ecd in extra_classes_list: nn_class_action = action_group.addAction( QAction(ecd['display'], mw, checkable=True)) mw.extra_class_submenu.addAction(nn_class_action) mw.connect(nn_class_action, SIGNAL("triggered()"), lambda ec=ecd['class']: set_extra_class(ec))
def generateViewMenu(self): values = [ (self.tr("Nothing"), "Nothing"), (self.tr("POV"), "POV"), (self.tr("Label"), "Label"), (self.tr("Progress"), "Progress"), (self.tr("Compile"), "Compile"), ] menus = [ (self.tr("Tree"), "Tree"), (self.tr("Index cards"), "Cork"), (self.tr("Outline"), "Outline") ] submenus = { "Tree": [ (self.tr("Icon color"), "Icon"), (self.tr("Text color"), "Text"), (self.tr("Background color"), "Background"), ], "Cork": [ (self.tr("Icon"), "Icon"), (self.tr("Text"), "Text"), (self.tr("Background"), "Background"), (self.tr("Border"), "Border"), (self.tr("Corner"), "Corner"), ], "Outline": [ (self.tr("Icon color"), "Icon"), (self.tr("Text color"), "Text"), (self.tr("Background color"), "Background"), ], } self.menuView.clear() self.menuView.addMenu(self.menuMode) self.menuView.addSeparator() # print("Generating menus with", settings.viewSettings) for mnu, mnud in menus: m = QMenu(mnu, self.menuView) for s, sd in submenus[mnud]: m2 = QMenu(s, m) agp = QActionGroup(m2) for v, vd in values: a = QAction(v, m) a.setCheckable(True) a.setData("{},{},{}".format(mnud, sd, vd)) if settings.viewSettings[mnud][sd] == vd: a.setChecked(True) a.triggered.connect(self.setViewSettingsAction, AUC) agp.addAction(a) m2.addAction(a) m.addMenu(m2) self.menuView.addMenu(m)
def _create_actions(self): group = QActionGroup(self) self.grid_action = QAction( '&Grid', self, shortcut='ctrl+G', triggered=self.show_grid, checkable=True, icon=load_icon(':/icons/show_grid.png') ) self.grid_action.setChecked(True) group.addAction(self.grid_action) self.expanded_action = QAction( '&Expanded', self, shortcut='ctrl+E', triggered=self.show_expanded, checkable=True, icon=load_icon(':/icons/show_expanded.png') ) group.addAction(self.expanded_action)
def on_main_window_start(main_window): main_window.theme_menu = main_window.menuBar().addMenu( main_window.tr('Themes')) themes_directory = QFileInfo('themes') if themes_directory.exists(): active_theme = ThemeManager.get_active_theme() path = themes_directory.absoluteFilePath() group_action = QActionGroup(main_window) group_action.setExclusive(True) for theme in os.listdir(path): action = QAction(theme, main_window) action.setData(theme) action.setCheckable(True) if theme == active_theme: action.setChecked(True) action.changed.connect(ThemeManager.wrapper(main_window)) group_action.addAction(action) group_action.addAction(action) main_window.theme_menu.addAction(action)
def _create_actions(self): group = QActionGroup(self) self.grid_action = QAction( "&Grid", self, shortcut="ctrl+G", triggered=self.show_grid, checkable=True, icon=load_icon(":/icons/show_grid.png"), ) self.grid_action.setChecked(True) group.addAction(self.grid_action) self.expanded_action = QAction( "&Expanded", self, shortcut="ctrl+E", triggered=self.show_expanded, checkable=True, icon=load_icon(":/icons/show_expanded.png"), ) group.addAction(self.expanded_action)
def __init__(self, win): super(PathToolManager, self).__init__() self.window = win self._active_tool = None self._active_part = None self.select_tool = SelectTool(self) self.pencil_tool = PencilTool(self) self.break_tool = BreakTool(self) self.erase_tool = EraseTool(self) self.insertion_tool = InsertionTool(self) self.skip_tool = SkipTool(self) self.paint_tool = PaintTool(self) # (self, win.path_graphics_view.toolbar) self.add_seq_tool = AddSeqTool(self) self.mods_tool = ModsTool(self) def installTool(tool_name, window): l_tool_name = tool_name.lower() tool_widget = getattr(window, 'action_path_' + l_tool_name) tool = getattr(self, l_tool_name + '_tool') tool.action_name = 'action_path_' + tool_name def clickHandler(self): tool_widget.setChecked(True) self.setActiveTool(tool) if hasattr(tool, 'widgetClicked'): tool.widgetClicked() # end def select_tool_method_name = 'choose' + tool_name + 'Tool' setattr(self.__class__, select_tool_method_name, clickHandler) handler = getattr(self, select_tool_method_name) tool_widget.triggered.connect(handler) return tool_widget # end def tools = ('Select', 'Pencil', 'Break', 'Erase', 'Insertion', 'Skip', 'Paint', 'Add_Seq', 'Mods') ag = QActionGroup(win) # Call installTool on every tool list(map((lambda tool_name: ag.addAction(installTool(tool_name, win))), tools)) ag.setExclusive(True) # Select the preferred Startup tool startup_tool_name = app().prefs.getStartupToolName() getattr(self, 'choose' + startup_tool_name + 'Tool')()
class SearchOptionsButton(QPushButton): def __init__(self, **kwargs): super(SearchOptionsButton, self).__init__(**kwargs) self.setText(self.tr('Options')) menu = QMenu() self.actionCi = menu.addAction(self.tr('Case insensitive')) menu.addSeparator() self.actionFormat = QActionGroup(self) self.actionPlain = menu.addAction(self.tr('Plain text')) self.actionPlain.setEnabled(False) self.actionRe = menu.addAction(self.tr('Regular expression')) self.actionGlob = menu.addAction(self.tr('Glob pattern')) self.actionGlob.setEnabled(False) self.actionFormat.addAction(self.actionPlain) self.actionFormat.addAction(self.actionRe) self.actionFormat.addAction(self.actionGlob) self.actionRoot = menu.addAction(self.tr('Search in best root dir')) for act in [self.actionCi, self.actionRe, self.actionPlain, self.actionGlob, self.actionRoot]: act.setCheckable(True) self.actionRe.setChecked(True) self.setMenu(menu) def shouldFindRoot(self): return self.actionRoot.isChecked() def caseSensitive(self): return not self.actionCi.isChecked() def reFormat(self): if self.actionPlain.isChecked(): return QRegExp.FixedString elif self.actionRe.isChecked(): return QRegExp.RegExp elif self.actionGlob.isChecked(): return QRegExp.WildcardUnix
class revisions(QWidget, Ui_revisions): def __init__(self, parent=None): QWidget.__init__(self, parent) self.setupUi(self) self.splitter.setStretchFactor(0, 5) self.splitter.setStretchFactor(1, 70) self.listDelegate = listCompleterDelegate(self) self.list.setItemDelegate(self.listDelegate) self.list.itemClicked.connect(self.showDiff) self.list.setContextMenuPolicy(Qt.CustomContextMenu) self.list.customContextMenuRequested.connect(self.popupMenu) self.btnDelete.setEnabled(False) self.btnDelete.clicked.connect(self.delete) self.btnRestore.clicked.connect(self.restore) self.btnRestore.setEnabled(False) # self.list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.updateTimer = QTimer() self.updateTimer.setSingleShot(True) self.updateTimer.setInterval(500) self.updateTimer.timeout.connect(self.update) self.updateTimer.stop() self.menu = QMenu(self) self.actGroup = QActionGroup(self) self.actShowDiff = QAction(self.tr("Show modifications"), self.menu) self.actShowDiff.setCheckable(True) self.actShowDiff.setChecked(True) self.actShowDiff.triggered.connect(self.showDiff) self.menu.addAction(self.actShowDiff) self.actGroup.addAction(self.actShowDiff) self.actShowVersion = QAction(self.tr("Show ancient version"), self.menu) self.actShowVersion.setCheckable(True) self.actShowVersion.setChecked(False) self.actShowVersion.triggered.connect(self.showDiff) self.menu.addAction(self.actShowVersion) self.actGroup.addAction(self.actShowVersion) self.menu.addSeparator() self.actShowSpaces = QAction(self.tr("Show spaces"), self.menu) self.actShowSpaces.setCheckable(True) self.actShowSpaces.setChecked(False) self.actShowSpaces.triggered.connect(self.showDiff) self.menu.addAction(self.actShowSpaces) self.actDiffOnly = QAction(self.tr("Show modifications only"), self.menu) self.actDiffOnly.setCheckable(True) self.actDiffOnly.setChecked(True) self.actDiffOnly.triggered.connect(self.showDiff) self.menu.addAction(self.actDiffOnly) self.btnOptions.setMenu(self.menu) self._model = None self._index = None def setModel(self, model): self._model = model self._model.dataChanged.connect(self.updateMaybe) def setCurrentModelIndex(self, index): self._index = index self.view.setText("") self.update() def updateMaybe(self, topLeft, bottomRight): if self._index and \ topLeft.column() <= Outline.revisions <= bottomRight.column() and \ topLeft.row() <= self._index.row() <= bottomRight.row(): # self.update() self.updateTimer.start() def update(self): self.list.clear() item = self._index.internalPointer() rev = item.revisions() # Sort revisions rev = sorted(rev, key=lambda x: x[0], reverse=True) for r in rev: timestamp = datetime.datetime.fromtimestamp(r[0]).strftime('%Y-%m-%d %H:%M:%S') readable = self.readableDelta(r[0]) i = QListWidgetItem(readable) i.setData(Qt.UserRole, r[0]) i.setData(Qt.UserRole + 1, timestamp) self.list.addItem(i) def readableDelta(self, timestamp): now = datetime.datetime.now() delta = now - datetime.datetime.fromtimestamp(timestamp) if delta.days > 365: return self.tr("{} years ago").format(str(int(delta.days / 365))) elif delta.days > 30: return self.tr("{} months ago").format(str(int(delta.days / 30.5))) elif delta.days > 0: return self.tr("{} days ago").format(str(delta.days)) if delta.days == 1: return self.tr("1 day ago") elif delta.seconds > 60 * 60: return self.tr("{} hours ago").format(str(int(delta.seconds / 60 / 60))) elif delta.seconds > 60: return self.tr("{} minutes ago").format(str(int(delta.seconds / 60))) else: return self.tr("{} seconds ago").format(str(delta.seconds)) def showDiff(self): # UI stuff self.actShowSpaces.setEnabled(self.actShowDiff.isChecked()) self.actDiffOnly.setEnabled(self.actShowDiff.isChecked()) # FIXME: Errors in line number i = self.list.currentItem() if not i: self.btnDelete.setEnabled(False) self.btnRestore.setEnabled(False) return self.btnDelete.setEnabled(True) self.btnRestore.setEnabled(True) ts = i.data(Qt.UserRole) item = self._index.internalPointer() textNow = item.text() textBefore = [r[1] for r in item.revisions() if r[0] == ts][0] if self.actShowVersion.isChecked(): self.view.setText(textBefore) return textNow = textNow.splitlines() textBefore = textBefore.splitlines() d = difflib.Differ() diff = list(d.compare(textBefore, textNow)) if self.actShowSpaces.isChecked(): _format = lambda x: x.replace(" ", "␣ ") else: _format = lambda x: x extra = "<br>" diff = [d for d in diff if d and not d[:2] == "? "] mydiff = "" skip = False for n, l in enumerate(diff): l = diff[n] op = l[:2] txt = l[2:] op2 = diff[n + 1][:2] if n + 1 < len(diff) else None txt2 = diff[n + 1][2:] if n + 1 < len(diff) else None if skip: skip = False continue # Same line if op == " " and not self.actDiffOnly.isChecked(): mydiff += "{}{}".format(txt, extra) elif op == "- " and op2 == "+ ": if self.actDiffOnly.isChecked(): mydiff += "<br><span style='color: blue;'>{}</span><br>".format( self.tr("Line {}:").format(str(n))) s = difflib.SequenceMatcher(None, txt, txt2, autojunk=True) newline = "" for tag, i1, i2, j1, j2 in s.get_opcodes(): if tag == "equal": newline += txt[i1:i2] elif tag == "delete": newline += "<span style='color:red; background:yellow;'>{}</span>".format(_format(txt[i1:i2])) elif tag == "insert": newline += "<span style='color:green; background:yellow;'>{}</span>".format( _format(txt2[j1:j2])) elif tag == "replace": newline += "<span style='color:red; background:yellow;'>{}</span>".format(_format(txt[i1:i2])) newline += "<span style='color:green; background:yellow;'>{}</span>".format( _format(txt2[j1:j2])) # Few ugly tweaks for html diffs newline = re.sub(r"(<span style='color.*?><span.*?>)</span>(.*)<span style='color:.*?>(</span></span>)", "\\1\\2\\3", newline) newline = re.sub( r"<p align=\"<span style='color:red; background:yellow;'>cen</span><span style='color:green; background:yellow;'>righ</span>t<span style='color:red; background:yellow;'>er</span>\" style=\" -qt-block-indent:0; -qt-user-state:0; \">(.*?)</p>", "<p align=\"right\"><span style='color:green; background:yellow;'>\\1</span></p>", newline) newline = re.sub( r"<p align=\"<span style='color:green; background:yellow;'>cente</span>r<span style='color:red; background:yellow;'>ight</span>\" style=\" -qt-block-indent:0; -qt-user-state:0; \">(.*)</p>", "<p align=\"center\"><span style='color:green; background:yellow;'>\\1</span></p>", newline) newline = re.sub(r"<p(<span.*?>)(.*?)(</span>)(.*?)>(.*?)</p>", "<p\\2\\4>\\1\\5\\3</p>", newline) mydiff += newline + extra skip = True elif op == "- ": if self.actDiffOnly.isChecked(): mydiff += "<br>{}:<br>".format(str(n)) mydiff += "<span style='color:red;'>{}</span>{}".format(txt, extra) elif op == "+ ": if self.actDiffOnly.isChecked(): mydiff += "<br>{}:<br>".format(str(n)) mydiff += "<span style='color:green;'>{}</span>{}".format(txt, extra) self.view.setText(mydiff) def restore(self): i = self.list.currentItem() if not i: return ts = i.data(Qt.UserRole) item = self._index.internalPointer() textBefore = [r[1] for r in item.revisions() if r[0] == ts][0] index = self._index.sibling(self._index.row(), Outline.text) self._index.model().setData(index, textBefore) # item.setData(Outline.text, textBefore) def delete(self): i = self.list.currentItem() if not i: return ts = i.data(Qt.UserRole) self._index.internalPointer().deleteRevision(ts) def clearAll(self): self._index.internalPointer().clearAllRevisions() def saveState(self): return [ self.actShowDiff.isChecked(), self.actShowVersion.isChecked(), self.actShowSpaces.isChecked(), self.actDiffOnly.isChecked(), ] def popupMenu(self, pos): i = self.list.itemAt(pos) m = QMenu(self) if i: m.addAction(self.tr("Restore")).triggered.connect(self.restore) m.addAction(self.tr("Delete")).triggered.connect(self.delete) m.addSeparator() if self.list.count(): m.addAction(self.tr("Clear all")).triggered.connect(self.clearAll) m.popup(self.list.mapToGlobal(pos)) def restoreState(self, state): self.actShowDiff.setChecked(state[0]) self.actShowVersion.setChecked(state[1]) self.actShowSpaces.setChecked(state[2]) self.actDiffOnly.setChecked(state[3]) self.actShowSpaces.setEnabled(self.actShowDiff.isChecked()) self.actDiffOnly.setEnabled(self.actShowDiff.isChecked())
def __init__(self): super(MainWindow, self).__init__() self.currentPath = '' self.view = SvgView() fileMenu = QMenu("&File", self) openAction = fileMenu.addAction("&Open...") openAction.setShortcut("Ctrl+O") quitAction = fileMenu.addAction("E&xit") quitAction.setShortcut("Ctrl+Q") self.menuBar().addMenu(fileMenu) viewMenu = QMenu("&View", self) self.backgroundAction = viewMenu.addAction("&Background") self.backgroundAction.setEnabled(False) self.backgroundAction.setCheckable(True) self.backgroundAction.setChecked(False) self.backgroundAction.toggled.connect(self.view.setViewBackground) self.outlineAction = viewMenu.addAction("&Outline") self.outlineAction.setEnabled(False) self.outlineAction.setCheckable(True) self.outlineAction.setChecked(True) self.outlineAction.toggled.connect(self.view.setViewOutline) self.menuBar().addMenu(viewMenu) rendererMenu = QMenu("&Renderer", self) self.nativeAction = rendererMenu.addAction("&Native") self.nativeAction.setCheckable(True) self.nativeAction.setChecked(True) if QGLFormat.hasOpenGL(): self.glAction = rendererMenu.addAction("&OpenGL") self.glAction.setCheckable(True) self.imageAction = rendererMenu.addAction("&Image") self.imageAction.setCheckable(True) if QGLFormat.hasOpenGL(): rendererMenu.addSeparator() self.highQualityAntialiasingAction = rendererMenu.addAction("&High Quality Antialiasing") self.highQualityAntialiasingAction.setEnabled(False) self.highQualityAntialiasingAction.setCheckable(True) self.highQualityAntialiasingAction.setChecked(False) self.highQualityAntialiasingAction.toggled.connect(self.view.setHighQualityAntialiasing) rendererGroup = QActionGroup(self) rendererGroup.addAction(self.nativeAction) if QGLFormat.hasOpenGL(): rendererGroup.addAction(self.glAction) rendererGroup.addAction(self.imageAction) self.menuBar().addMenu(rendererMenu) openAction.triggered.connect(self.openFile) quitAction.triggered.connect(QApplication.instance().quit) rendererGroup.triggered.connect(self.setRenderer) self.setCentralWidget(self.view) self.setWindowTitle("SVG Viewer")
def __init__(self, supported_exts, parent=None): super().__init__(parent) self._diasshowRunning = False # a dummy widget to center actions spacer1 = QWidget() spacer1.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.addWidget(spacer1) self.supportedExts = supported_exts self._fromFile = self.addAction(QIcon("icons/image-outline.svg"), "", self.chooseFile) # load images from file self._fromFile.setToolTip("Load image") self._fromFolder = self.addAction(QIcon("icons/folder-open.svg"), "", self.chooseFolder) # load images from folder self._fromFolder.setToolTip("Load from directory") # View in native size, fit width, fit height or fit image self._imageMode = QToolButton(self) self._imageMode.setIcon(QIcon("icons/eye-outline.svg")) self._imageMode.setToolTip("Image view mode") self._imageMode.setPopupMode(QToolButton.InstantPopup) self.addWidget(self._imageMode) # imageMode menu imageModeMenu = QMenu(self) imageModeActions = QActionGroup(imageModeMenu) imModeAct1 = imageModeActions.addAction("Native size") imModeAct1.setCheckable(True) imModeAct1.triggered.connect(lambda: self.imageModeChanged.emit(0)) imModeAct2 = imageModeActions.addAction("Fit in view") imModeAct2.setCheckable(True) imModeAct2.triggered.connect(lambda: self.imageModeChanged.emit(1)) imModeAct3 = imageModeActions.addAction("Fit width") imModeAct3.setCheckable(True) imModeAct3.triggered.connect(lambda: self.imageModeChanged.emit(2)) imModeAct4 = imageModeActions.addAction("Fit height") imModeAct4.setCheckable(True) imModeAct4.triggered.connect(lambda: self.imageModeChanged.emit(3)) imageModeActions.setExclusive(True) imageModeMenu.addActions(imageModeActions.actions()) self._imageMode.setMenu(imageModeMenu) self._imgDirection = self.addAction(QIcon("icons/arrow-move-outline.svg"), "", self.imageDirectionChanged.emit) # Horizontal or Vertical self._imgDirection.setToolTip("Toggle image direction") # start or stop diasshow self._playDias = self.addAction(QIcon("icons/media-play-outline.svg"), "", self.diasshowState) self._playDias.setToolTip("Start/stop diasshow") #diasshow menu self._diasMenu = QMenu(self) self._diasMenu.addAction("5 seconds", lambda: self.diasshowState(5)) self._diasMenu.addAction("10 seconds", lambda: self.diasshowState(10)) self._diasMenu.addAction("30 seconds", lambda: self.diasshowState(30)) self._diasMenu.addAction("5 minutes", lambda: self.diasshowState(60*5)) self._diasMenu.addAction("10 minutes", lambda: self.diasshowState(600)) self._playDias.setMenu(self._diasMenu) self._zoomIn = self.addAction(QIcon("icons/zoom-in-outline.svg"), "", lambda: self.zoomChanged.emit(True)) self._zoomOut = self.addAction(QIcon("icons/zoom-out-outline.svg"), "", lambda: self.zoomChanged.emit(False)) self._rotateCW = self.addAction(QIcon("icons/rotate-cw-outline.svg"), "", self.rotateChanged.emit) # Clockwise self._rotateCW.setToolTip("Rotate Clockwise") #self._rotateCCW = self.addAction("Rotate Left") # Counter clockwise # a dummy widget to center actions spacer2 = QWidget() spacer2.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.addWidget(spacer2)
class MainUI(QtWidgets.QMainWindow, main_window_class): connectionLostSignal = pyqtSignal(str, str) connectionInitiatedSignal = pyqtSignal(str) batteryUpdatedSignal = pyqtSignal(int, object, object) connectionDoneSignal = pyqtSignal(str) connectionFailedSignal = pyqtSignal(str, str) disconnectedSignal = pyqtSignal(str) linkQualitySignal = pyqtSignal(int) _input_device_error_signal = pyqtSignal(str) _input_discovery_signal = pyqtSignal(object) _log_error_signal = pyqtSignal(object, str) def __init__(self, *args): super(MainUI, self).__init__(*args) self.setupUi(self) # Restore window size if present in the config file try: size = Config().get("window_size") self.resize(size[0], size[1]) except KeyError: pass ###################################################### # By lxrocks # 'Skinny Progress Bar' tweak for Yosemite # Tweak progress bar - artistic I am not - so pick your own colors !!! # Only apply to Yosemite ###################################################### import platform if platform.system() == 'Darwin': (Version, junk, machine) = platform.mac_ver() logger.info("This is a MAC - checking if we can apply Progress " "Bar Stylesheet for Yosemite Skinny Bars ") yosemite = (10, 10, 0) tVersion = tuple(map(int, (Version.split(".")))) if tVersion >= yosemite: logger.info("Found Yosemite - applying stylesheet") tcss = """ QProgressBar { border: 1px solid grey; border-radius: 5px; text-align: center; } QProgressBar::chunk { background-color: """ + COLOR_BLUE + """; } """ self.setStyleSheet(tcss) else: logger.info("Pre-Yosemite - skinny bar stylesheet not applied") ###################################################### self.cf = Crazyflie(ro_cache=None, rw_cache=cfclient.config_path + "/cache") cflib.crtp.init_drivers( enable_debug_driver=Config().get("enable_debug_driver")) zmq_params = ZMQParamAccess(self.cf) zmq_params.start() zmq_leds = ZMQLEDDriver(self.cf) zmq_leds.start() self.scanner = ScannerThread() self.scanner.interfaceFoundSignal.connect(self.foundInterfaces) self.scanner.start() # Create and start the Input Reader self._statusbar_label = QLabel("No input-device found, insert one to" " fly.") self.statusBar().addWidget(self._statusbar_label) self.joystickReader = JoystickReader() self._active_device = "" # self.configGroup = QActionGroup(self._menu_mappings, exclusive=True) self._mux_group = QActionGroup(self._menu_inputdevice, exclusive=True) # TODO: Need to reload configs # ConfigManager().conf_needs_reload.add_callback(self._reload_configs) self.cf.connection_failed.add_callback( self.connectionFailedSignal.emit) self.connectionFailedSignal.connect(self._connection_failed) self._input_device_error_signal.connect( self._display_input_device_error) self.joystickReader.device_error.add_callback( self._input_device_error_signal.emit) self._input_discovery_signal.connect(self.device_discovery) self.joystickReader.device_discovery.add_callback( self._input_discovery_signal.emit) # Hide the 'File' menu on OS X, since its only item, 'Exit', gets # merged into the application menu. if sys.platform == 'darwin': self.menuFile.menuAction().setVisible(False) # Connect UI signals self.logConfigAction.triggered.connect(self._show_connect_dialog) self.interfaceCombo.currentIndexChanged['QString'].connect( self.interfaceChanged) self.connectButton.clicked.connect(self._connect) self.scanButton.clicked.connect(self._scan) self.menuItemConnect.triggered.connect(self._connect) self.menuItemConfInputDevice.triggered.connect( self._show_input_device_config_dialog) self.menuItemExit.triggered.connect(self.closeAppRequest) self.batteryUpdatedSignal.connect(self._update_battery) self._menuitem_rescandevices.triggered.connect(self._rescan_devices) self._menuItem_openconfigfolder.triggered.connect( self._open_config_folder) self.address.setValue(0xE7E7E7E7E7) self._auto_reconnect_enabled = Config().get("auto_reconnect") self.autoReconnectCheckBox.toggled.connect( self._auto_reconnect_changed) self.autoReconnectCheckBox.setChecked(Config().get("auto_reconnect")) self.joystickReader.input_updated.add_callback( self.cf.commander.send_setpoint) self.joystickReader.assisted_input_updated.add_callback( self.cf.commander.send_velocity_world_setpoint) # Connection callbacks and signal wrappers for UI protection self.cf.connected.add_callback(self.connectionDoneSignal.emit) self.connectionDoneSignal.connect(self._connected) self.cf.disconnected.add_callback(self.disconnectedSignal.emit) self.disconnectedSignal.connect(self._disconnected) self.cf.connection_lost.add_callback(self.connectionLostSignal.emit) self.connectionLostSignal.connect(self._connection_lost) self.cf.connection_requested.add_callback( self.connectionInitiatedSignal.emit) self.connectionInitiatedSignal.connect(self._connection_initiated) self._log_error_signal.connect(self._logging_error) self.batteryBar.setTextVisible(False) self.batteryBar.setStyleSheet(progressbar_stylesheet(COLOR_BLUE)) self.linkQualityBar.setTextVisible(False) self.linkQualityBar.setStyleSheet(progressbar_stylesheet(COLOR_BLUE)) # Connect link quality feedback self.cf.link_quality_updated.add_callback(self.linkQualitySignal.emit) self.linkQualitySignal.connect( lambda percentage: self.linkQualityBar.setValue(percentage)) self._selected_interface = None self._initial_scan = True self._scan() # Parse the log configuration files self.logConfigReader = LogConfigReader(self.cf) self._current_input_config = None self._active_config = None self._active_config = None self.inputConfig = None # Add things to helper so tabs can access it cfclient.ui.pluginhelper.cf = self.cf cfclient.ui.pluginhelper.inputDeviceReader = self.joystickReader cfclient.ui.pluginhelper.logConfigReader = self.logConfigReader self.logConfigDialogue = LogConfigDialogue(cfclient.ui.pluginhelper) self._bootloader_dialog = BootloaderDialog(cfclient.ui.pluginhelper) self._cf2config_dialog = Cf2ConfigDialog(cfclient.ui.pluginhelper) self._cf1config_dialog = Cf1ConfigDialog(cfclient.ui.pluginhelper) self.menuItemBootloader.triggered.connect(self._bootloader_dialog.show) self._about_dialog = AboutDialog(cfclient.ui.pluginhelper) self.menuItemAbout.triggered.connect(self._about_dialog.show) self._menu_cf2_config.triggered.connect(self._cf2config_dialog.show) self._menu_cf1_config.triggered.connect(self._cf1config_dialog.show) # Load and connect tabs self.tabsMenuItem = QMenu("Tabs", self.menuView, enabled=True) self.menuView.addMenu(self.tabsMenuItem) # self.tabsMenuItem.setMenu(QtWidgets.QMenu()) tabItems = {} self.loadedTabs = [] for tabClass in cfclient.ui.tabs.available: tab = tabClass(self.tabs, cfclient.ui.pluginhelper) item = QtWidgets.QAction(tab.getMenuName(), self, checkable=True) item.toggled.connect(tab.toggleVisibility) self.tabsMenuItem.addAction(item) tabItems[tab.getTabName()] = item self.loadedTabs.append(tab) if not tab.enabled: item.setEnabled(False) # First instantiate all tabs and then open them in the correct order try: for tName in Config().get("open_tabs").split(","): t = tabItems[tName] if (t is not None and t.isEnabled()): # Toggle though menu so it's also marked as open there t.toggle() except Exception as e: logger.warning("Exception while opening tabs [{}]".format(e)) # Loading toolboxes (A bit of magic for a lot of automatic) self.toolboxesMenuItem = QMenu("Toolboxes", self.menuView, enabled=True) self.menuView.addMenu(self.toolboxesMenuItem) self.toolboxes = [] for t_class in cfclient.ui.toolboxes.toolboxes: toolbox = t_class(cfclient.ui.pluginhelper) dockToolbox = MyDockWidget(toolbox.getName()) dockToolbox.setWidget(toolbox) self.toolboxes += [ dockToolbox, ] # Add menu item for the toolbox item = QtWidgets.QAction(toolbox.getName(), self) item.setCheckable(True) item.triggered.connect(self.toggleToolbox) self.toolboxesMenuItem.addAction(item) dockToolbox.closed.connect(lambda: self.toggleToolbox(False)) # Setup some introspection item.dockToolbox = dockToolbox item.menuItem = item dockToolbox.dockToolbox = dockToolbox dockToolbox.menuItem = item # References to all the device sub-menus in the "Input device" menu self._all_role_menus = () # Used to filter what new devices to add default mapping to self._available_devices = () # Keep track of mux nodes so we can enable according to how many # devices we have self._all_mux_nodes = () # Check which Input muxes are available self._mux_group = QActionGroup(self._menu_inputdevice, exclusive=True) for m in self.joystickReader.available_mux(): node = QAction(m.name, self._menu_inputdevice, checkable=True, enabled=False) node.toggled.connect(self._mux_selected) self._mux_group.addAction(node) self._menu_inputdevice.addAction(node) self._all_mux_nodes += (node, ) mux_subnodes = () for name in m.supported_roles(): sub_node = QMenu(" {}".format(name), self._menu_inputdevice, enabled=False) self._menu_inputdevice.addMenu(sub_node) mux_subnodes += (sub_node, ) self._all_role_menus += ({ "muxmenu": node, "rolemenu": sub_node }, ) node.setData((m, mux_subnodes)) self._mapping_support = True def interfaceChanged(self, interface): if interface == INTERFACE_PROMPT_TEXT: self._selected_interface = None else: self._selected_interface = interface self._update_ui_state() def foundInterfaces(self, interfaces): selected_interface = self._selected_interface self.interfaceCombo.clear() self.interfaceCombo.addItem(INTERFACE_PROMPT_TEXT) formatted_interfaces = [] for i in interfaces: if len(i[1]) > 0: interface = "%s - %s" % (i[0], i[1]) else: interface = i[0] formatted_interfaces.append(interface) self.interfaceCombo.addItems(formatted_interfaces) if self._initial_scan: self._initial_scan = False try: if len(Config().get("link_uri")) > 0: formatted_interfaces.index(Config().get("link_uri")) selected_interface = Config().get("link_uri") except KeyError: # The configuration for link_uri was not found pass except ValueError: # The saved URI was not found while scanning pass if len(interfaces) == 1 and selected_interface is None: selected_interface = interfaces[0][0] newIndex = 0 if selected_interface is not None: try: newIndex = formatted_interfaces.index(selected_interface) + 1 except ValueError: pass self.interfaceCombo.setCurrentIndex(newIndex) self.uiState = UIState.DISCONNECTED self._update_ui_state() def _update_ui_state(self): if self.uiState == UIState.DISCONNECTED: self.setWindowTitle("Not connected") canConnect = self._selected_interface is not None self.menuItemConnect.setText("Connect to Crazyflie") self.menuItemConnect.setEnabled(canConnect) self.connectButton.setText("Connect") self.connectButton.setToolTip( "Connect to the Crazyflie on the selected interface") self.connectButton.setEnabled(canConnect) self.scanButton.setText("Scan") self.scanButton.setEnabled(True) self.address.setEnabled(True) self.batteryBar.setValue(3000) self._menu_cf2_config.setEnabled(False) self._menu_cf1_config.setEnabled(True) self.linkQualityBar.setValue(0) self.menuItemBootloader.setEnabled(True) self.logConfigAction.setEnabled(False) self.interfaceCombo.setEnabled(True) elif self.uiState == UIState.CONNECTED: s = "Connected on %s" % self._selected_interface self.setWindowTitle(s) self.menuItemConnect.setText("Disconnect") self.menuItemConnect.setEnabled(True) self.connectButton.setText("Disconnect") self.connectButton.setToolTip("Disconnect from the Crazyflie") self.scanButton.setEnabled(False) self.logConfigAction.setEnabled(True) # Find out if there's an I2C EEPROM, otherwise don't show the # dialog. if len(self.cf.mem.get_mems(MemoryElement.TYPE_I2C)) > 0: self._menu_cf2_config.setEnabled(True) self._menu_cf1_config.setEnabled(False) elif self.uiState == UIState.CONNECTING: s = "Connecting to {} ...".format(self._selected_interface) self.setWindowTitle(s) self.menuItemConnect.setText("Cancel") self.menuItemConnect.setEnabled(True) self.connectButton.setText("Cancel") self.connectButton.setToolTip("Cancel connecting to the Crazyflie") self.scanButton.setEnabled(False) self.address.setEnabled(False) self.menuItemBootloader.setEnabled(False) self.interfaceCombo.setEnabled(False) elif self.uiState == UIState.SCANNING: self.setWindowTitle("Scanning ...") self.connectButton.setText("Connect") self.menuItemConnect.setEnabled(False) self.connectButton.setText("Connect") self.connectButton.setEnabled(False) self.scanButton.setText("Scanning...") self.scanButton.setEnabled(False) self.address.setEnabled(False) self.menuItemBootloader.setEnabled(False) self.interfaceCombo.setEnabled(False) @pyqtSlot(bool) def toggleToolbox(self, display): menuItem = self.sender().menuItem dockToolbox = self.sender().dockToolbox if display and not dockToolbox.isVisible(): dockToolbox.widget().enable() self.addDockWidget(dockToolbox.widget().preferedDockArea(), dockToolbox) dockToolbox.show() elif not display: dockToolbox.widget().disable() self.removeDockWidget(dockToolbox) dockToolbox.hide() menuItem.setChecked(False) def _rescan_devices(self): self._statusbar_label.setText("No inputdevice connected!") self._menu_devices.clear() self._active_device = "" self.joystickReader.stop_input() # for c in self._menu_mappings.actions(): # c.setEnabled(False) # devs = self.joystickReader.available_devices() # if (len(devs) > 0): # self.device_discovery(devs) def _show_input_device_config_dialog(self): self.inputConfig = InputConfigDialogue(self.joystickReader) self.inputConfig.show() def _auto_reconnect_changed(self, checked): self._auto_reconnect_enabled = checked Config().set("auto_reconnect", checked) logger.info("Auto reconnect enabled: {}".format(checked)) def _show_connect_dialog(self): self.logConfigDialogue.show() def _update_battery(self, timestamp, data, logconf): self.batteryBar.setValue(int(data["pm.vbat"] * 1000)) color = COLOR_BLUE # TODO firmware reports fully-charged state as 'Battery', # rather than 'Charged' if data["pm.state"] in [BatteryStates.CHARGING, BatteryStates.CHARGED]: color = COLOR_GREEN elif data["pm.state"] == BatteryStates.LOW_POWER: color = COLOR_RED self.batteryBar.setStyleSheet(progressbar_stylesheet(color)) def _connected(self): self.uiState = UIState.CONNECTED self._update_ui_state() Config().set("link_uri", str(self._selected_interface)) lg = LogConfig("Battery", 1000) lg.add_variable("pm.vbat", "float") lg.add_variable("pm.state", "int8_t") try: self.cf.log.add_config(lg) lg.data_received_cb.add_callback(self.batteryUpdatedSignal.emit) lg.error_cb.add_callback(self._log_error_signal.emit) lg.start() except KeyError as e: logger.warning(str(e)) mems = self.cf.mem.get_mems(MemoryElement.TYPE_DRIVER_LED) if len(mems) > 0: mems[0].write_data(self._led_write_done) def _disconnected(self): self.uiState = UIState.DISCONNECTED self._update_ui_state() def _connection_initiated(self): self.uiState = UIState.CONNECTING self._update_ui_state() def _led_write_done(self, mem, addr): logger.info("LED write done callback") def _logging_error(self, log_conf, msg): QMessageBox.about( self, "Log error", "Error when starting log config" " [{}]: {}".format(log_conf.name, msg)) def _connection_lost(self, linkURI, msg): if not self._auto_reconnect_enabled: if self.isActiveWindow(): warningCaption = "Communication failure" error = "Connection lost to {}: {}".format(linkURI, msg) QMessageBox.critical(self, warningCaption, error) self.uiState = UIState.DISCONNECTED self._update_ui_state() else: self._connect() def _connection_failed(self, linkURI, error): if not self._auto_reconnect_enabled: msg = "Failed to connect on {}: {}".format(linkURI, error) warningCaption = "Communication failure" QMessageBox.critical(self, warningCaption, msg) self.uiState = UIState.DISCONNECTED self._update_ui_state() else: self._connect() def closeEvent(self, event): self.hide() self.cf.close_link() Config().save_file() def resizeEvent(self, event): Config().set( "window_size", [event.size().width(), event.size().height()]) def _connect(self): if self.uiState == UIState.CONNECTED: self.cf.close_link() elif self.uiState == UIState.CONNECTING: self.cf.close_link() self.uiState = UIState.DISCONNECTED self._update_ui_state() else: self.cf.open_link(self._selected_interface) def _scan(self): self.uiState = UIState.SCANNING self._update_ui_state() self.scanner.scanSignal.emit(self.address.value()) def _display_input_device_error(self, error): self.cf.close_link() QMessageBox.critical(self, "Input device error", error) def _mux_selected(self, checked): """Called when a new mux is selected. The menu item contains a reference to the raw mux object as well as to the associated device sub-nodes""" if not checked: (mux, sub_nodes) = self.sender().data() for s in sub_nodes: s.setEnabled(False) else: (mux, sub_nodes) = self.sender().data() for s in sub_nodes: s.setEnabled(True) self.joystickReader.set_mux(mux=mux) # Go though the tree and select devices/mapping that was # selected before it was disabled. for role_node in sub_nodes: for dev_node in role_node.children(): if type(dev_node) is QAction and dev_node.isChecked(): dev_node.toggled.emit(True) self._update_input_device_footer() def _get_dev_status(self, device): msg = "{}".format(device.name) if device.supports_mapping: map_name = "N/A" if device.input_map: map_name = device.input_map_name msg += " ({})".format(map_name) return msg def _update_input_device_footer(self): """Update the footer in the bottom of the UI with status for the input device and its mapping""" msg = "" if len(self.joystickReader.available_devices()) > 0: mux = self.joystickReader._selected_mux msg = "Using {} mux with ".format(mux.name) for key in list(mux._devs.keys())[:-1]: if mux._devs[key]: msg += "{}, ".format(self._get_dev_status(mux._devs[key])) else: msg += "N/A, " # Last item key = list(mux._devs.keys())[-1] if mux._devs[key]: msg += "{}".format(self._get_dev_status(mux._devs[key])) else: msg += "N/A" else: msg = "No input device found" self._statusbar_label.setText(msg) def _inputdevice_selected(self, checked): """Called when a new input device has been selected from the menu. The data in the menu object is the associated map menu (directly under the item in the menu) and the raw device""" (map_menu, device, mux_menu) = self.sender().data() if not checked: if map_menu: map_menu.setEnabled(False) # Do not close the device, since we don't know exactly # how many devices the mux can have open. When selecting a # new mux the old one will take care of this. else: if map_menu: map_menu.setEnabled(True) (mux, sub_nodes) = mux_menu.data() for role_node in sub_nodes: for dev_node in role_node.children(): if type(dev_node) is QAction and dev_node.isChecked(): if device.id == dev_node.data()[1].id \ and dev_node is not self.sender(): dev_node.setChecked(False) role_in_mux = str(self.sender().parent().title()).strip() logger.info("Role of {} is {}".format(device.name, role_in_mux)) Config().set("input_device", str(device.name)) self._mapping_support = self.joystickReader.start_input( device.name, role_in_mux) self._update_input_device_footer() def _inputconfig_selected(self, checked): """Called when a new configuration has been selected from the menu. The data in the menu object is a referance to the device QAction in parent menu. This contains a referance to the raw device.""" if not checked: return selected_mapping = str(self.sender().text()) device = self.sender().data().data()[1] self.joystickReader.set_input_map(device.name, selected_mapping) self._update_input_device_footer() def device_discovery(self, devs): """Called when new devices have been added""" for menu in self._all_role_menus: role_menu = menu["rolemenu"] mux_menu = menu["muxmenu"] dev_group = QActionGroup(role_menu, exclusive=True) for d in devs: dev_node = QAction(d.name, role_menu, checkable=True, enabled=True) role_menu.addAction(dev_node) dev_group.addAction(dev_node) dev_node.toggled.connect(self._inputdevice_selected) map_node = None if d.supports_mapping: map_node = QMenu(" Input map", role_menu, enabled=False) map_group = QActionGroup(role_menu, exclusive=True) # Connect device node to map node for easy # enabling/disabling when selection changes and device # to easily enable it dev_node.setData((map_node, d)) for c in ConfigManager().get_list_of_configs(): node = QAction(c, map_node, checkable=True, enabled=True) node.toggled.connect(self._inputconfig_selected) map_node.addAction(node) # Connect all the map nodes back to the device # action node where we can access the raw device node.setData(dev_node) map_group.addAction(node) # If this device hasn't been found before, then # select the default mapping for it. if d not in self._available_devices: last_map = Config().get("device_config_mapping") if d.name in last_map and last_map[d.name] == c: node.setChecked(True) role_menu.addMenu(map_node) dev_node.setData((map_node, d, mux_menu)) # Update the list of what devices we found # to avoid selecting default mapping for all devices when # a new one is inserted self._available_devices = () for d in devs: self._available_devices += (d, ) # Only enable MUX nodes if we have enough devies to cover # the roles for mux_node in self._all_mux_nodes: (mux, sub_nodes) = mux_node.data() if len(mux.supported_roles()) <= len(self._available_devices): mux_node.setEnabled(True) # TODO: Currently only supports selecting default mux if self._all_mux_nodes[0].isEnabled(): self._all_mux_nodes[0].setChecked(True) # If the previous length of the available devies was 0, then select # the default on. If that's not available then select the first # on in the list. # TODO: This will only work for the "Normal" mux so this will be # selected by default if Config().get("input_device") in [d.name for d in devs]: for dev_menu in self._all_role_menus[0]["rolemenu"].actions(): if dev_menu.text() == Config().get("input_device"): dev_menu.setChecked(True) else: # Select the first device in the first mux (will always be "Normal" # mux) self._all_role_menus[0]["rolemenu"].actions()[0].setChecked(True) logger.info("Select first device") self._update_input_device_footer() def _open_config_folder(self): QDesktopServices.openUrl( QUrl("file:///" + QDir.toNativeSeparators(cfclient.config_path))) def closeAppRequest(self): self.close() sys.exit(0)
class MainWindow(QMainWindow, Ui_MainWindow): """docstring for MainWindow.""" def __init__(self, parent=None): super(MainWindow, self).__init__() self._csvFilePath = "" self.serialport = serial.Serial() self.receiver_thread = readerThread(self) self.receiver_thread.setPort(self.serialport) self._localEcho = None self._viewMode = None self._quickSendOptRow = 1 self.setupUi(self) self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea) self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea) font = QtGui.QFont() font.setFamily(EDITOR_FONT) font.setPointSize(9) self.txtEdtOutput.setFont(font) self.txtEdtInput.setFont(font) #self.quickSendTable.setFont(font) if UI_FONT is not None: font = QtGui.QFont() font.setFamily(UI_FONT) font.setPointSize(9) self.dockWidget_PortConfig.setFont(font) self.dockWidget_SendHex.setFont(font) self.dockWidget_QuickSend.setFont(font) self.setupMenu() self.setupFlatUi() self.onEnumPorts() icon = QtGui.QIcon(":/MyTerm.ico") self.setWindowIcon(icon) self.actionAbout.setIcon(icon) self.defaultStyleWidget = QWidget() self.defaultStyleWidget.setWindowIcon(icon) icon = QtGui.QIcon(":/qt_logo_16.ico") self.actionAbout_Qt.setIcon(icon) self._viewGroup = QActionGroup(self) self._viewGroup.addAction(self.actionAscii) self._viewGroup.addAction(self.actionHex_lowercase) self._viewGroup.addAction(self.actionHEX_UPPERCASE) self._viewGroup.setExclusive(True) # bind events self.actionOpen_Cmd_File.triggered.connect(self.openQuickSend) self.actionSave_Log.triggered.connect(self.onSaveLog) self.actionExit.triggered.connect(self.onExit) self.actionOpen.triggered.connect(self.openPort) self.actionClose.triggered.connect(self.closePort) self.actionPort_Config_Panel.triggered.connect(self.onTogglePrtCfgPnl) self.actionQuick_Send_Panel.triggered.connect(self.onToggleQckSndPnl) self.actionSend_Hex_Panel.triggered.connect(self.onToggleHexPnl) self.dockWidget_PortConfig.visibilityChanged.connect(self.onVisiblePrtCfgPnl) self.dockWidget_QuickSend.visibilityChanged.connect(self.onVisibleQckSndPnl) self.dockWidget_SendHex.visibilityChanged.connect(self.onVisibleHexPnl) self.actionLocal_Echo.triggered.connect(self.onLocalEcho) self.actionAlways_On_Top.triggered.connect(self.onAlwaysOnTop) self.actionAscii.triggered.connect(self.onViewChanged) self.actionHex_lowercase.triggered.connect(self.onViewChanged) self.actionHEX_UPPERCASE.triggered.connect(self.onViewChanged) self.actionAbout.triggered.connect(self.onAbout) self.actionAbout_Qt.triggered.connect(self.onAboutQt) self.btnOpen.clicked.connect(self.onOpen) self.btnClear.clicked.connect(self.onClear) self.btnSaveLog.clicked.connect(self.onSaveLog) self.btnEnumPorts.clicked.connect(self.onEnumPorts) self.btnSendHex.clicked.connect(self.onSend) self.receiver_thread.read.connect(self.onReceive) self.receiver_thread.exception.connect(self.onReaderExcept) self._signalMapQuickSendOpt = QSignalMapper(self) self._signalMapQuickSendOpt.mapped[int].connect(self.onQuickSendOptions) self._signalMapQuickSend = QSignalMapper(self) self._signalMapQuickSend.mapped[int].connect(self.onQuickSend) # initial action self.actionHEX_UPPERCASE.setChecked(True) self.receiver_thread.setViewMode(VIEWMODE_HEX_UPPERCASE) self.initQuickSend() self.restoreLayout() self.moveScreenCenter() self.syncMenu() if self.isMaximized(): self.setMaximizeButton("restore") else: self.setMaximizeButton("maximize") self.loadSettings() def setupMenu(self): self.menuMenu = QtWidgets.QMenu() self.menuMenu.setTitle("&File") self.menuMenu.setObjectName("menuMenu") self.menuView = QtWidgets.QMenu(self.menuMenu) self.menuView.setTitle("&View") self.menuView.setObjectName("menuView") self.menuView.addAction(self.actionAscii) self.menuView.addAction(self.actionHex_lowercase) self.menuView.addAction(self.actionHEX_UPPERCASE) self.menuMenu.addAction(self.actionOpen_Cmd_File) self.menuMenu.addAction(self.actionSave_Log) self.menuMenu.addSeparator() self.menuMenu.addAction(self.actionPort_Config_Panel) self.menuMenu.addAction(self.actionQuick_Send_Panel) self.menuMenu.addAction(self.actionSend_Hex_Panel) self.menuMenu.addAction(self.menuView.menuAction()) self.menuMenu.addAction(self.actionLocal_Echo) self.menuMenu.addAction(self.actionAlways_On_Top) self.menuMenu.addSeparator() self.menuMenu.addAction(self.actionAbout) self.menuMenu.addAction(self.actionAbout_Qt) self.menuMenu.addSeparator() self.menuMenu.addAction(self.actionExit) self.sendOptMenu = QtWidgets.QMenu() self.actionSend_Hex = QtWidgets.QAction(self) self.actionSend_Hex.setText("Send &Hex") self.actionSend_Hex.setStatusTip("Send Hex (e.g. 31 32 FF)") self.actionSend_Asc = QtWidgets.QAction(self) self.actionSend_Asc.setText("Send &Asc") self.actionSend_Asc.setStatusTip("Send Asc (e.g. abc123)") self.actionSend_TFH = QtWidgets.QAction(self) self.actionSend_TFH.setText("Send &Text file as hex") self.actionSend_TFH.setStatusTip('Send text file as hex (e.g. strings "31 32 FF" in the file)') self.actionSend_TFA = QtWidgets.QAction(self) self.actionSend_TFA.setText("Send t&Ext file as asc") self.actionSend_TFA.setStatusTip('Send text file as asc (e.g. strings "abc123" in the file)') self.actionSend_FB = QtWidgets.QAction(self) self.actionSend_FB.setText("Send &Bin/Hex file") self.actionSend_FB.setStatusTip("Send a bin file or a hex file") self.sendOptMenu.addAction(self.actionSend_Hex) self.sendOptMenu.addAction(self.actionSend_Asc) self.sendOptMenu.addAction(self.actionSend_TFH) self.sendOptMenu.addAction(self.actionSend_TFA) self.sendOptMenu.addAction(self.actionSend_FB) self.actionSend_Hex.triggered.connect(self.onSetSendHex) self.actionSend_Asc.triggered.connect(self.onSetSendAsc) self.actionSend_TFH.triggered.connect(self.onSetSendTFH) self.actionSend_TFA.triggered.connect(self.onSetSendTFA) self.actionSend_FB.triggered.connect(self.onSetSendFB) def setupFlatUi(self): self._dragPos = self.pos() self._isDragging = False self.setMouseTracking(True) self.setWindowFlags(Qt.FramelessWindowHint) self.setStyleSheet(""" QWidget { background-color: %(BackgroundColor)s; /*background-image: url(:/background.png);*/ outline: none; } QLabel { color:%(TextColor)s; font-size:12px; /*font-family:Century;*/ } QComboBox { color:%(TextColor)s; font-size:12px; /*font-family:Century;*/ } QComboBox { border: none; padding: 1px 1px 1px 3px; } QComboBox:editable { background: white; } QComboBox:!editable, QComboBox::drop-down:editable { background: #62c7e0; } QComboBox:!editable:hover, QComboBox::drop-down:editable:hover { background: #c7eaf3; } QComboBox:!editable:pressed, QComboBox::drop-down:editable:pressed { background: #35b6d7; } QComboBox:on { padding-top: 3px; padding-left: 4px; } QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 16px; border: none; } QComboBox::down-arrow { image: url(:/downarrow.png); } QComboBox::down-arrow:on { image: url(:/uparrow.png); } QGroupBox { color:%(TextColor)s; font-size:12px; /*font-family:Century;*/ border: 1px solid gray; margin-top: 15px; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; left:5px; top:3px; } QCheckBox { color:%(TextColor)s; spacing: 5px; font-size:12px; /*font-family:Century;*/ } QCheckBox::indicator:unchecked { image: url(:/checkbox_unchecked.png); } QCheckBox::indicator:unchecked:hover { image: url(:/checkbox_unchecked_hover.png); } QCheckBox::indicator:unchecked:pressed { image: url(:/checkbox_unchecked_pressed.png); } QCheckBox::indicator:checked { image: url(:/checkbox_checked.png); } QCheckBox::indicator:checked:hover { image: url(:/checkbox_checked_hover.png); } QCheckBox::indicator:checked:pressed { image: url(:/checkbox_checked_pressed.png); } QScrollBar:horizontal { background-color:%(BackgroundColor)s; border: none; height: 15px; margin: 0px 20px 0 20px; } QScrollBar::handle:horizontal { background: %(ScrollBar_Handle)s; min-width: 20px; } QScrollBar::add-line:horizontal { image: url(:/rightarrow.png); border: none; background: %(ScrollBar_Line)s; width: 20px; subcontrol-position: right; subcontrol-origin: margin; } QScrollBar::sub-line:horizontal { image: url(:/leftarrow.png); border: none; background: %(ScrollBar_Line)s; width: 20px; subcontrol-position: left; subcontrol-origin: margin; } QScrollBar:vertical { background-color:%(BackgroundColor)s; border: none; width: 15px; margin: 20px 0px 20px 0px; } QScrollBar::handle::vertical { background: %(ScrollBar_Handle)s; min-height: 20px; } QScrollBar::add-line::vertical { image: url(:/downarrow.png); border: none; background: %(ScrollBar_Line)s; height: 20px; subcontrol-position: bottom; subcontrol-origin: margin; } QScrollBar::sub-line::vertical { image: url(:/uparrow.png); border: none; background: %(ScrollBar_Line)s; height: 20px; subcontrol-position: top; subcontrol-origin: margin; } QTableView { background-color: white; /*selection-background-color: #FF92BB;*/ border: 1px solid %(TableView_Border)s; color: %(TextColor)s; } QTableView::focus { /*border: 1px solid #2a7fff;*/ } QTableView QTableCornerButton::section { border: none; border-right: 1px solid %(TableView_Border)s; border-bottom: 1px solid %(TableView_Border)s; background-color: %(TableView_Corner)s; } QTableView QWidget { background-color: white; } QTableView::item:focus { border: 1px red; background-color: transparent; color: %(TextColor)s; } QHeaderView::section { border: none; border-right: 1px solid %(TableView_Border)s; border-bottom: 1px solid %(TableView_Border)s; padding-left: 2px; padding-right: 2px; color: #444444; background-color: %(TableView_Header)s; } QTextEdit { background-color:white; color:%(TextColor)s; border-top: none; border-bottom: none; border-left: 2px solid %(BackgroundColor)s; border-right: 2px solid %(BackgroundColor)s; } QTextEdit::focus { } QToolButton, QPushButton { background-color:#30a7b8; border:none; color:#ffffff; font-size:12px; /*font-family:Century;*/ } QToolButton:hover, QPushButton:hover { background-color:#51c0d1; } QToolButton:pressed, QPushButton:pressed { background-color:#3a9ecc; } QMenuBar { color: %(TextColor)s; height: 24px; } QMenuBar::item { background-color: transparent; margin: 8px 0px 0px 0px; padding: 1px 8px 1px 8px; height: 15px; } QMenuBar::item:selected { background: #51c0d1; } QMenuBar::item:pressed { } /* QMenu { color: %(TextColor)s; background: #ffffff; } QMenu { margin: 2px; } QMenu::item { padding: 2px 25px 2px 21px; border: 1px solid transparent; } QMenu::item:selected { background: #51c0d1; } QMenu::icon { background: transparent; border: 2px inset transparent; }*/ QDockWidget { font-size:12px; /*font-family:Century;*/ color: %(TextColor)s; titlebar-close-icon: none; titlebar-normal-icon: none; } QDockWidget::title { margin: 0; padding: 2px; subcontrol-origin: content; subcontrol-position: right top; text-align: left; background: #67baed; } QDockWidget::float-button { max-width: 12px; max-height: 12px; background-color:transparent; border:none; image: url(:/restore_inactive.png); } QDockWidget::float-button:hover { background-color:#227582; image: url(:/restore_active.png); } QDockWidget::float-button:pressed { padding: 0; background-color:#14464e; image: url(:/restore_active.png); } QDockWidget::close-button { max-width: 12px; max-height: 12px; background-color:transparent; border:none; image: url(:/close_inactive.png); } QDockWidget::close-button:hover { background-color:#ea5e00; image: url(:/close_active.png); } QDockWidget::close-button:pressed { background-color:#994005; image: url(:/close_active.png); padding: 0; } """ % dict( BackgroundColor = '#99d9ea', TextColor = '#202020', ScrollBar_Handle = '#61b9e1', ScrollBar_Line = '#7ecfe4', TableView_Corner = '#8ae6d2', TableView_Header = '#8ae6d2', TableView_Border = '#eeeeee' )) self.dockWidgetContents.setStyleSheet(""" QPushButton { min-height:23px; } """) self.dockWidget_QuickSend.setStyleSheet(""" QToolButton, QPushButton { background-color:#27b798; /*font-family:Consolas;*/ /*font-size:12px;*/ /*min-width:46px;*/ } QToolButton:hover, QPushButton:hover { background-color:#3bd5b4; } QToolButton:pressed, QPushButton:pressed { background-color:#1d8770; } """) self.dockWidgetContents_2.setStyleSheet(""" QPushButton { min-height:23px; min-width:50px; } """) w = self.frameGeometry().width() self._minBtn = QPushButton(self) self._minBtn.setGeometry(w-103,0,28,24) self._minBtn.clicked.connect(self.onMinimize) self._minBtn.setStyleSheet(""" QPushButton { background-color:transparent; border:none; outline: none; image: url(:/minimize_inactive.png); } QPushButton:hover { background-color:#227582; image: url(:/minimize_active.png); } QPushButton:pressed { background-color:#14464e; image: url(:/minimize_active.png); } """) self._maxBtn = QPushButton(self) self._maxBtn.setGeometry(w-74,0,28,24) self._maxBtn.clicked.connect(self.onMaximize) self.setMaximizeButton("maximize") self._closeBtn = QPushButton(self) self._closeBtn.setGeometry(w-45,0,36,24) self._closeBtn.clicked.connect(self.onExit) self._closeBtn.setStyleSheet(""" QPushButton { background-color:transparent; border:none; outline: none; image: url(:/close_inactive.png); } QPushButton:hover { background-color:#ea5e00; image: url(:/close_active.png); } QPushButton:pressed { background-color:#994005; image: url(:/close_active.png); } """) self.btnMenu = QtWidgets.QToolButton(self) self.btnMenu.setEnabled(True) self.btnMenu.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon) self.btnMenu.setIcon(QtGui.QIcon(':/MyTerm.ico')) self.btnMenu.setText('Myterm ') self.btnMenu.setGeometry(3,3,80,18) self.btnMenu.setMenu(self.menuMenu) self.btnMenu.setPopupMode(QtWidgets.QToolButton.InstantPopup) self.btnRefresh = QtWidgets.QToolButton(self) self.btnRefresh.setEnabled(True) self.btnRefresh.setIcon(QtGui.QIcon(':/refresh.ico')) self.btnRefresh.setGeometry(110,3,18,18) self.btnRefresh.clicked.connect(self.onEnumPorts) self.verticalLayout_1.removeWidget(self.cmbPort) self.cmbPort.setParent(self) self.cmbPort.setGeometry(128,3,60,18) self.verticalLayout_1.removeWidget(self.btnOpen) self.btnOpen.setParent(self) self.btnOpen.setGeometry(210,3,60,18) def resizeEvent(self, event): w = event.size().width() self._minBtn.move(w-103,0) self._maxBtn.move(w-74,0) self._closeBtn.move(w-45,0) def onMinimize(self): self.showMinimized() def isMaximized(self): return ((self.windowState() == Qt.WindowMaximized)) def onMaximize(self): if self.isMaximized(): self.showNormal() self.setMaximizeButton("maximize") else: self.showMaximized() self.setMaximizeButton("restore") def setMaximizeButton(self, style): if "maximize" == style: self._maxBtn.setStyleSheet(""" QPushButton { background-color:transparent; border:none; outline: none; image: url(:/maximize_inactive.png); } QPushButton:hover { background-color:#227582; image: url(:/maximize_active.png); } QPushButton:pressed { background-color:#14464e; image: url(:/maximize_active.png); } """) elif "restore" == style: self._maxBtn.setStyleSheet(""" QPushButton { background-color:transparent; border:none; outline: none; image: url(:/restore_inactive.png); } QPushButton:hover { background-color:#227582; image: url(:/restore_active.png); } QPushButton:pressed { background-color:#14464e; image: url(:/restore_active.png); } """) def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self._isDragging = True self._dragPos = event.globalPos() - self.pos() event.accept() def mouseMoveEvent(self, event): if event.buttons() == Qt.LeftButton and self._isDragging and not self.isMaximized(): self.move(event.globalPos() - self._dragPos) event.accept() def mouseReleaseEvent(self, event): self._isDragging = False event.accept() def saveSettings(self): root = ET.Element("MyTerm") GUISettings = ET.SubElement(root, "GUISettings") PortCfg = ET.SubElement(GUISettings, "PortConfig") ET.SubElement(PortCfg, "port").text = self.cmbPort.currentText() ET.SubElement(PortCfg, "baudrate").text = self.cmbBaudRate.currentText() ET.SubElement(PortCfg, "databits").text = self.cmbDataBits.currentText() ET.SubElement(PortCfg, "parity").text = self.cmbParity.currentText() ET.SubElement(PortCfg, "stopbits").text = self.cmbStopBits.currentText() ET.SubElement(PortCfg, "rtscts").text = self.chkRTSCTS.isChecked() and "on" or "off" ET.SubElement(PortCfg, "xonxoff").text = self.chkXonXoff.isChecked() and "on" or "off" View = ET.SubElement(GUISettings, "View") ET.SubElement(View, "LocalEcho").text = self.actionLocal_Echo.isChecked() and "on" or "off" ET.SubElement(View, "ReceiveView").text = self._viewGroup.checkedAction().text() with open(get_config_path('MyTerm.xml'), 'w') as f: f.write('<?xml version="1.0" encoding="UTF-8"?>\n') f.write(ET.tostring(root, encoding='utf-8', pretty_print=True).decode("utf-8")) def loadSettings(self): if os.path.isfile(get_config_path("MyTerm.xml")): with open(get_config_path("MyTerm.xml"), 'r') as f: tree = safeET.parse(f) port = tree.findtext('GUISettings/PortConfig/port', default='') if port != '': self.cmbPort.setCurrentText(port) baudrate = tree.findtext('GUISettings/PortConfig/baudrate', default='38400') if baudrate != '': self.cmbBaudRate.setCurrentText(baudrate) databits = tree.findtext('GUISettings/PortConfig/databits', default='8') id = self.cmbDataBits.findText(databits) if id >= 0: self.cmbDataBits.setCurrentIndex(id) parity = tree.findtext('GUISettings/PortConfig/parity', default='None') id = self.cmbParity.findText(parity) if id >= 0: self.cmbParity.setCurrentIndex(id) stopbits = tree.findtext('GUISettings/PortConfig/stopbits', default='1') id = self.cmbStopBits.findText(stopbits) if id >= 0: self.cmbStopBits.setCurrentIndex(id) rtscts = tree.findtext('GUISettings/PortConfig/rtscts', default='off') if 'on' == rtscts: self.chkRTSCTS.setChecked(True) else: self.chkRTSCTS.setChecked(False) xonxoff = tree.findtext('GUISettings/PortConfig/xonxoff', default='off') if 'on' == xonxoff: self.chkXonXoff.setChecked(True) else: self.chkXonXoff.setChecked(False) LocalEcho = tree.findtext('GUISettings/View/LocalEcho', default='off') if 'on' == LocalEcho: self.actionLocal_Echo.setChecked(True) self._localEcho = True else: self.actionLocal_Echo.setChecked(False) self._localEcho = False ReceiveView = tree.findtext('GUISettings/View/ReceiveView', default='HEX(UPPERCASE)') if 'Ascii' in ReceiveView: self.actionAscii.setChecked(True) self._viewMode = VIEWMODE_ASCII elif 'lowercase' in ReceiveView: self.actionHex_lowercase.setChecked(True) self._viewMode = VIEWMODE_HEX_LOWERCASE elif 'UPPERCASE' in ReceiveView: self.actionHEX_UPPERCASE.setChecked(True) self._viewMode = VIEWMODE_HEX_UPPERCASE self.receiver_thread.setViewMode(self._viewMode) def closeEvent(self, event): self.saveLayout() self.saveQuickSend() self.saveSettings() event.accept() def initQuickSend(self): #self.quickSendTable.horizontalHeader().setDefaultSectionSize(40) #self.quickSendTable.horizontalHeader().setMinimumSectionSize(25) self.quickSendTable.setRowCount(50) self.quickSendTable.setColumnCount(3) self.quickSendTable.verticalHeader().setSectionsClickable(True) for row in range(50): self.initQuickSendButton(row) if os.path.isfile(get_config_path('QuickSend.csv')): self.loadQuickSend(get_config_path('QuickSend.csv')) self.quickSendTable.resizeColumnsToContents() def initQuickSendButton(self, row, cmd = 'cmd', opt = 'H', dat = ''): if self.quickSendTable.cellWidget(row, 0) is None: item = QToolButton(self) item.setText(cmd) item.clicked.connect(self._signalMapQuickSend.map) self._signalMapQuickSend.setMapping(item, row) self.quickSendTable.setCellWidget(row, 0, item) else: self.quickSendTable.cellWidget(row, 0).setText(cmd) if self.quickSendTable.cellWidget(row, 1) is None: item = QToolButton(self) item.setText(opt) #item.setMaximumSize(QtCore.QSize(16, 16)) item.clicked.connect(self._signalMapQuickSendOpt.map) self._signalMapQuickSendOpt.setMapping(item, row) self.quickSendTable.setCellWidget(row, 1, item) else: self.quickSendTable.cellWidget(row, 1).setText(opt) if self.quickSendTable.item(row, 2) is None: self.quickSendTable.setItem(row, 2, QTableWidgetItem(dat)) else: self.quickSendTable.item(row, 2).setText(dat) self.quickSendTable.setRowHeight(row, 16) def onSetSendHex(self): item = self.quickSendTable.cellWidget(self._quickSendOptRow, 1) item.setText('H') def onSetSendAsc(self): item = self.quickSendTable.cellWidget(self._quickSendOptRow, 1) item.setText('A') def onSetSendTFH(self): item = self.quickSendTable.cellWidget(self._quickSendOptRow, 1) item.setText('FH') def onSetSendTFA(self): item = self.quickSendTable.cellWidget(self._quickSendOptRow, 1) item.setText('FA') def onSetSendFB(self): item = self.quickSendTable.cellWidget(self._quickSendOptRow, 1) item.setText('FB') def onQuickSendOptions(self, row): self._quickSendOptRow = row item = self.quickSendTable.cellWidget(row, 1) self.sendOptMenu.popup(item.mapToGlobal(QPoint(item.size().width(), item.size().height()))) def openQuickSend(self): fileName = QFileDialog.getOpenFileName(self, "Select a file", os.getcwd(), "CSV Files (*.csv)")[0] if fileName: self.loadQuickSend(fileName, notifyExcept = True) def saveQuickSend(self): # scan table rows = self.quickSendTable.rowCount() #cols = self.quickSendTable.columnCount() save_data = [[self.quickSendTable.cellWidget(row, 0).text(), self.quickSendTable.cellWidget(row, 1).text(), self.quickSendTable.item(row, 2) is not None and self.quickSendTable.item(row, 2).text() or ''] for row in range(rows)] #import pprint #pprint.pprint(save_data, width=120, compact=True) # write to file with open(get_config_path('QuickSend.csv'), 'w') as csvfile: csvwriter = csv.writer(csvfile, delimiter=',', lineterminator='\n') csvwriter.writerows(save_data) def loadQuickSend(self, path, notifyExcept = False): data = [] set_rows = 0 set_cols = 0 try: with open(path) as csvfile: csvData = csv.reader(csvfile) for row in csvData: data.append(row) set_rows = set_rows + 1 if len(row) > set_cols: set_cols = len(row) except IOError as e: print("({})".format(e)) if notifyExcept: QMessageBox.critical(self.defaultStyleWidget, "Open failed", str(e), QMessageBox.Close) return rows = self.quickSendTable.rowCount() cols = self.quickSendTable.columnCount() if rows < set_rows: rows = set_rows + 10 self.quickSendTable.setRowCount(rows) for row, rowdat in enumerate(data): if len(rowdat) >= 3: cmd, opt, dat = rowdat[0:3] self.initQuickSendButton(row, cmd, opt, dat) # self.quickSendTable.cellWidget(row, 0).setText(cmd) # self.quickSendTable.cellWidget(row, 1).setText(opt) # self.quickSendTable.setItem(row, 2, QTableWidgetItem(dat)) self.quickSendTable.resizeColumnsToContents() #self.quickSendTable.resizeRowsToContents() def onQuickSend(self, row): if self.quickSendTable.item(row, 2) is not None: tablestring = self.quickSendTable.item(row, 2).text() format = self.quickSendTable.cellWidget(row, 1).text() if 'H' == format: self.transmitHex(tablestring) elif 'A' == format: self.transmitAsc(tablestring) elif 'FB' == format: try: with open(tablestring, 'rb') as f: bytes = f.read() self.transmitBytearray(bytes) except IOError as e: print("({})".format(e)) QMessageBox.critical(self.defaultStyleWidget, "Open failed", str(e), QMessageBox.Close) else: try: with open(tablestring, 'rt') as f: filestring = f.read() if 'FH' == format: self.transmitHex(filestring) elif 'FA' == format: self.transmitAsc(filestring) except IOError as e: print("({})".format(e)) QMessageBox.critical(self.defaultStyleWidget, "Open failed", str(e), QMessageBox.Close) def onSend(self): sendstring = self.txtEdtInput.toPlainText() self.transmitHex(sendstring) def transmitHex(self, hexstring): if len(hexstring) > 0: hexarray = [] _hexstring = hexstring.replace(' ', '') _hexstring = _hexstring.replace('\r', '') _hexstring = _hexstring.replace('\n', '') for i in range(0, len(_hexstring), 2): word = _hexstring[i:i+2] if is_hex(word): hexarray.append(int(word, 16)) else: QMessageBox.critical(self.defaultStyleWidget, "Error", "'%s' is not hexadecimal." % (word), QMessageBox.Close) return self.transmitBytearray(bytearray(hexarray)) def transmitAsc(self, text): if len(text) > 0: byteArray = [ord(char) for char in text] self.transmitBytearray(bytearray(byteArray)) def transmitBytearray(self, byteArray): if self.serialport.isOpen(): try: self.serialport.write(byteArray) except Exception as e: QMessageBox.critical(self.defaultStyleWidget, "Exception in transmit", str(e), QMessageBox.Close) print("Exception in transmitBytearray(%s)" % text) else: if self._viewMode == VIEWMODE_ASCII: text = byteArray.decode('unicode_escape') elif self._viewMode == VIEWMODE_HEX_LOWERCASE: text = ''.join('%02x ' % t for t in byteArray) elif self._viewMode == VIEWMODE_HEX_UPPERCASE: text = ''.join('%02X ' % t for t in byteArray) self.appendOutputText("\n%s T->:%s" % (self.timestamp(), text), Qt.blue) def onReaderExcept(self, e): self.closePort() QMessageBox.critical(self.defaultStyleWidget, "Read failed", str(e), QMessageBox.Close) def timestamp(self): return datetime.datetime.now().time().isoformat()[:-3] def onReceive(self, data): self.appendOutputText("\n%s R<-:%s" % (self.timestamp(), data)) def appendOutputText(self, data, color=Qt.black): # the qEditText's "append" methon will add a unnecessary newline. # self.txtEdtOutput.append(data.decode('utf-8')) tc=self.txtEdtOutput.textColor() self.txtEdtOutput.moveCursor(QtGui.QTextCursor.End) self.txtEdtOutput.setTextColor(QtGui.QColor(color)) self.txtEdtOutput.insertPlainText(data) self.txtEdtOutput.moveCursor(QtGui.QTextCursor.End) self.txtEdtOutput.setTextColor(tc) def getPort(self): return self.cmbPort.currentText() def getDataBits(self): return {'5':serial.FIVEBITS, '6':serial.SIXBITS, '7':serial.SEVENBITS, '8':serial.EIGHTBITS}[self.cmbDataBits.currentText()] def getParity(self): return {'None' :serial.PARITY_NONE, 'Even' :serial.PARITY_EVEN, 'Odd' :serial.PARITY_ODD, 'Mark' :serial.PARITY_MARK, 'Space':serial.PARITY_SPACE}[self.cmbParity.currentText()] def getStopBits(self): return {'1' :serial.STOPBITS_ONE, '1.5':serial.STOPBITS_ONE_POINT_FIVE, '2' :serial.STOPBITS_TWO}[self.cmbStopBits.currentText()] def openPort(self): if self.serialport.isOpen(): return _port = self.getPort() if '' == _port: QMessageBox.information(self.defaultStyleWidget, "Invalid parameters", "Port is empty.") return _baudrate = self.cmbBaudRate.currentText() if '' == _baudrate: QMessageBox.information(self.defaultStyleWidget, "Invalid parameters", "Baudrate is empty.") return self.serialport.port = _port self.serialport.baudrate = _baudrate self.serialport.bytesize = self.getDataBits() self.serialport.stopbits = self.getStopBits() self.serialport.parity = self.getParity() self.serialport.rtscts = self.chkRTSCTS.isChecked() self.serialport.xonxoff = self.chkXonXoff.isChecked() # self.serialport.timeout = THREAD_TIMEOUT # self.serialport.writeTimeout = SERIAL_WRITE_TIMEOUT try: self.serialport.open() except Exception as e: QMessageBox.critical(self.defaultStyleWidget, "Could not open serial port", str(e), QMessageBox.Close) else: self._start_reader() self.setWindowTitle("%s on %s [%s, %s%s%s%s%s]" % ( appInfo.title, self.serialport.portstr, self.serialport.baudrate, self.serialport.bytesize, self.serialport.parity, self.serialport.stopbits, self.serialport.rtscts and ' RTS/CTS' or '', self.serialport.xonxoff and ' Xon/Xoff' or '', ) ) pal = self.btnOpen.palette() pal.setColor(QtGui.QPalette.Button, QtGui.QColor(0,0xff,0x7f)) self.btnOpen.setAutoFillBackground(True) self.btnOpen.setPalette(pal) self.btnOpen.setText('Close') self.btnOpen.update() def closePort(self): if self.serialport.isOpen(): self._stop_reader() self.serialport.close() self.setWindowTitle(appInfo.title) pal = self.btnOpen.style().standardPalette() self.btnOpen.setAutoFillBackground(True) self.btnOpen.setPalette(pal) self.btnOpen.setText('Open') self.btnOpen.update() def _start_reader(self): """Start reader thread""" self.receiver_thread.start() def _stop_reader(self): """Stop reader thread only, wait for clean exit of thread""" self.receiver_thread.join() def onTogglePrtCfgPnl(self): if self.actionPort_Config_Panel.isChecked(): self.dockWidget_PortConfig.show() else: self.dockWidget_PortConfig.hide() def onToggleQckSndPnl(self): if self.actionQuick_Send_Panel.isChecked(): self.dockWidget_QuickSend.show() else: self.dockWidget_QuickSend.hide() def onToggleHexPnl(self): if self.actionSend_Hex_Panel.isChecked(): self.dockWidget_SendHex.show() else: self.dockWidget_SendHex.hide() def onVisiblePrtCfgPnl(self, visible): self.actionPort_Config_Panel.setChecked(visible) def onVisibleQckSndPnl(self, visible): self.actionQuick_Send_Panel.setChecked(visible) def onVisibleHexPnl(self, visible): self.actionSend_Hex_Panel.setChecked(visible) def onLocalEcho(self): self._localEcho = self.actionLocal_Echo.isChecked() def onAlwaysOnTop(self): if self.actionAlways_On_Top.isChecked(): style = self.windowFlags() self.setWindowFlags(style|Qt.WindowStaysOnTopHint) self.show() else: style = self.windowFlags() self.setWindowFlags(style & ~Qt.WindowStaysOnTopHint) self.show() def onOpen(self): if self.serialport.isOpen(): self.closePort() else: self.openPort() def onClear(self): self.txtEdtOutput.clear() def onSaveLog(self): fileName = QFileDialog.getSaveFileName(self, "Save as", os.getcwd(), "Log files (*.log);;Text files (*.txt);;All files (*.*)")[0] if fileName: import codecs with codecs.open(fileName, 'w', 'utf-8') as f: f.write(self.txtEdtOutput.toPlainText()) def moveScreenCenter(self): w = self.frameGeometry().width() h = self.frameGeometry().height() desktop = QDesktopWidget() screenW = desktop.screen().width() screenH = desktop.screen().height() self.setGeometry((screenW-w)/2, (screenH-h)/2, w, h) def onEnumPorts(self): self.cmbPort.clear() for p in enum_ports(): self.cmbPort.addItem(p) def onAbout(self): QMessageBox.about(self.defaultStyleWidget, "About MyTerm", appInfo.aboutme) def onAboutQt(self): QMessageBox.aboutQt(self.defaultStyleWidget) def onExit(self): if self.serialport.isOpen(): self.closePort() self.close() def restoreLayout(self): if os.path.isfile(get_config_path("UILayout.dat")): try: f=open(get_config_path("UILayout.dat"), 'rb') geometry, state=pickle.load(f) self.restoreGeometry(geometry) self.restoreState(state) except Exception as e: print("Exception on restoreLayout, {}".format(e)) else: try: f=QFile(':/default_layout_qt5.dat') f.open(QIODevice.ReadOnly) geometry, state=pickle.loads(f.readAll()) self.restoreGeometry(geometry) self.restoreState(state) except Exception as e: print("Exception on restoreLayout, {}".format(e)) def saveLayout(self): with open(get_config_path("UILayout.dat"), 'wb') as f: pickle.dump((self.saveGeometry(), self.saveState()), f) def syncMenu(self): self.actionPort_Config_Panel.setChecked(not self.dockWidget_PortConfig.isHidden()) self.actionQuick_Send_Panel.setChecked(not self.dockWidget_QuickSend.isHidden()) self.actionSend_Hex_Panel.setChecked(not self.dockWidget_SendHex.isHidden()) def onViewChanged(self): checked = self._viewGroup.checkedAction() if checked is None: self._viewMode = VIEWMODE_HEX_UPPERCASE self.actionHEX_UPPERCASE.setChecked(True) else: if 'Ascii' in checked.text(): self._viewMode = VIEWMODE_ASCII elif 'lowercase' in checked.text(): self._viewMode = VIEWMODE_HEX_LOWERCASE elif 'UPPERCASE' in checked.text(): self._viewMode = VIEWMODE_HEX_UPPERCASE self.receiver_thread.setViewMode(self._viewMode)
class Actions(QObject): def __init__(self, controller: 'Controller') -> None: super().__init__() self.controller = controller self.make_actions() def make_actions(self) -> None: self.save_as = QAction(QIcon.fromTheme('save-as'), 'Save Filelist As', self) self.save_as.setShortcut('Ctrl+s') self.save_as.setStatusTip('Save the current file selection') self.save_as.triggered.connect(self.controller.save_as) def on_debug(enabled): if enabled: logging.getLogger().setLevel(logging.DEBUG) else: logging.getLogger().setLevel(logging.ERROR) self.debug = QAction(QIcon.fromTheme('media-record'), '&Debug', self, checkable=True) self.debug.setStatusTip('Debug application') self.debug.setChecked(logging.getLogger().getEffectiveLevel() == logging.DEBUG) self.debug.triggered.connect(lambda: on_debug(self.debug.isChecked())) self.exit = QAction(QIcon.fromTheme('window-close'), '&Exit', self) self.exit.setShortcut('Ctrl+W') self.exit.setStatusTip('Close Window') self.exit.triggered.connect(self.controller.close_window) self.home = QAction(QIcon.fromTheme('go-home'), '&Go to Home', self) self.home.setStatusTip('Go to the Home directory') self.home.triggered.connect(self.controller.go_home) self.undo = QAction(QIcon.fromTheme('undo'), '&Undo', self) self.undo.setShortcut('Ctrl+Z') self.undo.setStatusTip('Undo the last action') self.redo = QAction(QIcon.fromTheme('redo'), '&Redo', self) self.redo.setShortcut('Ctrl+Y') self.redo.setStatusTip('Redo the last action') self.edit_copy = QAction(QIcon.fromTheme('edit-copy'), '&Copy', self) self.edit_copy.setShortcut('Ctrl+C') self.edit_copy.setStatusTip('Copy Selected Files') self.edit_copy.triggered.connect(self.controller.on_edit_copy) self.edit_cut = QAction(QIcon.fromTheme('edit-cut'), 'Cu&t', self) self.edit_cut.setShortcut('Ctrl+X') self.edit_cut.setStatusTip('Cut Selected Files') self.edit_cut.triggered.connect(self.controller.on_edit_cut) self.edit_paste = QAction(QIcon.fromTheme('edit-paste'), '&Paste', self) self.edit_paste.setShortcut('Ctrl+V') self.edit_paste.setStatusTip('Paste Files') self.edit_paste.triggered.connect(self.controller.on_edit_paste) self.edit_delete = QAction(QIcon.fromTheme('edit-delete'), '&Delete', self) self.edit_delete.setStatusTip('Delete Selected Files') self.edit_select_all = QAction(QIcon.fromTheme('edit-select-all'), '&Select All', self) self.edit_select_all.setShortcut('Ctrl+A') self.edit_select_all.setStatusTip('Select All') self.edit_select_all.triggered.connect(self.controller.select_all) self.zoom_in = QAction(QIcon.fromTheme('zoom-in'), "Zoom &In", self) self.zoom_in.triggered.connect(self.controller.zoom_in) self.zoom_in.setShortcut('Ctrl+=') self.zoom_out = QAction(QIcon.fromTheme('zoom-out'), "Zoom &Out", self) self.zoom_out.triggered.connect(self.controller.zoom_out) self.zoom_out.setShortcut('Ctrl+-') self.lod_in = QAction(QIcon.fromTheme('zoom-in'), "Level of Detail &In", self) self.lod_in.triggered.connect(self.controller.more_details) self.lod_in.setShortcut('Alt+=') self.lod_out = QAction(QIcon.fromTheme('zoom-out'), "Level of Detail &Out", self) self.lod_out.triggered.connect(self.controller.less_details) self.lod_out.setShortcut('Alt+-') self.crop_thumbnails = QAction(QIcon.fromTheme('zoom-fit-best'), "Crop Thumbnails", self, checkable=True) self.crop_thumbnails.triggered.connect( lambda: self.controller.set_crop_thumbnails(self.crop_thumbnails.isChecked())) self.new_window = QAction(QIcon.fromTheme('window-new'), "New Window", self) self.new_window.triggered.connect(lambda x: self.controller.new_controller(clone=True)) self.new_window.setShortcut('Ctrl+N') self.parent_directory = QAction(self.controller.app.qapp.style().standardIcon(QStyle.SP_FileDialogToParent), "Parent Directory") self.parent_directory.triggered.connect(self.controller.parent_directory) self.back = QAction(QIcon.fromTheme('back'), 'Go &back', self) self.back.setShortcut('Alt+Left') self.back.setStatusTip('Go back in history') self.back.setEnabled(False) self.back.triggered.connect(self.controller.go_back) self.forward = QAction(QIcon.fromTheme('forward'), 'Go &forward', self) self.forward.setShortcut('Alt+Right') self.forward.setStatusTip('Go forward in history') self.forward.setEnabled(False) self.forward.triggered.connect(self.controller.go_forward) self.search = QAction(QIcon.fromTheme('system-search'), 'Search', self) self.search.setShortcut('F3') self.search.setStatusTip('Search for files') self.search.triggered.connect(self.controller.show_search) self.reload = QAction(QIcon.fromTheme('reload'), 'Reload', self) self.reload.setShortcut('F5') self.reload.setStatusTip('Reload the View') self.reload.triggered.connect(self.controller.reload) self.rename = QAction(QIcon.fromTheme('rename'), 'Rename', self) self.rename.setShortcut('F2') self.rename.setStatusTip('Rename the current file') self.rename.triggered.connect(lambda checked: self.controller.show_rename_dialog()) self.reload_thumbnails = QAction(QIcon.fromTheme('edit-delete'), 'Reload Thumbnails', self) self.reload_thumbnails.setStatusTip('Reload Thumbnails') self.reload_thumbnails.triggered.connect(self.controller.reload_thumbnails) self.reload_metadata = QAction(QIcon.fromTheme('edit-delete'), 'Reload MetaData', self) self.reload_metadata.setStatusTip('Reload MetaData') self.reload_metadata.triggered.connect(self.controller.reload_metadata) self.make_directory_thumbnails = QAction(QIcon.fromTheme('folder'), 'Make Directory Thumbnails', self) self.make_directory_thumbnails.setStatusTip('Make Directory Thumbnails') self.make_directory_thumbnails.triggered.connect(self.controller.make_directory_thumbnails) self.prepare = QAction(QIcon.fromTheme('media-playback-start'), 'Load Thumbnails', self) self.prepare.setShortcut('F6') self.prepare.setStatusTip('Load Thumbnails') self.prepare.triggered.connect(self.controller.prepare) self.view_detail_view = QAction("Detail View", self, checkable=True) self.view_icon_view = QAction("Icon View", self, checkable=True) self.view_small_icon_view = QAction("Small Icon View", self, checkable=True) self.view_icon_view = QAction(QIcon.fromTheme("view-grid-symbolic"), "Icon View") self.view_icon_view.triggered.connect(self.controller.view_icon_view) self.view_small_icon_view = QAction(QIcon.fromTheme("view-list-symbolic"), "Small Icon View") self.view_small_icon_view.triggered.connect(self.controller.view_small_icon_view) self.view_detail_view = QAction(QIcon.fromTheme("view-more-horizontal-symbolic"), "Detail View") self.view_detail_view.triggered.connect(self.controller.view_detail_view) self.view_group = QActionGroup(self) self.view_group.addAction(self.view_detail_view) self.view_group.addAction(self.view_icon_view) self.view_group.addAction(self.view_small_icon_view) self.show_hidden = QAction(QIcon.fromTheme('camera-photo'), "Show Hidden", self, checkable=True) self.show_hidden.triggered.connect(self.controller.show_hidden) self.show_hidden.setShortcut('Ctrl+H') self.show_hidden.setChecked(settings.value("globals/show_hidden", False, bool)) self.show_filtered = QAction(QIcon.fromTheme('camera-photo'), "Show Filtered", self, checkable=True) self.show_filtered.triggered.connect(self.controller.show_filtered) self.show_abspath = QAction("Show AbsPath", self, checkable=True) self.show_abspath.triggered.connect(self.controller.show_abspath) self.show_basename = QAction("Show Basename", self, checkable=True) self.show_basename.triggered.connect(self.controller.show_basename) self.path_options_group = QActionGroup(self) self.path_options_group.addAction(self.show_abspath) self.path_options_group.addAction(self.show_basename) self.toggle_timegaps = QAction("Show Time Gaps", self, checkable=True) self.toggle_timegaps.triggered.connect(self.controller.toggle_timegaps) # Sorting Options self.sort_directories_first = QAction("Directories First", checkable=True) self.sort_directories_first.triggered.connect( lambda: self.controller._sorter.set_directories_first(self.sort_directories_first.isChecked())) self.sort_directories_first.setChecked(True) self.sort_reversed = QAction("Reverse Sort", checkable=True) self.sort_reversed.triggered.connect( lambda: self.controller.set_sort_reversed(self.sort_reversed.isChecked())) self.sort_by_name = QAction("Sort by Name", checkable=True) self.sort_by_name.triggered.connect(lambda: self.controller.set_sort_key_func( lambda x: numeric_sort_key(x.basename().lower()))) self.sort_by_name.setChecked(True) self.sort_by_size = QAction("Sort by Size", checkable=True) self.sort_by_size.triggered.connect(lambda: self.controller.set_sort_key_func(FileInfo.size)) self.sort_by_ext = QAction("Sort by Extension", checkable=True) self.sort_by_ext.triggered.connect(lambda: self.controller.set_sort_key_func(FileInfo.ext)) self.sort_by_date = QAction("Sort by Date", checkable=True) self.sort_by_date.triggered.connect(lambda: self.controller.set_sort_key_func(FileInfo.mtime)) def framerate_key(fileinfo): metadata = fileinfo.metadata() return metadata.get('framerate', 0) self.sort_by_framerate = QAction("Sort by Framerate", checkable=True) self.sort_by_framerate.triggered.connect(lambda: self.controller.set_sort_key_func(framerate_key)) def aspect_ratio_key(fileinfo): metadata = fileinfo.metadata() if 'width' in metadata and 'height' in metadata and metadata['height'] != 0: return metadata['width'] / metadata['height'] else: return 0 self.sort_by_aspect_ratio = QAction("Sort by Aspect Ratio", checkable=True) self.sort_by_aspect_ratio.triggered.connect(lambda: self.controller.set_sort_key_func(aspect_ratio_key)) def area_key(fileinfo): metadata = fileinfo.metadata() if 'width' in metadata and 'height' in metadata: return metadata['width'] * metadata['height'] else: return 0 self.sort_by_area = QAction("Sort by Area", checkable=True) self.sort_by_area.triggered.connect(lambda: self.controller.set_sort_key_func(area_key)) def duration_key(fileinfo): metadata = fileinfo.metadata() if 'duration' in metadata: return metadata['duration'] else: return 0 self.sort_by_duration = QAction("Sort by Duration", checkable=True) self.sort_by_duration.triggered.connect(lambda: self.controller.set_sort_key_func(duration_key)) self.sort_by_user = QAction("Sort by User", checkable=True) self.sort_by_group = QAction("Sort by Group", checkable=True) self.sort_by_permission = QAction("Sort by Permission", checkable=True) self.sort_by_random = QAction("Random Shuffle", checkable=True) # self.sort_by_random.triggered.connect(lambda: self.controller.set_sort_key_func(None)) self.sort_by_random.triggered.connect(lambda: print("sort_by_random: not implemented")) self.sort_group = QActionGroup(self) self.sort_group.addAction(self.sort_by_name) self.sort_group.addAction(self.sort_by_size) self.sort_group.addAction(self.sort_by_ext) self.sort_group.addAction(self.sort_by_date) self.sort_group.addAction(self.sort_by_area) self.sort_group.addAction(self.sort_by_duration) self.sort_group.addAction(self.sort_by_aspect_ratio) self.sort_group.addAction(self.sort_by_framerate) self.sort_group.addAction(self.sort_by_user) self.sort_group.addAction(self.sort_by_group) self.sort_group.addAction(self.sort_by_permission) self.sort_group.addAction(self.sort_by_random) self.group_by_none = QAction("Don't Group", checkable=True) self.group_by_none.triggered.connect(self.controller.set_grouper_by_none) self.group_by_none.setChecked(True) self.group_by_day = QAction("Group by Day", checkable=True) self.group_by_day.triggered.connect(self.controller.set_grouper_by_day) self.group_by_directory = QAction("Group by Directory", checkable=True) self.group_by_directory.triggered.connect(self.controller.set_grouper_by_directory) self.group_by_duration = QAction("Group by Duration", checkable=True) self.group_by_duration.triggered.connect(self.controller.set_grouper_by_duration) self.group_group = QActionGroup(self) self.group_group.addAction(self.group_by_none) self.group_group.addAction(self.group_by_day) self.group_group.addAction(self.group_by_directory) self.group_group.addAction(self.group_by_duration) self.about = QAction(QIcon.fromTheme('help-about'), 'About dt-fileview', self) self.about.setStatusTip('Show About dialog') self.about.triggered.connect(self.controller.show_about_dialog) self.show_preferences = QAction(QIcon.fromTheme("preferences-system"), "Preferencs...") self.show_preferences.triggered.connect(self.controller.show_preferences) def on_filter_pin(checked) -> None: # FIXME: Could use icon state for this if checked: self.filter_pin.setIcon(QIcon.fromTheme("remmina-pin-down")) else: self.filter_pin.setIcon(QIcon.fromTheme("remmina-pin-up")) self.filter_pin = QAction(QIcon.fromTheme("remmina-pin-up"), "Pin the filter", checkable=True) self.filter_pin.triggered.connect(self.controller.set_filter_pin) # self.filter_pin.setToolTip("Pin the filter") self.filter_pin.triggered.connect(on_filter_pin)
class MainUI(QtWidgets.QMainWindow, main_window_class): connectionLostSignal = pyqtSignal(str, str) connectionInitiatedSignal = pyqtSignal(str) batteryUpdatedSignal = pyqtSignal(int, object, object) connectionDoneSignal = pyqtSignal(str) connectionFailedSignal = pyqtSignal(str, str) disconnectedSignal = pyqtSignal(str) linkQualitySignal = pyqtSignal(int) _input_device_error_signal = pyqtSignal(str) _input_discovery_signal = pyqtSignal(object) _log_error_signal = pyqtSignal(object, str) def __init__(self, *args): super(MainUI, self).__init__(*args) self.setupUi(self) # Restore window size if present in the config file try: size = Config().get("window_size") self.resize(size[0], size[1]) except KeyError: pass ###################################################### # By lxrocks # 'Skinny Progress Bar' tweak for Yosemite # Tweak progress bar - artistic I am not - so pick your own colors !!! # Only apply to Yosemite ###################################################### import platform if platform.system() == 'Darwin': (Version, junk, machine) = platform.mac_ver() logger.info("This is a MAC - checking if we can apply Progress " "Bar Stylesheet for Yosemite Skinny Bars ") yosemite = (10, 10, 0) tVersion = tuple(map(int, (Version.split(".")))) if tVersion >= yosemite: logger.info("Found Yosemite - applying stylesheet") tcss = """ QProgressBar { border: 1px solid grey; border-radius: 5px; text-align: center; } QProgressBar::chunk { background-color: """ + COLOR_BLUE + """; } """ self.setStyleSheet(tcss) else: logger.info("Pre-Yosemite - skinny bar stylesheet not applied") ###################################################### self.cf = Crazyflie(ro_cache=None, rw_cache=cfclient.config_path + "/cache") cflib.crtp.init_drivers(enable_debug_driver=Config() .get("enable_debug_driver")) zmq_params = ZMQParamAccess(self.cf) zmq_params.start() zmq_leds = ZMQLEDDriver(self.cf) zmq_leds.start() self.scanner = ScannerThread() self.scanner.interfaceFoundSignal.connect(self.foundInterfaces) self.scanner.start() # Create and start the Input Reader self._statusbar_label = QLabel("No input-device found, insert one to" " fly.") self.statusBar().addWidget(self._statusbar_label) self.joystickReader = JoystickReader() self._active_device = "" # self.configGroup = QActionGroup(self._menu_mappings, exclusive=True) self._mux_group = QActionGroup(self._menu_inputdevice, exclusive=True) # TODO: Need to reload configs # ConfigManager().conf_needs_reload.add_callback(self._reload_configs) self.connect_input = QShortcut("Ctrl+I", self.connectButton, self._connect) self.cf.connection_failed.add_callback( self.connectionFailedSignal.emit) self.connectionFailedSignal.connect(self._connection_failed) self._input_device_error_signal.connect( self._display_input_device_error) self.joystickReader.device_error.add_callback( self._input_device_error_signal.emit) self._input_discovery_signal.connect(self.device_discovery) self.joystickReader.device_discovery.add_callback( self._input_discovery_signal.emit) # Hide the 'File' menu on OS X, since its only item, 'Exit', gets # merged into the application menu. if sys.platform == 'darwin': self.menuFile.menuAction().setVisible(False) # Connect UI signals self.logConfigAction.triggered.connect(self._show_connect_dialog) self.interfaceCombo.currentIndexChanged['QString'].connect( self.interfaceChanged) self.connectButton.clicked.connect(self._connect) self.scanButton.clicked.connect(self._scan) self.menuItemConnect.triggered.connect(self._connect) self.menuItemConfInputDevice.triggered.connect( self._show_input_device_config_dialog) self.menuItemExit.triggered.connect(self.closeAppRequest) self.batteryUpdatedSignal.connect(self._update_battery) self._menuitem_rescandevices.triggered.connect(self._rescan_devices) self._menuItem_openconfigfolder.triggered.connect( self._open_config_folder) self.address.setValue(0xE7E7E7E7E7) self._auto_reconnect_enabled = Config().get("auto_reconnect") self.autoReconnectCheckBox.toggled.connect( self._auto_reconnect_changed) self.autoReconnectCheckBox.setChecked(Config().get("auto_reconnect")) self._disable_input = False self.joystickReader.input_updated.add_callback( lambda *args: self._disable_input or self.cf.commander.send_setpoint(*args)) self.joystickReader.assisted_input_updated.add_callback( lambda *args: self._disable_input or self.cf.commander.send_velocity_world_setpoint(*args)) self.joystickReader.heighthold_input_updated.add_callback( lambda *args: self._disable_input or self.cf.commander.send_zdistance_setpoint(*args)) self.joystickReader.hover_input_updated.add_callback( self.cf.commander.send_hover_setpoint) # Connection callbacks and signal wrappers for UI protection self.cf.connected.add_callback(self.connectionDoneSignal.emit) self.connectionDoneSignal.connect(self._connected) self.cf.disconnected.add_callback(self.disconnectedSignal.emit) self.disconnectedSignal.connect(self._disconnected) self.cf.connection_lost.add_callback(self.connectionLostSignal.emit) self.connectionLostSignal.connect(self._connection_lost) self.cf.connection_requested.add_callback( self.connectionInitiatedSignal.emit) self.connectionInitiatedSignal.connect(self._connection_initiated) self._log_error_signal.connect(self._logging_error) self.batteryBar.setTextVisible(False) self.batteryBar.setStyleSheet(progressbar_stylesheet(COLOR_BLUE)) self.linkQualityBar.setTextVisible(False) self.linkQualityBar.setStyleSheet(progressbar_stylesheet(COLOR_BLUE)) # Connect link quality feedback self.cf.link_quality_updated.add_callback(self.linkQualitySignal.emit) self.linkQualitySignal.connect( lambda percentage: self.linkQualityBar.setValue(percentage)) self._selected_interface = None self._initial_scan = True self._scan() # Parse the log configuration files self.logConfigReader = LogConfigReader(self.cf) self._current_input_config = None self._active_config = None self._active_config = None self.inputConfig = None # Add things to helper so tabs can access it cfclient.ui.pluginhelper.cf = self.cf cfclient.ui.pluginhelper.inputDeviceReader = self.joystickReader cfclient.ui.pluginhelper.logConfigReader = self.logConfigReader cfclient.ui.pluginhelper.mainUI = self self.logConfigDialogue = LogConfigDialogue(cfclient.ui.pluginhelper) self._bootloader_dialog = BootloaderDialog(cfclient.ui.pluginhelper) self._cf2config_dialog = Cf2ConfigDialog(cfclient.ui.pluginhelper) self._cf1config_dialog = Cf1ConfigDialog(cfclient.ui.pluginhelper) self.menuItemBootloader.triggered.connect(self._bootloader_dialog.show) self._about_dialog = AboutDialog(cfclient.ui.pluginhelper) self.menuItemAbout.triggered.connect(self._about_dialog.show) self._menu_cf2_config.triggered.connect(self._cf2config_dialog.show) self._menu_cf1_config.triggered.connect(self._cf1config_dialog.show) # Load and connect tabs self.tabsMenuItem = QMenu("Tabs", self.menuView, enabled=True) self.menuView.addMenu(self.tabsMenuItem) # self.tabsMenuItem.setMenu(QtWidgets.QMenu()) tabItems = {} self.loadedTabs = [] for tabClass in cfclient.ui.tabs.available: tab = tabClass(self.tabs, cfclient.ui.pluginhelper) item = QtWidgets.QAction(tab.getMenuName(), self, checkable=True) item.toggled.connect(tab.toggleVisibility) self.tabsMenuItem.addAction(item) tabItems[tab.getTabName()] = item self.loadedTabs.append(tab) if not tab.enabled: item.setEnabled(False) # First instantiate all tabs and then open them in the correct order try: for tName in Config().get("open_tabs").split(","): t = tabItems[tName] if (t is not None and t.isEnabled()): # Toggle though menu so it's also marked as open there t.toggle() except Exception as e: logger.warning("Exception while opening tabs [{}]".format(e)) # Loading toolboxes (A bit of magic for a lot of automatic) self.toolboxesMenuItem = QMenu("Toolboxes", self.menuView, enabled=True) self.menuView.addMenu(self.toolboxesMenuItem) self.toolboxes = [] for t_class in cfclient.ui.toolboxes.toolboxes: toolbox = t_class(cfclient.ui.pluginhelper) dockToolbox = MyDockWidget(toolbox.getName()) dockToolbox.setWidget(toolbox) self.toolboxes += [dockToolbox, ] # Add menu item for the toolbox item = QtWidgets.QAction(toolbox.getName(), self) item.setCheckable(True) item.triggered.connect(self.toggleToolbox) self.toolboxesMenuItem.addAction(item) dockToolbox.closed.connect(lambda: self.toggleToolbox(False)) # Setup some introspection item.dockToolbox = dockToolbox item.menuItem = item dockToolbox.dockToolbox = dockToolbox dockToolbox.menuItem = item # References to all the device sub-menus in the "Input device" menu self._all_role_menus = () # Used to filter what new devices to add default mapping to self._available_devices = () # Keep track of mux nodes so we can enable according to how many # devices we have self._all_mux_nodes = () # Check which Input muxes are available self._mux_group = QActionGroup(self._menu_inputdevice, exclusive=True) for m in self.joystickReader.available_mux(): node = QAction(m.name, self._menu_inputdevice, checkable=True, enabled=False) node.toggled.connect(self._mux_selected) self._mux_group.addAction(node) self._menu_inputdevice.addAction(node) self._all_mux_nodes += (node,) mux_subnodes = () for name in m.supported_roles(): sub_node = QMenu(" {}".format(name), self._menu_inputdevice, enabled=False) self._menu_inputdevice.addMenu(sub_node) mux_subnodes += (sub_node,) self._all_role_menus += ({"muxmenu": node, "rolemenu": sub_node},) node.setData((m, mux_subnodes)) self._mapping_support = True def disable_input(self, disable): """ Disable the gamepad input to be able to send setpoint from a tab """ self._disable_input = disable def interfaceChanged(self, interface): if interface == INTERFACE_PROMPT_TEXT: self._selected_interface = None else: self._selected_interface = interface self._update_ui_state() def foundInterfaces(self, interfaces): selected_interface = self._selected_interface self.interfaceCombo.clear() self.interfaceCombo.addItem(INTERFACE_PROMPT_TEXT) formatted_interfaces = [] for i in interfaces: if len(i[1]) > 0: interface = "%s - %s" % (i[0], i[1]) else: interface = i[0] formatted_interfaces.append(interface) self.interfaceCombo.addItems(formatted_interfaces) if self._initial_scan: self._initial_scan = False try: if len(Config().get("link_uri")) > 0: formatted_interfaces.index(Config().get("link_uri")) selected_interface = Config().get("link_uri") except KeyError: # The configuration for link_uri was not found pass except ValueError: # The saved URI was not found while scanning pass if len(interfaces) == 1 and selected_interface is None: selected_interface = interfaces[0][0] newIndex = 0 if selected_interface is not None: try: newIndex = formatted_interfaces.index(selected_interface) + 1 except ValueError: pass self.interfaceCombo.setCurrentIndex(newIndex) self.uiState = UIState.DISCONNECTED self._update_ui_state() def _update_ui_state(self): if self.uiState == UIState.DISCONNECTED: self.setWindowTitle("Not connected") canConnect = self._selected_interface is not None self.menuItemConnect.setText("Connect to Crazyflie") self.menuItemConnect.setEnabled(canConnect) self.connectButton.setText("Connect") self.connectButton.setToolTip("Connect to the Crazyflie on" "the selected interface (Ctrl+I)") self.connectButton.setEnabled(canConnect) self.scanButton.setText("Scan") self.scanButton.setEnabled(True) self.address.setEnabled(True) self.batteryBar.setValue(3000) self._menu_cf2_config.setEnabled(False) self._menu_cf1_config.setEnabled(True) self.linkQualityBar.setValue(0) self.menuItemBootloader.setEnabled(True) self.logConfigAction.setEnabled(False) self.interfaceCombo.setEnabled(True) elif self.uiState == UIState.CONNECTED: s = "Connected on %s" % self._selected_interface self.setWindowTitle(s) self.menuItemConnect.setText("Disconnect") self.menuItemConnect.setEnabled(True) self.connectButton.setText("Disconnect") self.connectButton.setToolTip("Disconnect from" "the Crazyflie (Ctrl+I)") self.scanButton.setEnabled(False) self.logConfigAction.setEnabled(True) # Find out if there's an I2C EEPROM, otherwise don't show the # dialog. if len(self.cf.mem.get_mems(MemoryElement.TYPE_I2C)) > 0: self._menu_cf2_config.setEnabled(True) self._menu_cf1_config.setEnabled(False) elif self.uiState == UIState.CONNECTING: s = "Connecting to {} ...".format(self._selected_interface) self.setWindowTitle(s) self.menuItemConnect.setText("Cancel") self.menuItemConnect.setEnabled(True) self.connectButton.setText("Cancel") self.connectButton.setToolTip( "Cancel connecting to the Crazyflie") self.scanButton.setEnabled(False) self.address.setEnabled(False) self.menuItemBootloader.setEnabled(False) self.interfaceCombo.setEnabled(False) elif self.uiState == UIState.SCANNING: self.setWindowTitle("Scanning ...") self.connectButton.setText("Connect") self.menuItemConnect.setEnabled(False) self.connectButton.setText("Connect") self.connectButton.setEnabled(False) self.scanButton.setText("Scanning...") self.scanButton.setEnabled(False) self.address.setEnabled(False) self.menuItemBootloader.setEnabled(False) self.interfaceCombo.setEnabled(False) @pyqtSlot(bool) def toggleToolbox(self, display): menuItem = self.sender().menuItem dockToolbox = self.sender().dockToolbox if display and not dockToolbox.isVisible(): dockToolbox.widget().enable() self.addDockWidget(dockToolbox.widget().preferedDockArea(), dockToolbox) dockToolbox.show() elif not display: dockToolbox.widget().disable() self.removeDockWidget(dockToolbox) dockToolbox.hide() menuItem.setChecked(False) def _rescan_devices(self): self._statusbar_label.setText("No inputdevice connected!") self._menu_devices.clear() self._active_device = "" self.joystickReader.stop_input() # for c in self._menu_mappings.actions(): # c.setEnabled(False) # devs = self.joystickReader.available_devices() # if (len(devs) > 0): # self.device_discovery(devs) def _show_input_device_config_dialog(self): self.inputConfig = InputConfigDialogue(self.joystickReader) self.inputConfig.show() def _auto_reconnect_changed(self, checked): self._auto_reconnect_enabled = checked Config().set("auto_reconnect", checked) logger.info("Auto reconnect enabled: {}".format(checked)) def _show_connect_dialog(self): self.logConfigDialogue.show() def _update_battery(self, timestamp, data, logconf): self.batteryBar.setValue(int(data["pm.vbat"] * 1000)) color = COLOR_BLUE # TODO firmware reports fully-charged state as 'Battery', # rather than 'Charged' if data["pm.state"] in [BatteryStates.CHARGING, BatteryStates.CHARGED]: color = COLOR_GREEN elif data["pm.state"] == BatteryStates.LOW_POWER: color = COLOR_RED self.batteryBar.setStyleSheet(progressbar_stylesheet(color)) self._aff_volts.setText(("%.3f" % data["pm.vbat"])) def _connected(self): self.uiState = UIState.CONNECTED self._update_ui_state() Config().set("link_uri", str(self._selected_interface)) lg = LogConfig("Battery", 1000) lg.add_variable("pm.vbat", "float") lg.add_variable("pm.state", "int8_t") try: self.cf.log.add_config(lg) lg.data_received_cb.add_callback(self.batteryUpdatedSignal.emit) lg.error_cb.add_callback(self._log_error_signal.emit) lg.start() except KeyError as e: logger.warning(str(e)) mems = self.cf.mem.get_mems(MemoryElement.TYPE_DRIVER_LED) if len(mems) > 0: mems[0].write_data(self._led_write_done) def _disconnected(self): self.uiState = UIState.DISCONNECTED self._update_ui_state() def _connection_initiated(self): self.uiState = UIState.CONNECTING self._update_ui_state() def _led_write_done(self, mem, addr): logger.info("LED write done callback") def _logging_error(self, log_conf, msg): QMessageBox.about(self, "Log error", "Error when starting log config" " [{}]: {}".format(log_conf.name, msg)) def _connection_lost(self, linkURI, msg): if not self._auto_reconnect_enabled: if self.isActiveWindow(): warningCaption = "Communication failure" error = "Connection lost to {}: {}".format(linkURI, msg) QMessageBox.critical(self, warningCaption, error) self.uiState = UIState.DISCONNECTED self._update_ui_state() else: self._connect() def _connection_failed(self, linkURI, error): if not self._auto_reconnect_enabled: msg = "Failed to connect on {}: {}".format(linkURI, error) warningCaption = "Communication failure" QMessageBox.critical(self, warningCaption, msg) self.uiState = UIState.DISCONNECTED self._update_ui_state() else: self._connect() def closeEvent(self, event): self.hide() self.cf.close_link() Config().save_file() def resizeEvent(self, event): Config().set("window_size", [event.size().width(), event.size().height()]) def _connect(self): if self.uiState == UIState.CONNECTED: self.cf.close_link() elif self.uiState == UIState.CONNECTING: self.cf.close_link() self.uiState = UIState.DISCONNECTED self._update_ui_state() else: self.cf.open_link(self._selected_interface) def _scan(self): self.uiState = UIState.SCANNING self._update_ui_state() self.scanner.scanSignal.emit(self.address.value()) def _display_input_device_error(self, error): self.cf.close_link() QMessageBox.critical(self, "Input device error", error) def _mux_selected(self, checked): """Called when a new mux is selected. The menu item contains a reference to the raw mux object as well as to the associated device sub-nodes""" if not checked: (mux, sub_nodes) = self.sender().data() for s in sub_nodes: s.setEnabled(False) else: (mux, sub_nodes) = self.sender().data() for s in sub_nodes: s.setEnabled(True) self.joystickReader.set_mux(mux=mux) # Go though the tree and select devices/mapping that was # selected before it was disabled. for role_node in sub_nodes: for dev_node in role_node.children(): if type(dev_node) is QAction and dev_node.isChecked(): dev_node.toggled.emit(True) self._update_input_device_footer() def _get_dev_status(self, device): msg = "{}".format(device.name) if device.supports_mapping: map_name = "No input mapping" if device.input_map: map_name = device.input_map_name msg += " ({})".format(map_name) return msg def _update_input_device_footer(self): """Update the footer in the bottom of the UI with status for the input device and its mapping""" msg = "" if len(self.joystickReader.available_devices()) > 0: mux = self.joystickReader._selected_mux msg = "Using {} mux with ".format(mux.name) for key in list(mux._devs.keys())[:-1]: if mux._devs[key]: msg += "{}, ".format(self._get_dev_status(mux._devs[key])) else: msg += "N/A, " # Last item key = list(mux._devs.keys())[-1] if mux._devs[key]: msg += "{}".format(self._get_dev_status(mux._devs[key])) else: msg += "N/A" else: msg = "No input device found" self._statusbar_label.setText(msg) def _inputdevice_selected(self, checked): """Called when a new input device has been selected from the menu. The data in the menu object is the associated map menu (directly under the item in the menu) and the raw device""" (map_menu, device, mux_menu) = self.sender().data() if not checked: if map_menu: map_menu.setEnabled(False) # Do not close the device, since we don't know exactly # how many devices the mux can have open. When selecting a # new mux the old one will take care of this. else: if map_menu: map_menu.setEnabled(True) (mux, sub_nodes) = mux_menu.data() for role_node in sub_nodes: for dev_node in role_node.children(): if type(dev_node) is QAction and dev_node.isChecked(): if device.id == dev_node.data()[1].id \ and dev_node is not self.sender(): dev_node.setChecked(False) role_in_mux = str(self.sender().parent().title()).strip() logger.info("Role of {} is {}".format(device.name, role_in_mux)) Config().set("input_device", str(device.name)) self._mapping_support = self.joystickReader.start_input( device.name, role_in_mux) self._update_input_device_footer() def _inputconfig_selected(self, checked): """Called when a new configuration has been selected from the menu. The data in the menu object is a referance to the device QAction in parent menu. This contains a referance to the raw device.""" if not checked: return selected_mapping = str(self.sender().text()) device = self.sender().data().data()[1] self.joystickReader.set_input_map(device.name, selected_mapping) self._update_input_device_footer() def device_discovery(self, devs): """Called when new devices have been added""" for menu in self._all_role_menus: role_menu = menu["rolemenu"] mux_menu = menu["muxmenu"] dev_group = QActionGroup(role_menu, exclusive=True) for d in devs: dev_node = QAction(d.name, role_menu, checkable=True, enabled=True) role_menu.addAction(dev_node) dev_group.addAction(dev_node) dev_node.toggled.connect(self._inputdevice_selected) map_node = None if d.supports_mapping: map_node = QMenu(" Input map", role_menu, enabled=False) map_group = QActionGroup(role_menu, exclusive=True) # Connect device node to map node for easy # enabling/disabling when selection changes and device # to easily enable it dev_node.setData((map_node, d)) for c in ConfigManager().get_list_of_configs(): node = QAction(c, map_node, checkable=True, enabled=True) node.toggled.connect(self._inputconfig_selected) map_node.addAction(node) # Connect all the map nodes back to the device # action node where we can access the raw device node.setData(dev_node) map_group.addAction(node) # If this device hasn't been found before, then # select the default mapping for it. if d not in self._available_devices: last_map = Config().get("device_config_mapping") if d.name in last_map and last_map[d.name] == c: node.setChecked(True) role_menu.addMenu(map_node) dev_node.setData((map_node, d, mux_menu)) # Update the list of what devices we found # to avoid selecting default mapping for all devices when # a new one is inserted self._available_devices = () for d in devs: self._available_devices += (d,) # Only enable MUX nodes if we have enough devies to cover # the roles for mux_node in self._all_mux_nodes: (mux, sub_nodes) = mux_node.data() if len(mux.supported_roles()) <= len(self._available_devices): mux_node.setEnabled(True) # TODO: Currently only supports selecting default mux if self._all_mux_nodes[0].isEnabled(): self._all_mux_nodes[0].setChecked(True) # If the previous length of the available devies was 0, then select # the default on. If that's not available then select the first # on in the list. # TODO: This will only work for the "Normal" mux so this will be # selected by default if Config().get("input_device") in [d.name for d in devs]: for dev_menu in self._all_role_menus[0]["rolemenu"].actions(): if dev_menu.text() == Config().get("input_device"): dev_menu.setChecked(True) else: # Select the first device in the first mux (will always be "Normal" # mux) self._all_role_menus[0]["rolemenu"].actions()[0].setChecked(True) logger.info("Select first device") self._update_input_device_footer() def _open_config_folder(self): QDesktopServices.openUrl( QUrl("file:///" + QDir.toNativeSeparators(cfclient.config_path))) def closeAppRequest(self): self.close() sys.exit(0)
class SpreadSheet(QMainWindow): dateFormats = ["dd/M/yyyy", "yyyy/M/dd", "dd.MM.yyyy"] currentDateFormat = dateFormats[0] def __init__(self, rows, cols, parent = None): super(SpreadSheet, self).__init__(parent) self.toolBar = QToolBar() self.addToolBar(self.toolBar) self.formulaInput = QLineEdit() self.cellLabel = QLabel(self.toolBar) self.cellLabel.setMinimumSize(80, 0) self.toolBar.addWidget(self.cellLabel) self.toolBar.addWidget(self.formulaInput) self.table = QTableWidget(rows, cols, self) for c in range(cols): character = chr(ord('A') + c) self.table.setHorizontalHeaderItem(c, QTableWidgetItem(character)) self.table.setItemPrototype(self.table.item(rows - 1, cols - 1)) self.table.setItemDelegate(SpreadSheetDelegate(self)) self.createActions() self.updateColor(0) self.setupMenuBar() self.setupContents() self.setupContextMenu() self.setCentralWidget(self.table) self.statusBar() self.table.currentItemChanged.connect(self.updateStatus) self.table.currentItemChanged.connect(self.updateColor) self.table.currentItemChanged.connect(self.updateLineEdit) self.table.itemChanged.connect(self.updateStatus) self.formulaInput.returnPressed.connect(self.returnPressed) self.table.itemChanged.connect(self.updateLineEdit) self.setWindowTitle("Spreadsheet") # self.circle_widget = QDockWidget(self) # self.circle_widget.setWidget(circleWidget(self)) # self.addDockWidget(Qt.AllDockWidgetAreas, self.circle_widget) # Create a sample listener and controller self.listener = handListener() self.controller = Leap.Controller() # def paintEvent(self, event): # self.painterIncetance = QPainter(self) # self.penCircle = QPen(Qt.red) # self.penCircle.setWidth(3) # self.painterIncetance.setPen(self.penCircle) # self.painterIncetance.setBrush(Qt.green) # self.painterIncetance.drawRect(20, 20, 100, 100) def createActions(self): self.cell_leapAction = QAction("Leap", self) self.cell_leapAction.triggered.connect(self.actionLeap) self.cell_sumAction = QAction("Sum", self) self.cell_sumAction.triggered.connect(self.actionSum) self.cell_addAction = QAction("&Add", self) self.cell_addAction.setShortcut(Qt.CTRL | Qt.Key_Plus) self.cell_addAction.triggered.connect(self.actionAdd) self.cell_subAction = QAction("&Subtract", self) self.cell_subAction.setShortcut(Qt.CTRL | Qt.Key_Minus) self.cell_subAction.triggered.connect(self.actionSubtract) self.cell_mulAction = QAction("&Multiply", self) self.cell_mulAction.setShortcut(Qt.CTRL | Qt.Key_multiply) self.cell_mulAction.triggered.connect(self.actionMultiply) self.cell_divAction = QAction("&Divide", self) self.cell_divAction.setShortcut(Qt.CTRL | Qt.Key_division) self.cell_divAction.triggered.connect(self.actionDivide) self.fontAction = QAction("Font...", self) self.fontAction.setShortcut(Qt.CTRL | Qt.Key_F) self.fontAction.triggered.connect(self.selectFont) self.colorAction = QAction(QIcon(QPixmap(16, 16)), "Background &Color...", self) self.colorAction.triggered.connect(self.selectColor) self.clearAction = QAction("Clear", self) self.clearAction.setShortcut(Qt.Key_Delete) self.clearAction.triggered.connect(self.clear) self.aboutSpreadSheet = QAction("About Spreadsheet", self) self.aboutSpreadSheet.triggered.connect(self.showAbout) self.exitAction = QAction("E&xit", self) self.exitAction.setShortcut(QKeySequence.Quit) self.exitAction.triggered.connect(QApplication.instance().quit) self.printAction = QAction("&Print", self) self.printAction.setShortcut(QKeySequence.Print) self.printAction.triggered.connect(self.print_) self.firstSeparator = QAction(self) self.firstSeparator.setSeparator(True) self.secondSeparator = QAction(self) self.secondSeparator.setSeparator(True) def setupMenuBar(self): self.fileMenu = self.menuBar().addMenu("&File") self.dateFormatMenu = self.fileMenu.addMenu("&Date format") self.dateFormatGroup = QActionGroup(self) for f in self.dateFormats: action = QAction(f, self, checkable=True, triggered=self.changeDateFormat) self.dateFormatGroup.addAction(action) self.dateFormatMenu.addAction(action) if f == self.currentDateFormat: action.setChecked(True) self.fileMenu.addAction(self.printAction) self.fileMenu.addAction(self.exitAction) self.cellMenu = self.menuBar().addMenu("&Cell") self.cellMenu.addAction(self.cell_addAction) self.cellMenu.addAction(self.cell_subAction) self.cellMenu.addAction(self.cell_mulAction) self.cellMenu.addAction(self.cell_divAction) self.cellMenu.addAction(self.cell_sumAction) self.cellMenu.addAction(self.cell_leapAction) self.cellMenu.addSeparator() self.cellMenu.addAction(self.colorAction) self.cellMenu.addAction(self.fontAction) self.menuBar().addSeparator() self.aboutMenu = self.menuBar().addMenu("&Help") self.aboutMenu.addAction(self.aboutSpreadSheet) def changeDateFormat(self): action = self.sender() oldFormat = self.currentDateFormat newFormat = self.currentDateFormat = action.text() for row in range(self.table.rowCount()): item = self.table.item(row, 1) date = QDate.fromString(item.text(), oldFormat) item.setText(date.toString(newFormat)) def updateStatus(self, item): if item and item == self.table.currentItem(): self.statusBar().showMessage(item.data(Qt.StatusTipRole), 1000) self.cellLabel.setText("Cell: (%s)" % encode_pos(self.table.row(item), self.table.column(item))) def updateColor(self, item): pixmap = QPixmap(16, 16) color = QColor() # if item: # color = item.backgroundColor() if not color.isValid(): color = self.palette().base().color() painter = QPainter(pixmap) painter.fillRect(0, 0, 16, 16, color) lighter = color.lighter() painter.setPen(lighter) # light frame painter.drawPolyline(QPoint(0, 15), QPoint(0, 0), QPoint(15, 0)) painter.setPen(color.darker()) # dark frame painter.drawPolyline(QPoint(1, 15), QPoint(15, 15), QPoint(15, 1)) painter.end() self.colorAction.setIcon(QIcon(pixmap)) def updateLineEdit(self, item): if item != self.table.currentItem(): return if item: self.formulaInput.setText(item.data(Qt.EditRole)) else: self.formulaInput.clear() def returnPressed(self): text = self.formulaInput.text() row = self.table.currentRow() col = self.table.currentColumn() item = self.table.item(row, col) if not item: self.table.setItem(row, col, SpreadSheetItem(text)) else: item.setData(Qt.EditRole, text) self.table.viewport().update() def selectColor(self): item = self.table.currentItem() color = item and QColor(item.background()) or self.table.palette().base().color() color = QColorDialog.getColor(color, self) if not color.isValid(): return selected = self.table.selectedItems() if not selected: return for i in selected: i and i.setBackground(color) self.updateColor(self.table.currentItem()) def selectFont(self): selected = self.table.selectedItems() if not selected: return font, ok = QFontDialog.getFont(self.font(), self) if not ok: return for i in selected: i and i.setFont(font) def runInputDialog(self, title, c1Text, c2Text, opText, outText, cell1, cell2, outCell): rows = [] cols = [] for r in range(self.table.rowCount()): rows.append(str(r + 1)) for c in range(self.table.columnCount()): cols.append(chr(ord('A') + c)) addDialog = QDialog(self) addDialog.setWindowTitle(title) group = QGroupBox(title, addDialog) group.setMinimumSize(250, 100) cell1Label = QLabel(c1Text, group) cell1RowInput = QComboBox(group) c1Row, c1Col = decode_pos(cell1) cell1RowInput.addItems(rows) cell1RowInput.setCurrentIndex(c1Row) cell1ColInput = QComboBox(group) cell1ColInput.addItems(cols) cell1ColInput.setCurrentIndex(c1Col) operatorLabel = QLabel(opText, group) operatorLabel.setAlignment(Qt.AlignHCenter) cell2Label = QLabel(c2Text, group) cell2RowInput = QComboBox(group) c2Row, c2Col = decode_pos(cell2) cell2RowInput.addItems(rows) cell2RowInput.setCurrentIndex(c2Row) cell2ColInput = QComboBox(group) cell2ColInput.addItems(cols) cell2ColInput.setCurrentIndex(c2Col) equalsLabel = QLabel("=", group) equalsLabel.setAlignment(Qt.AlignHCenter) outLabel = QLabel(outText, group) outRowInput = QComboBox(group) outRow, outCol = decode_pos(outCell) outRowInput.addItems(rows) outRowInput.setCurrentIndex(outRow) outColInput = QComboBox(group) outColInput.addItems(cols) outColInput.setCurrentIndex(outCol) cancelButton = QPushButton("Cancel", addDialog) cancelButton.clicked.connect(addDialog.reject) okButton = QPushButton("OK", addDialog) okButton.setDefault(True) okButton.clicked.connect(addDialog.accept) buttonsLayout = QHBoxLayout() buttonsLayout.addStretch(1) buttonsLayout.addWidget(okButton) buttonsLayout.addSpacing(10) buttonsLayout.addWidget(cancelButton) dialogLayout = QVBoxLayout(addDialog) dialogLayout.addWidget(group) dialogLayout.addStretch(1) dialogLayout.addItem(buttonsLayout) cell1Layout = QHBoxLayout() cell1Layout.addWidget(cell1Label) cell1Layout.addSpacing(10) cell1Layout.addWidget(cell1ColInput) cell1Layout.addSpacing(10) cell1Layout.addWidget(cell1RowInput) cell2Layout = QHBoxLayout() cell2Layout.addWidget(cell2Label) cell2Layout.addSpacing(10) cell2Layout.addWidget(cell2ColInput) cell2Layout.addSpacing(10) cell2Layout.addWidget(cell2RowInput) outLayout = QHBoxLayout() outLayout.addWidget(outLabel) outLayout.addSpacing(10) outLayout.addWidget(outColInput) outLayout.addSpacing(10) outLayout.addWidget(outRowInput) vLayout = QVBoxLayout(group) vLayout.addItem(cell1Layout) vLayout.addWidget(operatorLabel) vLayout.addItem(cell2Layout) vLayout.addWidget(equalsLabel) vLayout.addStretch(1) vLayout.addItem(outLayout) if addDialog.exec_(): cell1 = cell1ColInput.currentText() + cell1RowInput.currentText() cell2 = cell2ColInput.currentText() + cell2RowInput.currentText() outCell = outColInput.currentText() + outRowInput.currentText() return True, cell1, cell2, outCell return False, None, None, None def actionSum(self): row_first = 0 row_last = 0 row_cur = 0 col_first = 0 col_last = 0 col_cur = 0 selected = self.table.selectedItems() if selected: first = selected[0] last = selected[-1] row_first = self.table.row(first) row_last = self.table.row(last) col_first = self.table.column(first) col_last = self.table.column(last) current = self.table.currentItem() if current: row_cur = self.table.row(current) col_cur = self.table.column(current) cell1 = encode_pos(row_first, col_first) cell2 = encode_pos(row_last, col_last) out = encode_pos(row_cur, col_cur) ok, cell1, cell2, out = self.runInputDialog("Sum cells", "First cell:", "Last cell:", u"\N{GREEK CAPITAL LETTER SIGMA}", "Output to:", cell1, cell2, out) if ok: row, col = decode_pos(out) self.table.item(row, col).setText("sum %s %s" % (cell1, cell2)) def actionMath_helper(self, title, op): cell1 = "C1" cell2 = "C2" out = "C3" current = self.table.currentItem() if current: out = encode_pos(self.table.currentRow(), self.table.currentColumn()) ok, cell1, cell2, out = self.runInputDialog(title, "Cell 1", "Cell 2", op, "Output to:", cell1, cell2, out) if ok: row, col = decode_pos(out) self.table.item(row, col).setText("%s %s %s" % (op, cell1, cell2)) def actionAdd(self): self.actionMath_helper("Addition", "+") def actionSubtract(self): self.actionMath_helper("Subtraction", "-") def actionMultiply(self): self.actionMath_helper("Multiplication", "*") def actionDivide(self): self.actionMath_helper("Division", "/") def clear(self): for i in self.table.selectedItems(): i.setText("") def actionLeap(self): # Have the sample listener receive events from the controller self.controller.add_listener(self.listener) def setupContextMenu(self): self.addAction(self.cell_leapAction) self.addAction(self.cell_addAction) self.addAction(self.cell_subAction) self.addAction(self.cell_mulAction) self.addAction(self.cell_divAction) self.addAction(self.cell_sumAction) self.addAction(self.firstSeparator) self.addAction(self.colorAction) self.addAction(self.fontAction) self.addAction(self.secondSeparator) self.addAction(self.clearAction) self.setContextMenuPolicy(Qt.ActionsContextMenu) def setupContents(self): titleBackground = QColor(Qt.lightGray) titleFont = self.table.font() titleFont.setBold(True) # column 0 self.table.setItem(0, 0, SpreadSheetItem("Item")) self.table.item(0, 0).setBackground(titleBackground) self.table.item(0, 0).setToolTip("This column shows the purchased item/service") self.table.item(0, 0).setFont(titleFont) self.table.setItem(1, 0, SpreadSheetItem("AirportBus")) self.table.setItem(2, 0, SpreadSheetItem("Flight (Munich)")) self.table.setItem(3, 0, SpreadSheetItem("Lunch")) self.table.setItem(4, 0, SpreadSheetItem("Flight (LA)")) self.table.setItem(5, 0, SpreadSheetItem("Taxi")) self.table.setItem(6, 0, SpreadSheetItem("Dinner")) self.table.setItem(7, 0, SpreadSheetItem("Hotel")) self.table.setItem(8, 0, SpreadSheetItem("Flight (Oslo)")) self.table.setItem(9, 0, SpreadSheetItem("Total:")) self.table.item(9, 0).setFont(titleFont) self.table.item(9, 0).setBackground(Qt.lightGray) # column 1 self.table.setItem(0, 1, SpreadSheetItem("Date")) self.table.item(0, 1).setBackground(titleBackground) self.table.item(0, 1).setToolTip("This column shows the purchase date, double click to change") self.table.item(0, 1).setFont(titleFont) self.table.setItem(1, 1, SpreadSheetItem("15/6/2006")) self.table.setItem(2, 1, SpreadSheetItem("15/6/2006")) self.table.setItem(3, 1, SpreadSheetItem("15/6/2006")) self.table.setItem(4, 1, SpreadSheetItem("21/5/2006")) self.table.setItem(5, 1, SpreadSheetItem("16/6/2006")) self.table.setItem(6, 1, SpreadSheetItem("16/6/2006")) self.table.setItem(7, 1, SpreadSheetItem("16/6/2006")) self.table.setItem(8, 1, SpreadSheetItem("18/6/2006")) self.table.setItem(9, 1, SpreadSheetItem()) self.table.item(9, 1).setBackground(Qt.lightGray) # column 2 self.table.setItem(0, 2, SpreadSheetItem("Price")) self.table.item(0, 2).setBackground(titleBackground) self.table.item(0, 2).setToolTip("This column shows the price of the purchase") self.table.item(0, 2).setFont(titleFont) self.table.setItem(1, 2, SpreadSheetItem("150")) self.table.setItem(2, 2, SpreadSheetItem("2350")) self.table.setItem(3, 2, SpreadSheetItem("-14")) self.table.setItem(4, 2, SpreadSheetItem("980")) self.table.setItem(5, 2, SpreadSheetItem("5")) self.table.setItem(6, 2, SpreadSheetItem("120")) self.table.setItem(7, 2, SpreadSheetItem("300")) self.table.setItem(8, 2, SpreadSheetItem("1240")) self.table.setItem(9, 2, SpreadSheetItem()) self.table.item(9, 2).setBackground(Qt.lightGray) # column 3 self.table.setItem(0, 3, SpreadSheetItem("Currency")) self.table.item(0, 3).setBackground(titleBackground) self.table.item(0, 3).setToolTip("This column shows the currency") self.table.item(0, 3).setFont(titleFont) self.table.setItem(1, 3, SpreadSheetItem("NOK")) self.table.setItem(2, 3, SpreadSheetItem("NOK")) self.table.setItem(3, 3, SpreadSheetItem("EUR")) self.table.setItem(4, 3, SpreadSheetItem("EUR")) self.table.setItem(5, 3, SpreadSheetItem("USD")) self.table.setItem(6, 3, SpreadSheetItem("USD")) self.table.setItem(7, 3, SpreadSheetItem("USD")) self.table.setItem(8, 3, SpreadSheetItem("USD")) self.table.setItem(9, 3, SpreadSheetItem()) self.table.item(9,3).setBackground(Qt.lightGray) # column 4 self.table.setItem(0, 4, SpreadSheetItem("Ex. Rate")) self.table.item(0, 4).setBackground(titleBackground) self.table.item(0, 4).setToolTip("This column shows the exchange rate to NOK") self.table.item(0, 4).setFont(titleFont) self.table.setItem(1, 4, SpreadSheetItem("1")) self.table.setItem(2, 4, SpreadSheetItem("1")) self.table.setItem(3, 4, SpreadSheetItem("8")) self.table.setItem(4, 4, SpreadSheetItem("8")) self.table.setItem(5, 4, SpreadSheetItem("7")) self.table.setItem(6, 4, SpreadSheetItem("7")) self.table.setItem(7, 4, SpreadSheetItem("7")) self.table.setItem(8, 4, SpreadSheetItem("7")) self.table.setItem(9, 4, SpreadSheetItem()) self.table.item(9,4).setBackground(Qt.lightGray) # column 5 self.table.setItem(0, 5, SpreadSheetItem("NOK")) self.table.item(0, 5).setBackground(titleBackground) self.table.item(0, 5).setToolTip("This column shows the expenses in NOK") self.table.item(0, 5).setFont(titleFont) self.table.setItem(1, 5, SpreadSheetItem("* C2 E2")) self.table.setItem(2, 5, SpreadSheetItem("* C3 E3")) self.table.setItem(3, 5, SpreadSheetItem("* C4 E4")) self.table.setItem(4, 5, SpreadSheetItem("* C5 E5")) self.table.setItem(5, 5, SpreadSheetItem("* C6 E6")) self.table.setItem(6, 5, SpreadSheetItem("* C7 E7")) self.table.setItem(7, 5, SpreadSheetItem("* C8 E8")) self.table.setItem(8, 5, SpreadSheetItem("* C9 E9")) self.table.setItem(9, 5, SpreadSheetItem("sum F2 F9")) self.table.item(9,5).setBackground(Qt.lightGray) def showAbout(self): QMessageBox.about(self, "About Spreadsheet", """ <HTML> <p><b>This demo shows use of <c>QTableWidget</c> with custom handling for individual cells.</b></p> <p>Using a customized table item we make it possible to have dynamic output in different cells. The content that is implemented for this particular demo is: <ul> <li>Adding two cells.</li> <li>Subtracting one cell from another.</li> <li>Multiplying two cells.</li> <li>Dividing one cell with another.</li> <li>Summing the contents of an arbitrary number of cells.</li> </HTML> """) def print_(self): printer = QPrinter(QPrinter.ScreenResolution) dlg = QPrintPreviewDialog(printer) view = PrintView() view.setModel(self.table.model()) dlg.paintRequested.connect(view.print_) dlg.exec_()
class Menu: """ Menu class. """ # List of available backends Backends_List = { "Backend: JIT": True, "Backend: JIT + CUDA": BiotSavart_CUDA.is_available() } def __init__(self, gui): """ Creates the menu bar. @param gui: GUI """ self.gui = gui # List of checkboxes that are bound to configuration self.config_bound_checkboxes = {} # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # File menu file_menu = QMenu("&File", self.gui) file_menu.addAction(qta.icon("fa.folder"), "&Open File …", self.gui.file_open, Qt.CTRL + Qt.Key_O) file_menu.addAction(qta.icon("fa.save"), "&Save File", self.gui.file_save, Qt.CTRL + Qt.Key_S) file_menu.addAction( qta.icon("fa.save"), "Save File &As …", self.gui.file_save_as, Qt.CTRL + Qt.SHIFT + Qt.Key_S ) file_menu.addSeparator() file_menu.addAction(qta.icon("fa.picture-o"), "Save &Image …", self.gui.file_save_image, Qt.CTRL + Qt.Key_I) file_menu.addSeparator() file_menu.addAction(qta.icon("fa.window-close"), "&Quit", self.gui.close, Qt.CTRL + Qt.Key_Q) self.gui.menuBar().addMenu(file_menu) # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Wire menu wire_menu = QMenu("&Load Wire Preset", self.gui) for preset in Wire_Presets.List: action = QAction(preset["id"], wire_menu) action.setIcon(qta.icon("mdi.vector-square")) action.triggered.connect( partial(self.gui.sidebar_left.wire_widget.set_wire, points=preset["points"]) ) wire_menu.addAction(action) self.gui.menuBar().addMenu(wire_menu) # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # View menu view_menu = QMenu("&View", self.gui) self.add_config_bound_checkbox("Show Wire Segments", "show_wire_segments", view_menu, self.gui.redraw) self.add_config_bound_checkbox("Show Wire Points", "show_wire_points", view_menu, self.gui.redraw) view_menu.addSeparator() self.add_config_bound_checkbox("Show Colored Labels", "show_colored_labels", view_menu, self.gui.redraw) view_menu.addSeparator() self.add_config_bound_checkbox("Show Coordinate System", "show_coordinate_system", view_menu, self.gui.redraw) self.add_config_bound_checkbox("Show Perspective Info", "show_perspective_info", view_menu, self.gui.redraw) view_menu.addSeparator() self.add_config_bound_checkbox("Dark Background", "dark_background", view_menu, self.gui.redraw) self.gui.menuBar().addMenu(view_menu) # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Options menu options_menu = QMenu("&Options", self.gui) self.options_backend_group = QActionGroup(self.gui) self.options_backend_group.setExclusive(True) self.gui.blockSignals(True) self.backend_actions = [] for i, item in enumerate(self.Backends_List.items()): name, enabled = item action = QAction(name) self.backend_actions.append(action) action.setCheckable(True) action.setEnabled(enabled) action.changed.connect(partial(self.on_backend_changed, i)) self.options_backend_group.addAction(action) options_menu.addAction(action) self.gui.blockSignals(False) self.gui.menuBar().addMenu(options_menu) # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Help menu help_menu = QMenu("&Help", self.gui) help_menu.addAction(qta.icon("fa.info"), "&Usage", lambda: Usage_Dialog().show(), Qt.Key_F1) help_menu.addSeparator() help_menu.addAction(qta.icon("fa.coffee"), "&About", lambda: About_Dialog().show()) self.gui.menuBar().addMenu(help_menu) # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - self.reinitialize() def reinitialize(self): """ Re-initializes the menu. """ Debug(self, ".reinitialize()") self.gui.blockSignals(True) # Default to JIT backend if CUDA backend is selected but not available if self.gui.config.get_int("backend") == 1: if not BiotSavart_CUDA.is_available(): self.gui.config.set_int("backend", 0) for i, name in enumerate(self.Backends_List): self.backend_actions[i].setChecked(self.gui.config.get_int("backend") == i) self.reinitialize_config_bound_checkboxes() self.gui.blockSignals(False) # ------------------------------------------------------------------------------------------------------------------ def on_backend_changed(self, index): """ Gets called when the backend changed. @param index: Backend list index """ if self.gui.signalsBlocked(): return if self.backend_actions[index].isChecked(): self.gui.config.set_int("backend", index) self.gui.sidebar_right.field_widget.set_field() # ------------------------------------------------------------------------------------------------------------------ def add_config_bound_checkbox(self, label: str, key: str, menu, callback): """ Creates a checkbox inside some menu. Checkbox state is bound to configuration. @param label: Checkbox label @param key: Configuration key @param menu: Menu @param callback: """ checkbox = QAction(label, menu) checkbox.setCheckable(True) checkbox.triggered.connect(partial(self.config_bound_checkbox_changed, key)) self.config_bound_checkboxes[key] = {"checkbox": checkbox, "callback_final": callback} checkbox.setChecked(self.gui.config.get_bool(key)) menu.addAction(checkbox) def config_bound_checkbox_changed(self, key: str): """ Handles change of checkbox state. @param key: Configuration key """ self.gui.config.set_bool(key, self.config_bound_checkboxes[key]["checkbox"].isChecked()) self.config_bound_checkboxes[key]["callback_final"]() def reinitialize_config_bound_checkboxes(self): """ Re-initializes the configuration bound checkboxes. Note: This doesn't block the change signals. """ for item in self.config_bound_checkboxes.items(): key, dictionary = item dictionary["checkbox"].setChecked(self.gui.config.get_bool(key))
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() widget = QWidget() self.setCentralWidget(widget) topFiller = QWidget() topFiller.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.infoLabel = QLabel( "<i>Choose a menu option, or right-click to invoke a context menu</i>", alignment=Qt.AlignCenter, ) self.infoLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) bottomFiller = QWidget() bottomFiller.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) vbox = QVBoxLayout() vbox.setContentsMargins(5, 5, 5, 5) vbox.addWidget(topFiller) vbox.addWidget(self.infoLabel) vbox.addWidget(bottomFiller) widget.setLayout(vbox) self.createActions() self.createMenus() message = "A context menu is available by right-clicking" self.statusBar().showMessage(message) self.setWindowTitle("Menus") self.setMinimumSize(160, 160) self.resize(480, 320) def contextMenuEvent(self, event): menu = QMenu(self) menu.addAction(self.cutAct) menu.addAction(self.copyAct) menu.addAction(self.pasteAct) menu.exec_(event.globalPos()) def newFile(self): self.infoLabel.setText("Invoked <b>File|New</b>") def open(self): self.infoLabel.setText("Invoked <b>File|Open</b>") def save(self): self.infoLabel.setText("Invoked <b>File|Save</b>") def print_(self): self.infoLabel.setText("Invoked <b>File|Print</b>") def undo(self): self.infoLabel.setText("Invoked <b>Edit|Undo</b>") def redo(self): self.infoLabel.setText("Invoked <b>Edit|Redo</b>") def cut(self): self.infoLabel.setText("Invoked <b>Edit|Cut</b>") def copy(self): self.infoLabel.setText("Invoked <b>Edit|Copy</b>") def paste(self): self.infoLabel.setText("Invoked <b>Edit|Paste</b>") def bold(self): self.infoLabel.setText("Invoked <b>Edit|Format|Bold</b>") def italic(self): self.infoLabel.setText("Invoked <b>Edit|Format|Italic</b>") def leftAlign(self): self.infoLabel.setText("Invoked <b>Edit|Format|Left Align</b>") def rightAlign(self): self.infoLabel.setText("Invoked <b>Edit|Format|Right Align</b>") def justify(self): self.infoLabel.setText("Invoked <b>Edit|Format|Justify</b>") def center(self): self.infoLabel.setText("Invoked <b>Edit|Format|Center</b>") def setLineSpacing(self): self.infoLabel.setText("Invoked <b>Edit|Format|Set Line Spacing</b>") def setParagraphSpacing(self): self.infoLabel.setText( "Invoked <b>Edit|Format|Set Paragraph Spacing</b>") def about(self): self.infoLabel.setText("Invoked <b>Help|About</b>") QMessageBox.about( self, "About Menu", "The <b>Menu</b> example shows how to create menu-bar menus " "and context menus.", ) def aboutQt(self): self.infoLabel.setText("Invoked <b>Help|About Qt</b>") def createActions(self): self.newAct = QAction( "&New", self, shortcut=QKeySequence.New, statusTip="Create a new file", triggered=self.newFile, ) self.openAct = QAction( "&Open...", self, shortcut=QKeySequence.Open, statusTip="Open an existing file", triggered=self.open, ) self.saveAct = QAction( "&Save", self, shortcut=QKeySequence.Save, statusTip="Save the document to disk", triggered=self.save, ) self.printAct = QAction( "&Print...", self, shortcut=QKeySequence.Print, statusTip="Print the document", triggered=self.print_, ) self.exitAct = QAction( "E&xit", self, shortcut="Ctrl+Q", statusTip="Exit the application", triggered=self.close, ) self.undoAct = QAction( "&Undo", self, shortcut=QKeySequence.Undo, statusTip="Undo the last operation", triggered=self.undo, ) self.redoAct = QAction( "&Redo", self, shortcut=QKeySequence.Redo, statusTip="Redo the last operation", triggered=self.redo, ) self.cutAct = QAction( "Cu&t", self, shortcut=QKeySequence.Cut, statusTip="Cut the current selection's contents to the clipboard", triggered=self.cut, ) self.copyAct = QAction( "&Copy", self, shortcut=QKeySequence.Copy, statusTip="Copy the current selection's contents to the clipboard", triggered=self.copy, ) self.pasteAct = QAction( "&Paste", self, shortcut=QKeySequence.Paste, statusTip= "Paste the clipboard's contents into the current selection", triggered=self.paste, ) self.boldAct = QAction( "&Bold", self, checkable=True, shortcut="Ctrl+B", statusTip="Make the text bold", triggered=self.bold, ) boldFont = self.boldAct.font() boldFont.setBold(True) self.boldAct.setFont(boldFont) self.italicAct = QAction( "&Italic", self, checkable=True, shortcut="Ctrl+I", statusTip="Make the text italic", triggered=self.italic, ) italicFont = self.italicAct.font() italicFont.setItalic(True) self.italicAct.setFont(italicFont) self.setLineSpacingAct = QAction( "Set &Line Spacing...", self, statusTip="Change the gap between the lines of a paragraph", triggered=self.setLineSpacing, ) self.setParagraphSpacingAct = QAction( "Set &Paragraph Spacing...", self, statusTip="Change the gap between paragraphs", triggered=self.setParagraphSpacing, ) self.aboutAct = QAction( "&About", self, statusTip="Show the application's About box", triggered=self.about, ) self.aboutQtAct = QAction( "About &Qt", self, statusTip="Show the Qt library's About box", triggered=self.aboutQt, ) self.aboutQtAct.triggered.connect(QApplication.instance().aboutQt) self.leftAlignAct = QAction( "&Left Align", self, checkable=True, shortcut="Ctrl+L", statusTip="Left align the selected text", triggered=self.leftAlign, ) self.rightAlignAct = QAction( "&Right Align", self, checkable=True, shortcut="Ctrl+R", statusTip="Right align the selected text", triggered=self.rightAlign, ) self.justifyAct = QAction( "&Justify", self, checkable=True, shortcut="Ctrl+J", statusTip="Justify the selected text", triggered=self.justify, ) self.centerAct = QAction( "&Center", self, checkable=True, shortcut="Ctrl+C", statusTip="Center the selected text", triggered=self.center, ) self.alignmentGroup = QActionGroup(self) self.alignmentGroup.addAction(self.leftAlignAct) self.alignmentGroup.addAction(self.rightAlignAct) self.alignmentGroup.addAction(self.justifyAct) self.alignmentGroup.addAction(self.centerAct) self.leftAlignAct.setChecked(True) def createMenus(self): self.fileMenu = self.menuBar().addMenu("&File") self.fileMenu.addAction(self.newAct) self.fileMenu.addAction(self.openAct) self.fileMenu.addAction(self.saveAct) self.fileMenu.addAction(self.printAct) self.fileMenu.addSeparator() self.fileMenu.addAction(self.exitAct) self.editMenu = self.menuBar().addMenu("&Edit") self.editMenu.addAction(self.undoAct) self.editMenu.addAction(self.redoAct) self.editMenu.addSeparator() self.editMenu.addAction(self.cutAct) self.editMenu.addAction(self.copyAct) self.editMenu.addAction(self.pasteAct) self.editMenu.addSeparator() self.helpMenu = self.menuBar().addMenu("&Help") self.helpMenu.addAction(self.aboutAct) self.helpMenu.addAction(self.aboutQtAct) self.formatMenu = self.editMenu.addMenu("&Format") self.formatMenu.addAction(self.boldAct) self.formatMenu.addAction(self.italicAct) self.formatMenu.addSeparator().setText("Alignment") self.formatMenu.addAction(self.leftAlignAct) self.formatMenu.addAction(self.rightAlignAct) self.formatMenu.addAction(self.justifyAct) self.formatMenu.addAction(self.centerAct) self.formatMenu.addSeparator() self.formatMenu.addAction(self.setLineSpacingAct) self.formatMenu.addAction(self.setParagraphSpacingAct)
class ReTextWindow(QMainWindow): def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.resize(950, 700) screenRect = QDesktopWidget().screenGeometry() if globalSettings.windowGeometry: self.restoreGeometry(globalSettings.windowGeometry) else: self.move((screenRect.width()-self.width())/2, (screenRect.height()-self.height())/2) if not screenRect.contains(self.geometry()): self.showMaximized() if sys.platform.startswith('darwin'): # https://github.com/retext-project/retext/issues/198 searchPaths = QIcon.themeSearchPaths() searchPaths.append('/opt/local/share/icons') searchPaths.append('/usr/local/share/icons') QIcon.setThemeSearchPaths(searchPaths) if globalSettings.iconTheme: QIcon.setThemeName(globalSettings.iconTheme) if QIcon.themeName() in ('hicolor', ''): if not QFile.exists(getBundledIcon('document-new')): QIcon.setThemeName(get_icon_theme()) if QFile.exists(getBundledIcon('retext')): self.setWindowIcon(QIcon(getBundledIcon('retext'))) elif QFile.exists('/usr/share/pixmaps/retext.png'): self.setWindowIcon(QIcon('/usr/share/pixmaps/retext.png')) else: self.setWindowIcon(QIcon.fromTheme('retext', QIcon.fromTheme('accessories-text-editor'))) self.tabWidget = QTabWidget(self) self.initTabWidget() self.setCentralWidget(self.tabWidget) self.tabWidget.currentChanged.connect(self.changeIndex) self.tabWidget.tabCloseRequested.connect(self.closeTab) self.toolBar = QToolBar(self.tr('File toolbar'), self) self.addToolBar(Qt.TopToolBarArea, self.toolBar) self.editBar = QToolBar(self.tr('Edit toolbar'), self) self.addToolBar(Qt.TopToolBarArea, self.editBar) self.searchBar = QToolBar(self.tr('Search toolbar'), self) self.addToolBar(Qt.BottomToolBarArea, self.searchBar) self.toolBar.setVisible(not globalSettings.hideToolBar) self.editBar.setVisible(not globalSettings.hideToolBar) self.actionNew = self.act(self.tr('New'), 'document-new', self.createNew, shct=QKeySequence.New) self.actionNew.setPriority(QAction.LowPriority) self.actionOpen = self.act(self.tr('Open'), 'document-open', self.openFile, shct=QKeySequence.Open) self.actionOpen.setPriority(QAction.LowPriority) self.actionSetEncoding = self.act(self.tr('Set encoding'), trig=self.showEncodingDialog) self.actionSetEncoding.setEnabled(False) self.actionReload = self.act(self.tr('Reload'), 'view-refresh', lambda: self.currentTab.readTextFromFile()) self.actionReload.setEnabled(False) self.actionSave = self.act(self.tr('Save'), 'document-save', self.saveFile, shct=QKeySequence.Save) self.actionSave.setEnabled(False) self.actionSave.setPriority(QAction.LowPriority) self.actionSaveAs = self.act(self.tr('Save as'), 'document-save-as', self.saveFileAs, shct=QKeySequence.SaveAs) self.actionNextTab = self.act(self.tr('Next tab'), 'go-next', lambda: self.switchTab(1), shct=Qt.CTRL+Qt.Key_PageDown) self.actionPrevTab = self.act(self.tr('Previous tab'), 'go-previous', lambda: self.switchTab(-1), shct=Qt.CTRL+Qt.Key_PageUp) self.actionCloseCurrentTab = self.act(self.tr('Close tab'), 'window-close', lambda: self.closeTab(self.ind), shct=QKeySequence.Close) self.actionPrint = self.act(self.tr('Print'), 'document-print', self.printFile, shct=QKeySequence.Print) self.actionPrint.setPriority(QAction.LowPriority) self.actionPrintPreview = self.act(self.tr('Print preview'), 'document-print-preview', self.printPreview) self.actionViewHtml = self.act(self.tr('View HTML code'), 'text-html', self.viewHtml) self.actionChangeEditorFont = self.act(self.tr('Change editor font'), trig=self.changeEditorFont) self.actionChangePreviewFont = self.act(self.tr('Change preview font'), trig=self.changePreviewFont) self.actionSearch = self.act(self.tr('Find text'), 'edit-find', self.search, shct=QKeySequence.Find) self.actionGoToLine = self.act(self.tr('Go to line'), trig=self.goToLine, shct=Qt.CTRL+Qt.Key_G) self.searchBar.visibilityChanged.connect(self.searchBarVisibilityChanged) self.actionPreview = self.act(self.tr('Preview'), shct=Qt.CTRL+Qt.Key_E, trigbool=self.preview) if QIcon.hasThemeIcon('document-preview'): self.actionPreview.setIcon(QIcon.fromTheme('document-preview')) elif QIcon.hasThemeIcon('preview-file'): self.actionPreview.setIcon(QIcon.fromTheme('preview-file')) elif QIcon.hasThemeIcon('x-office-document'): self.actionPreview.setIcon(QIcon.fromTheme('x-office-document')) else: self.actionPreview.setIcon(QIcon(getBundledIcon('document-preview'))) self.actionLivePreview = self.act(self.tr('Live preview'), shct=Qt.CTRL+Qt.Key_L, trigbool=self.enableLivePreview) menuPreview = QMenu() menuPreview.addAction(self.actionLivePreview) self.actionPreview.setMenu(menuPreview) self.actionInsertTable = self.act(self.tr('Insert table'), trig=lambda: self.insertFormatting('table')) self.actionTableMode = self.act(self.tr('Table editing mode'), shct=Qt.CTRL+Qt.Key_T, trigbool=lambda x: self.currentTab.editBox.enableTableMode(x)) if ReTextFakeVimHandler: self.actionFakeVimMode = self.act(self.tr('FakeVim mode'), shct=Qt.CTRL+Qt.ALT+Qt.Key_V, trigbool=self.enableFakeVimMode) if globalSettings.useFakeVim: self.actionFakeVimMode.setChecked(True) self.enableFakeVimMode(True) self.actionFullScreen = self.act(self.tr('Fullscreen mode'), 'view-fullscreen', shct=Qt.Key_F11, trigbool=self.enableFullScreen) self.actionFullScreen.setChecked(self.isFullScreen()) self.actionFullScreen.setPriority(QAction.LowPriority) self.actionConfig = self.act(self.tr('Preferences'), icon='preferences-system', trig=self.openConfigDialog) self.actionConfig.setMenuRole(QAction.PreferencesRole) self.actionSaveHtml = self.act('HTML', 'text-html', self.saveFileHtml) self.actionPdf = self.act('PDF', 'application-pdf', self.savePdf) self.actionOdf = self.act('ODT', 'x-office-document', self.saveOdf) self.getExportExtensionsList() self.actionQuit = self.act(self.tr('Quit'), 'application-exit', shct=QKeySequence.Quit) self.actionQuit.setMenuRole(QAction.QuitRole) self.actionQuit.triggered.connect(self.close) self.actionUndo = self.act(self.tr('Undo'), 'edit-undo', lambda: self.currentTab.editBox.undo(), shct=QKeySequence.Undo) self.actionRedo = self.act(self.tr('Redo'), 'edit-redo', lambda: self.currentTab.editBox.redo(), shct=QKeySequence.Redo) self.actionCopy = self.act(self.tr('Copy'), 'edit-copy', lambda: self.currentTab.editBox.copy(), shct=QKeySequence.Copy) self.actionCut = self.act(self.tr('Cut'), 'edit-cut', lambda: self.currentTab.editBox.cut(), shct=QKeySequence.Cut) self.actionPaste = self.act(self.tr('Paste'), 'edit-paste', lambda: self.currentTab.editBox.paste(), shct=QKeySequence.Paste) self.actionPasteImage = self.act(self.tr('Paste image'), 'edit-paste', lambda: self.currentTab.editBox.pasteImage(), shct=Qt.CTRL+Qt.SHIFT+Qt.Key_V) self.actionMoveUp = self.act(self.tr('Move line up'), 'go-up', lambda: self.currentTab.editBox.moveLineUp(), shct=Qt.ALT+Qt.Key_Up) self.actionMoveDown = self.act(self.tr('Move line down'), 'go-down', lambda: self.currentTab.editBox.moveLineDown(), shct=Qt.ALT+Qt.Key_Down) self.actionUndo.setEnabled(False) self.actionRedo.setEnabled(False) self.actionCopy.setEnabled(False) self.actionCut.setEnabled(False) qApp = QApplication.instance() qApp.clipboard().dataChanged.connect(self.clipboardDataChanged) self.clipboardDataChanged() if enchant is not None: self.actionEnableSC = self.act(self.tr('Enable'), trigbool=self.enableSpellCheck) self.actionSetLocale = self.act(self.tr('Set locale'), trig=self.changeLocale) self.actionWebKit = self.act(self.tr('Use WebKit renderer'), trigbool=self.enableWebKit) if ReTextWebKitPreview is None: globalSettings.useWebKit = False self.actionWebKit.setEnabled(False) self.actionWebKit.setChecked(globalSettings.useWebKit) self.actionWebEngine = self.act(self.tr('Use WebEngine (Chromium) renderer'), trigbool=self.enableWebEngine) if ReTextWebEnginePreview is None: globalSettings.useWebEngine = False self.actionWebEngine.setChecked(globalSettings.useWebEngine) self.actionShow = self.act(self.tr('Show directory'), 'system-file-manager', self.showInDir) self.actionFind = self.act(self.tr('Next'), 'go-next', self.find, shct=QKeySequence.FindNext) self.actionFindPrev = self.act(self.tr('Previous'), 'go-previous', lambda: self.find(back=True), shct=QKeySequence.FindPrevious) self.actionReplace = self.act(self.tr('Replace'), 'edit-find-replace', lambda: self.find(replace=True)) self.actionReplaceAll = self.act(self.tr('Replace all'), trig=self.replaceAll) menuReplace = QMenu() menuReplace.addAction(self.actionReplaceAll) self.actionReplace.setMenu(menuReplace) self.actionCloseSearch = self.act(self.tr('Close'), 'window-close', lambda: self.searchBar.setVisible(False), shct=QKeySequence.Cancel) self.actionCloseSearch.setPriority(QAction.LowPriority) self.actionHelp = self.act(self.tr('Get help online'), 'help-contents', self.openHelp) self.aboutWindowTitle = self.tr('About ReText') self.actionAbout = self.act(self.aboutWindowTitle, 'help-about', self.aboutDialog) self.actionAbout.setMenuRole(QAction.AboutRole) self.actionAboutQt = self.act(self.tr('About Qt')) self.actionAboutQt.setMenuRole(QAction.AboutQtRole) self.actionAboutQt.triggered.connect(qApp.aboutQt) availableMarkups = markups.get_available_markups() if not availableMarkups: print('Warning: no markups are available!') if len(availableMarkups) > 1: self.chooseGroup = QActionGroup(self) markupActions = [] for markup in availableMarkups: markupAction = self.act(markup.name, trigbool=self.markupFunction(markup)) if markup.name == globalSettings.defaultMarkup: markupAction.setChecked(True) self.chooseGroup.addAction(markupAction) markupActions.append(markupAction) self.actionBold = self.act(self.tr('Bold'), shct=QKeySequence.Bold, trig=lambda: self.insertFormatting('bold')) self.actionItalic = self.act(self.tr('Italic'), shct=QKeySequence.Italic, trig=lambda: self.insertFormatting('italic')) self.actionUnderline = self.act(self.tr('Underline'), shct=QKeySequence.Underline, trig=lambda: self.insertFormatting('underline')) self.usefulTags = ('header', 'italic', 'bold', 'underline', 'numbering', 'bullets', 'image', 'link', 'inline code', 'code block', 'blockquote', 'table') self.usefulChars = ('deg', 'divide', 'euro', 'hellip', 'laquo', 'larr', 'lsquo', 'mdash', 'middot', 'minus', 'nbsp', 'ndash', 'raquo', 'rarr', 'rsquo', 'times') self.formattingBox = QComboBox(self.editBar) self.formattingBox.addItem(self.tr('Formatting')) self.formattingBox.addItems(self.usefulTags) self.formattingBox.activated[str].connect(self.insertFormatting) self.symbolBox = QComboBox(self.editBar) self.symbolBox.addItem(self.tr('Symbols')) self.symbolBox.addItems(self.usefulChars) self.symbolBox.activated.connect(self.insertSymbol) self.updateStyleSheet() menubar = self.menuBar() menuFile = menubar.addMenu(self.tr('File')) menuEdit = menubar.addMenu(self.tr('Edit')) menuHelp = menubar.addMenu(self.tr('Help')) menuFile.addAction(self.actionNew) menuFile.addAction(self.actionOpen) self.menuRecentFiles = menuFile.addMenu(self.tr('Open recent')) self.menuRecentFiles.aboutToShow.connect(self.updateRecentFiles) menuFile.addAction(self.actionShow) menuFile.addAction(self.actionSetEncoding) menuFile.addAction(self.actionReload) menuFile.addSeparator() menuFile.addAction(self.actionSave) menuFile.addAction(self.actionSaveAs) menuFile.addSeparator() menuFile.addAction(self.actionNextTab) menuFile.addAction(self.actionPrevTab) menuFile.addAction(self.actionCloseCurrentTab) menuFile.addSeparator() menuExport = menuFile.addMenu(self.tr('Export')) menuExport.addAction(self.actionSaveHtml) menuExport.addAction(self.actionOdf) menuExport.addAction(self.actionPdf) if self.extensionActions: menuExport.addSeparator() for action, mimetype in self.extensionActions: menuExport.addAction(action) menuExport.aboutToShow.connect(self.updateExtensionsVisibility) menuFile.addAction(self.actionPrint) menuFile.addAction(self.actionPrintPreview) menuFile.addSeparator() menuFile.addAction(self.actionQuit) menuEdit.addAction(self.actionUndo) menuEdit.addAction(self.actionRedo) menuEdit.addSeparator() menuEdit.addAction(self.actionCut) menuEdit.addAction(self.actionCopy) menuEdit.addAction(self.actionPaste) menuEdit.addAction(self.actionPasteImage) menuEdit.addSeparator() menuEdit.addAction(self.actionMoveUp) menuEdit.addAction(self.actionMoveDown) menuEdit.addSeparator() if enchant is not None: menuSC = menuEdit.addMenu(self.tr('Spell check')) menuSC.addAction(self.actionEnableSC) menuSC.addAction(self.actionSetLocale) menuEdit.addAction(self.actionSearch) menuEdit.addAction(self.actionGoToLine) menuEdit.addAction(self.actionChangeEditorFont) menuEdit.addAction(self.actionChangePreviewFont) menuEdit.addSeparator() if len(availableMarkups) > 1: self.menuMode = menuEdit.addMenu(self.tr('Default markup')) for markupAction in markupActions: self.menuMode.addAction(markupAction) menuFormat = menuEdit.addMenu(self.tr('Formatting')) menuFormat.addAction(self.actionBold) menuFormat.addAction(self.actionItalic) menuFormat.addAction(self.actionUnderline) if ReTextWebKitPreview is not None or ReTextWebEnginePreview is None: menuEdit.addAction(self.actionWebKit) else: menuEdit.addAction(self.actionWebEngine) menuEdit.addSeparator() menuEdit.addAction(self.actionViewHtml) menuEdit.addAction(self.actionPreview) menuEdit.addAction(self.actionInsertTable) menuEdit.addAction(self.actionTableMode) if ReTextFakeVimHandler: menuEdit.addAction(self.actionFakeVimMode) menuEdit.addSeparator() menuEdit.addAction(self.actionFullScreen) menuEdit.addAction(self.actionConfig) menuHelp.addAction(self.actionHelp) menuHelp.addSeparator() menuHelp.addAction(self.actionAbout) menuHelp.addAction(self.actionAboutQt) self.toolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.toolBar.addAction(self.actionNew) self.toolBar.addSeparator() self.toolBar.addAction(self.actionOpen) self.toolBar.addAction(self.actionSave) self.toolBar.addAction(self.actionPrint) self.toolBar.addSeparator() self.toolBar.addAction(self.actionPreview) self.toolBar.addAction(self.actionFullScreen) self.editBar.addAction(self.actionUndo) self.editBar.addAction(self.actionRedo) self.editBar.addSeparator() self.editBar.addAction(self.actionCut) self.editBar.addAction(self.actionCopy) self.editBar.addAction(self.actionPaste) self.editBar.addSeparator() self.editBar.addWidget(self.formattingBox) self.editBar.addWidget(self.symbolBox) self.searchEdit = QLineEdit(self.searchBar) self.searchEdit.setPlaceholderText(self.tr('Search')) self.searchEdit.returnPressed.connect(self.find) self.replaceEdit = QLineEdit(self.searchBar) self.replaceEdit.setPlaceholderText(self.tr('Replace with')) self.replaceEdit.returnPressed.connect(self.find) self.csBox = QCheckBox(self.tr('Case sensitively'), self.searchBar) self.searchBar.addWidget(self.searchEdit) self.searchBar.addWidget(self.replaceEdit) self.searchBar.addSeparator() self.searchBar.addWidget(self.csBox) self.searchBar.addAction(self.actionFindPrev) self.searchBar.addAction(self.actionFind) self.searchBar.addAction(self.actionReplace) self.searchBar.addAction(self.actionCloseSearch) self.searchBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.searchBar.setVisible(False) self.autoSaveEnabled = globalSettings.autoSave if self.autoSaveEnabled: timer = QTimer(self) timer.start(60000) timer.timeout.connect(self.saveAll) self.ind = None if enchant is not None: self.sl = globalSettings.spellCheckLocale try: enchant.Dict(self.sl or None) except enchant.errors.Error as e: warnings.warn(str(e), RuntimeWarning) globalSettings.spellCheck = False if globalSettings.spellCheck: self.actionEnableSC.setChecked(True) self.fileSystemWatcher = QFileSystemWatcher() self.fileSystemWatcher.fileChanged.connect(self.fileChanged) def restoreLastOpenedFiles(self): for file in readListFromSettings("lastFileList"): self.openFileWrapper(file) # Show the tab of last opened file lastTabIndex = globalSettings.lastTabIndex if lastTabIndex >= 0 and lastTabIndex < self.tabWidget.count(): self.tabWidget.setCurrentIndex(lastTabIndex) def iterateTabs(self): for i in range(self.tabWidget.count()): yield self.tabWidget.widget(i) def updateStyleSheet(self): self.ss = None if globalSettings.styleSheet: sheetfile = QFile(globalSettings.styleSheet) sheetfile.open(QIODevice.ReadOnly) self.ss = QTextStream(sheetfile).readAll() sheetfile.close() def initTabWidget(self): def dragEnterEvent(e): e.acceptProposedAction() def dropEvent(e): fn = bytes(e.mimeData().data('text/plain')).decode().rstrip() if fn.startswith('file:'): fn = QUrl(fn).toLocalFile() self.openFileWrapper(fn) self.tabWidget.setTabsClosable(True) self.tabWidget.setAcceptDrops(True) self.tabWidget.setMovable(True) self.tabWidget.dragEnterEvent = dragEnterEvent self.tabWidget.dropEvent = dropEvent self.tabWidget.setTabBarAutoHide(globalSettings.tabBarAutoHide) def act(self, name, icon=None, trig=None, trigbool=None, shct=None): if not isinstance(shct, QKeySequence): shct = QKeySequence(shct) if icon: action = QAction(self.actIcon(icon), name, self) else: action = QAction(name, self) if trig: action.triggered.connect(trig) elif trigbool: action.setCheckable(True) action.triggered[bool].connect(trigbool) if shct: action.setShortcut(shct) return action def actIcon(self, name): return QIcon.fromTheme(name, QIcon(getBundledIcon(name))) def printError(self): import traceback print('Exception occurred while parsing document:', file=sys.stderr) traceback.print_exc() def tabFileNameChanged(self, tab): ''' Perform all UI state changes that need to be done when the filename of the current tab has changed. ''' if tab == self.currentTab: if tab.fileName: self.setWindowTitle("") if globalSettings.windowTitleFullPath: self.setWindowTitle(tab.fileName + '[*]') self.setWindowFilePath(tab.fileName) self.tabWidget.setTabText(self.ind, tab.getBaseName()) self.tabWidget.setTabToolTip(self.ind, tab.fileName) QDir.setCurrent(QFileInfo(tab.fileName).dir().path()) else: self.setWindowFilePath('') self.setWindowTitle(self.tr('New document') + '[*]') canReload = bool(tab.fileName) and not self.autoSaveActive(tab) self.actionSetEncoding.setEnabled(canReload) self.actionReload.setEnabled(canReload) def tabActiveMarkupChanged(self, tab): ''' Perform all UI state changes that need to be done when the active markup class of the current tab has changed. ''' if tab == self.currentTab: markupClass = tab.getActiveMarkupClass() dtMarkdown = (markupClass == markups.MarkdownMarkup) dtMkdOrReST = dtMarkdown or (markupClass == markups.ReStructuredTextMarkup) self.formattingBox.setEnabled(dtMarkdown) self.symbolBox.setEnabled(dtMarkdown) self.actionUnderline.setEnabled(dtMarkdown) self.actionBold.setEnabled(dtMkdOrReST) self.actionItalic.setEnabled(dtMkdOrReST) def tabModificationStateChanged(self, tab): ''' Perform all UI state changes that need to be done when the modification state of the current tab has changed. ''' if tab == self.currentTab: changed = tab.editBox.document().isModified() if self.autoSaveActive(tab): changed = False self.actionSave.setEnabled(changed) self.setWindowModified(changed) def createTab(self, fileName): previewStatesByName = { 'editor': PreviewDisabled, 'normal-preview': PreviewNormal, 'live-preview': PreviewLive, } previewState = previewStatesByName.get(globalSettings.defaultPreviewState, PreviewDisabled) if previewState == PreviewNormal and not fileName: previewState = PreviewDisabled # Opening empty document in preview mode makes no sense self.currentTab = ReTextTab(self, fileName, previewState) self.currentTab.fileNameChanged.connect(lambda: self.tabFileNameChanged(self.currentTab)) self.currentTab.modificationStateChanged.connect(lambda: self.tabModificationStateChanged(self.currentTab)) self.currentTab.activeMarkupChanged.connect(lambda: self.tabActiveMarkupChanged(self.currentTab)) self.tabWidget.addTab(self.currentTab, self.tr("New document")) self.currentTab.updateBoxesVisibility() if previewState > 0: QTimer.singleShot(500, self.currentTab.triggerPreviewUpdate) def closeTab(self, ind): if self.maybeSave(ind): if self.tabWidget.count() == 1: self.createTab("") closedTab = self.tabWidget.widget(ind) if closedTab.fileName: self.fileSystemWatcher.removePath(closedTab.fileName) self.tabWidget.removeTab(ind) closedTab.deleteLater() def changeIndex(self, ind): ''' This function is called when a different tab is selected. It changes the state of the window to mirror the current state of the newly selected tab. Future changes to this state will be done in response to signals emitted by the tab, to which the window was subscribed when the tab was created. The window is subscribed to all tabs like this, but only the active tab will logically generate these signals. Aside from the above this function also calls the handlers for the other changes that are implied by a tab switch: filename change, modification state change and active markup change. ''' self.currentTab = self.tabWidget.currentWidget() editBox = self.currentTab.editBox previewState = self.currentTab.previewState self.actionUndo.setEnabled(editBox.document().isUndoAvailable()) self.actionRedo.setEnabled(editBox.document().isRedoAvailable()) self.actionCopy.setEnabled(editBox.textCursor().hasSelection()) self.actionCut.setEnabled(editBox.textCursor().hasSelection()) self.actionPreview.setChecked(previewState >= PreviewLive) self.actionLivePreview.setChecked(previewState == PreviewLive) self.actionTableMode.setChecked(editBox.tableModeEnabled) self.editBar.setEnabled(previewState < PreviewNormal) self.ind = ind editBox.setFocus(Qt.OtherFocusReason) self.tabFileNameChanged(self.currentTab) self.tabModificationStateChanged(self.currentTab) self.tabActiveMarkupChanged(self.currentTab) def changeEditorFont(self): font, ok = QFontDialog.getFont(globalSettings.editorFont, self) if ok: self.setEditorFont(font) def setEditorFont(self, font): globalSettings.editorFont = font for tab in self.iterateTabs(): tab.editBox.updateFont() def changePreviewFont(self): font, ok = QFontDialog.getFont(globalSettings.font, self) if ok: self.setPreviewFont(font) def setPreviewFont(self, font): globalSettings.font = font for tab in self.iterateTabs(): tab.triggerPreviewUpdate() def preview(self, viewmode): self.currentTab.previewState = viewmode * 2 self.actionLivePreview.setChecked(False) self.editBar.setDisabled(viewmode) self.currentTab.updateBoxesVisibility() self.currentTab.triggerPreviewUpdate() def enableLivePreview(self, livemode): self.currentTab.previewState = int(livemode) self.actionPreview.setChecked(livemode) self.editBar.setEnabled(True) self.currentTab.updateBoxesVisibility() self.currentTab.triggerPreviewUpdate() def enableWebKit(self, enable): globalSettings.useWebKit = enable globalSettings.useWebEngine = False for tab in self.iterateTabs(): tab.rebuildPreviewBox() def enableWebEngine(self, enable): globalSettings.useWebKit = False globalSettings.useWebEngine = enable for tab in self.iterateTabs(): tab.rebuildPreviewBox() def enableCopy(self, copymode): self.actionCopy.setEnabled(copymode) self.actionCut.setEnabled(copymode) def enableFullScreen(self, yes): if yes: self.showFullScreen() else: self.showNormal() def openConfigDialog(self): dlg = ConfigDialog(self) dlg.setWindowTitle(self.tr('Preferences')) dlg.show() def enableFakeVimMode(self, yes): globalSettings.useFakeVim = yes if yes: FakeVimMode.init(self) for tab in self.iterateTabs(): tab.editBox.installFakeVimHandler() else: FakeVimMode.exit(self) def enableSpellCheck(self, yes): try: dict = enchant.Dict(self.sl or None) except enchant.errors.Error as e: QMessageBox.warning(self, '', str(e)) self.actionEnableSC.setChecked(False) yes = False self.setAllDictionaries(dict if yes else None) globalSettings.spellCheck = yes def setAllDictionaries(self, dictionary): for tab in self.iterateTabs(): hl = tab.highlighter hl.dictionary = dictionary hl.rehighlight() def changeLocale(self): localedlg = LocaleDialog(self, defaultText=self.sl) if localedlg.exec() != QDialog.Accepted: return sl = localedlg.localeEdit.text() try: enchant.Dict(sl or None) except enchant.errors.Error as e: QMessageBox.warning(self, '', str(e)) else: self.sl = sl or None self.enableSpellCheck(self.actionEnableSC.isChecked()) if localedlg.checkBox.isChecked(): globalSettings.spellCheckLocale = sl def search(self): self.searchBar.setVisible(True) self.searchEdit.setFocus(Qt.ShortcutFocusReason) def goToLine(self): line, ok = QInputDialog.getInt(self, self.tr("Go to line"), self.tr("Type the line number")) if ok: self.currentTab.goToLine(line-1) def searchBarVisibilityChanged(self, visible): if visible: self.searchEdit.setFocus(Qt.ShortcutFocusReason) def find(self, back=False, replace=False): flags = QTextDocument.FindFlags() if back: flags |= QTextDocument.FindBackward if self.csBox.isChecked(): flags |= QTextDocument.FindCaseSensitively text = self.searchEdit.text() replaceText = self.replaceEdit.text() if replace else None found = self.currentTab.find(text, flags, replaceText=replaceText) self.setSearchEditColor(found) def replaceAll(self): text = self.searchEdit.text() replaceText = self.replaceEdit.text() found = self.currentTab.replaceAll(text, replaceText) self.setSearchEditColor(found) def setSearchEditColor(self, found): palette = self.searchEdit.palette() palette.setColor(QPalette.Active, QPalette.Base, Qt.white if found else QColor(255, 102, 102)) self.searchEdit.setPalette(palette) def showInDir(self): if self.currentTab.fileName: path = QFileInfo(self.currentTab.fileName).path() QDesktopServices.openUrl(QUrl.fromLocalFile(path)) else: QMessageBox.warning(self, '', self.tr("Please, save the file somewhere.")) def moveToTopOfRecentFileList(self, fileName): if fileName: files = readListFromSettings("recentFileList") if fileName in files: files.remove(fileName) files.insert(0, fileName) recentCount = globalSettings.recentDocumentsCount if len(files) > recentCount: del files[recentCount:] writeListToSettings("recentFileList", files) def createNew(self, text=None): self.createTab("") self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if text: self.currentTab.editBox.textCursor().insertText(text) def switchTab(self, shift=1): self.tabWidget.setCurrentIndex((self.ind + shift) % self.tabWidget.count()) def updateRecentFiles(self): self.menuRecentFiles.clear() self.recentFilesActions = [] filesOld = readListFromSettings("recentFileList") files = [] for f in filesOld: if QFile.exists(f): files.append(f) self.recentFilesActions.append(self.act(f, trig=self.openFunction(f))) writeListToSettings("recentFileList", files) for action in self.recentFilesActions: self.menuRecentFiles.addAction(action) def markupFunction(self, markup): return lambda: self.setDefaultMarkup(markup) def openFunction(self, fileName): return lambda: self.openFileWrapper(fileName) def extensionFunction(self, data): return lambda: \ self.runExtensionCommand(data['Exec'], data['FileFilter'], data['DefaultExtension']) def getExportExtensionsList(self): extensions = [] for extsprefix in datadirs: extsdir = QDir(extsprefix+'/export-extensions/') if extsdir.exists(): for fileInfo in extsdir.entryInfoList(['*.desktop', '*.ini'], QDir.Files | QDir.Readable): extensions.append(self.readExtension(fileInfo.filePath())) locale = QLocale.system().name() self.extensionActions = [] for extension in extensions: try: if ('Name[%s]' % locale) in extension: name = extension['Name[%s]' % locale] elif ('Name[%s]' % locale.split('_')[0]) in extension: name = extension['Name[%s]' % locale.split('_')[0]] else: name = extension['Name'] data = {} for prop in ('FileFilter', 'DefaultExtension', 'Exec'): if 'X-ReText-'+prop in extension: data[prop] = extension['X-ReText-'+prop] elif prop in extension: data[prop] = extension[prop] else: data[prop] = '' action = self.act(name, trig=self.extensionFunction(data)) if 'Icon' in extension: action.setIcon(self.actIcon(extension['Icon'])) mimetype = extension['MimeType'] if 'MimeType' in extension else None except KeyError: print('Failed to parse extension: Name is required', file=sys.stderr) else: self.extensionActions.append((action, mimetype)) def updateExtensionsVisibility(self): markupClass = self.currentTab.getActiveMarkupClass() for action in self.extensionActions: if markupClass is None: action[0].setEnabled(False) continue mimetype = action[1] if mimetype is None: enabled = True elif markupClass == markups.MarkdownMarkup: enabled = (mimetype in ("text/x-retext-markdown", "text/x-markdown", "text/markdown")) elif markupClass == markups.ReStructuredTextMarkup: enabled = (mimetype in ("text/x-retext-rst", "text/x-rst")) else: enabled = False action[0].setEnabled(enabled) def readExtension(self, fileName): extFile = QFile(fileName) extFile.open(QIODevice.ReadOnly) extension = {} stream = QTextStream(extFile) while not stream.atEnd(): line = stream.readLine() if '=' in line: index = line.index('=') extension[line[:index].rstrip()] = line[index+1:].lstrip() extFile.close() return extension def openFile(self): supportedExtensions = ['.txt'] for markup in markups.get_all_markups(): supportedExtensions += markup.file_extensions fileFilter = ' (' + str.join(' ', ['*'+ext for ext in supportedExtensions]) + ');;' fileNames = QFileDialog.getOpenFileNames(self, self.tr("Select one or several files to open"), QDir.currentPath(), self.tr("Supported files") + fileFilter + self.tr("All files (*)")) for fileName in fileNames[0]: self.openFileWrapper(fileName) @pyqtSlot(str) def openFileWrapper(self, fileName): if not fileName: return fileName = QFileInfo(fileName).canonicalFilePath() exists = False for i, tab in enumerate(self.iterateTabs()): if tab.fileName == fileName: exists = True ex = i if exists: self.tabWidget.setCurrentIndex(ex) elif QFile.exists(fileName): noEmptyTab = ( (self.ind is None) or self.currentTab.fileName or self.currentTab.editBox.toPlainText() or self.currentTab.editBox.document().isModified() ) if noEmptyTab: self.createTab(fileName) self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if fileName: self.fileSystemWatcher.addPath(fileName) self.currentTab.readTextFromFile(fileName) self.moveToTopOfRecentFileList(self.currentTab.fileName) def showEncodingDialog(self): if not self.maybeSave(self.ind): return codecsSet = set(bytes(QTextCodec.codecForName(alias).name()) for alias in QTextCodec.availableCodecs()) encoding, ok = QInputDialog.getItem(self, '', self.tr('Select file encoding from the list:'), [bytes(b).decode() for b in sorted(codecsSet)], 0, False) if ok: self.currentTab.readTextFromFile(None, encoding) def saveFileAs(self): self.saveFile(dlg=True) def saveAll(self): for tab in self.iterateTabs(): if (tab.fileName and tab.editBox.document().isModified() and QFileInfo(tab.fileName).isWritable()): tab.saveTextToFile() def saveFile(self, dlg=False): fileNameToSave = self.currentTab.fileName if (not fileNameToSave) or dlg: proposedFileName = "" markupClass = self.currentTab.getActiveMarkupClass() if (markupClass is None) or not hasattr(markupClass, 'default_extension'): defaultExt = self.tr("Plain text (*.txt)") ext = ".txt" else: defaultExt = self.tr('%s files', 'Example of final string: Markdown files') \ % markupClass.name + ' (' + str.join(' ', ('*'+extension for extension in markupClass.file_extensions)) + ')' if markupClass == markups.MarkdownMarkup: ext = globalSettings.markdownDefaultFileExtension elif markupClass == markups.ReStructuredTextMarkup: ext = globalSettings.restDefaultFileExtension else: ext = markupClass.default_extension if fileNameToSave is not None: proposedFileName = fileNameToSave fileNameToSave = QFileDialog.getSaveFileName(self, self.tr("Save file"), proposedFileName, defaultExt)[0] if fileNameToSave: if not QFileInfo(fileNameToSave).suffix(): fileNameToSave += ext # Make sure we don't overwrite a file opened in other tab for tab in self.iterateTabs(): if tab is not self.currentTab and tab.fileName == fileNameToSave: QMessageBox.warning(self, "", self.tr("Cannot save to file which is open in another tab!")) return False self.actionSetEncoding.setDisabled(self.autoSaveActive()) if fileNameToSave: if self.currentTab.saveTextToFile(fileNameToSave): self.moveToTopOfRecentFileList(self.currentTab.fileName) return True else: QMessageBox.warning(self, '', self.tr("Cannot save to file because it is read-only!")) return False def saveHtml(self, fileName): if not QFileInfo(fileName).suffix(): fileName += ".html" try: _, htmltext, _ = self.currentTab.getDocumentForExport(webenv=True) except Exception: return self.printError() htmlFile = QFile(fileName) result = htmlFile.open(QIODevice.WriteOnly) if not result: QMessageBox.warning(self, '', self.tr("Cannot save to file because it is read-only!")) return html = QTextStream(htmlFile) if globalSettings.defaultCodec: html.setCodec(globalSettings.defaultCodec) html << htmltext htmlFile.close() def textDocument(self, title, htmltext): td = QTextDocument() td.setMetaInformation(QTextDocument.DocumentTitle, title) td.setHtml(htmltext) td.setDefaultFont(globalSettings.font) return td def saveOdf(self): title, htmltext, _ = self.currentTab.getDocumentForExport() try: document = self.textDocument(title, htmltext) except Exception: return self.printError() fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to ODT"), self.currentTab.getBaseName() + ".odt", self.tr("OpenDocument text files (*.odt)"))[0] if not QFileInfo(fileName).suffix(): fileName += ".odt" writer = QTextDocumentWriter(fileName) writer.setFormat(b"odf") writer.write(document) def saveFileHtml(self): fileName = QFileDialog.getSaveFileName(self, self.tr("Save file"), self.currentTab.getBaseName() + ".html", self.tr("HTML files (*.html *.htm)"))[0] if fileName: self.saveHtml(fileName) def getDocumentForPrint(self, title, htmltext, preview): if globalSettings.useWebKit: return preview try: return self.textDocument(title, htmltext) except Exception: self.printError() def standardPrinter(self, title): printer = QPrinter(QPrinter.HighResolution) printer.setDocName(title) printer.setCreator('ReText %s' % app_version) if globalSettings.paperSize: pageSize = self.getPageSizeByName(globalSettings.paperSize) if pageSize is not None: printer.setPaperSize(pageSize) else: QMessageBox.warning(self, '', self.tr('Unrecognized paperSize setting "%s".') % globalSettings.paperSize) return printer def getPageSizeByName(self, pageSizeName): """ Returns a validated PageSize instance corresponding to the given name. Returns None if the name is not a valid PageSize. """ pageSize = None lowerCaseNames = {pageSize.lower(): pageSize for pageSize in self.availablePageSizes()} if pageSizeName.lower() in lowerCaseNames: pageSize = getattr(QPagedPaintDevice, lowerCaseNames[pageSizeName.lower()]) return pageSize def availablePageSizes(self): """ List available page sizes. """ sizes = [x for x in dir(QPagedPaintDevice) if type(getattr(QPagedPaintDevice, x)) == QPagedPaintDevice.PageSize] return sizes def savePdf(self): fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to PDF"), self.currentTab.getBaseName() + ".pdf", self.tr("PDF files (*.pdf)"))[0] if fileName: if not QFileInfo(fileName).suffix(): fileName += ".pdf" title, htmltext, preview = self.currentTab.getDocumentForExport() if globalSettings.useWebEngine and hasattr(preview.page(), "printToPdf"): pageSize = self.getPageSizeByName(globalSettings.paperSize) if pageSize is None: pageSize = QPageSize(QPageSize.A4) margins = QMarginsF(20, 20, 13, 20) # left, top, right, bottom (in millimeters) layout = QPageLayout(pageSize, QPageLayout.Portrait, margins, QPageLayout.Millimeter) preview.page().printToPdf(fileName, layout) # Available since Qt 5.7 return printer = self.standardPrinter(title) printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(fileName) document = self.getDocumentForPrint(title, htmltext, preview) if document != None: document.print(printer) def printFile(self): title, htmltext, preview = self.currentTab.getDocumentForExport() printer = self.standardPrinter(title) dlg = QPrintDialog(printer, self) dlg.setWindowTitle(self.tr("Print document")) if (dlg.exec() == QDialog.Accepted): document = self.getDocumentForPrint(title, htmltext, preview) if document != None: document.print(printer) def printPreview(self): title, htmltext, preview = self.currentTab.getDocumentForExport() document = self.getDocumentForPrint(title, htmltext, preview) if document is None: return printer = self.standardPrinter(title) preview = QPrintPreviewDialog(printer, self) preview.paintRequested.connect(document.print) preview.exec() def runExtensionCommand(self, command, filefilter, defaultext): import shlex of = ('%of' in command) html = ('%html' in command) if of: if defaultext and not filefilter: filefilter = '*'+defaultext fileName = QFileDialog.getSaveFileName(self, self.tr('Export document'), '', filefilter)[0] if not fileName: return if defaultext and not QFileInfo(fileName).suffix(): fileName += defaultext else: fileName = 'out' + defaultext basename = '.%s.retext-temp' % self.currentTab.getBaseName() if html: tmpname = basename+'.html' self.saveHtml(tmpname) else: tmpname = basename + self.currentTab.getActiveMarkupClass().default_extension self.currentTab.writeTextToFile(tmpname) command = command.replace('%of', shlex.quote(fileName)) command = command.replace('%html' if html else '%if', shlex.quote(tmpname)) try: Popen(str(command), shell=True).wait() except Exception as error: errorstr = str(error) QMessageBox.warning(self, '', self.tr('Failed to execute the command:') + '\n' + errorstr) QFile(tmpname).remove() def autoSaveActive(self, tab=None): tab = tab if tab else self.currentTab return bool(self.autoSaveEnabled and tab.fileName and QFileInfo(tab.fileName).isWritable()) def clipboardDataChanged(self): mimeData = QApplication.instance().clipboard().mimeData() if mimeData is not None: self.actionPaste.setEnabled(mimeData.hasText()) self.actionPasteImage.setEnabled(mimeData.hasImage()) def insertFormatting(self, formatting): if formatting == 'table': dialog = InsertTableDialog(self) dialog.show() self.formattingBox.setCurrentIndex(0) return cursor = self.currentTab.editBox.textCursor() text = cursor.selectedText() moveCursorTo = None def c(cursor): nonlocal moveCursorTo moveCursorTo = cursor.position() def ensurenl(cursor): if not cursor.atBlockStart(): cursor.insertText('\n\n') toinsert = { 'header': (ensurenl, '# ', text), 'italic': ('*', text, c, '*'), 'bold': ('**', text, c, '**'), 'underline': ('<u>', text, c, '</u>'), 'numbering': (ensurenl, ' 1. ', text), 'bullets': (ensurenl, ' * ', text), 'image': ('![', text or self.tr('Alt text'), c, '](', self.tr('URL'), ')'), 'link': ('[', text or self.tr('Link text'), c, '](', self.tr('URL'), ')'), 'inline code': ('`', text, c, '`'), 'code block': (ensurenl, ' ', text), 'blockquote': (ensurenl, '> ', text), } if formatting not in toinsert: return cursor.beginEditBlock() for token in toinsert[formatting]: if callable(token): token(cursor) else: cursor.insertText(token) cursor.endEditBlock() self.formattingBox.setCurrentIndex(0) # Bring back the focus on the editor self.currentTab.editBox.setFocus(Qt.OtherFocusReason) if moveCursorTo: cursor.setPosition(moveCursorTo) self.currentTab.editBox.setTextCursor(cursor) def insertSymbol(self, num): if num: self.currentTab.editBox.insertPlainText('&'+self.usefulChars[num-1]+';') self.symbolBox.setCurrentIndex(0) def fileChanged(self, fileName): tab = None for testtab in self.iterateTabs(): if testtab.fileName == fileName: tab = testtab if tab is None: self.fileSystemWatcher.removePath(fileName) return if not QFile.exists(fileName): self.tabWidget.setCurrentWidget(tab) tab.editBox.document().setModified(True) QMessageBox.warning(self, '', self.tr( 'This file has been deleted by other application.\n' 'Please make sure you save the file before exit.')) elif not tab.editBox.document().isModified(): # File was not modified in ReText, reload silently tab.readTextFromFile() else: self.tabWidget.setCurrentWidget(tab) text = self.tr( 'This document has been modified by other application.\n' 'Do you want to reload the file (this will discard all ' 'your changes)?\n') if self.autoSaveEnabled: text += self.tr( 'If you choose to not reload the file, auto save mode will ' 'be disabled for this session to prevent data loss.') messageBox = QMessageBox(QMessageBox.Warning, '', text) reloadButton = messageBox.addButton(self.tr('Reload'), QMessageBox.YesRole) messageBox.addButton(QMessageBox.Cancel) messageBox.exec() if messageBox.clickedButton() is reloadButton: tab.readTextFromFile() else: self.autoSaveEnabled = False tab.editBox.document().setModified(True) if fileName not in self.fileSystemWatcher.files(): # https://github.com/retext-project/retext/issues/137 self.fileSystemWatcher.addPath(fileName) def maybeSave(self, ind): tab = self.tabWidget.widget(ind) if self.autoSaveActive(tab): tab.saveTextToFile() return True if not tab.editBox.document().isModified(): return True self.tabWidget.setCurrentIndex(ind) ret = QMessageBox.warning(self, '', self.tr("The document has been modified.\nDo you want to save your changes?"), QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) if ret == QMessageBox.Save: return self.saveFile(False) elif ret == QMessageBox.Cancel: return False return True def closeEvent(self, closeevent): for ind in range(self.tabWidget.count()): if not self.maybeSave(ind): return closeevent.ignore() if globalSettings.saveWindowGeometry: globalSettings.windowGeometry = self.saveGeometry() if globalSettings.openLastFilesOnStartup: files = [tab.fileName for tab in self.iterateTabs()] writeListToSettings("lastFileList", files) globalSettings.lastTabIndex = self.tabWidget.currentIndex() closeevent.accept() def viewHtml(self): htmlDlg = HtmlDialog(self) try: _, htmltext, _ = self.currentTab.getDocumentForExport(includeStyleSheet=False) except Exception: return self.printError() winTitle = self.currentTab.getBaseName() htmlDlg.setWindowTitle(winTitle+" ("+self.tr("HTML code")+")") htmlDlg.textEdit.setPlainText(htmltext.rstrip()) htmlDlg.hl.rehighlight() htmlDlg.show() htmlDlg.raise_() htmlDlg.activateWindow() def openHelp(self): QDesktopServices.openUrl(QUrl('https://github.com/retext-project/retext/wiki')) def aboutDialog(self): QMessageBox.about(self, self.aboutWindowTitle, '<p><b>' + (self.tr('ReText %s (using PyMarkups %s)') % (app_version, markups.__version__)) +'</b></p>' + self.tr('Simple but powerful editor' ' for Markdown and reStructuredText') +'</p><p>'+self.tr('Author: Dmitry Shachnev, 2011').replace('2011', '2011–2017') +'<br><a href="https://github.com/retext-project/retext">'+self.tr('Website') +'</a> | <a href="http://daringfireball.net/projects/markdown/syntax">' +self.tr('Markdown syntax') +'</a> | <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">' +self.tr('reStructuredText syntax')+'</a></p>') def setDefaultMarkup(self, markupClass): globalSettings.defaultMarkup = markupClass.name for tab in self.iterateTabs(): if not tab.fileName: tab.updateActiveMarkupClass()
def initUI(self): self.view = TextView(self) self.setCentralWidget(self.view) self.toolbar = self.addToolBar('Text Effects') # Set up the text effects tools actionGroup = QActionGroup(self) noneAction = QAction(QIcon("none.png"), "&Clear", self) noneAction.setStatusTip("Clear Effects") noneAction.triggered.connect(self.view.noEffect) actionGroup.addAction(noneAction) blurAction = QAction(QIcon("blur.png"), "&Blur", self) blurAction.setStatusTip("Blur Text") blurAction.triggered.connect(self.view.blur) actionGroup.addAction(blurAction) opacityAction = QAction(QIcon("opacity.png"), "&Transparency", self) opacityAction.setStatusTip("Fade Text") opacityAction.triggered.connect(self.view.opacity) actionGroup.addAction(opacityAction) shadowAction = QAction(QIcon("shadow.png"), "&Drop Shadow", self) shadowAction.setStatusTip("Drop-shadow Text") shadowAction.triggered.connect(self.view.shadow) actionGroup.addAction(shadowAction) self.toolbar.addActions(actionGroup.actions()) self.toolbar.addSeparator() # Set up the font selection tools boldAction = QAction(QIcon("bold.png"), "&Bold", self) boldAction.setStatusTip("Bold Text") boldAction.setCheckable(True) boldAction.setChecked(True) boldAction.triggered[bool].connect(self.view.bold) self.toolbar.addAction(boldAction) italicAction = QAction(QIcon("italic.png"), "&Italic", self) italicAction.setStatusTip("Italic Text") italicAction.setCheckable(True) italicAction.triggered[bool].connect(self.view.italic) self.toolbar.addAction(italicAction) self.fontBox = QFontComboBox(self) self.fontBox.setCurrentFont(QFont("PT Sans", 16, QFont.Bold)) self.fontBox.currentFontChanged.connect(self.view.fontFamily) self.toolbar.addWidget(self.fontBox) self.fontSizeBox = QComboBox(self) self.fontSizeBox.setEditable(True) strlist = [] intlist = QFontDatabase.standardSizes() for item in intlist: strlist.append(str(item)) self.fontSizeBox.addItems(strlist) self.fontSizeBox.setCurrentText("16") self.fontSizeBox.currentTextChanged.connect(self.view.fontSize) self.toolbar.addWidget(self.fontSizeBox) self.setGeometry(300, 300, 600, 500) self.setWindowTitle('Renderer') self.show()
class MainFileMenu(QMenu): """Menu with file actions.""" def __init__(self, parent, layer_editor, title='&File'): """Initializes the class.""" super(MainFileMenu, self).__init__(parent) self.setTitle(title) self._layer_editor = layer_editor self.parent = parent self._about_dialog = GMAboutDialog(parent, 'GeoMop LayerEditor') self._new_file_action = QAction('&New File ...', self) self._new_file_action.setShortcut( cfg.get_shortcut('new_file').key_sequence) self._new_file_action.setStatusTip('New layer data file') self._new_file_action.triggered.connect(self._layer_editor.new_file) self.addAction(self._new_file_action) self._open_file_action = QAction('&Open File ...', self) self._open_file_action.setShortcut( cfg.get_shortcut('open_file').key_sequence) self._open_file_action.setStatusTip('Open layer data file') self._open_file_action.triggered.connect(self._layer_editor.open_file) self.addAction(self._open_file_action) self._save_file_action = QAction('&Save File', self) self._save_file_action.setShortcut( cfg.get_shortcut('save_file').key_sequence) self._save_file_action.setStatusTip('Save layer data file') self._save_file_action.triggered.connect(self._layer_editor.save_file) self.addAction(self._save_file_action) self._save_as_action = QAction('Save &As ...', self) self._save_as_action.setShortcut( cfg.get_shortcut('save_file_as').key_sequence) self._save_as_action.setStatusTip('Save layer data file as') self._save_as_action.triggered.connect(self._layer_editor.save_as) self.addAction(self._save_as_action) self._recent_file_signal_connect = False self._recent = self.addMenu('Open &Recent Files') self._recent_group = QActionGroup(self, exclusive=True) self.addSeparator() self._import_file_action = QAction('&Add Shape File ...', self) self._import_file_action.setStatusTip('Add shape file') self._import_file_action.triggered.connect( self._layer_editor.add_shape_file) self.addAction(self._import_file_action) self.addSeparator() self._about_action = QAction('About', self) self._about_action.triggered.connect(self._on_about_action_clicked) self.addAction(self._about_action) self._help_dialog = LE_help_dialog(parent) self._help_action = QAction('Help', self) self._help_action.triggered.connect(self._on_help_action_clicked) self.addAction(self._help_action) self.addSeparator() self._exit_action = QAction('E&xit', self) self._exit_action.setShortcut(cfg.get_shortcut('exit').key_sequence) self._exit_action.setStatusTip('Exit application') self._exit_action.triggered.connect(self._exit_clicked) self.addAction(self._exit_action) def update_recent_files(self, from_row=1): """update recent file in menu""" if self._recent_file_signal_connect: self._recent_group.triggered.disconnect() self._recent_file_signal_connect = False for action in self._recent_group.actions(): self._recent_group.removeAction(action) if len(cfg.config.recent_files) < from_row + 1: self._recent.setEnabled(False) return self._recent.setEnabled(True) for i in range(from_row, len(cfg.config.recent_files)): action = QAction(cfg.config.recent_files[i], self, checkable=True) action.setData(cfg.config.recent_files[i]) reaction = self._recent_group.addAction(action) self._recent.addAction(reaction) self._recent_group.triggered.connect(self._layer_editor.open_recent) self._recent_file_signal_connect = True def _on_about_action_clicked(self): """Displays about dialog.""" if not self._about_dialog.isVisible(): self._about_dialog.show() def _on_help_action_clicked(self): """Displays help dialog.""" if not self._help_dialog.isVisible(): self._help_dialog.show() def _exit_clicked(self): """Performs actions before app is closed.""" # prompt user to save changes (if any) if not self._layer_editor.mainwindow.close(): return qApp.quit()
class ThreadlinkUtility(QMainWindow): """Main class for the PCBA Test Utility. Creates main window for the program, the file menu, status bar, and the settings/configuration window. """ def __init__(self): super().__init__() self.system_font = QApplication.font().family() self.label_font = QFont(self.system_font, 12) self.config_font = QFont(self.system_font, 12) self.config_path_font = QFont(self.system_font, 12) self.settings = QSettings("BeadedStream", "Threadlink TestUtility") settings_defaults = { "user_id": "", "port1_tac_id": "", "hex_files_path": "/path/to/hex/files", "report_dir_path": "/path/to/report/folder", "atprogram_file_path": "/path/to/atprogram.exe" } for key in settings_defaults: if not self.settings.value(key): self.settings.setValue(key, settings_defaults[key]) self.sm = serialmanager.SerialManager() self.serial_thread = QThread() self.sm.moveToThread(self.serial_thread) self.serial_thread.start() self.m = model.Model() self.r = report.Report() self.sm.port_unavailable_signal.connect(self.port_unavailable) # Part number : [serial prefix, procedure class] self.product_data = { "45211-01": ["THL", threadlink.Threadlink], } # Create program actions. self.config = QAction("Settings", self) self.config.setShortcut("Ctrl+E") self.config.setStatusTip("Program Settings") self.config.triggered.connect(self.configuration) self.quit = QAction("Quit", self) self.quit.setShortcut("Ctrl+Q") self.quit.setStatusTip("Exit Program") self.quit.triggered.connect(self.close) self.about_tu = QAction("About Threadlink Utility", self) self.about_tu.setShortcut("Ctrl+U") self.about_tu.setStatusTip("About Program") self.about_tu.triggered.connect(self.about_program) self.aboutqt = QAction("About Qt", self) self.aboutqt.setShortcut("Ctrl+I") self.aboutqt.setStatusTip("About Qt") self.aboutqt.triggered.connect(self.about_qt) # Create menubar self.menubar = self.menuBar() self.file_menu = self.menubar.addMenu("&File") self.file_menu.addAction(self.config) self.file_menu.addAction(self.quit) self.serial_menu = self.menubar.addMenu("&Serial") self.serial_menu.installEventFilter(self) self.ports_menu = QMenu("&Ports", self) self.serial_menu.addMenu(self.ports_menu) self.ports_menu.aboutToShow.connect(self.populate_ports) self.ports_group = QActionGroup(self) self.ports_group.triggered.connect(self.connect_port) self.help_menu = self.menubar.addMenu("&Help") self.help_menu.addAction(self.about_tu) self.help_menu.addAction(self.aboutqt) self.initUI() self.center() def center(self): """Centers the application on the screen the mouse pointer is currently on.""" frameGm = self.frameGeometry() screen = QApplication.desktop().screenNumber( QApplication.desktop().cursor().pos()) centerPoint = QApplication.desktop().screenGeometry(screen).center() frameGm.moveCenter(centerPoint) self.move(frameGm.topLeft()) def resource_path(self, relative_path): """Gets the path of the application relative root path to allow us to find the logo.""" if hasattr(sys, '_MEIPASS'): return os.path.join(sys._MEIPASS, relative_path) return os.path.join(os.path.abspath("."), relative_path) def initUI(self): """"Sets up the UI.""" RIGHT_SPACING = 350 LINE_EDIT_WIDTH = 200 self.central_widget = QWidget() self.tester_id_lbl = QLabel("Please enter tester ID: ") self.tester_id_lbl.setFont(self.label_font) self.pcba_pn_lbl = QLabel("Please select PCBA part number: ") self.pcba_pn_lbl.setFont(self.label_font) self.pcba_sn_lbl = QLabel("Please enter or scan DUT serial number: ") self.pcba_sn_lbl.setFont(self.label_font) self.tester_id_input = QLineEdit() self.tester_id_input.setText(self.settings.value("user_id")) self.pcba_sn_input = QLineEdit() self.tester_id_input.setFixedWidth(LINE_EDIT_WIDTH) self.pcba_sn_input.setFixedWidth(LINE_EDIT_WIDTH) self.pcba_pn_input = QComboBox() self.pcba_pn_input.addItem("45211-01") self.pcba_pn_input.setFixedWidth(LINE_EDIT_WIDTH) self.start_btn = QPushButton("Start") self.start_btn.setFixedWidth(200) self.start_btn.setAutoDefault(True) self.start_btn.clicked.connect(self.parse_values) self.logo_img = QPixmap(self.resource_path("h_logo.png")) self.logo_img = self.logo_img.scaledToWidth(600) self.logo = QLabel() self.logo.setPixmap(self.logo_img) hbox_logo = QHBoxLayout() hbox_logo.addStretch() hbox_logo.addWidget(self.logo) hbox_logo.addStretch() hbox_test_id = QHBoxLayout() hbox_test_id.addStretch() hbox_test_id.addWidget(self.tester_id_lbl) hbox_test_id.addWidget(self.tester_id_input) hbox_test_id.addSpacing(RIGHT_SPACING) hbox_pn = QHBoxLayout() hbox_pn.addStretch() hbox_pn.addWidget(self.pcba_pn_lbl) hbox_pn.addWidget(self.pcba_pn_input) hbox_pn.addSpacing(RIGHT_SPACING) hbox_sn = QHBoxLayout() hbox_sn.addStretch() hbox_sn.addWidget(self.pcba_sn_lbl) hbox_sn.addWidget(self.pcba_sn_input) hbox_sn.addSpacing(RIGHT_SPACING) hbox_start_btn = QHBoxLayout() hbox_start_btn.addStretch() hbox_start_btn.addWidget(self.start_btn) hbox_start_btn.addSpacing(RIGHT_SPACING) vbox = QVBoxLayout() vbox.addStretch() vbox.addLayout(hbox_logo) vbox.addSpacing(100) vbox.addLayout(hbox_test_id) vbox.addSpacing(50) vbox.addLayout(hbox_pn) vbox.addSpacing(50) vbox.addLayout(hbox_sn) vbox.addSpacing(50) vbox.addLayout(hbox_start_btn) vbox.addStretch() self.central_widget.setLayout(vbox) self.setCentralWidget(self.central_widget) self.setFixedSize(WINDOW_WIDTH, WINDOW_HEIGHT) self.setWindowTitle( "BeadedStream Manufacturing Threadlink Test Utility") def create_messagebox(self, type, title, text, info_text): """A helper method for creating message boxes.""" msgbox = QMessageBox(self) msgbox.setWindowTitle(title) msgbox.setText(text) msgbox.setInformativeText(info_text) if type == "Warning": msgbox.setIcon(QMessageBox.Warning) elif type == "Error": msgbox.setIcon(QMessageBox.Error) elif type == "Information": msgbox.setIcon(QMessageBox.Information) else: raise InvalidMsgType return msgbox def about_program(self): """Displays information about the program.""" QMessageBox.about(self, "About Threadlink Test Utility", ABOUT_TEXT) def about_qt(self): """Displays information about Qt.""" QMessageBox.aboutQt(self, "About Qt") def populate_ports(self): """Populates ports menu from connected COM ports.""" ports = serialmanager.SerialManager.scan_ports() self.ports_menu.clear() if not ports: self.ports_menu.addAction("None") self.sm.close_port() for port in ports: port_description = port.description action = self.ports_menu.addAction(port_description) port_name = port.device if self.sm.is_connected(port_name): action.setCheckable(True) action.setChecked(True) self.ports_group.addAction(action) def connect_port(self, action: QAction): """Connects to a COM port by parsing the text from a clicked QAction menu object.""" p = "COM[0-9]+" m = re.search(p, action.text()) if m: port_name = m.group() self.sm.open_port(port_name) else: QMessageBox.warning(self, "Warning", "Invalid port selection!") def port_unavailable(self): """Displays warning message about unavailable port.""" QMessageBox.warning(self, "Warning", "Port unavailable!") def parse_values(self): """Parses and validates input values from the start page.""" self.tester_id = self.tester_id_input.text().upper() self.settings.setValue("user_id", self.tester_id) self.pcba_pn = self.pcba_pn_input.currentText() self.pcba_sn = self.pcba_sn_input.text().upper() if (self.tester_id and self.pcba_pn and self.pcba_sn): # The serial number should be seven characters long and start with # the specific prefix for the given product. if (self.pcba_sn[0:3] == self.product_data[self.pcba_pn][0] and len(self.pcba_sn) == 7): self.r.write_data("tester_id", self.tester_id, "PASS") self.r.write_data("pcba_sn", self.pcba_sn, "PASS") self.r.write_data("pcba_pn", self.pcba_pn, "PASS") else: self.err_msg = self.create_messagebox("Warning", "Error", "Error", "Bad serial number!") self.err_msg.show() return else: self.err_msg = self.create_messagebox("Warning", "Error", "Error", "Missing value!") self.err_msg.show() return self.start_procedure() def start_procedure(self): """Sets up procedure layout by creating test statuses and initializing the appropriate board class (currently D505, potentially others in the future).""" central_widget = QWidget() status_lbl_stylesheet = ("QLabel {border: 2px solid grey;" "color: black; font-size: 20px}") status_style_pass = """QLabel {background: #8cff66; border: 2px solid grey; font-size: 20px}""" # ______Labels______ self.tester_id_status = QLabel(f"Tester ID: {self.tester_id}") self.pcba_pn_status = QLabel(f"PCBA PN: {self.pcba_pn}") self.pcba_sn_status = QLabel(f"PCBA SN: {self.pcba_sn}") self.input_i_status = QLabel(f"Input Current: _____ mA") self.supply_5v_status = QLabel("5V Supply: _____V") self.output_2p5v_status = QLabel("2.5V Output: _____V") self.supply_1p8v_status = QLabel("1.8V Supply: _____V") self.xmega_prog_status = QLabel("XMega Programming: _____") self.one_wire_prog_status = QLabel("1-Wire Programming:_____") self.internal_5v_status = QLabel("Internal 5V: _____V") self.tac_id_status = QLabel("TAC ID: _____") self.hall_effect_status = QLabel("Hall Effect Sensor Test:_____") self.led_test_status = QLabel("LED Test:_____") self.tester_id_status.setStyleSheet(status_style_pass) self.pcba_pn_status.setStyleSheet(status_style_pass) self.pcba_sn_status.setStyleSheet(status_style_pass) self.input_i_status.setStyleSheet(status_lbl_stylesheet) self.supply_5v_status.setStyleSheet(status_lbl_stylesheet) self.output_2p5v_status.setStyleSheet(status_lbl_stylesheet) self.supply_1p8v_status.setStyleSheet(status_lbl_stylesheet) self.xmega_prog_status.setStyleSheet(status_lbl_stylesheet) self.one_wire_prog_status.setStyleSheet(status_lbl_stylesheet) self.internal_5v_status.setStyleSheet(status_lbl_stylesheet) self.tac_id_status.setStyleSheet(status_lbl_stylesheet) self.hall_effect_status.setStyleSheet(status_lbl_stylesheet) self.led_test_status.setStyleSheet(status_lbl_stylesheet) # ______Layout______ status_vbox1 = QVBoxLayout() status_vbox1.setSpacing(10) status_vbox1.addWidget(self.tester_id_status) status_vbox1.addWidget(self.pcba_pn_status) status_vbox1.addWidget(self.pcba_sn_status) status_vbox1.addWidget(self.input_i_status) status_vbox1.addWidget(self.supply_5v_status) status_vbox1.addWidget(self.output_2p5v_status) status_vbox1.addWidget(self.supply_1p8v_status) status_vbox1.addWidget(self.xmega_prog_status) status_vbox1.addWidget(self.one_wire_prog_status) status_vbox1.addWidget(self.internal_5v_status) status_vbox1.addWidget(self.tac_id_status) status_vbox1.addWidget(self.hall_effect_status) status_vbox1.addWidget(self.led_test_status) status_vbox1.addStretch() status_group = QGroupBox("Test Statuses") status_group.setFont(self.label_font) status_group.setLayout(status_vbox1) # Use the product data dictionary to call the procdure class that # corresponds to the part number. Create an instance of it passing it # the instances of test_utility, model, serial_manager and report. self.procedure = self.product_data[self.pcba_pn][1](self, self.m, self.sm, self.r) grid = QGridLayout() grid.setColumnStretch(0, 5) grid.setColumnStretch(1, 15) grid.addWidget(status_group, 0, 0, Qt.AlignTop) grid.addWidget(self.procedure, 0, 1) # layout = QHBoxLayout() # layout.addWidget(self.procedure) central_widget.setLayout(grid) self.setCentralWidget(central_widget) def configuration(self): """Sets up configuration/settings window elements.""" FILE_BTN_WIDTH = 30 self.settings_widget = QDialog(self) port1_lbl = QLabel("Port 1 TAC ID:") port1_lbl.setFont(self.config_font) self.port1_tac_id = QLineEdit(self.settings.value("port1_tac_id")) port_layout = QGridLayout() port_layout.addWidget(port1_lbl, 0, 0) port_layout.addWidget(self.port1_tac_id, 0, 1) port_group = QGroupBox("TAC IDs") port_group.setLayout(port_layout) self.hex_btn = QPushButton("[...]") self.hex_btn.setFixedWidth(FILE_BTN_WIDTH) self.hex_btn.clicked.connect(self.set_hex_dir) self.hex_lbl = QLabel("Choose the location of hex files: ") self.hex_lbl.setFont(self.config_font) self.hex_path_lbl = QLabel(self.settings.value("hex_files_path")) self.hex_path_lbl.setFont(self.config_path_font) self.hex_path_lbl.setStyleSheet("QLabel {color: blue}") self.report_btn = QPushButton("[...]") self.report_btn.setFixedWidth(FILE_BTN_WIDTH) self.report_btn.clicked.connect(self.set_report_location) self.report_lbl = QLabel("Set report save location: ") self.report_lbl.setFont(self.config_font) self.report_path_lbl = QLabel(self.settings.value("report_dir_path")) self.report_path_lbl.setFont(self.config_path_font) self.report_path_lbl.setStyleSheet("QLabel {color: blue}") self.atprogram_btn = QPushButton("[...]") self.atprogram_btn.setFixedWidth(FILE_BTN_WIDTH) self.atprogram_btn.clicked.connect(self.choose_atprogram_file) self.atprogram_lbl = QLabel("Select atprogram.exe.") self.atprogram_lbl.setFont(self.config_font) self.atprogram_path_lbl = QLabel( self.settings.value("atprogram_file_path")) self.atprogram_path_lbl.setFont(self.config_path_font) self.atprogram_path_lbl.setStyleSheet("QLabel {color: blue}") save_loc_layout = QGridLayout() save_loc_layout.addWidget(self.hex_lbl, 0, 0) save_loc_layout.addWidget(self.hex_btn, 0, 1) save_loc_layout.addWidget(self.hex_path_lbl, 1, 0) save_loc_layout.addWidget(self.report_lbl, 2, 0) save_loc_layout.addWidget(self.report_btn, 2, 1) save_loc_layout.addWidget(self.report_path_lbl, 3, 0) save_loc_layout.addWidget(self.atprogram_lbl, 4, 0) save_loc_layout.addWidget(self.atprogram_btn, 4, 1) save_loc_layout.addWidget(self.atprogram_path_lbl, 5, 0) save_loc_group = QGroupBox("Save Locations") save_loc_group.setLayout(save_loc_layout) apply_btn = QPushButton("Apply Settings") apply_btn.clicked.connect(self.apply_settings) cancel_btn = QPushButton("Cancel") cancel_btn.clicked.connect(self.cancel_settings) button_layout = QHBoxLayout() button_layout.addWidget(cancel_btn) button_layout.addStretch() button_layout.addWidget(apply_btn) hbox_top = QHBoxLayout() hbox_top.addWidget(port_group) hbox_bottom = QHBoxLayout() # hbox_bottom.addStretch() hbox_bottom.addWidget(save_loc_group) # hbox_bottom.addStretch() grid = QGridLayout() grid.addLayout(hbox_top, 0, 0) grid.addLayout(hbox_bottom, 1, 0) grid.addLayout(button_layout, 2, 0) grid.setHorizontalSpacing(100) self.settings_widget.setLayout(grid) self.settings_widget.setWindowTitle( "Threadlink Configuration Settings") self.settings_widget.show() # self.settings_widget.resize(800, 600) # frameGm = self.frameGeometry() # screen = QApplication.desktop().screenNumber( # QApplication.desktop().cursor().pos()) # centerPoint = QApplication.desktop().screenGeometry(screen).center() # frameGm.moveCenter(centerPoint) # self.settings_widget.move(frameGm.topLeft()) def set_hex_dir(self): """Opens file dialog for selecting the hex files directory.""" hex_files_path = QFileDialog.getExistingDirectory( self, "Select hex files directory.") self.hex_path_lbl.setText(hex_files_path) def set_report_location(self): """Opens file dialog for setting the save location for the report.""" report_dir = QFileDialog.getExistingDirectory( self, "Select report save location.") self.report_path_lbl.setText(report_dir) def choose_atprogram_file(self): """Opens file dialog for selecting the atprogram executable.""" atprogram_file_path = QFileDialog.getOpenFileName( self, "Select atprogram.exe.", "", "Application (*.exe)")[0] self.atprogram_path_lbl.setText(atprogram_file_path) def cancel_settings(self): """Close the settings widget without applying changes.""" self.settings_widget.close() def apply_settings(self): """Read user inputs and apply settings.""" p = r"([a-fA-F0-9]){8}" port1_value = self.port1_tac_id.text() if re.fullmatch(p, port1_value): self.settings.setValue("port1_tac_id", port1_value) else: QMessageBox.warning( self.settings_widget, "Warning!", f"Bad TAC ID!\n" "IDs are 8 digit hex values.\n" "E.g.: 000a5296") return self.settings.setValue("hex_files_path", self.hex_path_lbl.text()) self.settings.setValue("report_dir_path", self.report_path_lbl.text()) self.settings.setValue("atprogram_file_path", self.atprogram_path_lbl.text()) QMessageBox.information(self.settings_widget, "Information", "Settings applied!") self.settings_widget.close() def closeEvent(self, event): """Override QWidget closeEvent to provide user with confirmation dialog and ensure threads are terminated appropriately.""" event.accept() quit_msg = "Are you sure you want to exit the program?" confirmation = QMessageBox.question(self, 'Message', quit_msg, QMessageBox.Yes, QMessageBox.No) if confirmation == QMessageBox.Yes: self.serial_thread.quit() self.serial_thread.wait() event.accept() else: event.ignore()
class ReTextWindow(QMainWindow): def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.resize(950, 700) screenRect = QDesktopWidget().screenGeometry() if globalSettings.windowGeometry: self.restoreGeometry(globalSettings.windowGeometry) else: self.move((screenRect.width()-self.width())/2, (screenRect.height()-self.height())/2) if not screenRect.contains(self.geometry()): self.showMaximized() if globalSettings.iconTheme: QIcon.setThemeName(globalSettings.iconTheme) if QIcon.themeName() in ('hicolor', ''): if not QFile.exists(icon_path + 'document-new.png'): QIcon.setThemeName(get_icon_theme()) if QFile.exists(icon_path+'retext.png'): self.setWindowIcon(QIcon(icon_path+'retext.png')) elif QFile.exists('/usr/share/pixmaps/retext.png'): self.setWindowIcon(QIcon('/usr/share/pixmaps/retext.png')) else: self.setWindowIcon(QIcon.fromTheme('retext', QIcon.fromTheme('accessories-text-editor'))) self.editBoxes = [] self.previewBoxes = [] self.highlighters = [] self.markups = [] self.fileNames = [] self.actionPreviewChecked = [] self.actionLivePreviewChecked = [] self.tabWidget = QTabWidget(self) self.initTabWidget() self.setCentralWidget(self.tabWidget) self.tabWidget.currentChanged.connect(self.changeIndex) self.tabWidget.tabCloseRequested.connect(self.closeTab) toolBar = QToolBar(self.tr('File toolbar'), self) self.addToolBar(Qt.TopToolBarArea, toolBar) self.editBar = QToolBar(self.tr('Edit toolbar'), self) self.addToolBar(Qt.TopToolBarArea, self.editBar) self.searchBar = QToolBar(self.tr('Search toolbar'), self) self.addToolBar(Qt.BottomToolBarArea, self.searchBar) toolBar.setVisible(not globalSettings.hideToolBar) self.editBar.setVisible(not globalSettings.hideToolBar) self.actionNew = self.act(self.tr('New'), 'document-new', self.createNew, shct=QKeySequence.New) self.actionNew.setPriority(QAction.LowPriority) self.actionOpen = self.act(self.tr('Open'), 'document-open', self.openFile, shct=QKeySequence.Open) self.actionOpen.setPriority(QAction.LowPriority) self.actionSetEncoding = self.act(self.tr('Set encoding'), trig=self.showEncodingDialog) self.actionSetEncoding.setEnabled(False) self.actionReload = self.act(self.tr('Reload'), 'view-refresh', trig=self.openFileMain) self.actionReload.setEnabled(False) self.actionSave = self.act(self.tr('Save'), 'document-save', self.saveFile, shct=QKeySequence.Save) self.actionSave.setEnabled(False) self.actionSave.setPriority(QAction.LowPriority) self.actionSaveAs = self.act(self.tr('Save as'), 'document-save-as', self.saveFileAs, shct=QKeySequence.SaveAs) self.actionNextTab = self.act(self.tr('Next tab'), 'go-next', lambda: self.switchTab(1), shct=Qt.CTRL+Qt.Key_PageDown) self.actionPrevTab = self.act(self.tr('Previous tab'), 'go-previous', lambda: self.switchTab(-1), shct=Qt.CTRL+Qt.Key_PageUp) self.actionPrint = self.act(self.tr('Print'), 'document-print', self.printFile, shct=QKeySequence.Print) self.actionPrint.setPriority(QAction.LowPriority) self.actionPrintPreview = self.act(self.tr('Print preview'), 'document-print-preview', self.printPreview) self.actionViewHtml = self.act(self.tr('View HTML code'), 'text-html', self.viewHtml) self.actionChangeEditorFont = self.act(self.tr('Change editor font'), trig=self.changeEditorFont) self.actionChangePreviewFont = self.act(self.tr('Change preview font'), trig=self.changePreviewFont) self.actionSearch = self.act(self.tr('Find text'), 'edit-find', shct=QKeySequence.Find) self.actionSearch.setCheckable(True) self.actionSearch.triggered[bool].connect(self.searchBar.setVisible) self.searchBar.visibilityChanged.connect(self.searchBarVisibilityChanged) self.actionPreview = self.act(self.tr('Preview'), shct=Qt.CTRL+Qt.Key_E, trigbool=self.preview) if QIcon.hasThemeIcon('document-preview'): self.actionPreview.setIcon(QIcon.fromTheme('document-preview')) elif QIcon.hasThemeIcon('preview-file'): self.actionPreview.setIcon(QIcon.fromTheme('preview-file')) elif QIcon.hasThemeIcon('x-office-document'): self.actionPreview.setIcon(QIcon.fromTheme('x-office-document')) else: self.actionPreview.setIcon(QIcon(icon_path+'document-preview.png')) self.actionLivePreview = self.act(self.tr('Live preview'), shct=Qt.CTRL+Qt.Key_L, trigbool=self.enableLivePreview) self.actionTableMode = self.act(self.tr('Table mode'), shct=Qt.CTRL+Qt.Key_T, trigbool=lambda x: self.editBoxes[self.ind].enableTableMode(x)) if ReTextFakeVimHandler: self.actionFakeVimMode = self.act(self.tr('FakeVim mode'), shct=Qt.CTRL+Qt.ALT+Qt.Key_V, trigbool=self.enableFakeVimMode) if globalSettings.useFakeVim: self.actionFakeVimMode.setChecked(True) self.enableFakeVimMode(True) self.actionFullScreen = self.act(self.tr('Fullscreen mode'), 'view-fullscreen', shct=Qt.Key_F11, trigbool=self.enableFullScreen) self.actionFullScreen.setPriority(QAction.LowPriority) self.actionConfig = self.act(self.tr('Preferences'), icon='preferences-system', trig=self.openConfigDialog) self.actionConfig.setMenuRole(QAction.PreferencesRole) self.actionSaveHtml = self.act('HTML', 'text-html', self.saveFileHtml) self.actionPdf = self.act('PDF', 'application-pdf', self.savePdf) self.actionOdf = self.act('ODT', 'x-office-document', self.saveOdf) self.getExportExtensionsList() self.actionQuit = self.act(self.tr('Quit'), 'application-exit', shct=QKeySequence.Quit) self.actionQuit.setMenuRole(QAction.QuitRole) self.actionQuit.triggered.connect(self.close) self.actionUndo = self.act(self.tr('Undo'), 'edit-undo', lambda: self.editBoxes[self.ind].undo(), shct=QKeySequence.Undo) self.actionRedo = self.act(self.tr('Redo'), 'edit-redo', lambda: self.editBoxes[self.ind].redo(), shct=QKeySequence.Redo) self.actionCopy = self.act(self.tr('Copy'), 'edit-copy', lambda: self.editBoxes[self.ind].copy(), shct=QKeySequence.Copy) self.actionCut = self.act(self.tr('Cut'), 'edit-cut', lambda: self.editBoxes[self.ind].cut(), shct=QKeySequence.Cut) self.actionPaste = self.act(self.tr('Paste'), 'edit-paste', lambda: self.editBoxes[self.ind].paste(), shct=QKeySequence.Paste) self.actionUndo.setEnabled(False) self.actionRedo.setEnabled(False) self.actionCopy.setEnabled(False) self.actionCut.setEnabled(False) qApp = QApplication.instance() qApp.clipboard().dataChanged.connect(self.clipboardDataChanged) self.clipboardDataChanged() if enchant_available: self.actionEnableSC = self.act(self.tr('Enable'), trigbool=self.enableSpellCheck) self.actionSetLocale = self.act(self.tr('Set locale'), trig=self.changeLocale) self.actionWebKit = self.act(self.tr('Use WebKit renderer'), trigbool=self.enableWebKit) self.actionWebKit.setChecked(globalSettings.useWebKit) self.actionShow = self.act(self.tr('Show directory'), 'system-file-manager', self.showInDir) self.actionFind = self.act(self.tr('Next'), 'go-next', self.find, shct=QKeySequence.FindNext) self.actionFindPrev = self.act(self.tr('Previous'), 'go-previous', lambda: self.find(back=True), shct=QKeySequence.FindPrevious) self.actionHelp = self.act(self.tr('Get help online'), 'help-contents', self.openHelp) self.aboutWindowTitle = self.tr('About ReText') self.actionAbout = self.act(self.aboutWindowTitle, 'help-about', self.aboutDialog) self.actionAbout.setMenuRole(QAction.AboutRole) self.actionAboutQt = self.act(self.tr('About Qt')) self.actionAboutQt.setMenuRole(QAction.AboutQtRole) self.actionAboutQt.triggered.connect(qApp.aboutQt) availableMarkups = markups.get_available_markups() if not availableMarkups: print('Warning: no markups are available!') self.defaultMarkup = availableMarkups[0] if availableMarkups else None if globalSettings.defaultMarkup: mc = markups.find_markup_class_by_name(globalSettings.defaultMarkup) if mc and mc.available(): self.defaultMarkup = mc if len(availableMarkups) > 1: self.chooseGroup = QActionGroup(self) markupActions = [] for markup in availableMarkups: markupAction = self.act(markup.name, trigbool=self.markupFunction(markup)) if markup == self.defaultMarkup: markupAction.setChecked(True) self.chooseGroup.addAction(markupAction) markupActions.append(markupAction) self.actionBold = self.act(self.tr('Bold'), shct=QKeySequence.Bold, trig=lambda: self.insertChars('**')) self.actionItalic = self.act(self.tr('Italic'), shct=QKeySequence.Italic, trig=lambda: self.insertChars('*')) self.actionUnderline = self.act(self.tr('Underline'), shct=QKeySequence.Underline, trig=lambda: self.insertTag('u')) self.usefulTags = ('a', 'big', 'center', 'img', 's', 'small', 'span', 'table', 'td', 'tr', 'u') self.usefulChars = ('deg', 'divide', 'dollar', 'hellip', 'laquo', 'larr', 'lsquo', 'mdash', 'middot', 'minus', 'nbsp', 'ndash', 'raquo', 'rarr', 'rsquo', 'times') self.tagsBox = QComboBox(self.editBar) self.tagsBox.addItem(self.tr('Tags')) self.tagsBox.addItems(self.usefulTags) self.tagsBox.activated.connect(self.insertTag) self.symbolBox = QComboBox(self.editBar) self.symbolBox.addItem(self.tr('Symbols')) self.symbolBox.addItems(self.usefulChars) self.symbolBox.activated.connect(self.insertSymbol) self.updateStyleSheet() menubar = QMenuBar(self) menubar.setGeometry(QRect(0, 0, 800, 25)) self.setMenuBar(menubar) menuFile = menubar.addMenu(self.tr('File')) menuEdit = menubar.addMenu(self.tr('Edit')) menuHelp = menubar.addMenu(self.tr('Help')) menuFile.addAction(self.actionNew) menuFile.addAction(self.actionOpen) self.menuRecentFiles = menuFile.addMenu(self.tr('Open recent')) self.menuRecentFiles.aboutToShow.connect(self.updateRecentFiles) menuFile.addMenu(self.menuRecentFiles) menuFile.addAction(self.actionShow) menuFile.addAction(self.actionSetEncoding) menuFile.addAction(self.actionReload) menuFile.addSeparator() menuFile.addAction(self.actionSave) menuFile.addAction(self.actionSaveAs) menuFile.addSeparator() menuFile.addAction(self.actionNextTab) menuFile.addAction(self.actionPrevTab) menuFile.addSeparator() menuExport = menuFile.addMenu(self.tr('Export')) menuExport.addAction(self.actionSaveHtml) menuExport.addAction(self.actionOdf) menuExport.addAction(self.actionPdf) if self.extensionActions: menuExport.addSeparator() for action, mimetype in self.extensionActions: menuExport.addAction(action) menuExport.aboutToShow.connect(self.updateExtensionsVisibility) menuFile.addAction(self.actionPrint) menuFile.addAction(self.actionPrintPreview) menuFile.addSeparator() menuFile.addAction(self.actionQuit) menuEdit.addAction(self.actionUndo) menuEdit.addAction(self.actionRedo) menuEdit.addSeparator() menuEdit.addAction(self.actionCut) menuEdit.addAction(self.actionCopy) menuEdit.addAction(self.actionPaste) menuEdit.addSeparator() if enchant_available: menuSC = menuEdit.addMenu(self.tr('Spell check')) menuSC.addAction(self.actionEnableSC) menuSC.addAction(self.actionSetLocale) menuEdit.addAction(self.actionSearch) menuEdit.addAction(self.actionChangeEditorFont) menuEdit.addAction(self.actionChangePreviewFont) menuEdit.addSeparator() if len(availableMarkups) > 1: self.menuMode = menuEdit.addMenu(self.tr('Default markup')) for markupAction in markupActions: self.menuMode.addAction(markupAction) menuFormat = menuEdit.addMenu(self.tr('Formatting')) menuFormat.addAction(self.actionBold) menuFormat.addAction(self.actionItalic) menuFormat.addAction(self.actionUnderline) menuEdit.addAction(self.actionWebKit) menuEdit.addSeparator() menuEdit.addAction(self.actionViewHtml) menuEdit.addAction(self.actionLivePreview) menuEdit.addAction(self.actionPreview) menuEdit.addAction(self.actionTableMode) if ReTextFakeVimHandler: menuEdit.addAction(self.actionFakeVimMode) menuEdit.addSeparator() menuEdit.addAction(self.actionFullScreen) menuEdit.addAction(self.actionConfig) menuHelp.addAction(self.actionHelp) menuHelp.addSeparator() menuHelp.addAction(self.actionAbout) menuHelp.addAction(self.actionAboutQt) menubar.addMenu(menuFile) menubar.addMenu(menuEdit) menubar.addMenu(menuHelp) toolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) toolBar.addAction(self.actionNew) toolBar.addSeparator() toolBar.addAction(self.actionOpen) toolBar.addAction(self.actionSave) toolBar.addAction(self.actionPrint) toolBar.addSeparator() toolBar.addAction(self.actionPreview) toolBar.addAction(self.actionFullScreen) self.editBar.addAction(self.actionUndo) self.editBar.addAction(self.actionRedo) self.editBar.addSeparator() self.editBar.addAction(self.actionCut) self.editBar.addAction(self.actionCopy) self.editBar.addAction(self.actionPaste) self.editBar.addSeparator() self.editBar.addWidget(self.tagsBox) self.editBar.addWidget(self.symbolBox) self.searchEdit = QLineEdit(self.searchBar) self.searchEdit.setPlaceholderText(self.tr('Search')) self.searchEdit.returnPressed.connect(self.find) self.csBox = QCheckBox(self.tr('Case sensitively'), self.searchBar) self.searchBar.addWidget(self.searchEdit) self.searchBar.addSeparator() self.searchBar.addWidget(self.csBox) self.searchBar.addAction(self.actionFindPrev) self.searchBar.addAction(self.actionFind) self.searchBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.searchBar.setVisible(False) self.autoSaveEnabled = globalSettings.autoSave if self.autoSaveEnabled: timer = QTimer(self) timer.start(60000) timer.timeout.connect(self.saveAll) self.ind = None if enchant_available: self.sl = globalSettings.spellCheckLocale if self.sl: try: enchant.Dict(self.sl) except Exception as e: print(e, file=sys.stderr) self.sl = None if globalSettings.spellCheck: self.actionEnableSC.setChecked(True) self.enableSpellCheck(True) self.fileSystemWatcher = QFileSystemWatcher() self.fileSystemWatcher.fileChanged.connect(self.fileChanged) def updateStyleSheet(self): if globalSettings.styleSheet: sheetfile = QFile(globalSettings.styleSheet) sheetfile.open(QIODevice.ReadOnly) self.ss = QTextStream(sheetfile).readAll() sheetfile.close() else: self.ss = '' def initTabWidget(self): def dragEnterEvent(e): e.acceptProposedAction() def dropEvent(e): fn = bytes(e.mimeData().data('text/plain')).decode().rstrip() if fn.startswith('file:'): fn = QUrl(fn).toLocalFile() self.openFileWrapper(fn) self.tabWidget.setTabsClosable(True) self.tabWidget.setAcceptDrops(True) self.tabWidget.dragEnterEvent = dragEnterEvent self.tabWidget.dropEvent = dropEvent def act(self, name, icon=None, trig=None, trigbool=None, shct=None): if not isinstance(shct, QKeySequence): shct = QKeySequence(shct) if icon: action = QAction(self.actIcon(icon), name, self) else: action = QAction(name, self) if trig: action.triggered.connect(trig) elif trigbool: action.setCheckable(True) action.triggered[bool].connect(trigbool) if shct: action.setShortcut(shct) return action def actIcon(self, name): return QIcon.fromTheme(name, QIcon(icon_path+name+'.png')) def printError(self): import traceback print('Exception occured while parsing document:', file=sys.stderr) traceback.print_exc() def getSplitter(self, index): splitter = QSplitter(Qt.Horizontal) # Give both boxes a minimum size so the minimumSizeHint will be # ignored when splitter.setSizes is called below for widget in self.editBoxes[index], self.previewBoxes[index]: widget.setMinimumWidth(125) splitter.addWidget(widget) splitter.setSizes((50, 50)) splitter.setChildrenCollapsible(False) return splitter def getWebView(self): webView = QWebView() if not globalSettings.handleWebLinks: webView.page().setLinkDelegationPolicy(QWebPage.DelegateExternalLinks) webView.page().linkClicked.connect(QDesktopServices.openUrl) settings = webView.settings() settings.setAttribute(QWebSettings.LocalContentCanAccessFileUrls, False) settings.setDefaultTextEncoding('utf-8') return webView def createTab(self, fileName): self.previewBlocked = False self.editBoxes.append(ReTextEdit(self)) self.highlighters.append(ReTextHighlighter(self.editBoxes[-1].document())) if enchant_available and self.actionEnableSC.isChecked(): self.highlighters[-1].dictionary = \ enchant.Dict(self.sl) if self.sl else enchant.Dict() self.highlighters[-1].rehighlight() if globalSettings.useWebKit: self.previewBoxes.append(self.getWebView()) else: self.previewBoxes.append(QTextBrowser()) self.previewBoxes[-1].setOpenExternalLinks(True) self.previewBoxes[-1].setVisible(False) self.fileNames.append(fileName) markupClass = self.getMarkupClass(fileName) self.markups.append(self.getMarkup(fileName)) self.highlighters[-1].docType = (markupClass.name if markupClass else '') liveMode = globalSettings.restorePreviewState and globalSettings.previewState self.actionPreviewChecked.append(liveMode) self.actionLivePreviewChecked.append(liveMode) metrics = QFontMetrics(self.editBoxes[-1].font()) self.editBoxes[-1].setTabStopWidth(globalSettings.tabWidth * metrics.width(' ')) self.editBoxes[-1].textChanged.connect(self.updateLivePreviewBox) self.editBoxes[-1].undoAvailable.connect(self.actionUndo.setEnabled) self.editBoxes[-1].redoAvailable.connect(self.actionRedo.setEnabled) self.editBoxes[-1].copyAvailable.connect(self.enableCopy) self.editBoxes[-1].document().modificationChanged.connect(self.modificationChanged) if globalSettings.useFakeVim: self.installFakeVimHandler(self.editBoxes[-1]) return self.getSplitter(-1) def closeTab(self, ind): if self.maybeSave(ind): if self.tabWidget.count() == 1: self.tabWidget.addTab(self.createTab(""), self.tr("New document")) if self.fileNames[ind]: self.fileSystemWatcher.removePath(self.fileNames[ind]) del self.editBoxes[ind] del self.previewBoxes[ind] del self.highlighters[ind] del self.markups[ind] del self.fileNames[ind] del self.actionPreviewChecked[ind] del self.actionLivePreviewChecked[ind] self.tabWidget.removeTab(ind) def getMarkupClass(self, fileName=None): if fileName is None: fileName = self.fileNames[self.ind] if fileName: markupClass = markups.get_markup_for_file_name( fileName, return_class=True) if markupClass: return markupClass return self.defaultMarkup def getMarkup(self, fileName=None): if fileName is None: fileName = self.fileNames[self.ind] markupClass = self.getMarkupClass(fileName=fileName) if markupClass and markupClass.available(): return markupClass(filename=fileName) def docTypeChanged(self): oldType = self.highlighters[self.ind].docType markupClass = self.getMarkupClass() newType = markupClass.name if markupClass else '' if oldType != newType: self.markups[self.ind] = self.getMarkup() self.updatePreviewBox() self.highlighters[self.ind].docType = newType self.highlighters[self.ind].rehighlight() dtMarkdown = (newType == DOCTYPE_MARKDOWN) dtMkdOrReST = (newType in (DOCTYPE_MARKDOWN, DOCTYPE_REST)) self.tagsBox.setEnabled(dtMarkdown) self.symbolBox.setEnabled(dtMarkdown) self.actionUnderline.setEnabled(dtMarkdown) self.actionBold.setEnabled(dtMkdOrReST) self.actionItalic.setEnabled(dtMkdOrReST) canReload = bool(self.fileNames[self.ind]) and not self.autoSaveActive() self.actionSetEncoding.setEnabled(canReload) self.actionReload.setEnabled(canReload) def changeIndex(self, ind): if ind > -1: self.actionUndo.setEnabled(self.editBoxes[ind].document().isUndoAvailable()) self.actionRedo.setEnabled(self.editBoxes[ind].document().isRedoAvailable()) self.actionCopy.setEnabled(self.editBoxes[ind].textCursor().hasSelection()) self.actionCut.setEnabled(self.editBoxes[ind].textCursor().hasSelection()) self.actionPreview.setChecked(self.actionPreviewChecked[ind]) self.actionLivePreview.setChecked(self.actionLivePreviewChecked[ind]) self.actionTableMode.setChecked(self.editBoxes[ind].tableModeEnabled) self.editBar.setDisabled(self.actionPreviewChecked[ind]) self.ind = ind if self.fileNames[ind]: self.setCurrentFile() else: self.setWindowTitle(self.tr('New document') + '[*]') self.docTypeChanged() self.modificationChanged(self.editBoxes[ind].document().isModified()) if globalSettings.restorePreviewState: globalSettings.previewState = self.actionLivePreviewChecked[ind] if self.actionLivePreviewChecked[ind]: self.enableLivePreview(True) self.editBoxes[self.ind].setFocus(Qt.OtherFocusReason) def changeEditorFont(self): font, ok = QFontDialog.getFont(globalSettings.editorFont, self) if ok: globalSettings.editorFont = font for editor in self.editBoxes: editor.updateFont() def changePreviewFont(self): font, ok = QFontDialog.getFont(globalSettings.font, self) if ok: globalSettings.font = font self.updatePreviewBox() def preview(self, viewmode): self.actionPreviewChecked[self.ind] = viewmode if self.actionLivePreview.isChecked(): self.actionLivePreview.setChecked(False) return self.enableLivePreview(False) self.editBar.setDisabled(viewmode) self.editBoxes[self.ind].setVisible(not viewmode) self.previewBoxes[self.ind].setVisible(viewmode) if viewmode: self.updatePreviewBox() def enableLivePreview(self, livemode): if globalSettings.restorePreviewState: globalSettings.previewState = livemode self.actionLivePreviewChecked[self.ind] = livemode self.actionPreviewChecked[self.ind] = livemode self.actionPreview.setChecked(livemode) self.editBar.setEnabled(True) self.previewBoxes[self.ind].setVisible(livemode) self.editBoxes[self.ind].setVisible(True) if livemode: self.updatePreviewBox() def enableWebKit(self, enable): globalSettings.useWebKit = enable oldind = self.ind self.tabWidget.clear() for self.ind in range(len(self.editBoxes)): if enable: self.previewBoxes[self.ind] = self.getWebView() else: self.previewBoxes[self.ind] = QTextBrowser() self.previewBoxes[self.ind].setOpenExternalLinks(True) splitter = self.getSplitter(self.ind) self.tabWidget.addTab(splitter, self.getDocumentTitle(baseName=True)) self.updatePreviewBox() self.previewBoxes[self.ind].setVisible(self.actionPreviewChecked[self.ind]) self.ind = oldind self.tabWidget.setCurrentIndex(self.ind) def enableCopy(self, copymode): self.actionCopy.setEnabled(copymode) self.actionCut.setEnabled(copymode) def enableFullScreen(self, yes): if yes: self.showFullScreen() else: self.showNormal() def openConfigDialog(self): dlg = ConfigDialog(self) dlg.setWindowTitle(self.tr('Preferences')) dlg.show() def installFakeVimHandler(self, editor): if ReTextFakeVimHandler: fakeVimEditor = ReTextFakeVimHandler(editor, self) fakeVimEditor.setSaveAction(self.actionSave) fakeVimEditor.setQuitAction(self.actionQuit) self.actionFakeVimMode.triggered.connect(fakeVimEditor.remove) def enableFakeVimMode(self, yes): globalSettings.useFakeVim = yes if yes: FakeVimMode.init(self) for editor in self.editBoxes: self.installFakeVimHandler(editor) else: FakeVimMode.exit(self) def enableSpellCheck(self, yes): if yes: if self.sl: self.setAllDictionaries(enchant.Dict(self.sl)) else: self.setAllDictionaries(enchant.Dict()) else: self.setAllDictionaries(None) globalSettings.spellCheck = yes def setAllDictionaries(self, dictionary): for hl in self.highlighters: hl.dictionary = dictionary hl.rehighlight() def changeLocale(self): if self.sl: localedlg = LocaleDialog(self, defaultText=self.sl) else: localedlg = LocaleDialog(self) if localedlg.exec() != QDialog.Accepted: return sl = localedlg.localeEdit.text() setdefault = localedlg.checkBox.isChecked() if sl: try: sl = str(sl) enchant.Dict(sl) except Exception as e: QMessageBox.warning(self, '', str(e)) else: self.sl = sl self.enableSpellCheck(self.actionEnableSC.isChecked()) else: self.sl = None self.enableSpellCheck(self.actionEnableSC.isChecked()) if setdefault: globalSettings.spellCheckLocale = sl def searchBarVisibilityChanged(self, visible): self.actionSearch.setChecked(visible) if visible: self.searchEdit.setFocus(Qt.ShortcutFocusReason) def find(self, back=False): flags = QTextDocument.FindFlags() if back: flags |= QTextDocument.FindBackward if self.csBox.isChecked(): flags |= QTextDocument.FindCaseSensitively text = self.searchEdit.text() editBox = self.editBoxes[self.ind] cursor = editBox.textCursor() newCursor = editBox.document().find(text, cursor, flags) if not newCursor.isNull(): editBox.setTextCursor(newCursor) return self.setSearchEditColor(True) cursor.movePosition(QTextCursor.End if back else QTextCursor.Start) newCursor = editBox.document().find(text, cursor, flags) if not newCursor.isNull(): editBox.setTextCursor(newCursor) return self.setSearchEditColor(True) self.setSearchEditColor(False) def setSearchEditColor(self, found): palette = self.searchEdit.palette() palette.setColor(QPalette.Active, QPalette.Base, Qt.white if found else QColor(255, 102, 102)) self.searchEdit.setPalette(palette) def getHtml(self, includeStyleSheet=True, includeTitle=True, includeMeta=False, webenv=False): if self.markups[self.ind] is None: markupClass = self.getMarkupClass() errMsg = self.tr('Could not parse file contents, check if ' 'you have the <a href="%s">necessary module</a> installed!') try: errMsg %= markupClass.attributes[MODULE_HOME_PAGE] except (AttributeError, KeyError): # Remove the link if markupClass doesn't have the needed attribute errMsg = errMsg.replace('<a href="%s">', '') errMsg = errMsg.replace('</a>', '') return '<p style="color: red">%s</p>' % errMsg text = self.editBoxes[self.ind].toPlainText() headers = '' if includeStyleSheet: headers += '<style type="text/css">\n' + self.ss + '</style>\n' cssFileName = self.getDocumentTitle(baseName=True)+'.css' if QFile(cssFileName).exists(): headers += '<link rel="stylesheet" type="text/css" href="%s">\n' \ % cssFileName if includeMeta: headers += ('<meta name="generator" content="ReText %s">\n' % app_version) fallbackTitle = self.getDocumentTitle() if includeTitle else '' return self.markups[self.ind].get_whole_html(text, custom_headers=headers, include_stylesheet=includeStyleSheet, fallback_title=fallbackTitle, webenv=webenv) def updatePreviewBox(self): self.previewBlocked = False pb = self.previewBoxes[self.ind] textedit = isinstance(pb, QTextEdit) if textedit: scrollbar = pb.verticalScrollBar() disttobottom = scrollbar.maximum() - scrollbar.value() else: frame = pb.page().mainFrame() scrollpos = frame.scrollPosition() try: html = self.getHtml() except Exception: return self.printError() if textedit: pb.setHtml(html) pb.document().setDefaultFont(globalSettings.font) scrollbar.setValue(scrollbar.maximum() - disttobottom) else: pb.settings().setFontFamily(QWebSettings.StandardFont, globalSettings.font.family()) pb.settings().setFontSize(QWebSettings.DefaultFontSize, globalSettings.font.pointSize()) pb.setHtml(html, QUrl.fromLocalFile(self.fileNames[self.ind])) frame.setScrollPosition(scrollpos) def updateLivePreviewBox(self): if self.actionLivePreview.isChecked() and self.previewBlocked == False: self.previewBlocked = True QTimer.singleShot(1000, self.updatePreviewBox) def showInDir(self): if self.fileNames[self.ind]: path = QFileInfo(self.fileNames[self.ind]).path() QDesktopServices.openUrl(QUrl.fromLocalFile(path)) else: QMessageBox.warning(self, '', self.tr("Please, save the file somewhere.")) def setCurrentFile(self): self.setWindowTitle("") self.tabWidget.setTabText(self.ind, self.getDocumentTitle(baseName=True)) self.setWindowFilePath(self.fileNames[self.ind]) files = readListFromSettings("recentFileList") while self.fileNames[self.ind] in files: files.remove(self.fileNames[self.ind]) files.insert(0, self.fileNames[self.ind]) if len(files) > 10: del files[10:] writeListToSettings("recentFileList", files) QDir.setCurrent(QFileInfo(self.fileNames[self.ind]).dir().path()) self.docTypeChanged() def createNew(self, text=None): self.tabWidget.addTab(self.createTab(""), self.tr("New document")) self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if text: self.editBoxes[self.ind].textCursor().insertText(text) def switchTab(self, shift=1): self.tabWidget.setCurrentIndex((self.ind + shift) % self.tabWidget.count()) def updateRecentFiles(self): self.menuRecentFiles.clear() self.recentFilesActions = [] filesOld = readListFromSettings("recentFileList") files = [] for f in filesOld: if QFile.exists(f): files.append(f) self.recentFilesActions.append(self.act(f, trig=self.openFunction(f))) writeListToSettings("recentFileList", files) for action in self.recentFilesActions: self.menuRecentFiles.addAction(action) def markupFunction(self, markup): return lambda: self.setDefaultMarkup(markup) def openFunction(self, fileName): return lambda: self.openFileWrapper(fileName) def extensionFuntion(self, data): return lambda: \ self.runExtensionCommand(data['Exec'], data['FileFilter'], data['DefaultExtension']) def getExportExtensionsList(self): extensions = [] for extsprefix in datadirs: extsdir = QDir(extsprefix+'/export-extensions/') if extsdir.exists(): for fileInfo in extsdir.entryInfoList(['*.desktop', '*.ini'], QDir.Files | QDir.Readable): extensions.append(self.readExtension(fileInfo.filePath())) locale = QLocale.system().name() self.extensionActions = [] for extension in extensions: try: if ('Name[%s]' % locale) in extension: name = extension['Name[%s]' % locale] elif ('Name[%s]' % locale.split('_')[0]) in extension: name = extension['Name[%s]' % locale.split('_')[0]] else: name = extension['Name'] data = {} for prop in ('FileFilter', 'DefaultExtension', 'Exec'): if 'X-ReText-'+prop in extension: data[prop] = extension['X-ReText-'+prop] elif prop in extension: data[prop] = extension[prop] else: data[prop] = '' action = self.act(name, trig=self.extensionFuntion(data)) if 'Icon' in extension: action.setIcon(self.actIcon(extension['Icon'])) mimetype = extension['MimeType'] if 'MimeType' in extension else None except KeyError: print('Failed to parse extension: Name is required', file=sys.stderr) else: self.extensionActions.append((action, mimetype)) def updateExtensionsVisibility(self): markupClass = self.getMarkupClass() for action in self.extensionActions: if markupClass is None: action[0].setEnabled(False) continue mimetype = action[1] if mimetype == None: enabled = True elif markupClass == markups.MarkdownMarkup: enabled = (mimetype in ("text/x-retext-markdown", "text/x-markdown")) elif markupClass == markups.ReStructuredTextMarkup: enabled = (mimetype in ("text/x-retext-rst", "text/x-rst")) else: enabled = False action[0].setEnabled(enabled) def readExtension(self, fileName): extFile = QFile(fileName) extFile.open(QIODevice.ReadOnly) extension = {} stream = QTextStream(extFile) while not stream.atEnd(): line = stream.readLine() if '=' in line: index = line.index('=') extension[line[:index].rstrip()] = line[index+1:].lstrip() extFile.close() return extension def openFile(self): supportedExtensions = ['.txt'] for markup in markups.get_all_markups(): supportedExtensions += markup.file_extensions fileFilter = ' (' + str.join(' ', ['*'+ext for ext in supportedExtensions]) + ');;' fileNames = QFileDialog.getOpenFileNames(self, self.tr("Select one or several files to open"), "", self.tr("Supported files") + fileFilter + self.tr("All files (*)")) for fileName in fileNames[0]: self.openFileWrapper(fileName) def openFileWrapper(self, fileName): if not fileName: return fileName = QFileInfo(fileName).canonicalFilePath() exists = False for i in range(self.tabWidget.count()): if self.fileNames[i] == fileName: exists = True ex = i if exists: self.tabWidget.setCurrentIndex(ex) elif QFile.exists(fileName): noEmptyTab = ( (self.ind is None) or self.fileNames[self.ind] or self.editBoxes[self.ind].toPlainText() or self.editBoxes[self.ind].document().isModified() ) if noEmptyTab: self.tabWidget.addTab(self.createTab(fileName), "") self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if fileName: self.fileSystemWatcher.addPath(fileName) self.fileNames[self.ind] = fileName self.openFileMain() def openFileMain(self, encoding=None): openfile = QFile(self.fileNames[self.ind]) openfile.open(QIODevice.ReadOnly) stream = QTextStream(openfile) if encoding: stream.setCodec(encoding) elif globalSettings.defaultCodec: stream.setCodec(globalSettings.defaultCodec) text = stream.readAll() openfile.close() markupClass = markups.get_markup_for_file_name( self.fileNames[self.ind], return_class=True) self.highlighters[self.ind].docType = (markupClass.name if markupClass else '') self.markups[self.ind] = self.getMarkup() if self.defaultMarkup: self.highlighters[self.ind].docType = self.defaultMarkup.name editBox = self.editBoxes[self.ind] modified = bool(encoding) and (editBox.toPlainText() != text) editBox.setPlainText(text) self.setCurrentFile() editBox.document().setModified(modified) self.setWindowModified(modified) def showEncodingDialog(self): if not self.maybeSave(self.ind): return encoding, ok = QInputDialog.getItem(self, '', self.tr('Select file encoding from the list:'), [bytes(b).decode() for b in QTextCodec.availableCodecs()], 0, False) if ok: self.openFileMain(encoding) def saveFile(self): self.saveFileMain(dlg=False) def saveFileAs(self): self.saveFileMain(dlg=True) def saveAll(self): oldind = self.ind for self.ind in range(self.tabWidget.count()): if self.fileNames[self.ind] and QFileInfo(self.fileNames[self.ind]).isWritable(): self.saveFileCore(self.fileNames[self.ind]) self.editBoxes[self.ind].document().setModified(False) self.ind = oldind def saveFileMain(self, dlg): if (not self.fileNames[self.ind]) or dlg: markupClass = self.getMarkupClass() if (markupClass is None) or not hasattr(markupClass, 'default_extension'): defaultExt = self.tr("Plain text (*.txt)") ext = ".txt" else: defaultExt = self.tr('%s files', 'Example of final string: Markdown files') \ % markupClass.name + ' (' + str.join(' ', ('*'+extension for extension in markupClass.file_extensions)) + ')' if markupClass == markups.MarkdownMarkup: ext = globalSettings.markdownDefaultFileExtension elif markupClass == markups.ReStructuredTextMarkup: ext = globalSettings.restDefaultFileExtension else: ext = markupClass.default_extension newFileName = QFileDialog.getSaveFileName(self, self.tr("Save file"), "", defaultExt)[0] if newFileName: if not QFileInfo(newFileName).suffix(): newFileName += ext if self.fileNames[self.ind]: self.fileSystemWatcher.removePath(self.fileNames[self.ind]) self.fileNames[self.ind] = newFileName self.actionSetEncoding.setDisabled(self.autoSaveActive()) if self.fileNames[self.ind]: result = self.saveFileCore(self.fileNames[self.ind]) if result: self.setCurrentFile() self.editBoxes[self.ind].document().setModified(False) self.setWindowModified(False) return True else: QMessageBox.warning(self, '', self.tr("Cannot save to file because it is read-only!")) return False def saveFileCore(self, fn, addToWatcher=True): self.fileSystemWatcher.removePath(fn) savefile = QFile(fn) result = savefile.open(QIODevice.WriteOnly) if result: savestream = QTextStream(savefile) if globalSettings.defaultCodec: savestream.setCodec(globalSettings.defaultCodec) savestream << self.editBoxes[self.ind].toPlainText() savefile.close() if result and addToWatcher: self.fileSystemWatcher.addPath(fn) return result def saveHtml(self, fileName): if not QFileInfo(fileName).suffix(): fileName += ".html" try: htmltext = self.getHtml(includeStyleSheet=False, includeMeta=True, webenv=True) except Exception: return self.printError() htmlFile = QFile(fileName) htmlFile.open(QIODevice.WriteOnly) html = QTextStream(htmlFile) if globalSettings.defaultCodec: html.setCodec(globalSettings.defaultCodec) html << htmltext htmlFile.close() def textDocument(self): td = QTextDocument() td.setMetaInformation(QTextDocument.DocumentTitle, self.getDocumentTitle()) if self.ss: td.setDefaultStyleSheet(self.ss) td.setHtml(self.getHtml()) td.setDefaultFont(globalSettings.font) return td def saveOdf(self): try: document = self.textDocument() except Exception: return self.printError() fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to ODT"), "", self.tr("OpenDocument text files (*.odt)"))[0] if not QFileInfo(fileName).suffix(): fileName += ".odt" writer = QTextDocumentWriter(fileName) writer.setFormat("odf") writer.write(document) def saveFileHtml(self): fileName = QFileDialog.getSaveFileName(self, self.tr("Save file"), "", self.tr("HTML files (*.html *.htm)"))[0] if fileName: self.saveHtml(fileName) def getDocumentForPrint(self): if globalSettings.useWebKit: return self.previewBoxes[self.ind] try: return self.textDocument() except Exception: self.printError() def standardPrinter(self): printer = QPrinter(QPrinter.HighResolution) printer.setDocName(self.getDocumentTitle()) printer.setCreator('ReText %s' % app_version) return printer def savePdf(self): self.updatePreviewBox() fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to PDF"), "", self.tr("PDF files (*.pdf)"))[0] if fileName: if not QFileInfo(fileName).suffix(): fileName += ".pdf" printer = self.standardPrinter() printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(fileName) document = self.getDocumentForPrint() if document != None: document.print(printer) def printFile(self): self.updatePreviewBox() printer = self.standardPrinter() dlg = QPrintDialog(printer, self) dlg.setWindowTitle(self.tr("Print document")) if (dlg.exec() == QDialog.Accepted): document = self.getDocumentForPrint() if document != None: document.print(printer) def printPreview(self): document = self.getDocumentForPrint() if document == None: return printer = self.standardPrinter() preview = QPrintPreviewDialog(printer, self) preview.paintRequested.connect(document.print) preview.exec() def runExtensionCommand(self, command, filefilter, defaultext): of = ('%of' in command) html = ('%html' in command) if of: if defaultext and not filefilter: filefilter = '*'+defaultext fileName = QFileDialog.getSaveFileName(self, self.tr('Export document'), '', filefilter)[0] if not fileName: return if defaultext and not QFileInfo(fileName).suffix(): fileName += defaultext basename = '.%s.retext-temp' % self.getDocumentTitle(baseName=True) if html: tmpname = basename+'.html' self.saveHtml(tmpname) else: tmpname = basename+self.getMarkupClass().default_extension self.saveFileCore(tmpname, addToWatcher=False) command = command.replace('%of', '"out'+defaultext+'"') command = command.replace('%html' if html else '%if', '"'+tmpname+'"') try: Popen(str(command), shell=True).wait() except Exception as error: errorstr = str(error) QMessageBox.warning(self, '', self.tr('Failed to execute the command:') + '\n' + errorstr) QFile(tmpname).remove() if of: QFile('out'+defaultext).rename(fileName) def getDocumentTitle(self, baseName=False): markup = self.markups[self.ind] realTitle = '' if markup and not baseName: text = self.editBoxes[self.ind].toPlainText() try: realTitle = markup.get_document_title(text) except Exception: self.printError() if realTitle: return realTitle elif self.fileNames[self.ind]: fileinfo = QFileInfo(self.fileNames[self.ind]) basename = fileinfo.completeBaseName() return (basename if basename else fileinfo.fileName()) return self.tr("New document") def autoSaveActive(self): return self.autoSaveEnabled and self.fileNames[self.ind] and \ QFileInfo(self.fileNames[self.ind]).isWritable() def modificationChanged(self, changed): if self.autoSaveActive(): changed = False self.actionSave.setEnabled(changed) self.setWindowModified(changed) def clipboardDataChanged(self): mimeData = QApplication.instance().clipboard().mimeData() if mimeData is not None: self.actionPaste.setEnabled(mimeData.hasText()) def insertChars(self, chars): tc = self.editBoxes[self.ind].textCursor() if tc.hasSelection(): selection = tc.selectedText() if selection.startswith(chars) and selection.endswith(chars): if len(selection) > 2*len(chars): selection = selection[len(chars):-len(chars)] tc.insertText(selection) else: tc.insertText(chars+tc.selectedText()+chars) else: tc.insertText(chars) def insertTag(self, ut): if not ut: return if isinstance(ut, int): ut = self.usefulTags[ut - 1] arg = ' style=""' if ut == 'span' else '' tc = self.editBoxes[self.ind].textCursor() if ut == 'img': toinsert = ('<a href="' + tc.selectedText() + '" target="_blank"><img src="' + tc.selectedText() + '"/></a>') elif ut == 'a': toinsert = ('<a href="' + tc.selectedText() + '" target="_blank">' + tc.selectedText() + '</a>') else: toinsert = '<'+ut+arg+'>'+tc.selectedText()+'</'+ut+'>' tc.insertText(toinsert) self.tagsBox.setCurrentIndex(0) def insertSymbol(self, num): if num: self.editBoxes[self.ind].insertPlainText('&'+self.usefulChars[num-1]+';') self.symbolBox.setCurrentIndex(0) def fileChanged(self, fileName): ind = self.fileNames.index(fileName) self.tabWidget.setCurrentIndex(ind) if not QFile.exists(fileName): self.editBoxes[ind].document().setModified(True) QMessageBox.warning(self, '', self.tr( 'This file has been deleted by other application.\n' 'Please make sure you save the file before exit.')) elif not self.editBoxes[ind].document().isModified(): # File was not modified in ReText, reload silently self.openFileMain() self.updatePreviewBox() else: text = self.tr( 'This document has been modified by other application.\n' 'Do you want to reload the file (this will discard all ' 'your changes)?\n') if self.autoSaveEnabled: text += self.tr( 'If you choose to not reload the file, auto save mode will ' 'be disabled for this session to prevent data loss.') messageBox = QMessageBox(QMessageBox.Warning, '', text) reloadButton = messageBox.addButton(self.tr('Reload'), QMessageBox.YesRole) messageBox.addButton(QMessageBox.Cancel) messageBox.exec() if messageBox.clickedButton() is reloadButton: self.openFileMain() self.updatePreviewBox() else: self.autoSaveEnabled = False self.editBoxes[ind].document().setModified(True) if fileName not in self.fileSystemWatcher.files(): # https://github.com/retext-project/retext/issues/137 self.fileSystemWatcher.addPath(fileName) def maybeSave(self, ind): if self.autoSaveActive(): self.saveFileCore(self.fileNames[self.ind]) return True if not self.editBoxes[ind].document().isModified(): return True self.tabWidget.setCurrentIndex(ind) ret = QMessageBox.warning(self, '', self.tr("The document has been modified.\nDo you want to save your changes?"), QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) if ret == QMessageBox.Save: return self.saveFileMain(False) elif ret == QMessageBox.Cancel: return False return True def closeEvent(self, closeevent): for self.ind in range(self.tabWidget.count()): if not self.maybeSave(self.ind): return closeevent.ignore() if globalSettings.saveWindowGeometry and not self.isMaximized(): globalSettings.windowGeometry = self.saveGeometry() closeevent.accept() def viewHtml(self): htmlDlg = HtmlDialog(self) try: htmltext = self.getHtml(includeStyleSheet=False, includeTitle=False) except Exception: return self.printError() winTitle = self.getDocumentTitle(baseName=True) htmlDlg.setWindowTitle(winTitle+" ("+self.tr("HTML code")+")") htmlDlg.textEdit.setPlainText(htmltext.rstrip()) htmlDlg.hl.rehighlight() htmlDlg.show() htmlDlg.raise_() htmlDlg.activateWindow() def openHelp(self): QDesktopServices.openUrl(QUrl('https://github.com/retext-project/retext/wiki')) def aboutDialog(self): QMessageBox.about(self, self.aboutWindowTitle, '<p><b>' + (self.tr('ReText %s (using PyMarkups %s)') % (app_version, markups.__version__)) +'</b></p>' + self.tr('Simple but powerful editor' ' for Markdown and reStructuredText') +'</p><p>'+self.tr('Author: Dmitry Shachnev, 2011').replace('2011', '2011\u2013' '2015') +'<br><a href="https://github.com/retext-project/retext">'+self.tr('Website') +'</a> | <a href="http://daringfireball.net/projects/markdown/syntax">' +self.tr('Markdown syntax') +'</a> | <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">' +self.tr('reStructuredText syntax')+'</a></p>') def setDefaultMarkup(self, markup): self.defaultMarkup = markup defaultName = markups.get_available_markups()[0].name writeToSettings('defaultMarkup', markup.name, defaultName) oldind = self.ind for self.ind in range(len(self.previewBoxes)): self.docTypeChanged() self.ind = oldind
class MainWindowActions(AttrDict): """Holds all QActions for the main window""" def __init__(self, parent): super().__init__() self.parent = parent self.create_file_actions() self.create_edit_actions() self.create_view_actions() self.create_format_actions() self.create_macro_actions() self.create_help_actions() self.disable_unavailable() def create_file_actions(self): """actions for File menu""" self.new = Action(self.parent, "&New", self.parent.workflows.file_new, icon=Icon.new, shortcut='Ctrl+n', statustip='Create a new, empty spreadsheet') self.open = Action(self.parent, "&Open", self.parent.workflows.file_open, icon=Icon.open, statustip='Open spreadsheet from file') self.save = Action(self.parent, "&Save", self.parent.workflows.file_save, icon=Icon.save, shortcut='Ctrl+s', statustip='Save spreadsheet') self.save_as = Action(self.parent, "Save &As", self.parent.workflows.file_save_as, icon=Icon.save_as, shortcut='Shift+Ctrl+s', statustip='Save spreadsheet to a new file') self.imprt = Action(self.parent, "&Import", self.parent.workflows.file_import, icon=Icon.imprt, statustip='Import a file and paste it into the ' 'current grid') self.export = Action(self.parent, "&Export", self.parent.workflows.file_export, icon=Icon.export, statustip="Export selection to a file") self.approve = Action(self.parent, "&Approve file", self.parent.on_approve, icon=Icon.approve, statustip='Approve, unfreeze and sign the ' 'current file') self.clear_globals = Action(self.parent, "&Clear globals", self.parent.on_clear_globals, icon=Icon.clear_globals, statustip='Deletes global variables ' 'and reloads base modules') self.print_preview = Action(self.parent, "Print preview", self.parent.on_preview, icon=Icon.print_preview, statustip='Print preview') self.print = Action(self.parent, "Print", self.parent.on_print, icon=Icon.print, shortcut='Ctrl+p', statustip='Print current spreadsheet') self.preferences = Action(self.parent, "Preferences...", self.parent.on_preferences, icon=Icon.preferences, statustip='Pyspread setup parameters') self.quit = Action(self.parent, "&Quit", self.parent.closeEvent, icon=Icon.quit, shortcut='Ctrl+Q', statustip='Exit pyspread') def create_edit_actions(self): """actions for Edit menu""" self.undo = Action(self.parent, "&Undo", self.parent.on_undo, icon=Icon.undo, shortcut='Ctrl+z', statustip='Undo last step') self.redo = Action(self.parent, "&Redo", self.parent.on_redo, icon=Icon.redo, shortcut='Shift+Ctrl+z', statustip='Redo last undone step') self.cut = Action(self.parent, "Cut", self.parent.workflows.edit_cut, icon=Icon.cut, shortcut='Ctrl+x', statustip='Cut cell to the clipboard') self.copy = Action(self.parent, "&Copy", self.parent.workflows.edit_copy, icon=Icon.copy, shortcut='Ctrl+c', statustip='Copy the input strings of the cells ' 'to the clipboard') self.copy_results = Action(self.parent, "Copy results", self.parent.workflows.edit_copy_results, icon=Icon.copy_results, shortcut='Shift+Ctrl+c', statustip='Copy the result strings of ' 'the cells to the clipboard') self.paste = Action(self.parent, "&Paste", self.parent.workflows.edit_paste, icon=Icon.paste, shortcut='Ctrl+v', statustip='Paste cells from the clipboard') self.paste_as = Action(self.parent, "Paste as...", self.parent.workflows.edit_paste_as, icon=Icon.paste_as, shortcut='Shift+Ctrl+v', statustip='Transform clipboard and paste ' 'results') self.find = Action(self.parent, "&Find...", self.parent.workflows.edit_find, icon=Icon.find, shortcut='Ctrl+f', statustip='Find dialog') self.find_next = Action(self.parent, "&Find next", self.parent.workflows.edit_find_next, icon=Icon.find_next, shortcut='F3', statustip='Find next matching cell') self.replace = Action(self.parent, "&Replace...", self.parent.workflows.edit_replace, icon=Icon.replace, shortcut='Shift+Ctrl+f', statustip='Replace sub-strings in cells') self.quote = Action(self.parent, "&Quote", self.parent.grid.on_quote, icon=Icon.quote, shortcut='Ctrl+Return', statustip="Convert cells' code to strings by " "addding quotes") self.insert_rows = Action(self.parent, "Insert rows", self.parent.grid.on_insert_rows, icon=Icon.insert_row, statustip='Insert max(1, no. selected ' 'rows) rows at cursor') self.insert_columns = Action(self.parent, "Insert columns", self.parent.grid.on_insert_columns, icon=Icon.insert_column, statustip='Insert max(1, no. selected ' 'columns) columns at cursor') self.insert_table = Action(self.parent, "Insert table", self.parent.grid.on_insert_table, icon=Icon.insert_table, statustip='Insert table before current ' 'table') self.delete_rows = Action(self.parent, "Delete rows", self.parent.grid.on_delete_rows, icon=Icon.delete_row, statustip='Delete max(1, no. selected ' 'rows) rows at cursor') self.delete_columns = Action(self.parent, "Delete columns", self.parent.grid.on_delete_columns, icon=Icon.delete_column, statustip='Delete max(1, no. selected ' 'columns) columns at cursor') self.delete_table = Action(self.parent, "Delete table", self.parent.grid.on_delete_table, icon=Icon.delete_table, statustip='Delete current table') self.resize_grid = Action(self.parent, "Resize grid", self.parent.workflows.edit_resize, icon=Icon.resize_grid, statustip='Resizes the current grid') def create_view_actions(self): """actions for View menu""" self.fullscreen = Action(self.parent, "Fullscreen", self.parent.on_fullscreen, icon=Icon.fullscreen, shortcut='F11', statustip='Show grid in fullscreen mode ' '(press <F11> to leave)') self.toggle_main_toolbar = Action(self.parent, "Main toolbar", self.parent.on_toggle_main_toolbar, checkable=True, statustip='Show/hide the main ' 'toolbar') self.toggle_macro_toolbar = Action(self.parent, "Macro toolbar", self.parent.on_toggle_macro_toolbar, checkable=True, statustip='Show/hide the macro ' 'toolbar') self.toggle_format_toolbar = \ Action(self.parent, "Format toolbar", self.parent.on_toggle_format_toolbar, checkable=True, statustip='Show/hide the format toolbar') self.toggle_find_toolbar = Action(self.parent, "Find toolbar", self.parent.on_toggle_find_toolbar, checkable=True, statustip='Show/hide the find ' 'toolbar') self.toggle_entry_line = Action(self.parent, "Entry line", self.parent.on_toggle_entry_line, checkable=True, statustip='Show/hide the entry line') self.toggle_macro_panel = Action(self.parent, "Macro panel", self.parent.on_toggle_macro_panel, checkable=True, shortcut='F4', statustip='Show/hide the macro panel') self.goto_cell = Action(self.parent, "Go to cell", self.parent.workflows.view_goto_cell, icon=Icon.goto_cell, shortcut='Ctrl+g', statustip='Select a cell and put it into view') self.toggle_spell_checker = \ Action(self.parent, "Toggle spell checker", self.parent.entry_line.on_toggle_spell_check, icon=Icon.check_spelling, checkable=True, statustip='Turn the spell checker in the entry line on/off') self.zoom_in = Action(self.parent, "Zoom in", self.parent.grid.on_zoom_in, icon=Icon.zoom_in, shortcut='Ctrl++', statustip='Zoom in the grid') self.zoom_out = Action(self.parent, "Zoom out", self.parent.grid.on_zoom_out, icon=Icon.zoom_out, shortcut='Ctrl+-', statustip='Zoom out the grid') self.zoom_1 = Action(self.parent, "Original size", self.parent.grid.on_zoom_1, icon=Icon.zoom_1, shortcut='Ctrl+0', statustip='Show grid on standard zoom level') self.refresh_cells = \ Action(self.parent, "Refresh selected cells", self.parent.grid.refresh_selected_frozen_cells, icon=Icon.refresh, shortcut=QKeySequence.Refresh, statustip='Refresh selected cells even when frozen') self.toggle_periodic_updates = \ Action(self.parent, "Toggle periodic updates", self.parent.on_toggle_refresh_timer, icon=Icon.toggle_periodic_updates, checkable=True, statustip='Toggles periodic updates for frozen cells') self.show_frozen = Action(self.parent, "Show frozen", self.parent.grid.on_show_frozen_pressed, icon=Icon.show_frozen, checkable=True, statustip='Indicates frozen cells with a ' 'background crosshatch') def create_format_actions(self): """actions for Format menu""" self.copy_format = Action(self.parent, "&Copy format", self.parent.workflows.format_copy_format, icon=Icon.copy_format, statustip='Copy format of selection to ' 'the clipboard') self.paste_format = \ Action(self.parent, "&Paste format", self.parent.workflows.format_paste_format, icon=Icon.paste_format, statustip='Apply format from the clipboard to the selected ' 'cells') self.font = Action(self.parent, "&Font...", self.parent.grid.on_font_dialog, icon=Icon.font_dialog, shortcut='Ctrl+n', statustip='Lauch font dialog') self.bold = Action(self.parent, "&Bold", self.parent.grid.on_bold_pressed, icon=Icon.bold, shortcut='Ctrl+b', checkable=True, statustip='Toggle bold font weight for the ' 'selected cells') self.italics = Action(self.parent, "&Italics", self.parent.grid.on_italics_pressed, icon=Icon.italics, shortcut='Ctrl+i', checkable=True, statustip='Toggle italics font style for the ' 'selected cells') self.underline = Action(self.parent, "&Underline", self.parent.grid.on_underline_pressed, icon=Icon.underline, shortcut='Ctrl+u', checkable=True, statustip='Toggle underline for the ' 'selected cells') self.strikethrough = Action(self.parent, "&Strikethrough", self.parent.grid.on_strikethrough_pressed, icon=Icon.strikethrough, checkable=True, statustip='Toggle strikethrough for the ' 'selected cells') self.text = Action(self.parent, "Text renderer", self.parent.grid.on_text_renderer_pressed, icon=Icon.text, checkable=True, statustip='Show cell results as text (default). ' 'Formats affect the whole cell') self.markup = Action(self.parent, "Markup renderer", self.parent.grid.on_markup_renderer_pressed, icon=Icon.markup, checkable=True, statustip='Show cell results as markup, which ' 'allows partly formatted output') self.image = Action(self.parent, "Image renderer", self.parent.grid.on_image_renderer_pressed, icon=Icon.image, checkable=True, statustip='Show cell results as image. A numpy ' 'array of shape (x, y, 3) ' 'is expected') if matplotlib_figure is not None: self.matplotlib = \ Action(self.parent, "Matplotlib chart renderer", self.parent.grid.on_matplotlib_renderer_pressed, icon=Icon.matplotlib, checkable=True, statustip='Show cell results as matplotlib chart. A ' 'numpy array of shape (x, y, 3) is expected') renderer_group = QActionGroup(self.parent) renderer_group.addAction(self.text) renderer_group.addAction(self.markup) renderer_group.addAction(self.image) if matplotlib_figure is not None: renderer_group.addAction(self.matplotlib) self.text_color = Action( self.parent, "Text color...", self.parent.widgets.text_color_button.on_pressed, icon=Icon.text_color, statustip='Lauch text color dialog') self.line_color = Action( self.parent, "Line color...", self.parent.widgets.line_color_button.on_pressed, icon=Icon.line_color, statustip='Lauch line color dialog') self.background_color = Action( self.parent, "Background color...", self.parent.widgets.background_color_button.on_pressed, icon=Icon.background_color, statustip='Lauch background color dialog') self.freeze_cell = Action(self.parent, "Freeze cell", self.parent.grid.on_freeze_pressed, icon=Icon.freeze, checkable=True, statustip='Freeze the selected cell so that ' 'is is only updated when <F5> is ' 'pressed') self.lock_cell = Action(self.parent, "Lock cell", self.parent.grid.on_lock_pressed, icon=Icon.lock, checkable=True, statustip='Lock cell so that its code ' 'cannot be changed') self.button_cell = Action(self.parent, "Button cell", self.parent.grid.on_button_cell_pressed, icon=Icon.button, checkable=True, statustip='Make cell a button cell that is ' 'executed only when pressed') self.merge_cells = Action(self.parent, "Merge cells", self.parent.grid.on_merge_pressed, icon=Icon.merge_cells, checkable=True, statustip='Merge/unmerge selected cells') self.rotate_0 = Action(self.parent, "0°", self.parent.grid.on_rotate_0, icon=Icon.rotate_0, checkable=True, statustip='Set text rotation to 0°') self.rotate_90 = Action(self.parent, "90°", self.parent.grid.on_rotate_90, icon=Icon.rotate_90, checkable=True, statustip='Set text rotation to 90°') self.rotate_180 = Action(self.parent, "180°", self.parent.grid.on_rotate_180, icon=Icon.rotate_180, checkable=True, statustip='Set text rotation to 180°') self.rotate_270 = Action(self.parent, "270°", self.parent.grid.on_rotate_270, icon=Icon.rotate_270, checkable=True, statustip='Set text rotation to 270°') rotate_group = QActionGroup(self.parent) rotate_group.addAction(self.rotate_0) rotate_group.addAction(self.rotate_90) rotate_group.addAction(self.rotate_180) rotate_group.addAction(self.rotate_270) self.justify_left = Action(self.parent, "Left", self.parent.grid.on_justify_left, icon=Icon.justify_left, checkable=True, statustip='Display cell result text ' 'left justified') self.justify_center = Action(self.parent, "Center", self.parent.grid.on_justify_center, checkable=True, icon=Icon.justify_center, statustip='Display cell result text ' 'centered') self.justify_right = Action(self.parent, "Right", self.parent.grid.on_justify_right, checkable=True, icon=Icon.justify_right, statustip='Display cell result text ' 'right justified') self.justify_fill = Action(self.parent, "Fill", self.parent.grid.on_justify_fill, icon=Icon.justify_fill, checkable=True, statustip='Display cell result text ' 'filled into the cell') justify_group = QActionGroup(self.parent) justify_group.addAction(self.justify_left) justify_group.addAction(self.justify_center) justify_group.addAction(self.justify_right) justify_group.addAction(self.justify_fill) self.align_top = Action(self.parent, "Top", self.parent.grid.on_align_top, icon=Icon.align_top, checkable=True, statustip='Align cell result at the top of ' 'the cell') self.align_center = Action(self.parent, "Center", self.parent.grid.on_align_middle, icon=Icon.align_center, checkable=True, statustip='Center cell result within ' 'the cell') self.align_bottom = Action(self.parent, "Bottom", self.parent.grid.on_align_bottom, icon=Icon.align_bottom, checkable=True, statustip='Align cell result at the ' 'bottom of the cell') align_group = QActionGroup(self.parent) align_group.addAction(self.align_top) align_group.addAction(self.align_center) align_group.addAction(self.align_bottom) self.format_borders_all = \ Action(self.parent, "All borders", self.parent.grid.on_border_choice, icon=Icon.format_borders_all, checkable=True, statustip='Format all borders of selection') self.format_borders_top = \ Action(self.parent, "Top border", self.parent.grid.on_border_choice, icon=Icon.format_borders_top, checkable=True, statustip='Format top border of selection') self.format_borders_bottom = \ Action(self.parent, "Bottom border", self.parent.grid.on_border_choice, icon=Icon.format_borders_bottom, checkable=True, statustip='Format bottom border of selection') self.format_borders_left = \ Action(self.parent, "Left border", self.parent.grid.on_border_choice, icon=Icon.format_borders_left, checkable=True, statustip='Format left border of selection') self.format_borders_right = \ Action(self.parent, "Right border", self.parent.grid.on_border_choice, icon=Icon.format_borders_right, checkable=True, statustip='Format right border of selection') self.format_borders_outer = \ Action(self.parent, "Outer borders", self.parent.grid.on_border_choice, icon=Icon.format_borders_outer, checkable=True, statustip='Format outer borders of selection') self.format_borders_inner = \ Action(self.parent, "Inner borders", self.parent.grid.on_border_choice, icon=Icon.format_borders_inner, checkable=True, statustip='Format inner borders of selection') self.format_borders_top_bottom = \ Action(self.parent, "Top and bottom borders", self.parent.grid.on_border_choice, icon=Icon.format_borders_top_bottom, checkable=True, statustip='Format top and bottom borders of selection') self.border_group = QActionGroup(self.parent) self.border_group.addAction(self.format_borders_all) self.border_group.addAction(self.format_borders_top) self.border_group.addAction(self.format_borders_bottom) self.border_group.addAction(self.format_borders_left) self.border_group.addAction(self.format_borders_right) self.border_group.addAction(self.format_borders_outer) self.border_group.addAction(self.format_borders_inner) self.border_group.addAction(self.format_borders_top_bottom) self.format_borders_all.setChecked(True) self.format_borders_0 = Action(self.parent, "Border width 0", self.parent.grid.on_borderwidth, icon=Icon.format_borders_0, statustip='Set border width to 0') self.format_borders_1 = Action(self.parent, "Border width 1", self.parent.grid.on_borderwidth, icon=Icon.format_borders_1, statustip='Set border width to 1') self.format_borders_2 = Action(self.parent, "Border width 2", self.parent.grid.on_borderwidth, icon=Icon.format_borders_2, statustip='Set border width to 2') self.format_borders_4 = Action(self.parent, "Border width 4", self.parent.grid.on_borderwidth, icon=Icon.format_borders_4, statustip='Set border width to 4') self.format_borders_8 = Action(self.parent, "Border width 8", self.parent.grid.on_borderwidth, icon=Icon.format_borders_8, statustip='Set border width to 8') self.format_borders_16 = Action(self.parent, "Border width 16", self.parent.grid.on_borderwidth, icon=Icon.format_borders_16, statustip='Set border width to 16') self.format_borders_32 = Action(self.parent, "Border width 32", self.parent.grid.on_borderwidth, icon=Icon.format_borders_32, statustip='Set border width to 32') self.format_borders_64 = Action(self.parent, "Border width 64", self.parent.grid.on_borderwidth, icon=Icon.format_borders_64, statustip='Set border width to 64') self.border_width_group = QActionGroup(self.parent) self.border_width_group.addAction(self.format_borders_0) self.border_width_group.addAction(self.format_borders_1) self.border_width_group.addAction(self.format_borders_2) self.border_width_group.addAction(self.format_borders_4) self.border_width_group.addAction(self.format_borders_8) self.border_width_group.addAction(self.format_borders_16) self.border_width_group.addAction(self.format_borders_32) self.border_width_group.addAction(self.format_borders_64) self.format_borders_1.setChecked(True) def create_macro_actions(self): """Create actions for Macro menu""" self.insert_image = Action(self.parent, "Insert image...", self.parent.workflows.macro_insert_image, icon=Icon.insert_image, statustip='Load an image from a file ' 'into a cell') self.insert_chart = Action(self.parent, "Insert chart...", self.parent.workflows.macro_insert_chart, icon=Icon.insert_chart, statustip='Create and display matplotlib ' 'chart') def create_help_actions(self): """actions for Help menu""" self.manual = Action(self.parent, "Manual...", self.parent.on_manual, icon=Icon.help, shortcut='F1', statustip='Display the pyspread manual') self.tutorial = Action(self.parent, "Tutorial...", self.parent.on_tutorial, icon=Icon.tutorial, statustip='Display a pyspread tutorial') self.dependencies = Action(self.parent, "Dependencies...", self.parent.on_dependencies, icon=Icon.dependencies, statustip='List and install dependencies') self.about = Action(self.parent, "About pyspread...", self.parent.on_about, icon=Icon.pyspread, statustip='About pyspread') def disable_unavailable(self): """Disables unavailable menu items e.g. due to missing dependencies""" if get_enchant_version() is None: self.toggle_spell_checker.setEnabled(False)
class MyPlot(QObject): def __init__(self, fig, plt, parent): QtCore.QObject.__init__(self, parent) self.fig = fig self.plt = plt self.fig.canvas.mpl_connect('scroll_event', self.onScroll) self.fig.canvas.mpl_connect('button_press_event', self.onPress) self.fig.canvas.mpl_connect('button_release_event', self.onRelease) self.pan = None self.sel = None # Derived classes should initialize to Rectangle self.fieldfilterX = None # Derived classes should intialize if manageX self.fieldfilterY = None # Derived classes should intialize if manageY self.selp = None self.cb = None # Colorbar self.manageX = False self.manageY = False self.menu = QMenu() saveAct = self.menu.addAction("Save PNG") saveAct.triggered.connect(parent.onSave) saveAct = self.menu.addAction("Save SVG") saveAct.triggered.connect(parent.onSaveSVG) resetAct = self.menu.addAction("Reset") resetAct.triggered.connect(self.onReset) drawActionGroup = QActionGroup(self) self.drawAll = drawActionGroup.addAction("Draw All (Ignore Selection)") self.drawAll.triggered.connect(self.mydraw) self.drawAll.setCheckable(True) self.drawBoth = drawActionGroup.addAction( "Draw All Semi-transparent; Selection Full Color") self.drawBoth.triggered.connect(self.mydraw) self.drawBoth.setCheckable(True) self.drawBoth.setChecked(True) self.drawSelection = drawActionGroup.addAction("Draw Selection Only") self.drawSelection.triggered.connect(self.mydraw) self.drawSelection.setCheckable(True) self.datamenu = self.menu.addMenu("Data") self.datamenu.addAction(self.drawAll) self.datamenu.addAction(self.drawBoth) self.datamenu.addAction(self.drawSelection) self.legendActionGroup = None self.range = None self.origRange = None def legendMenu(self): self.legendActionGroup = QActionGroup(self) legMenu = self.menu.addMenu("Legend") legInit = None legOpt = self.legendActionGroup.addAction("None") legOpt.triggered.connect(self.mydraw) legOpt.setCheckable(True) legOpt.setChecked(legInit is None) legOpt.setData(None) legMenu.addAction(legOpt) legendOpts = [ 'best', 'upper right', 'upper left', 'lower left', 'lower right', 'right', 'center left', 'center right', 'lower center', 'upper center', 'center' ] for i, opt in enumerate(legendOpts): legOpt = self.legendActionGroup.addAction(opt) legOpt.triggered.connect(self.mydraw) legOpt.setCheckable(True) legOpt.setChecked(i == legInit) legOpt.setData(i) legMenu.addAction(legOpt) def initRange(self, xrange, yrange): if self.manageX and self.manageY: self.origRange = (tuple(xrange), tuple(yrange)) rangeAct = self.menu.addAction("Set Range to Current Limits") rangeAct.triggered.connect(self.onSetRange) xrangeAct = self.menu.addAction("Set X Range to Current Limits") xrangeAct.triggered.connect(self.onSetXRange) yrangeAct = self.menu.addAction("Set Y Range to Current Limits") yrangeAct.triggered.connect(self.onSetYRange) elif self.manageX: self.origRange = tuple(xrange) rangeAct = self.menu.addAction("Set Range to Current Limits") rangeAct.triggered.connect(self.onSetXRange) elif self.manageY: self.origRange = tuple(yrange) rangeAct = self.menu.addAction("Set Range to Current Limits") rangeAct.triggered.connect(self.onSetYRange) self.range = self.origRange def onSetRange(self): self.range = (tuple(self.plt.get_xlim()), tuple(self.plt.get_ylim())) self.mydraw() def onSetXRange(self): if self.manageY: self.range = (tuple(self.plt.get_xlim()), self.range[1]) else: self.range = tuple(self.plt.get_xlim()) self.mydraw(False) def onSetYRange(self): if self.manageX: self.range = (self.range[0], tuple(self.plt.get_ylim())) else: self.range = tuple(self.plt.get_ylim()) self.mydraw(False) def onReset(self): self.range = self.origRange self.mydraw(False) def datadraw(self): pass def mydraw(self, keeplimits=True): if keeplimits: xlim = self.plt.get_xlim() ylim = self.plt.get_ylim() self.plt.cla() self.datadraw() if self.sel is not None: self.plt.add_patch(self.sel) if keeplimits and self.manageX: self.plt.set_xlim(xlim) if keeplimits and self.manageY: self.plt.set_ylim(ylim) self.parent().draw() def onScroll(self, event): xcenter = event.xdata ycenter = event.ydata x, y, axis = event.x, event.y, event.inaxes # Axis scroll xAxes, yAxes = self.plt.transAxes.inverted().transform([x, y]) if xAxes < 0 and xAxes >= -0.2 and yAxes >= 0 and yAxes <= 1: # Hover over y axis ycenter = self.plt.transData.inverted().transform([0, y])[1] axis = self.plt if yAxes < 0 and yAxes >= -0.2 and xAxes >= 0 and xAxes <= 1: # Hover over x axis xcenter = self.plt.transData.inverted().transform([x, 0])[0] axis = self.plt if xcenter is None and ycenter is None or axis != self.plt: return scale = 1 factor = 1.5 if event.button == 'up': scale = factor else: scale = 1.0 / factor if scale != 1: if self.manageX and xcenter is not None: cur_xlim = self.plt.get_xlim() self.plt.set_xlim([ xcenter - (xcenter - cur_xlim[0]) / scale, xcenter + (cur_xlim[1] - xcenter) / scale ]) if self.manageY and ycenter is not None: cur_ylim = self.plt.get_ylim() self.plt.set_ylim([ ycenter - (ycenter - cur_ylim[0]) / scale, ycenter + (cur_ylim[1] - ycenter) / scale ]) if (self.manageX and xcenter is not None) or (self.manageY and ycenter is not None): self.parent().draw() def onPress(self, event): if event.button == 1 and event.xdata is not None and event.ydata is not None and event.inaxes == self.plt: if event.key == 'shift' or QtCore.Qt.ShiftModifier & QApplication.keyboardModifiers( ): self.selp = (event.xdata, event.ydata) if self.manageX and self.fieldfilterX is not None: self.sel.set_x(event.xdata) self.sel.set_width(0) if self.manageY and self.fieldfilterY is not None: self.sel.set_y(event.ydata) self.sel.set_height(0) if (self.manageX and self.fieldfilterX is not None) or \ (self.manageY and self.fieldfilterY is not None): self.sel.set_visible(True) self.parent().draw() else: self.pan = (event.xdata, event.ydata) elif event.button == 3 and event.inaxes == self.plt: self.menu.popup(QCursor.pos()) def onRelease(self, event): if self.pan is not None: self.pan = None elif self.selp is not None: if not self.manageX or not self.manageY: self.selp = None # Only managing 0 or 1 axis, clear right away if self.manageX and self.fieldfilterX is not None: if self.sel.get_width() == 0: self.sel.set_visible(False) self.fieldfilterX.setActive(False) else: self.fieldfilterX.setRange( self.sel.get_x(), self.sel.get_width() + self.sel.get_x()) self.fieldfilterX.setActive(True) self.selp = None # In case we were managing two axis, clear here if self.manageY and self.fieldfilterY is not None: if self.sel.get_height() == 0: self.sel.set_visible(False) self.fieldfilterY.setActive(False) else: self.fieldfilterY.setRange( self.sel.get_y(), self.sel.get_height() + self.sel.get_y()) self.fieldfilterY.setActive(True) self.selp = None # Probably unneccessary but make sure this does get cleared if self.manageX or self.manageY: self.mydraw() def onMotion(self, event): handled = False if event.xdata and event.ydata and event.inaxes == self.plt: if self.pan is not None: handled = True if self.manageX: xlim = self.plt.get_xlim() xlim -= (event.xdata - self.pan[0]) self.plt.set_xlim(xlim) if self.manageY: ylim = self.plt.get_ylim() ylim -= (event.ydata - self.pan[1]) self.plt.set_ylim(ylim) if self.manageX or self.manageY: self.parent().draw() elif self.selp is not None: handled = True if self.manageX: if self.selp[0] <= event.xdata: self.sel.set_width(event.xdata - self.selp[0]) else: self.sel.set_x(event.xdata) self.sel.set_width(self.selp[0] - event.xdata) if self.manageY: if self.selp[1] <= event.ydata: self.sel.set_height(event.ydata - self.selp[1]) else: self.sel.set_y(event.ydata) self.sel.set_height(self.selp[1] - event.ydata) if self.manageX or self.manageY: self.parent().draw() def onFilterChange(self): if self.selp is not None: return # Selecting is from this plot, wait for both filters to update so we don't clear variables ylim = self.plt.get_ylim() xlim = self.plt.get_xlim() self.sel.set_x(xlim[0]) self.sel.set_y(ylim[0]) self.sel.set_width(xlim[1] - xlim[0]) self.sel.set_height(ylim[1] - ylim[0]) if self.manageX and self.fieldfilterX is not None: self.sel.set_x(self.fieldfilterX.minimum) self.sel.set_width(self.fieldfilterX.maximum - self.fieldfilterX.minimum) self.sel.set_visible(self.fieldfilterX.isActive()) if self.manageY and self.fieldfilterY is not None: self.sel.set_y(self.fieldfilterY.minimum) self.sel.set_height(self.fieldfilterY.maximum - self.fieldfilterY.minimum) self.sel.set_visible(self.fieldfilterY.isActive() or (self.fieldfilterX is not None and self.fieldfilterX.isActive())) self.parent().draw() def xlabels(self, model, field, distribute=False): if model.isCategorical(field): lbls = model.labels(field) ticks = model.labelints(field) if distribute: ticks = ticks.astype(float) + 0.5 self.plt.set_xticks(ticks) self.plt.set_xticklabels(lbls) else: self.plt.locator_params(axis='x', nbins=model.cfg.numXticks) def ylabels(self, model, field, distribute=False): if model.isCategorical(field): lbls = model.labels(field) ticks = model.labelints(field) if distribute: ticks = ticks.astype(float) + 0.5 self.plt.set_yticks(ticks) self.plt.set_yticklabels(lbls)
class UserAgentMenu(QMenu): """ Class implementing a menu to select the user agent string. """ def __init__(self, title, url=None, parent=None): """ Constructor @param title title of the menu (string) @param url URL to set user agent for (QUrl) @param parent reference to the parent widget (QWidget) """ super(UserAgentMenu, self).__init__(title, parent) self.__manager = None self.__url = url if self.__url: if self.__url.isValid(): import Helpviewer.HelpWindow self.__manager = \ Helpviewer.HelpWindow.HelpWindow.userAgentsManager() else: self.__url = None self.aboutToShow.connect(self.__populateMenu) def __populateMenu(self): """ Private slot to populate the menu. """ self.aboutToShow.disconnect(self.__populateMenu) self.__actionGroup = QActionGroup(self) # add default action self.__defaultUserAgent = QAction(self) self.__defaultUserAgent.setText(self.tr("Default")) self.__defaultUserAgent.setCheckable(True) self.__defaultUserAgent.triggered.connect( self.__switchToDefaultUserAgent) if self.__url: self.__defaultUserAgent.setChecked( self.__manager.userAgentForUrl(self.__url) == "") else: from Helpviewer.HelpBrowserWV import HelpWebPage self.__defaultUserAgent.setChecked(HelpWebPage().userAgent() == "") self.addAction(self.__defaultUserAgent) self.__actionGroup.addAction(self.__defaultUserAgent) isChecked = self.__defaultUserAgent.isChecked() # add default extra user agents isChecked = self.__addDefaultActions() or isChecked # add other action self.addSeparator() self.__otherUserAgent = QAction(self) self.__otherUserAgent.setText(self.tr("Other...")) self.__otherUserAgent.setCheckable(True) self.__otherUserAgent.triggered.connect( self.__switchToOtherUserAgent) self.addAction(self.__otherUserAgent) self.__actionGroup.addAction(self.__otherUserAgent) self.__otherUserAgent.setChecked(not isChecked) def __switchToDefaultUserAgent(self): """ Private slot to set the default user agent. """ if self.__url: self.__manager.removeUserAgent(self.__url.host()) else: from Helpviewer.HelpBrowserWV import HelpWebPage HelpWebPage().setUserAgent("") def __switchToOtherUserAgent(self): """ Private slot to set a custom user agent string. """ from Helpviewer.HelpBrowserWV import HelpWebPage userAgent, ok = QInputDialog.getText( self, self.tr("Custom user agent"), self.tr("User agent:"), QLineEdit.Normal, HelpWebPage().userAgent(resolveEmpty=True)) if ok: if self.__url: self.__manager.setUserAgentForUrl(self.__url, userAgent) else: HelpWebPage().setUserAgent(userAgent) def __changeUserAgent(self): """ Private slot to change the user agent. """ act = self.sender() if self.__url: self.__manager.setUserAgentForUrl(self.__url, act.data()) else: from Helpviewer.HelpBrowserWV import HelpWebPage HelpWebPage().setUserAgent(act.data()) def __addDefaultActions(self): """ Private slot to add the default user agent entries. @return flag indicating that a user agent entry is checked (boolean) """ from . import UserAgentDefaults_rc # __IGNORE_WARNING__ defaultUserAgents = QFile(":/UserAgentDefaults.xml") defaultUserAgents.open(QIODevice.ReadOnly) menuStack = [] isChecked = False if self.__url: currentUserAgentString = self.__manager.userAgentForUrl(self.__url) else: from Helpviewer.HelpBrowserWV import HelpWebPage currentUserAgentString = HelpWebPage().userAgent() xml = QXmlStreamReader(defaultUserAgents) while not xml.atEnd(): xml.readNext() if xml.isStartElement() and xml.name() == "separator": if menuStack: menuStack[-1].addSeparator() else: self.addSeparator() continue if xml.isStartElement() and xml.name() == "useragent": attributes = xml.attributes() title = attributes.value("description") userAgent = attributes.value("useragent") act = QAction(self) act.setText(title) act.setData(userAgent) act.setToolTip(userAgent) act.setCheckable(True) act.setChecked(userAgent == currentUserAgentString) act.triggered.connect(self.__changeUserAgent) if menuStack: menuStack[-1].addAction(act) else: self.addAction(act) self.__actionGroup.addAction(act) isChecked = isChecked or act.isChecked() if xml.isStartElement() and xml.name() == "useragentmenu": attributes = xml.attributes() title = attributes.value("title") if title == "v_a_r_i_o_u_s": title = self.tr("Various") menu = QMenu(self) menu.setTitle(title) self.addMenu(menu) menuStack.append(menu) if xml.isEndElement() and xml.name() == "useragentmenu": menuStack.pop() if xml.hasError(): E5MessageBox.critical( self, self.tr("Parsing default user agents"), self.tr( """<p>Error parsing default user agents.</p><p>{0}</p>""") .format(xml.errorString())) return isChecked
class WMain(QMainWindow): def __init__(self): super().__init__() self.ctrl = QApplication.instance().ctrl self.ctrl.switch_language.connect(self.on_switch_language) self.ctrl.switch_configuration.connect(self.on_switch_configuration) self.window_title = (_("Metadata Publishing Tool")) self.paras = self.ctrl.paras self.config = GuiConf() self.init_ui() self.on_switch_language() self.on_switch_configuration() def init_ui(self): self.create_menus() self.tabframe = TabbedFrame(self) self.setCentralWidget(self.tabframe) self.about_widget = None self.resize(self.ctrl.config.window_width(), self.ctrl.config.window_height()) ####### menu bar ###################################### def create_menus(self): self.menubar = self.menuBar() self.menubar.setNativeMenuBar(False) self.menu_file = QMenu(_("File"), self) self.menubar.addMenu(self.menu_file) self.menu_open_config = QMenu(_("Load configuration"), self) self.menu_file.addMenu(self.menu_open_config) self.config_action_group = QActionGroup(self) self.config_action_group.triggered.connect( self.on_action_switch_config_triggered) self.menu_open_config.aboutToShow.connect( self.on_menu_open_config_about_to_show) self.action_save_configuration_as = QAction( _("Save configuration as..."), self) self.menu_file.addAction(self.action_save_configuration_as) self.action_save_configuration_as.triggered.connect( self.on_action_save_configuration_as_triggered) self.action_configurations = QAction(_("Configurations..."), self) self.menu_file.addAction(self.action_configurations) self.action_configurations.triggered.connect( self.on_action_configurations_triggered) self.menu_file.addSeparator() self.action_exit = QAction(_("Exit"), self) self.action_exit.setShortcut("Ctrl+Q") self.action_exit.triggered.connect(self.on_action_exit_triggered) self.menu_file.addAction(self.action_exit) self.menu_settings = self.create_menu_settings() self.menubar.addMenu(self.menu_settings) self.menu_window = QMenu(_("Window"), self) self.menu_window.aboutToShow.connect(self.on_menu_window_about_to_show) self.menubar.addMenu(self.menu_window) self.window_action_group = QActionGroup(self) self.window_action_group.triggered.connect( self.on_action_switch_window_triggered) self.action_about = QAction(_("About..."), self) self.action_about.triggered.connect(self.on_action_about_triggered) ####### menus ###################################### def create_menu_settings(self): menu_settings = QMenu(_("Preferences"), self) self.menu_language = self.create_menu_language() menu_settings.addMenu(self.menu_language) return menu_settings def create_menu_language(self): language_menu = QMenu(_("Language"), self) action_group = QActionGroup(self) action_group.triggered.connect( self.on_action_language_switch_triggered) iso_idx = self.ctrl.iso_lang() current_locale = self.ctrl.current_language() for locale in self.ctrl.locales(): short_locale = locale.split("-")[0] if short_locale in iso_idx: iso = iso_idx[short_locale] else: iso = {'nativeName': '-', 'name': '-'} action = QAction( "%s - %s [%s]" % (iso["nativeName"], iso["name"], locale), self) action.setCheckable(True) action.setData(locale) language_menu.addAction(action) action_group.addAction(action) if locale == current_locale: action.setChecked(True) return language_menu def on_menu_window_about_to_show(self): self.menu_window.clear() for child in self.window_action_group.children(): self.window_action_group.removeAction(child) for window in QApplication.instance().topLevelWindows(): if window.type() == 1 and not window.title() == self.windowTitle(): action = QAction(window.title(), self) action.setData(window.title()) action.setCheckable(True) self.window_action_group.addAction(action) self.menu_window.addAction(action) self.menu_window.addSeparator() self.menu_window.addAction(self.action_about) ####### functions ####################################### def on_switch_language(self, code=None): LOG.debug("Switch language: %s" % code) self.menu_file.setTitle(_("File")) self.action_exit.setText(_("Exit")) self.action_exit.setStatusTip(_("Exit application")) self.menu_open_config.setTitle(_("Load configuration")) self.action_save_configuration_as.setText( _("Save configuration as...")) self.action_configurations.setText(_("Configurations...")) self.action_about.setText(_("About...")) self.menu_settings.setTitle(_("Preferences")) self.menu_language.setTitle(_("Language")) self.menu_window.setTitle(_("Window")) self.window_title = (_("Metadata Publishing Tool")) self.setWindowTitle(self.window_title + " [" + self.paras.configuration_name() + "]") def on_switch_configuration(self, name=None): LOG.debug("Switch configuration: %s" % name) self.paras = self.ctrl.paras self.setWindowTitle(self.window_title + " [" + self.paras.configuration_name() + "]") def on_menu_open_config_about_to_show(self): self.menu_open_config.clear() for child in self.config_action_group.children(): self.config_action_group.removeAction(child) current_name = Configurations.current_configuration_name() for name in Configurations.list_configurations(): action = QAction(name, self) action.setCheckable(True) action.setData(name) self.config_action_group.addAction(action) self.menu_open_config.addAction(action) if name == current_name: action.setChecked(True) def on_action_switch_config_triggered(self): action_group = self.sender() name = action_group.checkedAction().data() if self.tabframe.about_to_change(_("Switch configuration...")): self.ctrl.load_configuration(name) def on_action_save_configuration_as_triggered(self): text = _("Save configuration as...") if self.tabframe.about_to_change(text): dlg = QInputDialog(self) dlg.setInputMode(QInputDialog.TextInput) dlg.setWindowTitle(text) dlg.setLabelText(_("Configuration name:")) dlg.setTextValue(self.paras.configuration_name()) dlg.resize(300, 100) if dlg.exec_(): self.ctrl.save_configuration_as(dlg.textValue()) def on_action_configurations_triggered(self): ConfigurationsDialog(self).exec_() def on_action_language_switch_triggered(self): action_group = self.sender() locale = action_group.checkedAction().data() self.ctrl.set_language(locale) def on_action_switch_window_triggered(self): action_group = self.sender() window_title = action_group.checkedAction().data() for window in QApplication.instance().topLevelWindows(): if window_title == window.title(): window.show() window.raise_() window.requestActivate() def on_action_about_triggered(self): if self.about_widget is None: LOG.debug("Creating About window") self.about_widget = AboutWidget(self) else: self.about_widget.show() self.about_widget.setWindowState(Qt.WindowActive) self.about_widget.activateWindow() self.about_widget.raise_() def on_action_exit_triggered(self): LOG.debug("action_exit_triggered") if self.tabframe.about_to_change(_("Closing application...")): qApp.quit() def closeEvent(self, event): LOG.debug("closeEvent was triggered") if self.tabframe.about_to_change(_("Closing application...")): LOG.debug("Accepting closeEvent") event.accept() else: LOG.debug("Ignoring closeEvent") event.ignore() def close(self): LOG.debug("window closing") self.ctrl.config.set_window_height(self.height()) self.ctrl.config.set_window_width(self.width()) self.ctrl.config.set_last_configuration_name( self.paras.configuration_name()) self.ctrl.config.persist() self.tabframe.close() if self.about_widget: self.about_widget.close()
def main(): exitcode = 0 try: menu = QMenu() # Proxy m_proxy = QAction("Proxy: Disabled") m_proxy.setShortcut('Ctrl+P') m_proxy.setCheckable(True) m_proxy.setDisabled(True) m_proxy.triggered.connect(lambda: current['proxy'].disable(m_proxy)) menu.addAction(m_proxy) proxy_dict = {} proxy_group = QActionGroup(menu) for proxy in profile.get_items('Proxy'): proxyname = proxy[0] proxy_dict[proxyname] = Proxy(proxy, m_proxy) proxy_group.addAction(proxy_dict[proxyname].QAction) menu.addAction(proxy_dict[proxyname].QAction) # Bypass menu.addSeparator() m_bypass = QAction("Bypass: Disabled") m_bypass.setShortcut('Ctrl+B') m_bypass.setCheckable(True) m_bypass.setDisabled(True) m_bypass.triggered.connect(lambda: current['bypass'].disable(m_bypass)) menu.addAction(m_bypass) bypass_dict = {} bypass_group = QActionGroup(menu) for bypass in profile.get_items('Bypass'): bypassname = bypass[0] bypass_dict[bypassname] = Bypass(bypass, m_bypass) bypass_group.addAction(bypass_dict[bypassname].QAction) menu.addAction(bypass_dict[bypassname].QAction) # Capture menu.addSeparator() m_capture = QAction("Capture: Disabled") m_capture.setShortcut('Ctrl+C') m_capture.setCheckable(True) m_capture.setDisabled(True) m_dashboard = QAction("Open Dashboard...") m_dashboard.setShortcut('Ctrl+D') m_dashboard.setDisabled(True) m_capture.triggered.connect( lambda: current['capture'].disable(m_capture, m_dashboard)) menu.addAction(m_capture) capture_dict = {} capture_group = QActionGroup(menu) for capture in profile.get_items('Capture'): capturename = capture[0] capture_dict[capturename] = Capture(capture, m_capture, m_dashboard) capture_group.addAction(capture_dict[capturename].QAction) menu.addAction(capture_dict[capturename].QAction) menu.addAction(m_dashboard) # Common m_log = QAction("Log Folder") m_log.setShortcut('Ctrl+L') m_log.triggered.connect(lambda: subprocess.call(["open", log_path])) m_profile = QAction("Profile Folder") m_profile.setShortcut('Ctrl+F') m_profile.triggered.connect( lambda: subprocess.call(["open", profile_path])) m_extension = QAction("Extension Folder") m_extension.setShortcut('Ctrl+E') m_extension.triggered.connect( lambda: subprocess.call(["open", ext_path])) m_copy_shell = QAction("Copy Shell Command") m_copy_shell.setShortcut('Ctrl+S') m_set_system = QAction("As System Proxy: " + user_port) m_set_system.setShortcut('Ctrl+A') m_set_system.triggered.connect(lambda: setproxy_menu(m_set_system)) m_copy_shell.triggered.connect(copy_shell) m_set_system.setCheckable(True) if system: m_set_system.setChecked(True) setproxy() menu.addSeparator() menu.addAction(m_set_system) menu.addSeparator() menu.addAction(m_log) menu.addAction(m_profile) menu.addAction(m_extension) menu.addAction(m_copy_shell) menu.addSeparator() m_quit = QAction('Quit V2Net (' + version + ')') m_quit.setShortcut('Ctrl+Q') m_quit.triggered.connect(APP.quit) menu.addAction(m_quit) # Add Tray tray = QSystemTrayIcon() tray.setIcon(QIcon("icon.png")) tray.setVisible(True) tray.setContextMenu(menu) # sys.exit(app.exec_()) exitcode = APP.exec_() finally: quitapp(exitcode)
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 FrameZoomMenu(QMenu): def __init__(self, main_win, zoom_ctrl_mngr, listplayer, media_player): super().__init__(parent=main_win) self.main_win = main_win self.zoom_ctrl_mngr = zoom_ctrl_mngr self.listplayer = listplayer self.mp = media_player self.setTitle("Zoom") self.setIcon(icons.get("zoom_menu_button")) self.quarter = QAction("1:4 Quarter", self) self.quarter.triggered.connect( lambda: self.zoom_ctrl_mngr.set_scale(0.25)) self.quarter.setCheckable(True) self.half = QAction("1:2 Half", self) self.half.triggered.connect(lambda: self.zoom_ctrl_mngr.set_scale(0.5)) self.half.setCheckable(True) self.original = QAction("1:1 Original", self) self.original.triggered.connect( lambda: self.zoom_ctrl_mngr.set_scale(1)) self.original.setCheckable(True) self.double = QAction("1:2 Double", self) self.double.triggered.connect(lambda: self.zoom_ctrl_mngr.set_scale(2)) self.double.setCheckable(True) self.explicit_zooms = QActionGroup(self) self.explicit_zooms.addAction(self.quarter) self.explicit_zooms.addAction(self.half) self.explicit_zooms.addAction(self.original) self.explicit_zooms.addAction(self.double) self.addActions(self.explicit_zooms.actions()) self.option_action_map = { 0.25: self.quarter, 0.5: self.half, 1: self.original, 2: self.double, } self.zoom_in = ZoomInAction(self, zoom_ctrl_mngr=self.zoom_ctrl_mngr) self.zoom_in.setIcon(icons.get("zoom_in_menu_item")) self.addAction(self.zoom_in) self.zoom_out = ZoomOutAction(self, zoom_ctrl_mngr=self.zoom_ctrl_mngr) self.zoom_out.setIcon(icons.get("zoom_out_menu_item")) self.addAction(self.zoom_out) self.addSeparator() self.addAction(AutoResizeAction(self)) self.conform_to_media() self.zoom_ctrl_mngr.zoomchanged.connect(self.on_zoomchanged) self.listplayer.mediachanged.connect(self.on_mediachanged) def on_zoomchanged(self, value): self.option_action_map[value].setChecked(True) def on_mediachanged(self): self.conform_to_media() def conform_to_media(self): self.option_action_map[config.state.view_scale].setChecked(True) def enable_zoom_actions(self): """Enable zoom actions if media is loaded and disable them if not.""" has_media = self.mp.has_media() for a in self.actions(): a.setEnabled(has_media)
class ReTextWindow(QMainWindow): def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.initConfig() self.resize(950, 700) screenRect = QDesktopWidget().screenGeometry() if globalSettings.windowGeometry: self.restoreGeometry(globalSettings.windowGeometry) else: self.move((screenRect.width()-self.width())/2, (screenRect.height()-self.height())/2) if not screenRect.contains(self.geometry()): self.showMaximized() if globalSettings.iconTheme: QIcon.setThemeName(globalSettings.iconTheme) if QFile.exists(icon_path+'retext.png'): self.setWindowIcon(QIcon(icon_path+'retext.png')) elif QFile.exists('/usr/share/pixmaps/retext.png'): self.setWindowIcon(QIcon('/usr/share/pixmaps/retext.png')) else: self.setWindowIcon(QIcon.fromTheme('retext', QIcon.fromTheme('accessories-text-editor'))) self.editBoxes = [] self.previewBoxes = [] self.highlighters = [] self.markups = [] self.fileNames = [] self.actionPreviewChecked = [] self.actionLivePreviewChecked = [] self.tabWidget = QTabWidget(self) self.initTabWidget() self.setCentralWidget(self.tabWidget) self.tabWidget.currentChanged.connect(self.changeIndex) self.tabWidget.tabCloseRequested.connect(self.closeTab) toolBar = QToolBar(self.tr('File toolbar'), self) self.addToolBar(Qt.TopToolBarArea, toolBar) self.editBar = QToolBar(self.tr('Edit toolbar'), self) self.addToolBar(Qt.TopToolBarArea, self.editBar) self.searchBar = QToolBar(self.tr('Search toolbar'), self) self.addToolBar(Qt.BottomToolBarArea, self.searchBar) toolBar.setVisible(not globalSettings.hideToolBar) self.editBar.setVisible(not globalSettings.hideToolBar) self.actionNew = self.act(self.tr('New'), 'document-new', self.createNew, shct=QKeySequence.New) self.actionNew.setPriority(QAction.LowPriority) self.actionOpen = self.act(self.tr('Open'), 'document-open', self.openFile, shct=QKeySequence.Open) self.actionOpen.setPriority(QAction.LowPriority) self.actionSetEncoding = self.act(self.tr('Set encoding'), trig=self.showEncodingDialog) self.actionSetEncoding.setEnabled(False) self.actionReload = self.act(self.tr('Reload'), 'view-refresh', trig=self.openFileMain) self.actionReload.setEnabled(False) self.actionSave = self.act(self.tr('Save'), 'document-save', self.saveFile, shct=QKeySequence.Save) self.actionSave.setEnabled(False) self.actionSave.setPriority(QAction.LowPriority) self.actionSaveAs = self.act(self.tr('Save as'), 'document-save-as', self.saveFileAs, shct=QKeySequence.SaveAs) self.actionNextTab = self.act(self.tr('Next tab'), 'go-next', lambda: self.switchTab(1), shct=Qt.CTRL+Qt.Key_PageDown) self.actionPrevTab = self.act(self.tr('Previous tab'), 'go-previous', lambda: self.switchTab(-1), shct=Qt.CTRL+Qt.Key_PageUp) self.actionPrint = self.act(self.tr('Print'), 'document-print', self.printFile, shct=QKeySequence.Print) self.actionPrint.setPriority(QAction.LowPriority) self.actionPrintPreview = self.act(self.tr('Print preview'), 'document-print-preview', self.printPreview) self.actionViewHtml = self.act(self.tr('View HTML code'), 'text-html', self.viewHtml) self.actionChangeFont = self.act(self.tr('Change default font'), trig=self.changeFont) self.actionSearch = self.act(self.tr('Find text'), 'edit-find', shct=QKeySequence.Find) self.actionSearch.setCheckable(True) self.actionSearch.triggered[bool].connect(self.searchBar.setVisible) self.searchBar.visibilityChanged.connect(self.searchBarVisibilityChanged) self.actionPreview = self.act(self.tr('Preview'), shct=Qt.CTRL+Qt.Key_E, trigbool=self.preview) if QIcon.hasThemeIcon('document-preview'): self.actionPreview.setIcon(QIcon.fromTheme('document-preview')) elif QIcon.hasThemeIcon('preview-file'): self.actionPreview.setIcon(QIcon.fromTheme('preview-file')) elif QIcon.hasThemeIcon('x-office-document'): self.actionPreview.setIcon(QIcon.fromTheme('x-office-document')) else: self.actionPreview.setIcon(QIcon(icon_path+'document-preview.png')) self.actionLivePreview = self.act(self.tr('Live preview'), shct=Qt.CTRL+Qt.Key_L, trigbool=self.enableLivePreview) self.actionTableMode = self.act(self.tr('Table mode'), shct=Qt.CTRL+Qt.Key_T, trigbool=lambda x: self.editBoxes[self.ind].enableTableMode(x)) if ReTextFakeVimHandler: self.actionFakeVimMode = self.act(self.tr('FakeVim mode'), shct=Qt.CTRL+Qt.ALT+Qt.Key_V, trigbool=self.enableFakeVimMode) if globalSettings.useFakeVim: self.actionFakeVimMode.setChecked(True) self.enableFakeVimMode(True) self.actionFullScreen = self.act(self.tr('Fullscreen mode'), 'view-fullscreen', shct=Qt.Key_F11, trigbool=self.enableFullScreen) self.actionFullScreen.setPriority(QAction.LowPriority) self.actionConfig = self.act(self.tr('Preferences'), icon='preferences-system', trig=self.openConfigDialog) self.actionConfig.setMenuRole(QAction.PreferencesRole) self.actionSaveHtml = self.act('HTML', 'text-html', self.saveFileHtml) self.actionPdf = self.act('PDF', 'application-pdf', self.savePdf) self.actionOdf = self.act('ODT', 'x-office-document', self.saveOdf) self.getExportExtensionsList() self.actionQuit = self.act(self.tr('Quit'), 'application-exit', shct=QKeySequence.Quit) self.actionQuit.setMenuRole(QAction.QuitRole) self.actionQuit.triggered.connect(self.close) self.actionUndo = self.act(self.tr('Undo'), 'edit-undo', lambda: self.editBoxes[self.ind].undo(), shct=QKeySequence.Undo) self.actionRedo = self.act(self.tr('Redo'), 'edit-redo', lambda: self.editBoxes[self.ind].redo(), shct=QKeySequence.Redo) self.actionCopy = self.act(self.tr('Copy'), 'edit-copy', lambda: self.editBoxes[self.ind].copy(), shct=QKeySequence.Copy) self.actionCut = self.act(self.tr('Cut'), 'edit-cut', lambda: self.editBoxes[self.ind].cut(), shct=QKeySequence.Cut) self.actionPaste = self.act(self.tr('Paste'), 'edit-paste', lambda: self.editBoxes[self.ind].paste(), shct=QKeySequence.Paste) self.actionUndo.setEnabled(False) self.actionRedo.setEnabled(False) self.actionCopy.setEnabled(False) self.actionCut.setEnabled(False) qApp = QApplication.instance() qApp.clipboard().dataChanged.connect(self.clipboardDataChanged) self.clipboardDataChanged() if enchant_available: self.actionEnableSC = self.act(self.tr('Enable'), trigbool=self.enableSpellCheck) self.actionSetLocale = self.act(self.tr('Set locale'), trig=self.changeLocale) self.actionWebKit = self.act(self.tr('Use WebKit renderer'), trigbool=self.enableWebKit) self.actionWebKit.setChecked(globalSettings.useWebKit) self.actionShow = self.act(self.tr('Show directory'), 'system-file-manager', self.showInDir) self.actionFind = self.act(self.tr('Next'), 'go-next', self.find, shct=QKeySequence.FindNext) self.actionFindPrev = self.act(self.tr('Previous'), 'go-previous', lambda: self.find(back=True), shct=QKeySequence.FindPrevious) self.actionHelp = self.act(self.tr('Get help online'), 'help-contents', self.openHelp) self.aboutWindowTitle = self.tr('About %s', 'Example of final string: About ReText') self.aboutWindowTitle = self.aboutWindowTitle % app_name self.actionAbout = self.act(self.aboutWindowTitle, 'help-about', self.aboutDialog) self.actionAbout.setMenuRole(QAction.AboutRole) self.actionAboutQt = self.act(self.tr('About Qt')) self.actionAboutQt.setMenuRole(QAction.AboutQtRole) self.actionAboutQt.triggered.connect(qApp.aboutQt) availableMarkups = markups.get_available_markups() if not availableMarkups: print('Warning: no markups are available!') self.defaultMarkup = availableMarkups[0] if availableMarkups else None if globalSettings.defaultMarkup: mc = markups.find_markup_class_by_name(globalSettings.defaultMarkup) if mc and mc.available(): self.defaultMarkup = mc if len(availableMarkups) > 1: self.chooseGroup = QActionGroup(self) markupActions = [] for markup in availableMarkups: markupAction = self.act(markup.name, trigbool=self.markupFunction(markup)) if markup == self.defaultMarkup: markupAction.setChecked(True) self.chooseGroup.addAction(markupAction) markupActions.append(markupAction) self.actionBold = self.act(self.tr('Bold'), shct=QKeySequence.Bold, trig=lambda: self.insertChars('**')) self.actionItalic = self.act(self.tr('Italic'), shct=QKeySequence.Italic, trig=lambda: self.insertChars('*')) self.actionUnderline = self.act(self.tr('Underline'), shct=QKeySequence.Underline, trig=lambda: self.insertTag('u')) self.usefulTags = ('a', 'big', 'center', 'img', 's', 'small', 'span', 'table', 'td', 'tr', 'u') self.usefulChars = ('deg', 'divide', 'dollar', 'hellip', 'laquo', 'larr', 'lsquo', 'mdash', 'middot', 'minus', 'nbsp', 'ndash', 'raquo', 'rarr', 'rsquo', 'times') self.tagsBox = QComboBox(self.editBar) self.tagsBox.addItem(self.tr('Tags')) self.tagsBox.addItems(self.usefulTags) self.tagsBox.activated.connect(self.insertTag) self.symbolBox = QComboBox(self.editBar) self.symbolBox.addItem(self.tr('Symbols')) self.symbolBox.addItems(self.usefulChars) self.symbolBox.activated.connect(self.insertSymbol) self.updateStyleSheet() menubar = QMenuBar(self) menubar.setGeometry(QRect(0, 0, 800, 25)) self.setMenuBar(menubar) menuFile = menubar.addMenu(self.tr('File')) menuEdit = menubar.addMenu(self.tr('Edit')) menuHelp = menubar.addMenu(self.tr('Help')) menuFile.addAction(self.actionNew) menuFile.addAction(self.actionOpen) self.menuRecentFiles = menuFile.addMenu(self.tr('Open recent')) self.menuRecentFiles.aboutToShow.connect(self.updateRecentFiles) menuFile.addMenu(self.menuRecentFiles) menuFile.addAction(self.actionShow) menuFile.addAction(self.actionSetEncoding) menuFile.addAction(self.actionReload) menuFile.addSeparator() menuFile.addAction(self.actionSave) menuFile.addAction(self.actionSaveAs) menuFile.addSeparator() menuFile.addAction(self.actionNextTab) menuFile.addAction(self.actionPrevTab) menuFile.addSeparator() menuExport = menuFile.addMenu(self.tr('Export')) menuExport.addAction(self.actionSaveHtml) menuExport.addAction(self.actionOdf) menuExport.addAction(self.actionPdf) if self.extensionActions: menuExport.addSeparator() for action, mimetype in self.extensionActions: menuExport.addAction(action) menuExport.aboutToShow.connect(self.updateExtensionsVisibility) menuFile.addAction(self.actionPrint) menuFile.addAction(self.actionPrintPreview) menuFile.addSeparator() menuFile.addAction(self.actionQuit) menuEdit.addAction(self.actionUndo) menuEdit.addAction(self.actionRedo) menuEdit.addSeparator() menuEdit.addAction(self.actionCut) menuEdit.addAction(self.actionCopy) menuEdit.addAction(self.actionPaste) menuEdit.addSeparator() if enchant_available: menuSC = menuEdit.addMenu(self.tr('Spell check')) menuSC.addAction(self.actionEnableSC) menuSC.addAction(self.actionSetLocale) menuEdit.addAction(self.actionSearch) menuEdit.addAction(self.actionChangeFont) menuEdit.addSeparator() if len(availableMarkups) > 1: self.menuMode = menuEdit.addMenu(self.tr('Default markup')) for markupAction in markupActions: self.menuMode.addAction(markupAction) menuFormat = menuEdit.addMenu(self.tr('Formatting')) menuFormat.addAction(self.actionBold) menuFormat.addAction(self.actionItalic) menuFormat.addAction(self.actionUnderline) menuEdit.addAction(self.actionWebKit) menuEdit.addSeparator() menuEdit.addAction(self.actionViewHtml) menuEdit.addAction(self.actionLivePreview) menuEdit.addAction(self.actionPreview) menuEdit.addAction(self.actionTableMode) if ReTextFakeVimHandler: menuEdit.addAction(self.actionFakeVimMode) menuEdit.addSeparator() menuEdit.addAction(self.actionFullScreen) menuEdit.addAction(self.actionConfig) menuHelp.addAction(self.actionHelp) menuHelp.addSeparator() menuHelp.addAction(self.actionAbout) menuHelp.addAction(self.actionAboutQt) menubar.addMenu(menuFile) menubar.addMenu(menuEdit) menubar.addMenu(menuHelp) toolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) toolBar.addAction(self.actionNew) toolBar.addSeparator() toolBar.addAction(self.actionOpen) toolBar.addAction(self.actionSave) toolBar.addAction(self.actionPrint) toolBar.addSeparator() toolBar.addAction(self.actionPreview) toolBar.addAction(self.actionFullScreen) self.editBar.addAction(self.actionUndo) self.editBar.addAction(self.actionRedo) self.editBar.addSeparator() self.editBar.addAction(self.actionCut) self.editBar.addAction(self.actionCopy) self.editBar.addAction(self.actionPaste) self.editBar.addSeparator() self.editBar.addWidget(self.tagsBox) self.editBar.addWidget(self.symbolBox) self.searchEdit = QLineEdit(self.searchBar) self.searchEdit.setPlaceholderText(self.tr('Search')) self.searchEdit.returnPressed.connect(self.find) self.csBox = QCheckBox(self.tr('Case sensitively'), self.searchBar) self.searchBar.addWidget(self.searchEdit) self.searchBar.addSeparator() self.searchBar.addWidget(self.csBox) self.searchBar.addAction(self.actionFindPrev) self.searchBar.addAction(self.actionFind) self.searchBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.searchBar.setVisible(False) self.autoSaveEnabled = globalSettings.autoSave if self.autoSaveEnabled: timer = QTimer(self) timer.start(60000) timer.timeout.connect(self.saveAll) self.ind = None if enchant_available: self.sl = globalSettings.spellCheckLocale if self.sl: try: enchant.Dict(self.sl) except Exception as e: print(e, file=sys.stderr) self.sl = None if globalSettings.spellCheck: self.actionEnableSC.setChecked(True) self.enableSpellCheck(True) self.fileSystemWatcher = QFileSystemWatcher() self.fileSystemWatcher.fileChanged.connect(self.fileChanged) def initConfig(self): self.font = None if globalSettings.font: self.font = QFont(globalSettings.font) if self.font and globalSettings.fontSize: self.font.setPointSize(globalSettings.fontSize) def updateStyleSheet(self): if globalSettings.styleSheet: sheetfile = QFile(globalSettings.styleSheet) sheetfile.open(QIODevice.ReadOnly) self.ss = QTextStream(sheetfile).readAll() sheetfile.close() else: self.ss = '' def initTabWidget(self): def dragEnterEvent(e): e.acceptProposedAction() def dropEvent(e): fn = bytes(e.mimeData().data('text/plain')).decode().rstrip() if fn.startswith('file:'): fn = QUrl(fn).toLocalFile() self.openFileWrapper(fn) self.tabWidget.setTabsClosable(True) self.tabWidget.setAcceptDrops(True) self.tabWidget.dragEnterEvent = dragEnterEvent self.tabWidget.dropEvent = dropEvent def act(self, name, icon=None, trig=None, trigbool=None, shct=None): if not isinstance(shct, QKeySequence): shct = QKeySequence(shct) if icon: action = QAction(self.actIcon(icon), name, self) else: action = QAction(name, self) if trig: action.triggered.connect(trig) elif trigbool: action.setCheckable(True) action.triggered[bool].connect(trigbool) if shct: action.setShortcut(shct) return action def actIcon(self, name): return QIcon.fromTheme(name, QIcon(icon_path+name+'.png')) def printError(self): import traceback print('Exception occured while parsing document:', file=sys.stderr) traceback.print_exc() def getSplitter(self, index): splitter = QSplitter(Qt.Horizontal) # Give both boxes a minimum size so the minimumSizeHint will be # ignored when splitter.setSizes is called below for widget in self.editBoxes[index], self.previewBoxes[index]: widget.setMinimumWidth(125) splitter.addWidget(widget) splitter.setSizes((50, 50)) splitter.setChildrenCollapsible(False) return splitter def getWebView(self): webView = QWebView() if not globalSettings.handleWebLinks: webView.page().setLinkDelegationPolicy(QWebPage.DelegateExternalLinks) webView.page().linkClicked.connect(QDesktopServices.openUrl) webView.settings().setAttribute(QWebSettings.LocalContentCanAccessFileUrls, False) return webView def createTab(self, fileName): self.previewBlocked = False self.editBoxes.append(ReTextEdit(self)) self.highlighters.append(ReTextHighlighter(self.editBoxes[-1].document())) if enchant_available and self.actionEnableSC.isChecked(): self.highlighters[-1].dictionary = \ enchant.Dict(self.sl) if self.sl else enchant.Dict() self.highlighters[-1].rehighlight() if globalSettings.useWebKit: self.previewBoxes.append(self.getWebView()) else: self.previewBoxes.append(QTextBrowser()) self.previewBoxes[-1].setOpenExternalLinks(True) self.previewBoxes[-1].setVisible(False) self.fileNames.append(fileName) markupClass = self.getMarkupClass(fileName) self.markups.append(self.getMarkup(fileName)) self.highlighters[-1].docType = (markupClass.name if markupClass else '') liveMode = globalSettings.restorePreviewState and globalSettings.previewState self.actionPreviewChecked.append(liveMode) self.actionLivePreviewChecked.append(liveMode) metrics = QFontMetrics(self.editBoxes[-1].font()) self.editBoxes[-1].setTabStopWidth(globalSettings.tabWidth * metrics.width(' ')) self.editBoxes[-1].textChanged.connect(self.updateLivePreviewBox) self.editBoxes[-1].undoAvailable.connect(self.actionUndo.setEnabled) self.editBoxes[-1].redoAvailable.connect(self.actionRedo.setEnabled) self.editBoxes[-1].copyAvailable.connect(self.enableCopy) self.editBoxes[-1].document().modificationChanged.connect(self.modificationChanged) if globalSettings.useFakeVim: self.installFakeVimHandler(self.editBoxes[-1]) return self.getSplitter(-1) def closeTab(self, ind): if self.maybeSave(ind): if self.tabWidget.count() == 1: self.tabWidget.addTab(self.createTab(""), self.tr("New document")) if self.fileNames[ind]: self.fileSystemWatcher.removePath(self.fileNames[ind]) del self.editBoxes[ind] del self.previewBoxes[ind] del self.highlighters[ind] del self.markups[ind] del self.fileNames[ind] del self.actionPreviewChecked[ind] del self.actionLivePreviewChecked[ind] self.tabWidget.removeTab(ind) def getMarkupClass(self, fileName=None): if fileName is None: fileName = self.fileNames[self.ind] if fileName: markupClass = markups.get_markup_for_file_name( fileName, return_class=True) if markupClass: return markupClass return self.defaultMarkup def getMarkup(self, fileName=None): if fileName is None: fileName = self.fileNames[self.ind] markupClass = self.getMarkupClass(fileName=fileName) if markupClass and markupClass.available(): return markupClass(filename=fileName) def docTypeChanged(self): oldType = self.highlighters[self.ind].docType markupClass = self.getMarkupClass() newType = markupClass.name if markupClass else '' if oldType != newType: self.markups[self.ind] = self.getMarkup() self.updatePreviewBox() self.highlighters[self.ind].docType = newType self.highlighters[self.ind].rehighlight() dtMarkdown = (newType == DOCTYPE_MARKDOWN) dtMkdOrReST = (newType in (DOCTYPE_MARKDOWN, DOCTYPE_REST)) self.tagsBox.setEnabled(dtMarkdown) self.symbolBox.setEnabled(dtMarkdown) self.actionUnderline.setEnabled(dtMarkdown) self.actionBold.setEnabled(dtMkdOrReST) self.actionItalic.setEnabled(dtMkdOrReST) canReload = bool(self.fileNames[self.ind]) and not self.autoSaveActive() self.actionSetEncoding.setEnabled(canReload) self.actionReload.setEnabled(canReload) def changeIndex(self, ind): if ind > -1: self.actionUndo.setEnabled(self.editBoxes[ind].document().isUndoAvailable()) self.actionRedo.setEnabled(self.editBoxes[ind].document().isRedoAvailable()) self.actionCopy.setEnabled(self.editBoxes[ind].textCursor().hasSelection()) self.actionCut.setEnabled(self.editBoxes[ind].textCursor().hasSelection()) self.actionPreview.setChecked(self.actionPreviewChecked[ind]) self.actionLivePreview.setChecked(self.actionLivePreviewChecked[ind]) self.actionTableMode.setChecked(self.editBoxes[ind].tableModeEnabled) self.editBar.setDisabled(self.actionPreviewChecked[ind]) self.ind = ind if self.fileNames[ind]: self.setCurrentFile() else: self.setWindowTitle(self.tr('New document') + '[*]') self.docTypeChanged() self.modificationChanged(self.editBoxes[ind].document().isModified()) if globalSettings.restorePreviewState: globalSettings.previewState = self.actionLivePreviewChecked[ind] if self.actionLivePreviewChecked[ind]: self.enableLivePreview(True) self.editBoxes[self.ind].setFocus(Qt.OtherFocusReason) def changeFont(self): if not self.font: self.font = QFont() fd = QFontDialog.getFont(self.font, self) if fd[1]: self.font = QFont() self.font.setFamily(fd[0].family()) settings.setValue('font', fd[0].family()) self.font.setPointSize(fd[0].pointSize()) settings.setValue('fontSize', fd[0].pointSize()) self.updatePreviewBox() def preview(self, viewmode): self.actionPreviewChecked[self.ind] = viewmode if self.actionLivePreview.isChecked(): self.actionLivePreview.setChecked(False) return self.enableLivePreview(False) self.editBar.setDisabled(viewmode) self.editBoxes[self.ind].setVisible(not viewmode) self.previewBoxes[self.ind].setVisible(viewmode) if viewmode: self.updatePreviewBox() def enableLivePreview(self, livemode): if globalSettings.restorePreviewState: globalSettings.previewState = livemode self.actionLivePreviewChecked[self.ind] = livemode self.actionPreviewChecked[self.ind] = livemode self.actionPreview.setChecked(livemode) self.editBar.setEnabled(True) self.previewBoxes[self.ind].setVisible(livemode) self.editBoxes[self.ind].setVisible(True) if livemode: self.updatePreviewBox() def enableWebKit(self, enable): globalSettings.useWebKit = enable oldind = self.ind self.tabWidget.clear() for self.ind in range(len(self.editBoxes)): if enable: self.previewBoxes[self.ind] = self.getWebView() else: self.previewBoxes[self.ind] = QTextBrowser() self.previewBoxes[self.ind].setOpenExternalLinks(True) splitter = self.getSplitter(self.ind) self.tabWidget.addTab(splitter, self.getDocumentTitle(baseName=True)) self.updatePreviewBox() self.previewBoxes[self.ind].setVisible(self.actionPreviewChecked[self.ind]) self.ind = oldind self.tabWidget.setCurrentIndex(self.ind) def enableCopy(self, copymode): self.actionCopy.setEnabled(copymode) self.actionCut.setEnabled(copymode) def enableFullScreen(self, yes): if yes: self.showFullScreen() else: self.showNormal() def openConfigDialog(self): dlg = ConfigDialog(self) dlg.setWindowTitle(self.tr('Preferences')) dlg.show() def installFakeVimHandler(self, editor): if ReTextFakeVimHandler: fakeVimEditor = ReTextFakeVimHandler(editor, self) fakeVimEditor.setSaveAction(self.actionSave) fakeVimEditor.setQuitAction(self.actionQuit) self.actionFakeVimMode.triggered.connect(fakeVimEditor.remove) def enableFakeVimMode(self, yes): globalSettings.useFakeVim = yes if yes: FakeVimMode.init(self) for editor in self.editBoxes: self.installFakeVimHandler(editor) else: FakeVimMode.exit(self) def enableSpellCheck(self, yes): if yes: if self.sl: self.setAllDictionaries(enchant.Dict(self.sl)) else: self.setAllDictionaries(enchant.Dict()) else: self.setAllDictionaries(None) globalSettings.spellCheck = yes def setAllDictionaries(self, dictionary): for hl in self.highlighters: hl.dictionary = dictionary hl.rehighlight() def changeLocale(self): if self.sl: localedlg = LocaleDialog(self, defaultText=self.sl) else: localedlg = LocaleDialog(self) if localedlg.exec() != QDialog.Accepted: return sl = localedlg.localeEdit.text() setdefault = localedlg.checkBox.isChecked() if sl: try: sl = str(sl) enchant.Dict(sl) except Exception as e: QMessageBox.warning(self, '', str(e)) else: self.sl = sl self.enableSpellCheck(self.actionEnableSC.isChecked()) else: self.sl = None self.enableSpellCheck(self.actionEnableSC.isChecked()) if setdefault: globalSettings.spellCheckLocale = sl def searchBarVisibilityChanged(self, visible): self.actionSearch.setChecked(visible) if visible: self.searchEdit.setFocus(Qt.ShortcutFocusReason) def find(self, back=False): flags = QTextDocument.FindFlags() if back: flags |= QTextDocument.FindBackward if self.csBox.isChecked(): flags |= QTextDocument.FindCaseSensitively text = self.searchEdit.text() editBox = self.editBoxes[self.ind] cursor = editBox.textCursor() newCursor = editBox.document().find(text, cursor, flags) if not newCursor.isNull(): editBox.setTextCursor(newCursor) return self.setSearchEditColor(True) cursor.movePosition(QTextCursor.End if back else QTextCursor.Start) newCursor = editBox.document().find(text, cursor, flags) if not newCursor.isNull(): editBox.setTextCursor(newCursor) return self.setSearchEditColor(True) self.setSearchEditColor(False) def setSearchEditColor(self, found): palette = self.searchEdit.palette() palette.setColor(QPalette.Active, QPalette.Base, Qt.white if found else QColor(255, 102, 102)) self.searchEdit.setPalette(palette) def getHtml(self, includeStyleSheet=True, includeTitle=True, includeMeta=False, styleForWebKit=False, webenv=False): if self.markups[self.ind] is None: markupClass = self.getMarkupClass() errMsg = self.tr('Could not parse file contents, check if ' 'you have the <a href="%s">necessary module</a> installed!') try: errMsg %= markupClass.attributes[MODULE_HOME_PAGE] except (AttributeError, KeyError): # Remove the link if markupClass doesn't have the needed attribute errMsg = errMsg.replace('<a href="%s">', '') errMsg = errMsg.replace('</a>', '') return '<p style="color: red">%s</p>' % errMsg text = self.editBoxes[self.ind].toPlainText() headers = '' if includeStyleSheet: fontline = '' if styleForWebKit: fontname = self.font.family() if self.font else 'Sans' fontsize = (self.font if self.font else QFont()).pointSize() fontline = 'body { font-family: %s; font-size: %spt }\n' % \ (fontname, fontsize) headers += '<style type="text/css">\n' + fontline + self.ss + '</style>\n' cssFileName = self.getDocumentTitle(baseName=True)+'.css' if QFile(cssFileName).exists(): headers += '<link rel="stylesheet" type="text/css" href="%s">\n' \ % cssFileName if includeMeta: headers += '<meta name="generator" content="%s %s">\n' % \ (app_name, app_version) fallbackTitle = self.getDocumentTitle() if includeTitle else '' return self.markups[self.ind].get_whole_html(text, custom_headers=headers, include_stylesheet=includeStyleSheet, fallback_title=fallbackTitle, webenv=webenv) def updatePreviewBox(self): self.previewBlocked = False pb = self.previewBoxes[self.ind] textedit = isinstance(pb, QTextEdit) if textedit: scrollbar = pb.verticalScrollBar() disttobottom = scrollbar.maximum() - scrollbar.value() else: frame = pb.page().mainFrame() scrollpos = frame.scrollPosition() try: html = self.getHtml(styleForWebKit=(not textedit)) except Exception: return self.printError() if textedit: pb.setHtml(html) else: pb.setHtml(html, QUrl.fromLocalFile(self.fileNames[self.ind])) if self.font and textedit: pb.document().setDefaultFont(self.font) if textedit: scrollbar.setValue(scrollbar.maximum() - disttobottom) else: frame.setScrollPosition(scrollpos) def updateLivePreviewBox(self): if self.actionLivePreview.isChecked() and self.previewBlocked == False: self.previewBlocked = True QTimer.singleShot(1000, self.updatePreviewBox) def showInDir(self): if self.fileNames[self.ind]: path = QFileInfo(self.fileNames[self.ind]).path() QDesktopServices.openUrl(QUrl.fromLocalFile(path)) else: QMessageBox.warning(self, '', self.tr("Please, save the file somewhere.")) def setCurrentFile(self): self.setWindowTitle("") self.tabWidget.setTabText(self.ind, self.getDocumentTitle(baseName=True)) self.setWindowFilePath(self.fileNames[self.ind]) files = readListFromSettings("recentFileList") while self.fileNames[self.ind] in files: files.remove(self.fileNames[self.ind]) files.insert(0, self.fileNames[self.ind]) if len(files) > 10: del files[10:] writeListToSettings("recentFileList", files) QDir.setCurrent(QFileInfo(self.fileNames[self.ind]).dir().path()) self.docTypeChanged() def createNew(self, text=None): self.tabWidget.addTab(self.createTab(""), self.tr("New document")) self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if text: self.editBoxes[self.ind].textCursor().insertText(text) def switchTab(self, shift=1): self.tabWidget.setCurrentIndex((self.ind + shift) % self.tabWidget.count()) def updateRecentFiles(self): self.menuRecentFiles.clear() self.recentFilesActions = [] filesOld = readListFromSettings("recentFileList") files = [] for f in filesOld: if QFile.exists(f): files.append(f) self.recentFilesActions.append(self.act(f, trig=self.openFunction(f))) writeListToSettings("recentFileList", files) for action in self.recentFilesActions: self.menuRecentFiles.addAction(action) def markupFunction(self, markup): return lambda: self.setDefaultMarkup(markup) def openFunction(self, fileName): return lambda: self.openFileWrapper(fileName) def extensionFuntion(self, data): return lambda: \ self.runExtensionCommand(data['Exec'], data['FileFilter'], data['DefaultExtension']) def getExportExtensionsList(self): extensions = [] for extsprefix in datadirs: extsdir = QDir(extsprefix+'/export-extensions/') if extsdir.exists(): for fileInfo in extsdir.entryInfoList(['*.desktop', '*.ini'], QDir.Files | QDir.Readable): extensions.append(self.readExtension(fileInfo.filePath())) locale = QLocale.system().name() self.extensionActions = [] for extension in extensions: try: if ('Name[%s]' % locale) in extension: name = extension['Name[%s]' % locale] elif ('Name[%s]' % locale.split('_')[0]) in extension: name = extension['Name[%s]' % locale.split('_')[0]] else: name = extension['Name'] data = {} for prop in ('FileFilter', 'DefaultExtension', 'Exec'): if 'X-ReText-'+prop in extension: data[prop] = extension['X-ReText-'+prop] elif prop in extension: data[prop] = extension[prop] else: data[prop] = '' action = self.act(name, trig=self.extensionFuntion(data)) if 'Icon' in extension: action.setIcon(self.actIcon(extension['Icon'])) mimetype = extension['MimeType'] if 'MimeType' in extension else None except KeyError: print('Failed to parse extension: Name is required', file=sys.stderr) else: self.extensionActions.append((action, mimetype)) def updateExtensionsVisibility(self): markupClass = self.getMarkupClass() for action in self.extensionActions: if markupClass is None: action[0].setEnabled(False) continue mimetype = action[1] if mimetype == None: enabled = True elif markupClass == markups.MarkdownMarkup: enabled = (mimetype in ("text/x-retext-markdown", "text/x-markdown")) elif markupClass == markups.ReStructuredTextMarkup: enabled = (mimetype in ("text/x-retext-rst", "text/x-rst")) else: enabled = False action[0].setEnabled(enabled) def readExtension(self, fileName): extFile = QFile(fileName) extFile.open(QIODevice.ReadOnly) extension = {} stream = QTextStream(extFile) while not stream.atEnd(): line = stream.readLine() if '=' in line: index = line.index('=') extension[line[:index].rstrip()] = line[index+1:].lstrip() extFile.close() return extension def openFile(self): supportedExtensions = ['.txt'] for markup in markups.get_all_markups(): supportedExtensions += markup.file_extensions fileFilter = ' (' + str.join(' ', ['*'+ext for ext in supportedExtensions]) + ');;' fileNames = QFileDialog.getOpenFileNames(self, self.tr("Select one or several files to open"), "", self.tr("Supported files") + fileFilter + self.tr("All files (*)")) for fileName in fileNames[0]: self.openFileWrapper(fileName) def openFileWrapper(self, fileName): if not fileName: return fileName = QFileInfo(fileName).canonicalFilePath() exists = False for i in range(self.tabWidget.count()): if self.fileNames[i] == fileName: exists = True ex = i if exists: self.tabWidget.setCurrentIndex(ex) elif QFile.exists(fileName): noEmptyTab = ( (self.ind is None) or self.fileNames[self.ind] or self.editBoxes[self.ind].toPlainText() or self.editBoxes[self.ind].document().isModified() ) if noEmptyTab: self.tabWidget.addTab(self.createTab(fileName), "") self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if fileName: self.fileSystemWatcher.addPath(fileName) self.fileNames[self.ind] = fileName self.openFileMain() def openFileMain(self, encoding=None): openfile = QFile(self.fileNames[self.ind]) openfile.open(QIODevice.ReadOnly) stream = QTextStream(openfile) if encoding: stream.setCodec(encoding) elif globalSettings.defaultCodec: stream.setCodec(globalSettings.defaultCodec) text = stream.readAll() openfile.close() markupClass = markups.get_markup_for_file_name( self.fileNames[self.ind], return_class=True) self.highlighters[self.ind].docType = (markupClass.name if markupClass else '') self.markups[self.ind] = self.getMarkup() if self.defaultMarkup: self.highlighters[self.ind].docType = self.defaultMarkup.name editBox = self.editBoxes[self.ind] modified = bool(encoding) and (editBox.toPlainText() != text) editBox.setPlainText(text) self.setCurrentFile() editBox.document().setModified(modified) self.setWindowModified(modified) def showEncodingDialog(self): if not self.maybeSave(self.ind): return encoding, ok = QInputDialog.getItem(self, '', self.tr('Select file encoding from the list:'), [bytes(b).decode() for b in QTextCodec.availableCodecs()], 0, False) if ok: self.openFileMain(encoding) def saveFile(self): self.saveFileMain(dlg=False) def saveFileAs(self): self.saveFileMain(dlg=True) def saveAll(self): oldind = self.ind for self.ind in range(self.tabWidget.count()): if self.fileNames[self.ind] and QFileInfo(self.fileNames[self.ind]).isWritable(): self.saveFileCore(self.fileNames[self.ind]) self.editBoxes[self.ind].document().setModified(False) self.ind = oldind def saveFileMain(self, dlg): if (not self.fileNames[self.ind]) or dlg: markupClass = self.getMarkupClass() if (markupClass is None) or not hasattr(markupClass, 'default_extension'): defaultExt = self.tr("Plain text (*.txt)") ext = ".txt" else: defaultExt = self.tr('%s files', 'Example of final string: Markdown files') \ % markupClass.name + ' (' + str.join(' ', ('*'+extension for extension in markupClass.file_extensions)) + ')' if markupClass == markups.MarkdownMarkup: ext = globalSettings.markdownDefaultFileExtension elif markupClass == markups.ReStructuredTextMarkup: ext = globalSettings.restDefaultFileExtension else: ext = markupClass.default_extension newFileName = QFileDialog.getSaveFileName(self, self.tr("Save file"), "", defaultExt)[0] if newFileName: if not QFileInfo(newFileName).suffix(): newFileName += ext if self.fileNames[self.ind]: self.fileSystemWatcher.removePath(self.fileNames[self.ind]) self.fileNames[self.ind] = newFileName self.actionSetEncoding.setDisabled(self.autoSaveActive()) if self.fileNames[self.ind]: result = self.saveFileCore(self.fileNames[self.ind]) if result: self.setCurrentFile() self.editBoxes[self.ind].document().setModified(False) self.setWindowModified(False) return True else: QMessageBox.warning(self, '', self.tr("Cannot save to file because it is read-only!")) return False def saveFileCore(self, fn): self.fileSystemWatcher.removePath(fn) savefile = QFile(fn) result = savefile.open(QIODevice.WriteOnly) if result: savestream = QTextStream(savefile) if globalSettings.defaultCodec: savestream.setCodec(globalSettings.defaultCodec) savestream << self.editBoxes[self.ind].toPlainText() savefile.close() self.fileSystemWatcher.addPath(fn) return result def saveHtml(self, fileName): if not QFileInfo(fileName).suffix(): fileName += ".html" try: htmltext = self.getHtml(includeStyleSheet=False, includeMeta=True, webenv=True) except Exception: return self.printError() htmlFile = QFile(fileName) htmlFile.open(QIODevice.WriteOnly) html = QTextStream(htmlFile) if globalSettings.defaultCodec: html.setCodec(globalSettings.defaultCodec) html << htmltext htmlFile.close() def textDocument(self): td = QTextDocument() td.setMetaInformation(QTextDocument.DocumentTitle, self.getDocumentTitle()) if self.ss: td.setDefaultStyleSheet(self.ss) td.setHtml(self.getHtml()) if self.font: td.setDefaultFont(self.font) return td def saveOdf(self): try: document = self.textDocument() except Exception: return self.printError() fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to ODT"), "", self.tr("OpenDocument text files (*.odt)"))[0] if not QFileInfo(fileName).suffix(): fileName += ".odt" writer = QTextDocumentWriter(fileName) writer.setFormat("odf") writer.write(document) def saveFileHtml(self): fileName = QFileDialog.getSaveFileName(self, self.tr("Save file"), "", self.tr("HTML files (*.html *.htm)"))[0] if fileName: self.saveHtml(fileName) def getDocumentForPrint(self): if globalSettings.useWebKit: return self.previewBoxes[self.ind] try: return self.textDocument() except Exception: self.printError() def standardPrinter(self): printer = QPrinter(QPrinter.HighResolution) printer.setDocName(self.getDocumentTitle()) printer.setCreator(app_name+" "+app_version) return printer def savePdf(self): self.updatePreviewBox() fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to PDF"), "", self.tr("PDF files (*.pdf)"))[0] if fileName: if not QFileInfo(fileName).suffix(): fileName += ".pdf" printer = self.standardPrinter() printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(fileName) document = self.getDocumentForPrint() if document != None: document.print(printer) def printFile(self): self.updatePreviewBox() printer = self.standardPrinter() dlg = QPrintDialog(printer, self) dlg.setWindowTitle(self.tr("Print document")) if (dlg.exec() == QDialog.Accepted): document = self.getDocumentForPrint() if document != None: document.print(printer) def printPreview(self): document = self.getDocumentForPrint() if document == None: return printer = self.standardPrinter() preview = QPrintPreviewDialog(printer, self) preview.paintRequested.connect(document.print) preview.exec() def runExtensionCommand(self, command, filefilter, defaultext): of = ('%of' in command) html = ('%html' in command) if of: if defaultext and not filefilter: filefilter = '*'+defaultext fileName = QFileDialog.getSaveFileName(self, self.tr('Export document'), '', filefilter)[0] if not fileName: return if defaultext and not QFileInfo(fileName).suffix(): fileName += defaultext basename = '.%s.retext-temp' % self.getDocumentTitle(baseName=True) if html: tmpname = basename+'.html' self.saveHtml(tmpname) else: tmpname = basename+self.getMarkupClass().default_extension self.saveFileCore(tmpname) command = command.replace('%of', '"out'+defaultext+'"') command = command.replace('%html' if html else '%if', '"'+tmpname+'"') try: Popen(str(command), shell=True).wait() except Exception as error: errorstr = str(error) QMessageBox.warning(self, '', self.tr('Failed to execute the command:') + '\n' + errorstr) QFile(tmpname).remove() if of: QFile('out'+defaultext).rename(fileName) def getDocumentTitle(self, baseName=False): markup = self.markups[self.ind] realTitle = '' if markup and not baseName: text = self.editBoxes[self.ind].toPlainText() try: realTitle = markup.get_document_title(text) except Exception: self.printError() if realTitle: return realTitle elif self.fileNames[self.ind]: fileinfo = QFileInfo(self.fileNames[self.ind]) basename = fileinfo.completeBaseName() return (basename if basename else fileinfo.fileName()) return self.tr("New document") def autoSaveActive(self): return self.autoSaveEnabled and self.fileNames[self.ind] and \ QFileInfo(self.fileNames[self.ind]).isWritable() def modificationChanged(self, changed): if self.autoSaveActive(): changed = False self.actionSave.setEnabled(changed) self.setWindowModified(changed) def clipboardDataChanged(self): mimeData = QApplication.instance().clipboard().mimeData() if mimeData is not None: self.actionPaste.setEnabled(mimeData.hasText()) def insertChars(self, chars): tc = self.editBoxes[self.ind].textCursor() if tc.hasSelection(): selection = tc.selectedText() if selection.startswith(chars) and selection.endswith(chars): if len(selection) > 2*len(chars): selection = selection[len(chars):-len(chars)] tc.insertText(selection) else: tc.insertText(chars+tc.selectedText()+chars) else: tc.insertText(chars) def insertTag(self, ut): if not ut: return if isinstance(ut, int): ut = self.usefulTags[ut - 1] arg = ' style=""' if ut == 'span' else '' tc = self.editBoxes[self.ind].textCursor() if ut == 'img': toinsert = ('<a href="' + tc.selectedText() + '" target="_blank"><img src="' + tc.selectedText() + '"/></a>') elif ut == 'a': toinsert = ('<a href="' + tc.selectedText() + '" target="_blank">' + tc.selectedText() + '</a>') else: toinsert = '<'+ut+arg+'>'+tc.selectedText()+'</'+ut+'>' tc.insertText(toinsert) self.tagsBox.setCurrentIndex(0) def insertSymbol(self, num): if num: self.editBoxes[self.ind].insertPlainText('&'+self.usefulChars[num-1]+';') self.symbolBox.setCurrentIndex(0) def fileChanged(self, fileName): ind = self.fileNames.index(fileName) self.tabWidget.setCurrentIndex(ind) if not QFile.exists(fileName): self.editBoxes[ind].document().setModified(True) QMessageBox.warning(self, '', self.tr( 'This file has been deleted by other application.\n' 'Please make sure you save the file before exit.')) elif not self.editBoxes[ind].document().isModified(): # File was not modified in ReText, reload silently self.openFileMain() self.updatePreviewBox() else: text = self.tr( 'This document has been modified by other application.\n' 'Do you want to reload the file (this will discard all ' 'your changes)?\n') if self.autoSaveEnabled: text += self.tr( 'If you choose to not reload the file, auto save mode will ' 'be disabled for this session to prevent data loss.') messageBox = QMessageBox(QMessageBox.Warning, '', text) reloadButton = messageBox.addButton(self.tr('Reload'), QMessageBox.YesRole) messageBox.addButton(QMessageBox.Cancel) messageBox.exec() if messageBox.clickedButton() is reloadButton: self.openFileMain() self.updatePreviewBox() else: self.autoSaveEnabled = False self.editBoxes[ind].document().setModified(True) if fileName not in self.fileSystemWatcher.files(): # https://sourceforge.net/p/retext/tickets/137/ self.fileSystemWatcher.addPath(fileName) def maybeSave(self, ind): if self.autoSaveActive(): self.saveFileCore(self.fileNames[self.ind]) return True if not self.editBoxes[ind].document().isModified(): return True self.tabWidget.setCurrentIndex(ind) ret = QMessageBox.warning(self, '', self.tr("The document has been modified.\nDo you want to save your changes?"), QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) if ret == QMessageBox.Save: return self.saveFileMain(False) elif ret == QMessageBox.Cancel: return False return True def closeEvent(self, closeevent): for self.ind in range(self.tabWidget.count()): if not self.maybeSave(self.ind): return closeevent.ignore() if globalSettings.saveWindowGeometry and not self.isMaximized(): globalSettings.windowGeometry = self.saveGeometry() closeevent.accept() def viewHtml(self): htmlDlg = HtmlDialog(self) try: htmltext = self.getHtml(includeStyleSheet=False, includeTitle=False) except Exception: return self.printError() winTitle = self.getDocumentTitle(baseName=True) htmlDlg.setWindowTitle(winTitle+" ("+self.tr("HTML code")+")") htmlDlg.textEdit.setPlainText(htmltext.rstrip()) htmlDlg.hl.rehighlight() htmlDlg.show() htmlDlg.raise_() htmlDlg.activateWindow() def openHelp(self): QDesktopServices.openUrl(QUrl('http://sourceforge.net/p/retext/home/Help and Support')) def aboutDialog(self): QMessageBox.about(self, self.aboutWindowTitle, '<p><b>' + (self.tr('ReText %s (using PyMarkups %s)') % (app_version, markups.__version__)) +'</b></p>' + self.tr('Simple but powerful editor' ' for Markdown and reStructuredText') +'</p><p>'+self.tr('Author: Dmitry Shachnev, 2011').replace('2011', '2011\u2013' '2015') +'<br><a href="http://sourceforge.net/p/retext/">'+self.tr('Website') +'</a> | <a href="http://daringfireball.net/projects/markdown/syntax">' +self.tr('Markdown syntax') +'</a> | <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">' +self.tr('reStructuredText syntax')+'</a></p>') def setDefaultMarkup(self, markup): self.defaultMarkup = markup defaultName = markups.get_available_markups()[0].name writeToSettings('defaultMarkup', markup.name, defaultName) oldind = self.ind for self.ind in range(len(self.previewBoxes)): self.docTypeChanged() self.ind = oldind
def device_discovery(self, devs): """Called when new devices have been added""" for menu in self._all_role_menus: role_menu = menu["rolemenu"] mux_menu = menu["muxmenu"] dev_group = QActionGroup(role_menu, exclusive=True) for d in devs: dev_node = QAction(d.name, role_menu, checkable=True, enabled=True) role_menu.addAction(dev_node) dev_group.addAction(dev_node) dev_node.toggled.connect(self._inputdevice_selected) map_node = None if d.supports_mapping: map_node = QMenu(" Input map", role_menu, enabled=False) map_group = QActionGroup(role_menu, exclusive=True) # Connect device node to map node for easy # enabling/disabling when selection changes and device # to easily enable it dev_node.setData((map_node, d)) for c in ConfigManager().get_list_of_configs(): node = QAction(c, map_node, checkable=True, enabled=True) node.toggled.connect(self._inputconfig_selected) map_node.addAction(node) # Connect all the map nodes back to the device # action node where we can access the raw device node.setData(dev_node) map_group.addAction(node) # If this device hasn't been found before, then # select the default mapping for it. if d not in self._available_devices: last_map = Config().get("device_config_mapping") if d.name in last_map and last_map[d.name] == c: node.setChecked(True) role_menu.addMenu(map_node) dev_node.setData((map_node, d, mux_menu)) # Update the list of what devices we found # to avoid selecting default mapping for all devices when # a new one is inserted self._available_devices = () for d in devs: self._available_devices += (d, ) # Only enable MUX nodes if we have enough devies to cover # the roles for mux_node in self._all_mux_nodes: (mux, sub_nodes) = mux_node.data() if len(mux.supported_roles()) <= len(self._available_devices): mux_node.setEnabled(True) # TODO: Currently only supports selecting default mux if self._all_mux_nodes[0].isEnabled(): self._all_mux_nodes[0].setChecked(True) # If the previous length of the available devies was 0, then select # the default on. If that's not available then select the first # on in the list. # TODO: This will only work for the "Normal" mux so this will be # selected by default if Config().get("input_device") in [d.name for d in devs]: for dev_menu in self._all_role_menus[0]["rolemenu"].actions(): if dev_menu.text() == Config().get("input_device"): dev_menu.setChecked(True) else: # Select the first device in the first mux (will always be "Normal" # mux) self._all_role_menus[0]["rolemenu"].actions()[0].setChecked(True) logger.info("Select first device") self._update_input_device_footer()
class ReciteGui(QMainWindow): test_version_signal = pyqtSignal() read_cables_signal = pyqtSignal() def __init__(self): super().__init__() self.sm = serial_manager.SerialManager() self.serial_thread = QThread() self.sm.moveToThread(self.serial_thread) self.serial_thread.start() self.test_version_signal.connect(self.sm.check_version) self.read_cables_signal.connect(self.sm.read_cables) self.sm.version_check_signal.connect(self.version_check) self.sm.port_unavailable_signal.connect(self.port_unavailable) self.sm.serial_error_signal.connect(self.serial_error) self.sm.cable_values_signal.connect(self.display_cables) self.sm.no_sensors_signal.connect(self.no_sensors) self.port_name = None self.system_font = QApplication.font().family() self.label_font = QFont(self.system_font, 14) self.sensor_font = QFont(self.system_font, 12) self.quit = QAction("Quit/退出;結束", self) self.quit.setShortcut("Ctrl+Q") self.quit.setStatusTip("Exit Program/退出;結束") self.quit.triggered.connect(self.close) self.about_tu = QAction("About Test Utility", self) self.about_tu.setShortcut("Ctrl+U") self.about_tu.setStatusTip("About Program") self.about_tu.triggered.connect(self.about_program) self.aboutqt = QAction("About Qt", self) self.aboutqt.setShortcut("Ctrl+I") self.aboutqt.setStatusTip("About Qt") self.aboutqt.triggered.connect(self.about_qt) # Create menubar self.menubar = self.menuBar() self.file_menu = self.menubar.addMenu("&File/文件") self.file_menu.addAction(self.quit) self.serial_menu = self.menubar.addMenu("&Serial/串行端口") self.serial_menu.installEventFilter(self) self.ports_menu = QMenu("&Ports/串行端口", self) self.serial_menu.addMenu(self.ports_menu) self.ports_menu.aboutToShow.connect(self.populate_ports) self.ports_group = QActionGroup(self) self.ports_group.triggered.connect(self.connect_port) self.help_menu = self.menubar.addMenu("&Help/帮助") self.help_menu.addAction(self.about_tu) self.help_menu.addAction(self.aboutqt) self.center() self.initUI() # Get logo path def resource_path(self, relative_path): if hasattr(sys, '_MEIPASS'): return os.path.join(sys._MEIPASS, relative_path) return os.path.join(os.path.abspath("."), relative_path) def initUI(self): RIGHT_SPACING = 350 LINE_EDIT_WIDTH = 200 self.central_widget = QWidget() self.start_btn = QPushButton("Start Cable Test / 开始测试") self.start_btn.setFixedWidth(350) self.start_btn.setAutoDefault(True) self.start_btn.setFont(self.label_font) self.start_btn.clicked.connect(self.start) self.logo_img = QPixmap(self.resource_path("h_logo.png")) self.logo_img = self.logo_img.scaledToWidth(600) self.logo = QLabel() self.logo.setPixmap(self.logo_img) hbox_logo = QHBoxLayout() hbox_logo.addStretch() hbox_logo.addWidget(self.logo) hbox_logo.addStretch() hbox_start_btn = QHBoxLayout() hbox_start_btn.addStretch() hbox_start_btn.addWidget(self.start_btn) hbox_start_btn.addStretch() vbox = QVBoxLayout() vbox.addStretch() vbox.addLayout(hbox_logo) vbox.addSpacing(100) vbox.addLayout(hbox_start_btn) vbox.addStretch() self.central_widget.setLayout(vbox) self.setCentralWidget(self.central_widget) self.setFixedSize(WINDOW_WIDTH, WINDOW_HEIGHT) self.setWindowTitle("BeadedStream Cable Test Utility") def center(self): qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) def about_program(self): QMessageBox.about(self, "About TestUtility", ABOUT_TEXT) def about_qt(self): QMessageBox.aboutQt(self, "About Qt") def populate_ports(self): ports = serial_manager.SerialManager.scan_ports() self.ports_menu.clear() if not ports: self.ports_menu.addAction("None/无连接") self.sm.close_port() for port in ports: port_description = str(port)[:-6] action = self.ports_menu.addAction(port_description) self.port_name = port_description[0:4] if self.sm.is_connected(self.port_name): action.setCheckable(True) action.setChecked(True) self.ports_group.addAction(action) def connect_port(self, action): p = "COM[0-9]+" self.port_name = re.search(p, action.text()).group() if (self.sm.is_connected(self.port_name)): action.setChecked self.sm.open_port(self.port_name) def port_unavailable(self): QMessageBox.warning(self, "Warning", "Port unavailable!\n" "没有串行端口") def serial_error(self): QMessageBox.warning( self, "Serial Error/串行端口错误", "Serial error! " "Please try the operation again.\n" "串行端口错误") self.setup_page2() def closeEvent(self, event): event.accept() quit_msg = "Are you sure you want to exit the program?" confirmation = QMessageBox.question(self, 'Message', quit_msg, QMessageBox.Yes, QMessageBox.No) if confirmation == QMessageBox.Yes: self.serial_thread.quit() self.serial_thread.wait() event.accept() else: event.ignore() def start(self): if not self.sm.is_connected(self.port_name): QMessageBox.warning(self, "Warning", "No serial port selected!\n" "未选串行端口") else: self.test_version_signal.emit() def version_check(self, result): if result: self.setup_page2() else: QMessageBox.warning( self, "Serial Error/串行端口错误", "No communication. " "Please make sure the Recite is powered and the cables are " "properly connected to computer.") def setup_page2(self): central_widget = QWidget() self.lbl = QLabel("Read sensors/读传感器") self.lbl.setFont(self.label_font) self.read_cables_btn = QPushButton("Read sensors/读传感器") self.read_cables_btn.clicked.connect(self.test_cables) self.hbox = QHBoxLayout() self.hbox.addWidget(self.lbl) self.hbox.addWidget(self.read_cables_btn) self.sensors = QLineEdit() self.sensors.setReadOnly(True) self.sensors.setFont(self.sensor_font) self.box1 = QTextEdit() self.box1.setReadOnly(True) self.box1.setFont(self.sensor_font) self.box2 = QTextEdit() self.box2.setReadOnly(True) self.box2.setFont(self.sensor_font) self.box3 = QTextEdit() self.box3.setReadOnly(True) self.box3.setFont(self.sensor_font) self.box4 = QTextEdit() self.box4.setReadOnly(True) self.box4.setFont(self.sensor_font) self.grid = QGridLayout() self.grid.setVerticalSpacing(10) self.grid.setHorizontalSpacing(15) self.grid.addLayout(self.hbox, 0, 0) self.grid.addWidget(self.sensors, 0, 1, 1, 2) self.grid.addWidget(self.box1, 1, 0, 8, 1) self.grid.addWidget(self.box2, 1, 1, 8, 1) self.grid.addWidget(self.box3, 1, 2, 8, 1) self.grid.addWidget(self.box4, 0, 3, 9, 1) central_widget.setLayout(self.grid) self.setCentralWidget(central_widget) def test_cables(self): self.lbl.setText("Reading.../读取...") self.read_cables_btn.setEnabled(False) self.sensors.clear() self.box1.clear() self.box2.clear() self.box3.clear() self.box4.clear() self.read_cables_signal.emit() def display_cables(self, boards_list, sensor_num, temps_dict): self.lbl.setText("Finished./完毕") self.read_cables_btn.setEnabled(True) self.sensors.setText(f"{sensor_num} sensors found") failed_sensors = self.check_cable_temps(temps_dict) # Display 30 boards each in first three box # remaining boards in the last one. There should be no more than 125 # sensors. boxes = [self.box1, self.box2, self.box3, self.box4] box_capacities = [30, 30, 30, 35] boards_list.reverse() box_num = 0 list_num = 1 temp = None while len(boards_list): for i in range(0, box_capacities[box_num]): try: sensor_id = boards_list.pop() except IndexError: break # Trim it to six bytes to match the ids from the temps sensor_id_temp = sensor_id[3:-3] if sensor_id_temp in failed_sensors: temp = failed_sensors[sensor_id_temp] sensor_text = (f" {list_num}) {sensor_id} FAILED {temp}") boxes[box_num].setTextColor(Qt.red) else: sensor_text = f" {list_num}) {sensor_id}" boxes[box_num].setTextColor(Qt.black) # Add a blank line every ten sensors if not list_num % 10: sensor_text += "\n" boxes[box_num].append(sensor_text) list_num += 1 box_num += 1 def check_cable_temps(self, temps): failed = {} for sensor, temp in temps.items(): if temp > 75.0: failed[sensor] = temp return failed def no_sensors(self): self.setup_page2() self.sensors.setText("0 sensors found")
class ListWidget(QWidget): deviceSelected = pyqtSignal(TasmotaDevice) openRulesEditor = pyqtSignal() openConsole = pyqtSignal() openTelemetry = pyqtSignal() openWebUI = pyqtSignal() def __init__(self, parent, *args, **kwargs): super(ListWidget, self).__init__(*args, **kwargs) self.setWindowTitle("Devices list") self.setWindowState(Qt.WindowMaximized) self.setLayout(VLayout(margin=0, spacing=0)) self.mqtt = parent.mqtt self.env = parent.env self.device = None self.idx = None self.nam = QNetworkAccessManager() self.backup = bytes() self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()), QSettings.IniFormat) views_order = self.settings.value("views_order", []) self.views = {} self.settings.beginGroup("Views") views = self.settings.childKeys() if views and views_order: for view in views_order.split(";"): view_list = self.settings.value(view).split(";") self.views[view] = base_view + view_list else: self.views = default_views self.settings.endGroup() self.tb = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon) self.tb_relays = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonIconOnly) # self.tb_filter = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon) self.tb_views = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon) self.pwm_sliders = [] self.layout().addWidget(self.tb) self.layout().addWidget(self.tb_relays) # self.layout().addWidget(self.tb_filter) self.device_list = TableView() self.device_list.setIconSize(QSize(24, 24)) self.model = parent.device_model self.model.setupColumns(self.views["Home"]) self.sorted_device_model = QSortFilterProxyModel() self.sorted_device_model.setFilterCaseSensitivity(Qt.CaseInsensitive) self.sorted_device_model.setSourceModel(parent.device_model) self.sorted_device_model.setSortRole(Qt.InitialSortOrderRole) self.sorted_device_model.setSortLocaleAware(True) self.sorted_device_model.setFilterKeyColumn(-1) self.device_list.setModel(self.sorted_device_model) self.device_list.setupView(self.views["Home"]) self.device_list.setSortingEnabled(True) self.device_list.setWordWrap(True) self.device_list.setItemDelegate(DeviceDelegate()) self.device_list.sortByColumn(self.model.columnIndex("FriendlyName"), Qt.AscendingOrder) self.device_list.setContextMenuPolicy(Qt.CustomContextMenu) self.device_list.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) self.layout().addWidget(self.device_list) self.layout().addWidget(self.tb_views) self.device_list.clicked.connect(self.select_device) self.device_list.customContextMenuRequested.connect(self.show_list_ctx_menu) self.ctx_menu = QMenu() self.create_actions() self.create_view_buttons() # self.create_view_filter() self.device_list.doubleClicked.connect(lambda: self.openConsole.emit()) def create_actions(self): actConsole = self.tb.addAction(QIcon(":/console.png"), "Console", self.openConsole.emit) actConsole.setShortcut("Ctrl+E") actRules = self.tb.addAction(QIcon(":/rules.png"), "Rules", self.openRulesEditor.emit) actRules.setShortcut("Ctrl+R") actTimers = self.tb.addAction(QIcon(":/timers.png"), "Timers", self.configureTimers) actButtons = self.tb.addAction(QIcon(":/buttons.png"), "Buttons", self.configureButtons) actButtons.setShortcut("Ctrl+B") actSwitches = self.tb.addAction(QIcon(":/switches.png"), "Switches", self.configureSwitches) actSwitches.setShortcut("Ctrl+S") actPower = self.tb.addAction(QIcon(":/power.png"), "Power", self.configurePower) actPower.setShortcut("Ctrl+P") # setopts = self.tb.addAction(QIcon(":/setoptions.png"), "SetOptions", self.configureSO) # setopts.setShortcut("Ctrl+S") self.tb.addSpacer() actTelemetry = self.tb.addAction(QIcon(":/telemetry.png"), "Telemetry", self.openTelemetry.emit) actTelemetry.setShortcut("Ctrl+T") actWebui = self.tb.addAction(QIcon(":/web.png"), "WebUI", self.openWebUI.emit) actWebui.setShortcut("Ctrl+U") self.ctx_menu.addActions([actRules, actTimers, actButtons, actSwitches, actPower, actTelemetry, actWebui]) self.ctx_menu.addSeparator() self.ctx_menu_cfg = QMenu("Configure") self.ctx_menu_cfg.setIcon(QIcon(":/settings.png")) self.ctx_menu_cfg.addAction("Module", self.configureModule) self.ctx_menu_cfg.addAction("GPIO", self.configureGPIO) self.ctx_menu_cfg.addAction("Template", self.configureTemplate) # self.ctx_menu_cfg.addAction("Wifi", self.ctx_menu_teleperiod) # self.ctx_menu_cfg.addAction("Time", self.cfgTime.emit) # self.ctx_menu_cfg.addAction("MQTT", self.ctx_menu_teleperiod) # self.ctx_menu_cfg.addAction("Logging", self.ctx_menu_teleperiod) self.ctx_menu.addMenu(self.ctx_menu_cfg) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon(":/refresh.png"), "Refresh", self.ctx_menu_refresh) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon(":/clear.png"), "Clear retained", self.ctx_menu_clear_retained) self.ctx_menu.addAction("Clear Backlog", self.ctx_menu_clear_backlog) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon(":/copy.png"), "Copy", self.ctx_menu_copy) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon(":/restart.png"), "Restart", self.ctx_menu_restart) self.ctx_menu.addAction(QIcon(), "Reset", self.ctx_menu_reset) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon(":/delete.png"), "Delete", self.ctx_menu_delete_device) # self.tb.addAction(QIcon(), "Multi Command", self.ctx_menu_webui) self.agAllPower = QActionGroup(self) self.agAllPower.addAction(QIcon(":/P_ON.png"), "All ON") self.agAllPower.addAction(QIcon(":/P_OFF.png"), "All OFF") self.agAllPower.setEnabled(False) self.agAllPower.setExclusive(False) self.agAllPower.triggered.connect(self.toggle_power_all) self.tb_relays.addActions(self.agAllPower.actions()) self.agRelays = QActionGroup(self) self.agRelays.setVisible(False) self.agRelays.setExclusive(False) for a in range(1, 9): act = QAction(QIcon(":/P{}_OFF.png".format(a)), "") act.setShortcut("F{}".format(a)) self.agRelays.addAction(act) self.agRelays.triggered.connect(self.toggle_power) self.tb_relays.addActions(self.agRelays.actions()) self.tb_relays.addSeparator() self.actColor = self.tb_relays.addAction(QIcon(":/color.png"), "Color", self.set_color) self.actColor.setEnabled(False) self.actChannels = self.tb_relays.addAction(QIcon(":/sliders.png"), "Channels") self.actChannels.setEnabled(False) self.mChannels = QMenu() self.actChannels.setMenu(self.mChannels) self.tb_relays.widgetForAction(self.actChannels).setPopupMode(QToolButton.InstantPopup) def create_view_buttons(self): self.tb_views.addWidget(QLabel("View mode: ")) ag_views = QActionGroup(self) ag_views.setExclusive(True) for v in self.views.keys(): a = QAction(v) a.triggered.connect(self.change_view) a.setCheckable(True) ag_views.addAction(a) self.tb_views.addActions(ag_views.actions()) ag_views.actions()[0].setChecked(True) stretch = QWidget() stretch.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)) self.tb_views.addWidget(stretch) # actEditView = self.tb_views.addAction("Edit views...") # def create_view_filter(self): # # self.tb_filter.addWidget(QLabel("Show devices: ")) # # self.cbxLWT = QComboBox() # # self.cbxLWT.addItems(["All", "Online"d, "Offline"]) # # self.cbxLWT.currentTextChanged.connect(self.build_filter_regex) # # self.tb_filter.addWidget(self.cbxLWT) # # self.tb_filter.addWidget(QLabel(" Search: ")) # self.leSearch = QLineEdit() # self.leSearch.setClearButtonEnabled(True) # self.leSearch.textChanged.connect(self.build_filter_regex) # self.tb_filter.addWidget(self.leSearch) # # def build_filter_regex(self, txt): # query = self.leSearch.text() # # if self.cbxLWT.currentText() != "All": # # query = "{}|{}".format(self.cbxLWT.currentText(), query) # self.sorted_device_model.setFilterRegExp(query) def change_view(self, a=None): view = self.views[self.sender().text()] self.model.setupColumns(view) self.device_list.setupView(view) def ctx_menu_copy(self): if self.idx: string = dumps(self.model.data(self.idx)) if string.startswith('"') and string.endswith('"'): string = string[1:-1] QApplication.clipboard().setText(string) def ctx_menu_clear_retained(self): if self.device: relays = self.device.power() if relays and len(relays.keys()) > 0: for r in relays.keys(): self.mqtt.publish(self.device.cmnd_topic(r), retain=True) QMessageBox.information(self, "Clear retained", "Cleared retained messages.") def ctx_menu_clear_backlog(self): if self.device: self.mqtt.publish(self.device.cmnd_topic("backlog"), "") QMessageBox.information(self, "Clear Backlog", "Backlog cleared.") def ctx_menu_restart(self): if self.device: self.mqtt.publish(self.device.cmnd_topic("restart"), payload="1") for k in list(self.device.power().keys()): self.device.p.pop(k) def ctx_menu_reset(self): if self.device: reset, ok = QInputDialog.getItem(self, "Reset device and restart", "Select reset mode", resets, editable=False) if ok: self.mqtt.publish(self.device.cmnd_topic("reset"), payload=reset.split(":")[0]) for k in list(self.device.power().keys()): self.device.p.pop(k) def ctx_menu_refresh(self): if self.device: for k in list(self.device.power().keys()): self.device.p.pop(k) for c in initial_commands(): cmd, payload = c cmd = self.device.cmnd_topic(cmd) self.mqtt.publish(cmd, payload, 1) def ctx_menu_delete_device(self): if self.device: if QMessageBox.question(self, "Confirm", "Do you want to remove the following device?\n'{}' ({})" .format(self.device.p['FriendlyName1'], self.device.p['Topic'])) == QMessageBox.Yes: self.model.deleteDevice(self.idx) def ctx_menu_teleperiod(self): if self.device: teleperiod, ok = QInputDialog.getInt(self, "Set telemetry period", "Input 1 to reset to default\n[Min: 10, Max: 3600]", self.device.p['TelePeriod'], 1, 3600) if ok: if teleperiod != 1 and teleperiod < 10: teleperiod = 10 self.mqtt.publish(self.device.cmnd_topic("teleperiod"), teleperiod) def ctx_menu_config_backup(self): if self.device: self.device_username = self.settings.value("device_username", "", str) self.device_password = self.settings.value("device_password", "", str) self.backup = bytes() self.dl = self.nam.get(QNetworkRequest(QUrl("http://{}:{}@{}/dl".format( \ self.device_username, urllib.parse.quote(self.device_password),self.device.p['IPAddress'])))) self.dl.readyRead.connect(self.get_dump) self.dl.finished.connect(self.save_dump) def ctx_menu_ota_set_url(self): if self.device: url, ok = QInputDialog.getText(self, "Set OTA URL", '100 chars max. Set to "1" to reset to default.', text=self.device.p['OtaUrl']) if ok: self.mqtt.publish(self.device.cmnd_topic("otaurl"), payload=url) def ctx_menu_ota_set_upgrade(self): if self.device: if QMessageBox.question(self, "OTA Upgrade", "Are you sure to OTA upgrade from\n{}".format(self.device.p['OtaUrl']), QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes: self.mqtt.publish(self.device.cmnd_topic("upgrade"), payload="1") def show_list_ctx_menu(self, at): self.select_device(self.device_list.indexAt(at)) self.ctx_menu.popup(self.device_list.viewport().mapToGlobal(at)) def select_device(self, idx): self.idx = self.sorted_device_model.mapToSource(idx) self.device = self.model.deviceAtRow(self.idx.row()) self.deviceSelected.emit(self.device) relays = self.device.power() self.agAllPower.setEnabled(len(relays) >= 1) for i, a in enumerate(self.agRelays.actions()): a.setVisible(len(relays) > 1 and i < len(relays)) color = self.device.color().get("Color", False) has_color = bool(color) self.actColor.setEnabled(has_color and not self.device.setoption(68)) self.actChannels.setEnabled(has_color) if has_color: self.actChannels.menu().clear() max_val = 100 if self.device.setoption(15) == 0: max_val = 1023 for k, v in self.device.pwm().items(): channel = SliderAction(self, k) channel.slider.setMaximum(max_val) channel.slider.setValue(int(v)) self.mChannels.addAction(channel) channel.slider.valueChanged.connect(self.set_channel) dimmer = self.device.color().get("Dimmer") if dimmer: saDimmer = SliderAction(self, "Dimmer") saDimmer.slider.setValue(int(dimmer)) self.mChannels.addAction(saDimmer) saDimmer.slider.valueChanged.connect(self.set_channel) def toggle_power(self, action): if self.device: idx = self.agRelays.actions().index(action) relay = sorted(list(self.device.power().keys()))[idx] self.mqtt.publish(self.device.cmnd_topic(relay), "toggle") def toggle_power_all(self, action): if self.device: idx = self.agAllPower.actions().index(action) for r in sorted(self.device.power().keys()): self.mqtt.publish(self.device.cmnd_topic(r), idx ^ 1) def set_color(self): if self.device: color = self.device.color().get("Color") if color: dlg = QColorDialog() new_color = dlg.getColor(QColor("#{}".format(color))) if new_color.isValid(): new_color = new_color.name() if new_color != color: self.mqtt.publish(self.device.cmnd_topic("color"), new_color) def set_channel(self, value=0): cmd = self.sender().objectName() if self.device: self.mqtt.publish(self.device.cmnd_topic(cmd), str(value)) def configureSO(self): if self.device: dlg = SetOptionsDialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureModule(self): if self.device: dlg = ModuleDialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureGPIO(self): if self.device: dlg = GPIODialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureTemplate(self): if self.device: dlg = TemplateDialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureTimers(self): if self.device: self.mqtt.publish(self.device.cmnd_topic("timers")) timers = TimersDialog(self.device) self.mqtt.messageSignal.connect(timers.parseMessage) timers.sendCommand.connect(self.mqtt.publish) timers.exec_() def configureButtons(self): if self.device: backlog = [] buttons = ButtonsDialog(self.device) if buttons.exec_() == QDialog.Accepted: for c, cw in buttons.command_widgets.items(): current_value = self.device.p.get(c) new_value = "" if isinstance(cw.input, SpinBox): new_value = cw.input.value() if isinstance(cw.input, QComboBox): new_value = cw.input.currentIndex() if current_value != new_value: backlog.append("{} {}".format(c, new_value)) so_error = False for so, sow in buttons.setoption_widgets.items(): current_value = None try: current_value = self.device.setoption(so) except ValueError: so_error = True new_value = -1 if isinstance(sow.input, SpinBox): new_value = sow.input.value() if isinstance(sow.input, QComboBox): new_value = sow.input.currentIndex() if not so_error and current_value and current_value != new_value: backlog.append("SetOption{} {}".format(so, new_value)) if backlog: backlog.append("status 3") self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog)) def configureSwitches(self): if self.device: backlog = [] switches = SwitchesDialog(self.device) if switches.exec_() == QDialog.Accepted: for c, cw in switches.command_widgets.items(): current_value = self.device.p.get(c) new_value = "" if isinstance(cw.input, SpinBox): new_value = cw.input.value() if isinstance(cw.input, QComboBox): new_value = cw.input.currentIndex() if current_value != new_value: backlog.append("{} {}".format(c, new_value)) so_error = False for so, sow in switches.setoption_widgets.items(): current_value = None try: current_value = self.device.setoption(so) except ValueError: so_error = True new_value = -1 if isinstance(sow.input, SpinBox): new_value = sow.input.value() if isinstance(sow.input, QComboBox): new_value = sow.input.currentIndex() if not so_error and current_value != new_value: backlog.append("SetOption{} {}".format(so, new_value)) for sw, sw_mode in enumerate(self.device.p['SwitchMode']): new_value = switches.sm.inputs[sw].currentIndex() if sw_mode != new_value: backlog.append("switchmode{} {}".format(sw+1, new_value)) if backlog: backlog.append("status") backlog.append("status 3") self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog)) def configurePower(self): if self.device: backlog = [] power = PowerDialog(self.device) if power.exec_() == QDialog.Accepted: for c, cw in power.command_widgets.items(): current_value = self.device.p.get(c) new_value = "" if isinstance(cw.input, SpinBox): new_value = cw.input.value() if isinstance(cw.input, QComboBox): new_value = cw.input.currentIndex() if current_value != new_value: backlog.append("{} {}".format(c, new_value)) so_error = False for so, sow in power.setoption_widgets.items(): current_value = None try: current_value = self.device.setoption(so) except ValueError: so_error = True new_value = -1 if isinstance(sow.input, SpinBox): new_value = sow.input.value() if isinstance(sow.input, QComboBox): new_value = sow.input.currentIndex() if not so_error and current_value != new_value: backlog.append("SetOption{} {}".format(so, new_value)) new_interlock_value = power.ci.input.currentData() new_interlock_grps = " ".join([grp.text().replace(" ", "") for grp in power.ci.groups]).rstrip() if new_interlock_value != self.device.p.get("Interlock", "OFF"): backlog.append("interlock {}".format(new_interlock_value)) if new_interlock_grps != self.device.p.get("Groups", ""): backlog.append("interlock {}".format(new_interlock_grps)) for i, pt in enumerate(power.cpt.inputs): ptime = "PulseTime{}".format(i+1) current_ptime = self.device.p.get(ptime) if current_ptime: current_value = list(current_ptime.keys())[0] new_value = str(pt.value()) if new_value != current_value: backlog.append("{} {}".format(ptime, new_value)) if backlog: backlog.append("status") backlog.append("status 3") self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog)) def get_dump(self): self.backup += self.dl.readAll() def save_dump(self): fname = self.dl.header(QNetworkRequest.ContentDispositionHeader) if fname: fname = fname.split('=')[1] save_file = QFileDialog.getSaveFileName(self, "Save config backup", "{}/TDM/{}".format(QDir.homePath(), fname))[0] if save_file: with open(save_file, "wb") as f: f.write(self.backup) def check_fulltopic(self, fulltopic): fulltopic += "/" if not fulltopic.endswith('/') else '' return "%prefix%" in fulltopic and "%topic%" in fulltopic def closeEvent(self, event): event.ignore()
def make_menu_bar(self): ''' Initialises the menu bar at the top. ''' menubar = self.menuBar() # Create a File menu and add an open button self.file_menu = menubar.addMenu('&File') open_action = QtGui.QAction('&Open', self) open_action.setShortcut('Ctrl+o') open_action.triggered.connect(self.choose_file) self.file_menu.addAction(open_action) # Add an exit button to the file menu exit_action = QtGui.QAction('&Exit', self) exit_action.setShortcut('Ctrl+Z') exit_action.setStatusTip('Exit application') exit_action.triggered.connect(QtGui.qApp.quit) self.file_menu.addAction(exit_action) ## Create a view manu self.view_menu = menubar.addMenu('&View') #self.view_menu.setShortcut('Alt+v') self.image_plot_options_menu = self.view_menu.addMenu( '&Image Plot Options') self.colormap_options_menu = self.image_plot_options_menu.addMenu( '&Colormap') group = QActionGroup(self.colormap_options_menu) texts = [ "default", "jet", 'jet_extended', 'albula', 'albula_r', 'goldish', "viridis", 'spectrum', 'vge', 'vge_hdr', ] for text in texts: action = QAction(text, self.colormap_options_menu, checkable=True, checked=text == texts[1]) self.colormap_options_menu.addAction(action) group.addAction(action) group.setExclusive(True) group.triggered.connect(self.onTriggered_colormap) self.colorscale_options_menu = self.image_plot_options_menu.addMenu( '&ColorScale') group = QActionGroup(self.colorscale_options_menu) texts = ["linear", "log"] for text in texts: action = QAction(text, self.colormap_options_menu, checkable=True, checked=text == texts[1]) self.colorscale_options_menu.addAction(action) group.addAction(action) group.setExclusive(True) group.triggered.connect(self.onTriggered_colorscale) self.display_image_data_options_menu = self.view_menu.addMenu( '&Display Image Data') show_image_data_action = QAction('show data', self, checkable=True, checked=False) show_image_data_action.triggered.connect( self.onTriggered_show_image_data) self.display_image_data_options_menu.addAction(show_image_data_action) # Create a Help menu and add an about button help_menu = menubar.addMenu('&Help') about_action = QtGui.QAction('About XSH5FView', self) about_action.setStatusTip('About this program') about_action.triggered.connect(self.show_about_menu) help_menu.addAction(about_action)
class NoteEditor(QPlainTextEdit): """The note editing class used uses a plain text editing window to edit markdown files (notes).""" def __init__(self, parent): super(NoteEditor, self).__init__(parent) self.highlighter = Highlighter( self.document() ) # the language dictionary to be used self.dictionary = None self.highlighter.setDictionary(self.dictionary) # create dictionary selector menu self.dictionarySelector = QMenu("&Dictionary") self.dictionaryActions = QActionGroup(self) self.noDicitionaryAction = self.dictionaryActions.addAction("None") self.noDicitionaryAction.setCheckable(True) self.dictionarySelector.addAction(self.noDicitionaryAction) self.defaultDicitionaryAction = self.dictionaryActions.addAction("Default") self.defaultDicitionaryAction.setCheckable(True) self.dictionarySelector.addAction(self.defaultDicitionaryAction) self.dictionarySelector.addSeparator() for lang in enchant.list_languages(): langAction = self.dictionaryActions.addAction(lang) langAction.setCheckable(True) self.dictionarySelector.addAction(langAction) # connect signal to change language dictionary and set the default language self.dictionaryActions.triggered.connect(self.changeDictionary) self.defaultDicitionaryAction.trigger() # configure the custom context menu for spelling suggestions self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.showContextMenu) # settings self.noteFilePath = "" #stores path to the file being edited self.setFont(QFont("Monospace")) tabWidth = QFontMetrics( self.currentCharFormat().font() ).width(" ")#get the width of four spaces self.setTabStopWidth(tabWidth) #set the default tab width to be that of four spaces #%%% To do: use 'keyPressEvent' to implement auto-indent %%% def setWrapMode(self, wrapMode): self.setWordWrapMode(wrapMode) def getDictionarySelector(self): return self.dictionarySelector def changeDictionary(self, action): """Change the spell check dictionary (language) based on the languaged selected from the 'Dictionary' menu.""" # change the language dictionary if action is self.noDicitionaryAction: self.dictionary = None elif action is self.defaultDicitionaryAction: self.dictionary = enchant.Dict(locale.getdefaultlocale()[0]) else: self.dictionary = enchant.Dict(action.text()) # rehighlight the text to find spelling errors self.highlighter.setDictionary(self.dictionary) self.highlighter.rehighlight() def showContextMenu(self, position): """Shows an appropriate context menu for the area of the editor that was right-clicked.""" # create the context menu that will be displayed contextMenu = QMenu() # select the word that was right-clicked textCursor = self.cursorForPosition(position) textCursor.select(QTextCursor.WordUnderCursor) word = textCursor.selectedText() # if a word was selected and it's misspelled, show a suggestion menu if word and not len(word) is 0: if not self.dictionary.check(word): # create the suggestion menu for suggestion in self.dictionary.suggest(word): a = contextMenu.addAction(suggestion) a.setData("custom") contextMenu.addSeparator() a = contextMenu.addAction("Add to dictionary") a.setData("custom") else: a = contextMenu.addAction("Remove from dictionary") a.setData("custom") contextMenu.addSeparator() # add standard context menu actions standardMenu = self.createStandardContextMenu(position); isFirst = True for a in standardMenu.children(): if isFirst: isFirst = False else: contextMenu.addAction(a) # show context menu action = contextMenu.exec(self.mapToGlobal(position)) # if a suggestion was selected, replace the current word with it if action and action.data() == "custom": if action.text() == "Add to dictionary": self.dictionary.add(word) elif action.text() == "Remove from dictionary": self.dictionary.remove(word) else: textCursor.removeSelectedText() textCursor.insertText(action.text()) def openFileRequest(self, filePath): """Open a file using its path (string) in the editor. Return 'True' if successful, 'False' otherwise.""" noErrors = True #set return state noteFile = open(filePath, "r") if noteFile: #if the file was opened successfully self.setPlainText( noteFile.read() ) #set its contents in the editor self.noteFilePath = filePath #save the file path to the internal variable self.noteFileChanged.emit( os.path.abspath(self.noteFilePath) ) #emit signal to notify other objects of file change else: #else, return an error noErrors = False noteFile.close() return noErrors def writeToFile(self, filePath): """Writes text in editor to file 'filePath'. Return 'True' if successful, 'False' otherwise.""" noErrors = True if os.path.exists(filePath): #check if file exists filePath = os.path.abspath(filePath) #make path absolute just in case... noteFile = open(filePath, "w") if noteFile: #if file was opened successfully noteFile.write( self.toPlainText() ) #write text to file else: noErrors = False else: noErrors = False return noErrors def saveFileRequest(self): """Saves text in editor to note file. Return 'True' if successful, 'False' otherwise.""" noErrors = True if self.noteFilePath != "": #if note path is set (the editor knows which file it should save to) noErrors = self.writeToFile( self.noteFilePath )#write text to note file else: noErrors = False return noErrors def saveAsRequested(self, filePath): """Save text in editor to a new file ('filePath') and set it as the new note file. Return 'True' if successful, 'False' otherwise.""" noErrors = True newFile = open(filePath, "w+") #create the new file newFile.close() noErrors = self.writeToFile( filePath ) #write text to the new file if noErrors: #if no errors occured self.noteFilePath = filePath #set the new file as the internal note file self.noteFileChanged.emit( os.path.abspath(self.noteFilePath) ) #emit signal to notify other objects of file change return noErrors def saveCopyAsRequested(self, filePath): """Save note to a new file without loading it. Return 'True' if successful, 'False' otherwise.""" noErrors = True newFile = open(filePath, "w+") #create the new file newFile.close() noErrors = self.writeToFile( filePath ) #wrtie text to the new file ######################################################################################## ### Note that 'self.noteFilePath' (path to the note file being edited) is not changed ## ######################################################################################## return noErrors def getNoteFileName(self): """Returns the file name of the note being edited.""" return os.path.basename(self.noteFilePath) def getNotePath(self): """Returns the path to the note being edited.""" return self.noteFilePath #~signals~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ noteFileChanged = pyqtSignal([str]) #a signal to be emitted when 'self.noteFilePath' (path to the note file being edited) is changed
class Menu(QMainWindow): def __init__(self): super().__init__() self.colors = COLORS self.qp = QPainter() self.init_game() self.size2handler = {} self.previous_size = "small" self.setMouseTracking(True) self.setFixedSize(800, 600) self.setWindowTitle('Cubes') self.setWindowIcon(QIcon('icons/cube.png')) self.menu = self.menuBar() self.statusBar() self.file = self.menu.addMenu('&File') self.settings = self.menu.addMenu('&Settings') self.exit = QAction('Exit', self) self.table_of_records = QAction('Table of records', self) self.table_of_records.setStatusTip('Show table of records') self.exit.setShortcut('Ctrl+Q') self.exit.setStatusTip('Exit application') self.file.addAction(self.table_of_records) self.file.addAction(self.exit) self.init_settings() self.exit.triggered.connect(self.close) self.table_of_records.triggered.connect(self.show_table_of_records) def init_settings(self): self.sizes = QActionGroup(self) self.small = QAction('Small', self, checkable=True) self.small.setChecked(True) self.small.triggered.connect(lambda: self.set_size('small')) self.sizes.addAction(self.small) self.settings.addAction(self.small) self.size2handler['small'] = self.small self.middle1 = QAction('Middle 1', self, checkable=True) self.middle1.triggered.connect(lambda: self.set_size('middle1')) self.sizes.addAction(self.middle1) self.settings.addAction(self.middle1) self.size2handler['middle1'] = self.middle1 self.middle2 = QAction('Middle 2', self, checkable=True) self.middle2.triggered.connect(lambda: self.set_size('middle2')) self.sizes.addAction(self.middle2) self.settings.addAction(self.middle2) self.size2handler['middle2'] = self.middle2 self.big = QAction('Big', self, checkable=True) self.big.triggered.connect(lambda: self.set_size('big')) self.sizes.addAction(self.big) self.settings.addAction(self.big) self.size2handler['big'] = self.big self.small.setStatusTip('A small field. Has 10x10 size and 4 colors') self.middle1.setStatusTip('A middle field. ' 'Has 14x14 size and 5 colors') self.middle2.setStatusTip('A middle field. ' 'Has 14x14 size and 6 colors') self.big.setStatusTip('A big field. Has 25x25 size and 7 colors') def set_size(self, size): if self.game.total_score != 0: if not self.game.exit(): if self.previous_size != size: self.size2handler[self.previous_size].setChecked(True) self.size2handler[size].setChecked(False) return self.size2handler[size].setChecked(True) self.init_game(size) self.previous_size = size def closeEvent(self, event): self.game.exit() event.accept() def init_game(self, size='small'): self.game = Game(self.colors, size) self.setCentralWidget(self.game) def show_table_of_records(self): w = QWidget(self, Qt.Window) w.setWindowModality(Qt.WindowModal) w.setFixedSize(300, 200) w.setWindowTitle('Table of records') w.setWindowIcon(QIcon('icons/trophy.png')) w.move(self.geometry().center() - w.rect().center() - QPoint(0, 30)) table = QTableWidget(self) vbox = QVBoxLayout() table.setColumnCount(4) table.setRowCount(10) sizes = ["small", "middle1", "middle2", "big"] table.setHorizontalHeaderLabels(sizes) with open('texts/best_results.txt') as f: best_results = eval(f.read()) for size in best_results: for item in best_results[size]: row = best_results[size].index(item) column = sizes.index(size) item = item[0], str(item[1]) table.setItem(row, column, QTableWidgetItem(', '.join(item))) vbox.addWidget(table) vbox.addStretch(1) w.setLayout(vbox) w.show() def paintEvent(self, QPaintEvent): self.qp.begin(self) self.draw_rectangles(QPaintEvent) self.drawText(QPaintEvent) self.qp.end() def drawText(self, QPaintEvent): x, y = self.game.width * 22 + 10, 40 self.qp.setPen(QColor(0, 0, 0)) self.qp.setFont(QFont('fonts/ARCADE.ttf', 15)) self.qp.drawText(x, y, "Score: {}".format(self.game.total_score)) self.qp.setFont(QFont('fonts/ARCADE.ttf', 10)) for i in self.game.information_about_colors: y = y + 25 self.qp.drawText(x, y, i) def mousePressEvent(self, event): if event.buttons() == Qt.LeftButton: pos = event.pos().x(), event.pos().y() for (x, y) in self.game.object.blocks2coordinates: if x * 22 < pos[0] < 23 + x * 22 and \ y * 22 < pos[1] < y * 22 + 23: score, color, is_exit = self.game.object.remove((x, y)) self.game.information_about_colors = \ self.game.set_information_about_colors(color, score) self.game.update_score(score) if is_exit: self.game.exit() self.update() def draw_rectangles(self, QPaintEvent): for (x, y) in self.game.object.blocks2coordinates: color = self.game.object.blocks2coordinates[(x, y)][0].color self.qp.setPen(QColor(color[0], color[1], color[2])) self.qp.setBrush(QColor(color[0], color[1], color[2])) self.qp.drawRect(x * 22, y * 22, 20, 20)
class AnalysisMainWindow(QMainWindow): DEFAULT_INTERVAL = 0.1 CMD_BIT_LENGTH = uavcan.get_uavcan_data_type(uavcan.equipment.esc.RawCommand().cmd).value_type.bitlen CMD_MAX = 2 ** (CMD_BIT_LENGTH - 1) - 1 CMD_MIN = -(2 ** (CMD_BIT_LENGTH - 1)) def __init__(self, parent, node): super(AnalysisMainWindow, self).__init__(parent) self.setAttribute(Qt.WA_DeleteOnClose) # This is required to stop background timers self._inferiors = None self._hook_handle = None # self.getTransfer=get_transfer_callback self._node = node self._initUI() node.add_handler(uavcan.equipment.esc.Status, self._updateEscInfo) self._bcast_timer = QTimer(self) self._bcast_timer.start(self.DEFAULT_INTERVAL * 1e3) self._bcast_timer.timeout.connect(self._do_broadcast) # self._transfer_timer = QTimer(self) # self._transfer_timer.setSingleShot(False) # self._transfer_timer.start(self.DEFAULT_INTERVAL * 1e3) # self._transfer_timer.timeout.connect(self._update) self._thrust_timer = QTimer(self) self._thrust_timer.start(self.DEFAULT_INTERVAL * 1e3) self._thrust_timer.timeout.connect(self._updateThrust) self._thrust_port = None self._thrust_queue = None self._refreshSerialPortsList() def _initUI(self): # # init layout # self.setGeometry(560, 240, 1000, 500) # self.setFixedSize(500, 500 self.setWindowTitle('Motor Analysis') self._mainWidget = QWidget(self) # init box layout mainHbox = QHBoxLayout(self._mainWidget) vboxLeft = QVBoxLayout(self._mainWidget) vboxRight = QVBoxLayout(self._mainWidget) mainHbox.addLayout(vboxLeft) mainHbox.addLayout(vboxRight) self._statusGroup = QGroupBox("Sensor Status") vboxStatus = QVBoxLayout(self._statusGroup) self._statusGroup.setLayout(vboxStatus) vboxLeft.addWidget(self._statusGroup) self._statusLabel = QLabel('Load info failed.') vboxStatus.addWidget(self._statusLabel) # fill right column self._controlTypeTabs = QTabWidget(self._mainWidget) self._manualControlPanel = ManualControlPanel(self._controlTypeTabs) self._manualControlPanel._recordWidget._getEscInfoCallback = self._getEscInfo self._manualControlPanel._recordWidget._getThrustCallback = self._getThrust self._automaticControlPanel = AutomaticControlPanel(self._controlTypeTabs, self._manualControlPanel._escSlider.set_value) self._automaticControlPanel._recordWidget._getEscInfoCallback = self._getEscInfo self._automaticControlPanel._recordWidget._getThrustCallback = self._getThrust self._controlTypeTabs.addTab(self._manualControlPanel, "Manual Control") self._controlTypeTabs.addTab(self._automaticControlPanel, "Automatic Control") vboxRight.addWidget(self._controlTypeTabs) self._plotWidget = PlotsWidget(self, self._getTransfer) vboxRight.addWidget(self._plotWidget) self._automaticControlPanel._recordWidget._setPlotStyle = self._plotWidget.setPlotStyle self.setCentralWidget(self._mainWidget) # # Control menu # control_menu = self.menuBar().addMenu('Plot &control') self._stop_action = QAction(get_icon('stop'), '&Stop Updates', self) self._stop_action.setStatusTip('While stopped, all new data will be discarded') self._stop_action.setShortcut(QKeySequence('Ctrl+Shift+S')) self._stop_action.setCheckable(True) self._stop_action.toggled.connect(self._on_stop_toggled) control_menu.addAction(self._stop_action) self._pause_action = QAction(get_icon('pause'), '&Pause Updates', self) self._pause_action.setStatusTip('While paused, new data will be accumulated in memory ' 'to be processed once un-paused') self._pause_action.setShortcut(QKeySequence('Ctrl+Shift+P')) self._pause_action.setCheckable(True) self._pause_action.toggled.connect(self._on_pause_toggled) control_menu.addAction(self._pause_action) control_menu.addSeparator() self._reset_time_action = QAction(get_icon('history'), '&Reset', self) self._reset_time_action.setStatusTip('Base time will be reset; all plots will be reset') self._reset_time_action.setShortcut(QKeySequence('Ctrl+Shift+R')) self._reset_time_action.triggered.connect(self._plotWidget._do_reset) control_menu.addAction(self._reset_time_action) self.setWindowTitle("Motor Status Plots") # # Config Menu # config_menu = self.menuBar().addMenu('Con&fig') self._thrust_ports_menu = QMenu("Thrust Sensor Ports") self._thrust_port_lists = QActionGroup(self) config_menu.addMenu(self._thrust_ports_menu) def _on_stop_toggled(self, checked): self._pause_action.setChecked(False) self.statusBar().showMessage('Stopped' if checked else 'Un-stopped') def _on_pause_toggled(self, checked): self.statusBar().showMessage('Paused' if checked else 'Un-paused') def _do_broadcast(self): try: msg = uavcan.equipment.esc.RPMCommand() raw_value = self._manualControlPanel._escSlider.get_value() value = raw_value msg.rpm.append(int(value)) self._node.broadcast(msg) except Exception as ex: logger.info("RPM Message publishing failed:" + str(ex)) return def _updateEscInfo(self, tr): self._tr=tr self._plotWidget._update() self._statusLabel.setText(''' Voltage :{} Current :{} Thrust :{} Torque :{} Weight :{} MotorSpeed:{} '''.format(str(tr.message.voltage)[0:5], str(tr.message.current)[0:5], self._getThrust(), "N/A", "N/A", tr.message.rpm)) def _getThrust(self): return self._plotWidget._plc_thrust._value def _getEscInfo(self): return self._tr.message def _getTransfer(self): return self._tr def _refreshSerialPortsList(self): print("refresh serial ports list") ports = list(serial.tools.list_ports.comports()) self._thrust_ports_menu.clear() if(len(ports)==0): action=QAction("No Serial Device Found.",self) self._thrust_ports_menu.addAction(action) return for p in ports: print("{}:{}".format(p[0],p[1])) action=QAction("{}:{}".format(p[0],p[1]),self) action.setCheckable(True) action.triggered.connect(partial(self._setThrustSerialPort,port=p[0])) self._thrust_port_lists.addAction(action) self._thrust_ports_menu.addAction(action) def _updateThrust(self): if (self._thrust_queue != None): try: thrust = self._thrust_queue.get_nowait() except Exception: return self._plotWidget._plc_thrust.setValue(thrust) def _setThrustSerialPort(self,port): print(port) self._thrust_port=port self._plotWidget._plc_thrust.setHowToLabel("Thrust data source successfully set to {}".format(port) , "success") self._startThrustAcquireLoop() def _startThrustAcquireLoop(self): def handle_data(data,q): if(data!=b'' and data!=b'-' and data!=b'\n'): q.put(data.decode().rstrip()) def read_from_port(ser,q): while ser.isOpen(): msg = ser.read(ser.inWaiting()) handle_data(msg,q) time.sleep(0.1) if(self._thrust_port!=None): print("set thrust port:{}".format(self._thrust_port)) arduino = serial.Serial(self._thrust_port, 9600, timeout=5) self._thrust_queue=Queue() self._thrust_read_thread=Thread(target=read_from_port,args=(arduino,self._thrust_queue)) self._thrust_read_thread.start() else: msgbox = QMessageBox(self) msgbox.setText("ERROR: Can not configure the thrust device automatically." + "Please select it manually in <Thrust Sensor Ports> menu.") msgbox.exec()
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
class MainWindow(QMainWindow, Ui_MainWindow): dictChanged = pyqtSignal(str) # Tab indexes TabInfos = 0 TabSummary = 1 TabPersos = 2 TabPlots = 3 TabWorld = 4 TabOutline = 5 TabRedac = 6 def __init__(self): QMainWindow.__init__(self) self.setupUi(self) self.currentProject = None self.readSettings() # UI self.setupMoreUi() # Welcome self.welcome.updateValues() # self.welcome.btnCreate.clicked.connect self.stack.setCurrentIndex(0) # Word count self.mprWordCount = QSignalMapper(self) for t, i in [ (self.txtSummarySentence, 0), (self.txtSummaryPara, 1), (self.txtSummaryPage, 2), (self.txtSummaryFull, 3) ]: t.textChanged.connect(self.mprWordCount.map) self.mprWordCount.setMapping(t, i) self.mprWordCount.mapped.connect(self.wordCount) # Snowflake Method Cycle self.mapperCycle = QSignalMapper(self) for t, i in [ (self.btnStepTwo, 0), (self.btnStepThree, 1), (self.btnStepFour, 2), (self.btnStepFive, 3), (self.btnStepSix, 4), (self.btnStepSeven, 5), (self.btnStepEight, 6) ]: t.clicked.connect(self.mapperCycle.map) self.mapperCycle.setMapping(t, i) self.mapperCycle.mapped.connect(self.clickCycle) self.cmbSummary.currentIndexChanged.connect(self.summaryPageChanged) self.cmbSummary.setCurrentIndex(0) self.cmbSummary.currentIndexChanged.emit(0) # Main Menu for i in [self.actSave, self.actSaveAs, self.actCloseProject, self.menuEdit, self.menuMode, self.menuView, self.menuTools, self.menuHelp]: i.setEnabled(False) self.actOpen.triggered.connect(self.welcome.openFile) self.actSave.triggered.connect(self.saveDatas) self.actSaveAs.triggered.connect(self.welcome.saveAsFile) self.actCompile.triggered.connect(self.doCompile) self.actLabels.triggered.connect(self.settingsLabel) self.actStatus.triggered.connect(self.settingsStatus) self.actSettings.triggered.connect(self.settingsWindow) self.actCloseProject.triggered.connect(self.closeProject) self.actQuit.triggered.connect(self.close) self.actToolFrequency.triggered.connect(self.frequencyAnalyzer) self.generateViewMenu() self.makeUIConnections() # self.loadProject(os.path.join(appPath(), "test_project.zip")) ############################################################################### # SUMMARY ############################################################################### def summaryPageChanged(self, index): fractalButtons = [ self.btnStepTwo, self.btnStepThree, self.btnStepFive, self.btnStepSeven, ] for b in fractalButtons: b.setVisible(fractalButtons.index(b) == index) ############################################################################### # OUTLINE ############################################################################### def outlineRemoveItemsRedac(self): self.treeRedacOutline.delete() def outlineRemoveItemsOutline(self): self.treeOutlineOutline.delete() ############################################################################### # PERSOS ############################################################################### def changeCurrentPerso(self, trash=None): index = self.lstPersos.currentPersoIndex() if not index.isValid(): self.tabPlot.setEnabled(False) return self.tabPersos.setEnabled(True) for w in [ self.txtPersoName, self.sldPersoImportance, self.txtPersoMotivation, self.txtPersoGoal, self.txtPersoConflict, self.txtPersoEpiphany, self.txtPersoSummarySentence, self.txtPersoSummaryPara, self.txtPersoSummaryFull, self.txtPersoNotes, ]: w.setCurrentModelIndex(index) # Button color self.mdlPersos.updatePersoColor(index) # Perso Infos self.tblPersoInfos.setRootIndex(index) if self.mdlPersos.rowCount(index): self.updatePersoInfoView() def updatePersoInfoView(self): # Hide columns for i in range(self.mdlPersos.columnCount()): self.tblPersoInfos.hideColumn(i) self.tblPersoInfos.showColumn(Perso.infoName.value) self.tblPersoInfos.showColumn(Perso.infoData.value) self.tblPersoInfos.horizontalHeader().setSectionResizeMode( Perso.infoName.value, QHeaderView.ResizeToContents) self.tblPersoInfos.horizontalHeader().setSectionResizeMode( Perso.infoData.value, QHeaderView.Stretch) self.tblPersoInfos.verticalHeader().hide() ############################################################################### # PLOTS ############################################################################### def changeCurrentPlot(self): index = self.lstPlots.currentPlotIndex() if not index.isValid(): self.tabPlot.setEnabled(False) return self.tabPlot.setEnabled(True) self.txtPlotName.setCurrentModelIndex(index) self.txtPlotDescription.setCurrentModelIndex(index) self.txtPlotResult.setCurrentModelIndex(index) self.sldPlotImportance.setCurrentModelIndex(index) self.lstPlotPerso.setRootIndex(index.sibling(index.row(), Plot.persos.value)) subplotindex = index.sibling(index.row(), Plot.subplots.value) self.lstSubPlots.setRootIndex(subplotindex) if self.mdlPlots.rowCount(subplotindex): self.updateSubPlotView() # self.txtSubPlotSummary.setCurrentModelIndex(QModelIndex()) self.txtSubPlotSummary.setEnabled(False) self._updatingSubPlot = True self.txtSubPlotSummary.setPlainText("") self._updatingSubPlot = False self.lstPlotPerso.selectionModel().clear() def updateSubPlotView(self): # Hide columns for i in range(self.mdlPlots.columnCount()): self.lstSubPlots.hideColumn(i) self.lstSubPlots.showColumn(Subplot.name.value) self.lstSubPlots.showColumn(Subplot.meta.value) self.lstSubPlots.horizontalHeader().setSectionResizeMode( Subplot.name.value, QHeaderView.Stretch) self.lstSubPlots.horizontalHeader().setSectionResizeMode( Subplot.meta.value, QHeaderView.ResizeToContents) self.lstSubPlots.verticalHeader().hide() def changeCurrentSubPlot(self, index): # Got segfaults when using textEditView model system, so ad hoc stuff. index = index.sibling(index.row(), Subplot.summary.value) item = self.mdlPlots.itemFromIndex(index) if not item: self.txtSubPlotSummary.setEnabled(False) return self.txtSubPlotSummary.setEnabled(True) txt = item.text() self._updatingSubPlot = True self.txtSubPlotSummary.setPlainText(txt) self._updatingSubPlot = False def updateSubPlotSummary(self): if self._updatingSubPlot: return index = self.lstSubPlots.currentIndex() if not index.isValid(): return index = index.sibling(index.row(), Subplot.summary.value) item = self.mdlPlots.itemFromIndex(index) self._updatingSubPlot = True item.setText(self.txtSubPlotSummary.toPlainText()) self._updatingSubPlot = False def plotPersoSelectionChanged(self): "Enables or disables remove plot perso button." self.btnRmPlotPerso.setEnabled( len(self.lstPlotPerso.selectedIndexes()) != 0) ############################################################################### # WORLD ############################################################################### def changeCurrentWorld(self): index = self.mdlWorld.selectedIndex() if not index.isValid(): self.tabWorld.setEnabled(False) return self.tabWorld.setEnabled(True) self.txtWorldName.setCurrentModelIndex(index) self.txtWorldDescription.setCurrentModelIndex(index) self.txtWorldPassion.setCurrentModelIndex(index) self.txtWorldConflict.setCurrentModelIndex(index) ############################################################################### # LOAD AND SAVE ############################################################################### def loadProject(self, project, loadFromFile=True): """Loads the project ``project``. If ``loadFromFile`` is False, then it does not load datas from file. It assumes that the datas have been populated in a different way.""" if loadFromFile and not os.path.exists(project): print(self.tr("The file {} does not exist. Try again.").format(project)) self.statusBar().showMessage( self.tr("The file {} does not exist. Try again.").format(project), 5000) return if loadFromFile: # Load empty settings imp.reload(settings) # Load data self.loadEmptyDatas() self.loadDatas(project) self.makeConnections() # Load settings for i in settings.openIndexes: idx = self.mdlOutline.indexFromPath(i) self.mainEditor.setCurrentModelIndex(idx, newTab=True) self.generateViewMenu() self.mainEditor.sldCorkSizeFactor.setValue(settings.corkSizeFactor) self.actSpellcheck.setChecked(settings.spellcheck) self.toggleSpellcheck(settings.spellcheck) self.updateMenuDict() self.setDictionary() self.mainEditor.setFolderView(settings.folderView) self.mainEditor.updateFolderViewButtons(settings.folderView) self.tabMain.setCurrentIndex(settings.lastTab) # We force to emit even if it opens on the current tab self.tabMain.currentChanged.emit(settings.lastTab) self.mainEditor.updateCorkBackground() # Set autosave self.saveTimer = QTimer() self.saveTimer.setInterval(settings.autoSaveDelay * 60 * 1000) self.saveTimer.setSingleShot(False) self.saveTimer.timeout.connect(self.saveDatas) if settings.autoSave: self.saveTimer.start() # Set autosave if no changes self.saveTimerNoChanges = QTimer() self.saveTimerNoChanges.setInterval(settings.autoSaveNoChangesDelay * 1000) self.saveTimerNoChanges.setSingleShot(True) self.mdlFlatData.dataChanged.connect(self.startTimerNoChanges) self.mdlOutline.dataChanged.connect(self.startTimerNoChanges) self.mdlPersos.dataChanged.connect(self.startTimerNoChanges) self.mdlPlots.dataChanged.connect(self.startTimerNoChanges) self.mdlWorld.dataChanged.connect(self.startTimerNoChanges) # self.mdlPersosInfos.dataChanged.connect(self.startTimerNoChanges) self.mdlStatus.dataChanged.connect(self.startTimerNoChanges) self.mdlLabels.dataChanged.connect(self.startTimerNoChanges) self.saveTimerNoChanges.timeout.connect(self.saveDatas) self.saveTimerNoChanges.stop() # UI for i in [self.actSave, self.actSaveAs, self.actCloseProject, self.menuEdit, self.menuMode, self.menuView, self.menuTools, self.menuHelp]: i.setEnabled(True) # FIXME: set Window's name: project name # Stuff # self.checkPersosID() # Should'n be necessary any longer self.currentProject = project QSettings().setValue("lastProject", project) # Show main Window self.stack.setCurrentIndex(1) def closeProject(self): # Save datas self.saveDatas() self.currentProject = None QSettings().setValue("lastProject", "") # FIXME: close all opened tabs in mainEditor # Clear datas self.loadEmptyDatas() self.saveTimer.stop() # UI for i in [self.actSave, self.actSaveAs, self.actCloseProject, self.menuEdit, self.menuMode, self.menuView, self.menuTools, self.menuHelp]: i.setEnabled(False) # Reload recent files self.welcome.updateValues() # Show welcome dialog self.stack.setCurrentIndex(0) def readSettings(self): # Load State and geometry sttgns = QSettings(qApp.organizationName(), qApp.applicationName()) if sttgns.contains("geometry"): self.restoreGeometry(sttgns.value("geometry")) if sttgns.contains("windowState"): self.restoreState(sttgns.value("windowState")) else: self.dckCheatSheet.hide() self.dckSearch.hide() if sttgns.contains("metadataState"): state = [False if v == "false" else True for v in sttgns.value("metadataState")] self.redacMetadata.restoreState(state) if sttgns.contains("revisionsState"): state = [False if v == "false" else True for v in sttgns.value("revisionsState")] self.redacMetadata.revisions.restoreState(state) if sttgns.contains("splitterRedacH"): self.splitterRedacH.restoreState(sttgns.value("splitterRedacH")) if sttgns.contains("splitterRedacV"): self.splitterRedacV.restoreState(sttgns.value("splitterRedacV")) if sttgns.contains("toolbar"): # self.toolbar is not initialized yet, so we just store balue self._toolbarState = sttgns.value("toolbar") else: self._toolbarState = "" def closeEvent(self, event): # Save State and geometry and other things sttgns = QSettings(qApp.organizationName(), qApp.applicationName()) sttgns.setValue("geometry", self.saveGeometry()) sttgns.setValue("windowState", self.saveState()) sttgns.setValue("metadataState", self.redacMetadata.saveState()) sttgns.setValue("revisionsState", self.redacMetadata.revisions.saveState()) sttgns.setValue("splitterRedacH", self.splitterRedacH.saveState()) sttgns.setValue("splitterRedacV", self.splitterRedacV.saveState()) sttgns.setValue("toolbar", self.toolbar.saveState()) # Specific settings to save before quitting settings.lastTab = self.tabMain.currentIndex() if self.currentProject: # Remembering the current items sel = [] for i in range(self.mainEditor.tab.count()): sel.append(self.mdlOutline.pathToIndex(self.mainEditor.tab.widget(i).currentIndex)) settings.openIndexes = sel # Save data from models if self.currentProject and settings.saveOnQuit: self.saveDatas() # closeEvent # QMainWindow.closeEvent(self, event) # Causin segfaults? def startTimerNoChanges(self): if settings.autoSaveNoChanges: self.saveTimerNoChanges.start() def saveDatas(self, projectName=None): """Saves the current project (in self.currentProject). If ``projectName`` is given, currentProject becomes projectName. In other words, it "saves as...". """ if projectName: self.currentProject = projectName QSettings().setValue("lastProject", projectName) # Saving files = [] files.append((saveStandardItemModelXML(self.mdlFlatData), "flatModel.xml")) files.append((saveStandardItemModelXML(self.mdlPersos), "perso.xml")) files.append((saveStandardItemModelXML(self.mdlWorld), "world.xml")) files.append((saveStandardItemModelXML(self.mdlLabels), "labels.xml")) files.append((saveStandardItemModelXML(self.mdlStatus), "status.xml")) files.append((saveStandardItemModelXML(self.mdlPlots), "plots.xml")) files.append((self.mdlOutline.saveToXML(), "outline.xml")) files.append((settings.save(), "settings.pickle")) saveFilesToZip(files, self.currentProject) # Giving some feedback print(self.tr("Project {} saved.").format(self.currentProject)) self.statusBar().showMessage( self.tr("Project {} saved.").format(self.currentProject), 5000) def loadEmptyDatas(self): self.mdlFlatData = QStandardItemModel(self) self.mdlPersos = persosModel(self) # self.mdlPersosProxy = persosProxyModel(self) # self.mdlPersosInfos = QStandardItemModel(self) self.mdlLabels = QStandardItemModel(self) self.mdlStatus = QStandardItemModel(self) self.mdlPlots = plotModel(self) self.mdlOutline = outlineModel(self) self.mdlWorld = worldModel(self) def loadDatas(self, project): # Loading files = loadFilesFromZip(project) errors = [] if "flatModel.xml" in files: loadStandardItemModelXML(self.mdlFlatData, files["flatModel.xml"], fromString=True) else: errors.append("flatModel.xml") if "perso.xml" in files: loadStandardItemModelXML(self.mdlPersos, files["perso.xml"], fromString=True) else: errors.append("perso.xml") if "world.xml" in files: loadStandardItemModelXML(self.mdlWorld, files["world.xml"], fromString=True) else: errors.append("world.xml") if "labels.xml" in files: loadStandardItemModelXML(self.mdlLabels, files["labels.xml"], fromString=True) else: errors.append("perso.xml") if "status.xml" in files: loadStandardItemModelXML(self.mdlStatus, files["status.xml"], fromString=True) else: errors.append("perso.xml") if "plots.xml" in files: loadStandardItemModelXML(self.mdlPlots, files["plots.xml"], fromString=True) else: errors.append("perso.xml") if "outline.xml" in files: self.mdlOutline.loadFromXML(files["outline.xml"], fromString=True) else: errors.append("perso.xml") if "settings.pickle" in files: settings.load(files["settings.pickle"], fromString=True) else: errors.append("perso.xml") # Giving some feedback if not errors: print(self.tr("Project {} loaded.").format(project)) self.statusBar().showMessage( self.tr("Project {} loaded.").format(project), 5000) else: print(self.tr("Project {} loaded with some errors:").format(project)) for e in errors: print(self.tr(" * {} wasn't found in project file.").format(e)) self.statusBar().showMessage( self.tr("Project {} loaded with some errors.").format(project), 5000) ############################################################################### # MAIN CONNECTIONS ############################################################################### def makeUIConnections(self): "Connections that have to be made once only, event when new project is loaded." self.lstPersos.currentItemChanged.connect(self.changeCurrentPerso, AUC) self.txtPlotFilter.textChanged.connect(self.lstPlots.setFilter, AUC) self.lstPlots.currentItemChanged.connect(self.changeCurrentPlot, AUC) self.txtSubPlotSummary.document().contentsChanged.connect( self.updateSubPlotSummary, AUC) self.lstSubPlots.activated.connect(self.changeCurrentSubPlot, AUC) self.btnRedacAddFolder.clicked.connect(self.treeRedacOutline.addFolder, AUC) self.btnOutlineAddFolder.clicked.connect(self.treeOutlineOutline.addFolder, AUC) self.btnRedacAddText.clicked.connect(self.treeRedacOutline.addText, AUC) self.btnOutlineAddText.clicked.connect(self.treeOutlineOutline.addText, AUC) self.btnRedacRemoveItem.clicked.connect(self.outlineRemoveItemsRedac, AUC) self.btnOutlineRemoveItem.clicked.connect(self.outlineRemoveItemsOutline, AUC) self.tabMain.currentChanged.connect(self.toolbar.setCurrentGroup) def makeConnections(self): # Flat datas (Summary and general infos) for widget, col in [ (self.txtSummarySituation, 0), (self.txtSummarySentence, 1), (self.txtSummarySentence_2, 1), (self.txtSummaryPara, 2), (self.txtSummaryPara_2, 2), (self.txtPlotSummaryPara, 2), (self.txtSummaryPage, 3), (self.txtSummaryPage_2, 3), (self.txtPlotSummaryPage, 3), (self.txtSummaryFull, 4), (self.txtPlotSummaryFull, 4), ]: widget.setModel(self.mdlFlatData) widget.setColumn(col) widget.setCurrentModelIndex(self.mdlFlatData.index(1, col)) for widget, col in [ (self.txtGeneralTitle, 0), (self.txtGeneralSubtitle, 1), (self.txtGeneralSerie, 2), (self.txtGeneralVolume, 3), (self.txtGeneralGenre, 4), (self.txtGeneralLicense, 5), (self.txtGeneralAuthor, 6), (self.txtGeneralEmail, 7), ]: widget.setModel(self.mdlFlatData) widget.setColumn(col) widget.setCurrentModelIndex(self.mdlFlatData.index(0, col)) # Persos self.lstPersos.setPersosModel(self.mdlPersos) self.tblPersoInfos.setModel(self.mdlPersos) self.btnAddPerso.clicked.connect(self.mdlPersos.addPerso, AUC) self.btnRmPerso.clicked.connect(self.mdlPersos.removePerso, AUC) self.btnPersoColor.clicked.connect(self.mdlPersos.chosePersoColor, AUC) self.btnPersoAddInfo.clicked.connect(self.mdlPersos.addPersoInfo, AUC) self.btnPersoRmInfo.clicked.connect(self.mdlPersos.removePersoInfo, AUC) for w, c in [ (self.txtPersoName, Perso.name.value), (self.sldPersoImportance, Perso.importance.value), (self.txtPersoMotivation, Perso.motivation.value), (self.txtPersoGoal, Perso.goal.value), (self.txtPersoConflict, Perso.conflict.value), (self.txtPersoEpiphany, Perso.epiphany.value), (self.txtPersoSummarySentence, Perso.summarySentence.value), (self.txtPersoSummaryPara, Perso.summaryPara.value), (self.txtPersoSummaryFull, Perso.summaryFull.value), (self.txtPersoNotes, Perso.notes.value) ]: w.setModel(self.mdlPersos) w.setColumn(c) self.tabPersos.setEnabled(False) # Plots self.lstPlots.setPlotModel(self.mdlPlots) self.lstPlotPerso.setModel(self.mdlPlots) self.lstSubPlots.setModel(self.mdlPlots) self._updatingSubPlot = False self.btnAddPlot.clicked.connect(self.mdlPlots.addPlot, AUC) self.btnRmPlot.clicked.connect(lambda: self.mdlPlots.removePlot(self.lstPlots.currentPlotIndex()), AUC) self.btnAddSubPlot.clicked.connect(self.mdlPlots.addSubPlot, AUC) self.btnRmSubPlot.clicked.connect(self.mdlPlots.removeSubPlot, AUC) self.lstPlotPerso.selectionModel().selectionChanged.connect(self.plotPersoSelectionChanged) self.btnRmPlotPerso.clicked.connect(self.mdlPlots.removePlotPerso, AUC) for w, c in [ (self.txtPlotName, Plot.name.value), (self.txtPlotDescription, Plot.description.value), (self.txtPlotResult, Plot.result.value), (self.sldPlotImportance, Plot.importance.value), ]: w.setModel(self.mdlPlots) w.setColumn(c) self.tabPlot.setEnabled(False) self.mdlPlots.updatePlotPersoButton() self.mdlPersos.dataChanged.connect(self.mdlPlots.updatePlotPersoButton) self.lstOutlinePlots.setPlotModel(self.mdlPlots) self.lstOutlinePlots.setShowSubPlot(True) self.plotPersoDelegate = outlinePersoDelegate(self.mdlPersos, self) self.lstPlotPerso.setItemDelegate(self.plotPersoDelegate) self.plotDelegate = plotDelegate(self) self.lstSubPlots.setItemDelegateForColumn(Subplot.meta.value, self.plotDelegate) # World self.treeWorld.setModel(self.mdlWorld) for i in range(self.mdlWorld.columnCount()): self.treeWorld.hideColumn(i) self.treeWorld.showColumn(0) self.btnWorldEmptyData.setMenu(self.mdlWorld.emptyDataMenu()) self.treeWorld.selectionModel().selectionChanged.connect(self.changeCurrentWorld, AUC) self.btnAddWorld.clicked.connect(self.mdlWorld.addItem, AUC) self.btnRmWorld.clicked.connect(self.mdlWorld.removeItem, AUC) for w, c in [ (self.txtWorldName, World.name.value), (self.txtWorldDescription, World.description.value), (self.txtWorldPassion, World.passion.value), (self.txtWorldConflict, World.conflict.value), ]: w.setModel(self.mdlWorld) w.setColumn(c) self.tabWorld.setEnabled(False) self.treeWorld.expandAll() # Outline self.treeRedacOutline.setModel(self.mdlOutline) self.treeOutlineOutline.setModelPersos(self.mdlPersos) self.treeOutlineOutline.setModelLabels(self.mdlLabels) self.treeOutlineOutline.setModelStatus(self.mdlStatus) self.redacMetadata.setModels(self.mdlOutline, self.mdlPersos, self.mdlLabels, self.mdlStatus) self.outlineItemEditor.setModels(self.mdlOutline, self.mdlPersos, self.mdlLabels, self.mdlStatus) self.treeOutlineOutline.setModel(self.mdlOutline) # self.redacEditor.setModel(self.mdlOutline) self.storylineView.setModels(self.mdlOutline, self.mdlPersos, self.mdlPlots) self.treeOutlineOutline.selectionModel().selectionChanged.connect(lambda: self.outlineItemEditor.selectionChanged( self.treeOutlineOutline), AUC) self.treeOutlineOutline.clicked.connect(lambda: self.outlineItemEditor.selectionChanged(self.treeOutlineOutline), AUC) # Sync selection self.treeRedacOutline.selectionModel().selectionChanged.connect( lambda: self.redacMetadata.selectionChanged(self.treeRedacOutline), AUC) self.treeRedacOutline.clicked.connect( lambda: self.redacMetadata.selectionChanged(self.treeRedacOutline), AUC) self.treeRedacOutline.selectionModel().selectionChanged.connect(self.mainEditor.selectionChanged, AUC) # Cheat Sheet self.cheatSheet.setModels() # Debug self.mdlFlatData.setVerticalHeaderLabels(["Infos générales", "Summary"]) self.tblDebugFlatData.setModel(self.mdlFlatData) self.tblDebugPersos.setModel(self.mdlPersos) self.tblDebugPersosInfos.setModel(self.mdlPersos) self.tblDebugPersos.selectionModel().currentChanged.connect( lambda: self.tblDebugPersosInfos.setRootIndex(self.mdlPersos.index( self.tblDebugPersos.selectionModel().currentIndex().row(), Perso.name.value)), AUC) self.tblDebugPlots.setModel(self.mdlPlots) self.tblDebugPlotsPersos.setModel(self.mdlPlots) self.tblDebugSubPlots.setModel(self.mdlPlots) self.tblDebugPlots.selectionModel().currentChanged.connect( lambda: self.tblDebugPlotsPersos.setRootIndex(self.mdlPlots.index( self.tblDebugPlots.selectionModel().currentIndex().row(), Plot.persos.value)), AUC) self.tblDebugPlots.selectionModel().currentChanged.connect( lambda: self.tblDebugSubPlots.setRootIndex(self.mdlPlots.index( self.tblDebugPlots.selectionModel().currentIndex().row(), Plot.subplots.value)), AUC) self.treeDebugWorld.setModel(self.mdlWorld) self.treeDebugOutline.setModel(self.mdlOutline) self.lstDebugLabels.setModel(self.mdlLabels) self.lstDebugStatus.setModel(self.mdlStatus) ############################################################################### # GENERAL AKA UNSORTED ############################################################################### def clickCycle(self, i): if i == 0: # step 2 - paragraph summary self.tabMain.setCurrentIndex(self.TabSummary) self.tabSummary.setCurrentIndex(1) if i == 1: # step 3 - characters summary self.tabMain.setCurrentIndex(self.TabPersos) self.tabPersos.setCurrentIndex(0) if i == 2: # step 4 - page summary self.tabMain.setCurrentIndex(self.TabSummary) self.tabSummary.setCurrentIndex(2) if i == 3: # step 5 - characters description self.tabMain.setCurrentIndex(self.TabPersos) self.tabPersos.setCurrentIndex(1) if i == 4: # step 6 - four page synopsis self.tabMain.setCurrentIndex(self.TabSummary) self.tabSummary.setCurrentIndex(3) if i == 5: # step 7 - full character charts self.tabMain.setCurrentIndex(self.TabPersos) self.tabPersos.setCurrentIndex(2) if i == 6: # step 8 - scene list self.tabMain.setCurrentIndex(self.TabPlots) def wordCount(self, i): src = { 0: self.txtSummarySentence, 1: self.txtSummaryPara, 2: self.txtSummaryPage, 3: self.txtSummaryFull }[i] lbl = { 0: self.lblSummaryWCSentence, 1: self.lblSummaryWCPara, 2: self.lblSummaryWCPage, 3: self.lblSummaryWCFull }[i] wc = wordCount(src.toPlainText()) if i in [2, 3]: pages = self.tr(" (~{} pages)").format(int(wc / 25) / 10.) else: pages = "" lbl.setText(self.tr("Words: {}{}").format(wc, pages)) def setupMoreUi(self): # Tool bar on the right self.toolbar = collapsibleDockWidgets(Qt.RightDockWidgetArea, self) self.toolbar.addCustomWidget(self.tr("Book summary"), self.grpPlotSummary, self.TabPlots) self.toolbar.addCustomWidget(self.tr("Project tree"), self.treeRedacWidget, self.TabRedac) self.toolbar.addCustomWidget(self.tr("Metadata"), self.redacMetadata, self.TabRedac) self.toolbar.addCustomWidget(self.tr("Story line"), self.storylineView, self.TabRedac) if self._toolbarState: self.toolbar.restoreState(self._toolbarState) # Custom "tab" bar on the left self.lstTabs.setIconSize(QSize(48, 48)) for i in range(self.tabMain.count()): icons = ["general-128px.png", "summary-128px.png", "characters-128px.png", "plot-128px.png", "world-128px.png", "outline-128px.png", "redaction-128px.png", "" ] self.tabMain.setTabIcon(i, QIcon(appPath("icons/Custom/Tabs/{}".format(icons[i])))) item = QListWidgetItem(self.tabMain.tabIcon(i), self.tabMain.tabText(i)) item.setSizeHint(QSize(item.sizeHint().width(), 64)) item.setTextAlignment(Qt.AlignCenter) self.lstTabs.addItem(item) self.tabMain.tabBar().hide() self.lstTabs.currentRowChanged.connect(self.tabMain.setCurrentIndex) self.tabMain.currentChanged.connect(self.lstTabs.setCurrentRow) # Splitters self.splitterPersos.setStretchFactor(0, 25) self.splitterPersos.setStretchFactor(1, 75) self.splitterPlot.setStretchFactor(0, 20) self.splitterPlot.setStretchFactor(1, 60) self.splitterPlot.setStretchFactor(2, 30) self.splitterWorld.setStretchFactor(0, 25) self.splitterWorld.setStretchFactor(1, 75) self.splitterOutlineH.setStretchFactor(0, 25) self.splitterOutlineH.setStretchFactor(1, 75) self.splitterOutlineV.setStretchFactor(0, 75) self.splitterOutlineV.setStretchFactor(1, 25) self.splitterRedacV.setStretchFactor(0, 75) self.splitterRedacV.setStretchFactor(1, 25) self.splitterRedacH.setStretchFactor(0, 30) self.splitterRedacH.setStretchFactor(1, 40) self.splitterRedacH.setStretchFactor(2, 30) # QFormLayout stretch for w in [self.txtWorldDescription, self.txtWorldPassion, self.txtWorldConflict]: s = w.sizePolicy() s.setVerticalStretch(1) w.setSizePolicy(s) # Help box references = [ (self.lytTabOverview, self.tr("Enter infos about your book, and yourself."), 0), (self.lytSituation, self.tr( """The basic situation, in the form of a 'What if...?' question. Ex: 'What if the most dangerous evil wizard could wasn't abled to kill a baby?' (Harry Potter)"""), 1), (self.lytSummary, self.tr( """Take time to think about a one sentence (~50 words) summary of your book. Then expand it to a paragraph, then to a page, then to a full summary."""), 1), (self.lytTabPersos, self.tr("Create your characters."), 0), (self.lytTabPlot, self.tr("Develop plots."), 0), (self.lytTabOutline, self.tr("Create the outline of your masterpiece."), 0), (self.lytTabRedac, self.tr("Write."), 0), (self.lytTabDebug, self.tr("Debug infos. Sometimes useful."), 0) ] for widget, text, pos in references: label = helpLabel(text, self) self.actShowHelp.toggled.connect(label.setVisible, AUC) widget.layout().insertWidget(pos, label) self.actShowHelp.setChecked(False) # Spellcheck if enchant: self.menuDict = QMenu(self.tr("Dictionary")) self.menuDictGroup = QActionGroup(self) self.updateMenuDict() self.menuTools.addMenu(self.menuDict) self.actSpellcheck.toggled.connect(self.toggleSpellcheck, AUC) self.dictChanged.connect(self.mainEditor.setDict, AUC) self.dictChanged.connect(self.redacMetadata.setDict, AUC) self.dictChanged.connect(self.outlineItemEditor.setDict, AUC) else: # No Spell check support self.actSpellcheck.setVisible(False) a = QAction(self.tr("Install PyEnchant to use spellcheck"), self) a.setIcon(self.style().standardIcon(QStyle.SP_MessageBoxWarning)) a.triggered.connect(self.openPyEnchantWebPage, AUC) self.menuTools.addAction(a) ############################################################################### # SPELLCHECK ############################################################################### def updateMenuDict(self): if not enchant: return self.menuDict.clear() for i in enchant.list_dicts(): a = QAction(str(i[0]), self) a.setCheckable(True) if settings.dict is None: settings.dict = enchant.get_default_language() if str(i[0]) == settings.dict: a.setChecked(True) a.triggered.connect(self.setDictionary, AUC) self.menuDictGroup.addAction(a) self.menuDict.addAction(a) def setDictionary(self): if not enchant: return for i in self.menuDictGroup.actions(): if i.isChecked(): # self.dictChanged.emit(i.text().replace("&", "")) settings.dict = i.text().replace("&", "") # Find all textEditView from self, and toggle spellcheck for w in self.findChildren(textEditView, QRegExp(".*"), Qt.FindChildrenRecursively): w.setDict(settings.dict) def openPyEnchantWebPage(self): from PyQt5.QtGui import QDesktopServices QDesktopServices.openUrl(QUrl("http://pythonhosted.org/pyenchant/")) def toggleSpellcheck(self, val): settings.spellcheck = val # Find all textEditView from self, and toggle spellcheck for w in self.findChildren(textEditView, QRegExp(".*"), Qt.FindChildrenRecursively): w.toggleSpellcheck(val) ############################################################################### # SETTINGS ############################################################################### def settingsLabel(self): self.settingsWindow(3) def settingsStatus(self): self.settingsWindow(4) def settingsWindow(self, tab=None): self.sw = settingsWindow(self) self.sw.hide() self.sw.setWindowModality(Qt.ApplicationModal) self.sw.setWindowFlags(Qt.Dialog) r = self.sw.geometry() r2 = self.geometry() self.sw.move(r2.center() - r.center()) if tab: self.sw.setTab(tab) self.sw.show() ############################################################################### # TOOLS ############################################################################### def frequencyAnalyzer(self): self.fw = frequencyAnalyzer(self) self.fw.show() ############################################################################### # VIEW MENU ############################################################################### def generateViewMenu(self): values = [ (self.tr("Nothing"), "Nothing"), (self.tr("POV"), "POV"), (self.tr("Label"), "Label"), (self.tr("Progress"), "Progress"), (self.tr("Compile"), "Compile"), ] menus = [ (self.tr("Tree"), "Tree"), (self.tr("Index cards"), "Cork"), (self.tr("Outline"), "Outline") ] submenus = { "Tree": [ (self.tr("Icon color"), "Icon"), (self.tr("Text color"), "Text"), (self.tr("Background color"), "Background"), ], "Cork": [ (self.tr("Icon"), "Icon"), (self.tr("Text"), "Text"), (self.tr("Background"), "Background"), (self.tr("Border"), "Border"), (self.tr("Corner"), "Corner"), ], "Outline": [ (self.tr("Icon color"), "Icon"), (self.tr("Text color"), "Text"), (self.tr("Background color"), "Background"), ], } self.menuView.clear() # print("Generating menus with", settings.viewSettings) for mnu, mnud in menus: m = QMenu(mnu, self.menuView) for s, sd in submenus[mnud]: m2 = QMenu(s, m) agp = QActionGroup(m2) for v, vd in values: a = QAction(v, m) a.setCheckable(True) a.setData("{},{},{}".format(mnud, sd, vd)) if settings.viewSettings[mnud][sd] == vd: a.setChecked(True) a.triggered.connect(self.setViewSettingsAction, AUC) agp.addAction(a) m2.addAction(a) m.addMenu(m2) self.menuView.addMenu(m) def setViewSettingsAction(self): action = self.sender() item, part, element = action.data().split(",") self.setViewSettings(item, part, element) def setViewSettings(self, item, part, element): settings.viewSettings[item][part] = element if item == "Cork": self.mainEditor.updateCorkView() if item == "Outline": self.mainEditor.updateTreeView() self.treeOutlineOutline.viewport().update() if item == "Tree": self.treeRedacOutline.viewport().update() ############################################################################### # COMPILE ############################################################################### def doCompile(self): self.compileDialog = compileDialog() self.compileDialog.show()
class MainWindow(QMainWindow, WindowSystemController): def __init__(self, parent): super(MainWindow, self).__init__() self.init_ui() cfg.core.subscriber.subscribe_update_func_to_domain( self._clear_project, "core.project.close") cfg.core.subscriber.subscribe_update_func_to_domain( self._enable_actions, "core.project.load") cfg.core.subscriber.subscribe_update_func_to_domain( self.set_project_is_saved, "core.project.saved") cfg.core.subscriber.subscribe_update_func_to_domain( self.set_project_is_not_saved, "core.project.notsaved") def init_ui(self): self.ui = Ui_MainWindow() self.ui.setupUi(self) widget = QWidget() layout = QHBoxLayout() self.stackWidget = SubWindowStack(self) self.side_bar = SideBar(self) # self.side_bar.detach_signal.connect(self.detach_window) layout.addWidget(self.side_bar) layout.addWidget(self.stackWidget) # layout.addWidget(self.sub_window) widget.setLayout(layout) self.setCentralWidget(widget) # window system : self.window_system_parent_widget = self.stackWidget self.side_bar.window_system_controller = self # sub_windows : self._sub_window_action_group = QActionGroup(self) # write window self.write_sub_window = WritePanel( parent=self, parent_window_system_controller=self) self.attach_sub_window(self.write_sub_window) self.ui.actionWrite.setProperty( "sub_window_object_name", "write_sub_window") self.add_action_to_window_system(self.ui.actionWrite) self._sub_window_action_group.addAction(self.ui.actionWrite) # binder window self.binder_sub_window = BinderPanel( parent=self, parent_window_system_controller=self) self.attach_sub_window(self.binder_sub_window) self.ui.actionBinder.setProperty( "sub_window_object_name", "binder_sub_window") self.add_action_to_window_system(self.ui.actionBinder) self._sub_window_action_group.addAction(self.ui.actionBinder) self.ui.actionWrite.trigger() # menu bar actions self.ui.actionOpen_test_project.triggered.connect( self.launch_open_test_project) self.ui.actionPreferences.triggered.connect(self.launch_preferences) self.ui.actionStart_window.triggered.connect(self.launch_start_window) self.ui.actionSave.triggered.connect(cfg.core.project.save) self.ui.actionSave_as.triggered.connect(self.launch_save_as_dialog) self.ui.actionOpen.triggered.connect(self.launch_open_dialog) self.ui.actionClose_project.triggered.connect(self.launch_close_dialog) self.ui.actionExit.triggered.connect(self.launch_exit_dialog) self._enable_actions(False) @pyqtSlot() def launch_open_test_project(self): if cfg.core.project.is_open() == True: if self.launch_close_dialog() == QMessageBox.Cancel: return cfg.core.project.open_test_project() self.setWindowTitle("Plume Creator - TEST") @pyqtSlot() def launch_preferences(self): pref = Preferences(self) pref.exec_() @pyqtSlot() def launch_start_window(self): pref = StartWindow(self) pref.exec_() @pyqtSlot() def launch_save_as_dialog(self): working_directory = QDir.homePath() fileName, selectedFilter = QFileDialog.getSaveFileName( self, _("Save as"), working_directory, _("Databases (*.sqlite *.plume);;All files (*)"), _(".sqlite")) if fileName is None: return cfg.core.project.save_as(fileName, selectedFilter) @pyqtSlot() def launch_open_dialog(self): working_directory = QDir.homePath() fileName, selectedFilter = QFileDialog.getOpenFileName( self, _("Open"), working_directory, _("Databases (*.sqlite *.plume);;All files (*)"), _(".sqlite")) if fileName is None: return if cfg.core.project.is_open() == True: if self.launch_close_dialog() == QMessageBox.Cancel: return cfg.core.project.open(fileName) self.setWindowTitle("Plume Creator - " + fileName) def launch_close_dialog(self): if cfg.core.project.is_open() == False: return result = QMessageBox.question( self, _("Close the current project"), _("The last changes are not yet saved. Do you really want to close the current project ?"), QMessageBox.StandardButtons( QMessageBox.Cancel | QMessageBox.Discard | QMessageBox.Save), QMessageBox.Cancel) if result == QMessageBox.Cancel: return QMessageBox.Cancel elif result == QMessageBox.Discard: cfg.core.project.close_project() elif result == QMessageBox.Save: cfg.core.project.save() cfg.core.project.close_project() def launch_exit_dialog(self): if cfg.core.project.is_open() == False: QApplication.quit() result = self.launch_close_dialog() if result == QMessageBox.Cancel: return QMessageBox.Cancel QApplication.quit() def set_project_is_saved(self): self.ui.actionSave.setEnabled(False) def set_project_is_not_saved(self): self.ui.actionSave.setEnabled(True) def _clear_project(self): self.setWindowTitle("Plume Creator") self._enable_actions(False) def _enable_actions(self, value=True): self.ui.actionSave_as.setEnabled(value) self.ui.actionSave.setEnabled(value) self.ui.actionClose_project.setEnabled(value) self.ui.actionImport.setEnabled(value) self.ui.actionExport.setEnabled(value) self.ui.actionPrint.setEnabled(value) def closeEvent(self, event): result = self.launch_exit_dialog() if result == QMessageBox.Cancel: event.ignore() else: event.accept()
def _get_menu(self): # main menu menu = QMenu() main_menu_action_group = QActionGroup(menu) main_menu_action_group.setObjectName("main") # character menu map_action = QAction(menu) map_action.setText("Toggle Map") main_menu_action_group.addAction(map_action) separator = QAction(menu) separator.setSeparator(True) main_menu_action_group.addAction(separator) characters_action = QAction(menu) characters_action.setText("Switch Characters") main_menu_action_group.addAction(characters_action) separator = QAction(menu) separator.setSeparator(True) main_menu_action_group.addAction(separator) settings_action = QAction(menu) settings_action.setText("Settings") main_menu_action_group.addAction(settings_action) quit_action = QAction(menu) quit_action.setText("Quit") main_menu_action_group.addAction(quit_action) menu.addActions(main_menu_action_group.actions()) menu.triggered[QAction].connect(self._menu_actions) return menu
class SpreadSheet(QMainWindow): dateFormats = ["dd/M/yyyy", "yyyy/M/dd", "dd.MM.yyyy"] currentDateFormat = dateFormats[0] def __init__(self, rows, cols, parent = None): super(SpreadSheet, self).__init__(parent) self.toolBar = QToolBar() self.addToolBar(self.toolBar) self.formulaInput = QLineEdit() self.cellLabel = QLabel(self.toolBar) self.cellLabel.setMinimumSize(80, 0) self.toolBar.addWidget(self.cellLabel) self.toolBar.addWidget(self.formulaInput) self.table = QTableWidget(rows, cols, self) for c in range(cols): character = chr(ord('A') + c) self.table.setHorizontalHeaderItem(c, QTableWidgetItem(character)) self.table.setItemPrototype(self.table.item(rows - 1, cols - 1)) self.table.setItemDelegate(SpreadSheetDelegate(self)) self.createActions() self.updateColor(0) self.setupMenuBar() self.setupContents() self.setupContextMenu() self.setCentralWidget(self.table) self.statusBar() self.table.currentItemChanged.connect(self.updateStatus) self.table.currentItemChanged.connect(self.updateColor) self.table.currentItemChanged.connect(self.updateLineEdit) self.table.itemChanged.connect(self.updateStatus) self.formulaInput.returnPressed.connect(self.returnPressed) self.table.itemChanged.connect(self.updateLineEdit) self.setWindowTitle("Spreadsheet") def createActions(self): self.cell_sumAction = QAction("Sum", self) self.cell_sumAction.triggered.connect(self.actionSum) self.cell_addAction = QAction("&Add", self) self.cell_addAction.setShortcut(Qt.CTRL | Qt.Key_Plus) self.cell_addAction.triggered.connect(self.actionAdd) self.cell_subAction = QAction("&Subtract", self) self.cell_subAction.setShortcut(Qt.CTRL | Qt.Key_Minus) self.cell_subAction.triggered.connect(self.actionSubtract) self.cell_mulAction = QAction("&Multiply", self) self.cell_mulAction.setShortcut(Qt.CTRL | Qt.Key_multiply) self.cell_mulAction.triggered.connect(self.actionMultiply) self.cell_divAction = QAction("&Divide", self) self.cell_divAction.setShortcut(Qt.CTRL | Qt.Key_division) self.cell_divAction.triggered.connect(self.actionDivide) self.fontAction = QAction("Font...", self) self.fontAction.setShortcut(Qt.CTRL | Qt.Key_F) self.fontAction.triggered.connect(self.selectFont) self.colorAction = QAction(QIcon(QPixmap(16, 16)), "Background &Color...", self) self.colorAction.triggered.connect(self.selectColor) self.clearAction = QAction("Clear", self) self.clearAction.setShortcut(Qt.Key_Delete) self.clearAction.triggered.connect(self.clear) self.aboutSpreadSheet = QAction("About Spreadsheet", self) self.aboutSpreadSheet.triggered.connect(self.showAbout) self.exitAction = QAction("E&xit", self) self.exitAction.setShortcut(QKeySequence.Quit) self.exitAction.triggered.connect(QApplication.instance().quit) self.printAction = QAction("&Print", self) self.printAction.setShortcut(QKeySequence.Print) self.printAction.triggered.connect(self.print_) self.firstSeparator = QAction(self) self.firstSeparator.setSeparator(True) self.secondSeparator = QAction(self) self.secondSeparator.setSeparator(True) def setupMenuBar(self): self.fileMenu = self.menuBar().addMenu("&File") self.dateFormatMenu = self.fileMenu.addMenu("&Date format") self.dateFormatGroup = QActionGroup(self) for f in self.dateFormats: action = QAction(f, self, checkable=True, triggered=self.changeDateFormat) self.dateFormatGroup.addAction(action) self.dateFormatMenu.addAction(action) if f == self.currentDateFormat: action.setChecked(True) self.fileMenu.addAction(self.printAction) self.fileMenu.addAction(self.exitAction) self.cellMenu = self.menuBar().addMenu("&Cell") self.cellMenu.addAction(self.cell_addAction) self.cellMenu.addAction(self.cell_subAction) self.cellMenu.addAction(self.cell_mulAction) self.cellMenu.addAction(self.cell_divAction) self.cellMenu.addAction(self.cell_sumAction) self.cellMenu.addSeparator() self.cellMenu.addAction(self.colorAction) self.cellMenu.addAction(self.fontAction) self.menuBar().addSeparator() self.aboutMenu = self.menuBar().addMenu("&Help") self.aboutMenu.addAction(self.aboutSpreadSheet) def changeDateFormat(self): action = self.sender() oldFormat = self.currentDateFormat newFormat = self.currentDateFormat = action.text() for row in range(self.table.rowCount()): item = self.table.item(row, 1) date = QDate.fromString(item.text(), oldFormat) item.setText(date.toString(newFormat)) def updateStatus(self, item): if item and item == self.table.currentItem(): self.statusBar().showMessage(item.data(Qt.StatusTipRole), 1000) self.cellLabel.setText("Cell: (%s)" % encode_pos(self.table.row(item), self.table.column(item))) def updateColor(self, item): pixmap = QPixmap(16, 16) color = QColor() if item: color = item.backgroundColor() if not color.isValid(): color = self.palette().base().color() painter = QPainter(pixmap) painter.fillRect(0, 0, 16, 16, color) lighter = color.lighter() painter.setPen(lighter) # light frame painter.drawPolyline(QPoint(0, 15), QPoint(0, 0), QPoint(15, 0)) painter.setPen(color.darker()) # dark frame painter.drawPolyline(QPoint(1, 15), QPoint(15, 15), QPoint(15, 1)) painter.end() self.colorAction.setIcon(QIcon(pixmap)) def updateLineEdit(self, item): if item != self.table.currentItem(): return if item: self.formulaInput.setText(item.data(Qt.EditRole)) else: self.formulaInput.clear() def returnPressed(self): text = self.formulaInput.text() row = self.table.currentRow() col = self.table.currentColumn() item = self.table.item(row, col) if not item: self.table.setItem(row, col, SpreadSheetItem(text)) else: item.setData(Qt.EditRole, text) self.table.viewport().update() def selectColor(self): item = self.table.currentItem() color = item and QColor(item.background()) or self.table.palette().base().color() color = QColorDialog.getColor(color, self) if not color.isValid(): return selected = self.table.selectedItems() if not selected: return for i in selected: i and i.setBackground(color) self.updateColor(self.table.currentItem()) def selectFont(self): selected = self.table.selectedItems() if not selected: return font, ok = QFontDialog.getFont(self.font(), self) if not ok: return for i in selected: i and i.setFont(font) def runInputDialog(self, title, c1Text, c2Text, opText, outText, cell1, cell2, outCell): rows = [] cols = [] for r in range(self.table.rowCount()): rows.append(str(r + 1)) for c in range(self.table.columnCount()): cols.append(chr(ord('A') + c)) addDialog = QDialog(self) addDialog.setWindowTitle(title) group = QGroupBox(title, addDialog) group.setMinimumSize(250, 100) cell1Label = QLabel(c1Text, group) cell1RowInput = QComboBox(group) c1Row, c1Col = decode_pos(cell1) cell1RowInput.addItems(rows) cell1RowInput.setCurrentIndex(c1Row) cell1ColInput = QComboBox(group) cell1ColInput.addItems(cols) cell1ColInput.setCurrentIndex(c1Col) operatorLabel = QLabel(opText, group) operatorLabel.setAlignment(Qt.AlignHCenter) cell2Label = QLabel(c2Text, group) cell2RowInput = QComboBox(group) c2Row, c2Col = decode_pos(cell2) cell2RowInput.addItems(rows) cell2RowInput.setCurrentIndex(c2Row) cell2ColInput = QComboBox(group) cell2ColInput.addItems(cols) cell2ColInput.setCurrentIndex(c2Col) equalsLabel = QLabel("=", group) equalsLabel.setAlignment(Qt.AlignHCenter) outLabel = QLabel(outText, group) outRowInput = QComboBox(group) outRow, outCol = decode_pos(outCell) outRowInput.addItems(rows) outRowInput.setCurrentIndex(outRow) outColInput = QComboBox(group) outColInput.addItems(cols) outColInput.setCurrentIndex(outCol) cancelButton = QPushButton("Cancel", addDialog) cancelButton.clicked.connect(addDialog.reject) okButton = QPushButton("OK", addDialog) okButton.setDefault(True) okButton.clicked.connect(addDialog.accept) buttonsLayout = QHBoxLayout() buttonsLayout.addStretch(1) buttonsLayout.addWidget(okButton) buttonsLayout.addSpacing(10) buttonsLayout.addWidget(cancelButton) dialogLayout = QVBoxLayout(addDialog) dialogLayout.addWidget(group) dialogLayout.addStretch(1) dialogLayout.addItem(buttonsLayout) cell1Layout = QHBoxLayout() cell1Layout.addWidget(cell1Label) cell1Layout.addSpacing(10) cell1Layout.addWidget(cell1ColInput) cell1Layout.addSpacing(10) cell1Layout.addWidget(cell1RowInput) cell2Layout = QHBoxLayout() cell2Layout.addWidget(cell2Label) cell2Layout.addSpacing(10) cell2Layout.addWidget(cell2ColInput) cell2Layout.addSpacing(10) cell2Layout.addWidget(cell2RowInput) outLayout = QHBoxLayout() outLayout.addWidget(outLabel) outLayout.addSpacing(10) outLayout.addWidget(outColInput) outLayout.addSpacing(10) outLayout.addWidget(outRowInput) vLayout = QVBoxLayout(group) vLayout.addItem(cell1Layout) vLayout.addWidget(operatorLabel) vLayout.addItem(cell2Layout) vLayout.addWidget(equalsLabel) vLayout.addStretch(1) vLayout.addItem(outLayout) if addDialog.exec_(): cell1 = cell1ColInput.currentText() + cell1RowInput.currentText() cell2 = cell2ColInput.currentText() + cell2RowInput.currentText() outCell = outColInput.currentText() + outRowInput.currentText() return True, cell1, cell2, outCell return False, None, None, None def actionSum(self): row_first = 0 row_last = 0 row_cur = 0 col_first = 0 col_last = 0 col_cur = 0 selected = self.table.selectedItems() if selected: first = selected[0] last = selected[-1] row_first = self.table.row(first) row_last = self.table.row(last) col_first = self.table.column(first) col_last = self.table.column(last) current = self.table.currentItem() if current: row_cur = self.table.row(current) col_cur = self.table.column(current) cell1 = encode_pos(row_first, col_first) cell2 = encode_pos(row_last, col_last) out = encode_pos(row_cur, col_cur) ok, cell1, cell2, out = self.runInputDialog("Sum cells", "First cell:", "Last cell:", u"\N{GREEK CAPITAL LETTER SIGMA}", "Output to:", cell1, cell2, out) if ok: row, col = decode_pos(out) self.table.item(row, col).setText("sum %s %s" % (cell1, cell2)) def actionMath_helper(self, title, op): cell1 = "C1" cell2 = "C2" out = "C3" current = self.table.currentItem() if current: out = encode_pos(self.table.currentRow(), self.table.currentColumn()) ok, cell1, cell2, out = self.runInputDialog(title, "Cell 1", "Cell 2", op, "Output to:", cell1, cell2, out) if ok: row, col = decode_pos(out) self.table.item(row, col).setText("%s %s %s" % (op, cell1, cell2)) def actionAdd(self): self.actionMath_helper("Addition", "+") def actionSubtract(self): self.actionMath_helper("Subtraction", "-") def actionMultiply(self): self.actionMath_helper("Multiplication", "*") def actionDivide(self): self.actionMath_helper("Division", "/") def clear(self): for i in self.table.selectedItems(): i.setText("") def setupContextMenu(self): self.addAction(self.cell_addAction) self.addAction(self.cell_subAction) self.addAction(self.cell_mulAction) self.addAction(self.cell_divAction) self.addAction(self.cell_sumAction) self.addAction(self.firstSeparator) self.addAction(self.colorAction) self.addAction(self.fontAction) self.addAction(self.secondSeparator) self.addAction(self.clearAction) self.setContextMenuPolicy(Qt.ActionsContextMenu) def setupContents(self): titleBackground = QColor(Qt.lightGray) titleFont = self.table.font() titleFont.setBold(True) # column 0 self.table.setItem(0, 0, SpreadSheetItem("Item")) self.table.item(0, 0).setBackground(titleBackground) self.table.item(0, 0).setToolTip("This column shows the purchased item/service") self.table.item(0, 0).setFont(titleFont) self.table.setItem(1, 0, SpreadSheetItem("AirportBus")) self.table.setItem(2, 0, SpreadSheetItem("Flight (Munich)")) self.table.setItem(3, 0, SpreadSheetItem("Lunch")) self.table.setItem(4, 0, SpreadSheetItem("Flight (LA)")) self.table.setItem(5, 0, SpreadSheetItem("Taxi")) self.table.setItem(6, 0, SpreadSheetItem("Dinner")) self.table.setItem(7, 0, SpreadSheetItem("Hotel")) self.table.setItem(8, 0, SpreadSheetItem("Flight (Oslo)")) self.table.setItem(9, 0, SpreadSheetItem("Total:")) self.table.item(9, 0).setFont(titleFont) self.table.item(9, 0).setBackground(Qt.lightGray) # column 1 self.table.setItem(0, 1, SpreadSheetItem("Date")) self.table.item(0, 1).setBackground(titleBackground) self.table.item(0, 1).setToolTip("This column shows the purchase date, double click to change") self.table.item(0, 1).setFont(titleFont) self.table.setItem(1, 1, SpreadSheetItem("15/6/2006")) self.table.setItem(2, 1, SpreadSheetItem("15/6/2006")) self.table.setItem(3, 1, SpreadSheetItem("15/6/2006")) self.table.setItem(4, 1, SpreadSheetItem("21/5/2006")) self.table.setItem(5, 1, SpreadSheetItem("16/6/2006")) self.table.setItem(6, 1, SpreadSheetItem("16/6/2006")) self.table.setItem(7, 1, SpreadSheetItem("16/6/2006")) self.table.setItem(8, 1, SpreadSheetItem("18/6/2006")) self.table.setItem(9, 1, SpreadSheetItem()) self.table.item(9, 1).setBackground(Qt.lightGray) # column 2 self.table.setItem(0, 2, SpreadSheetItem("Price")) self.table.item(0, 2).setBackground(titleBackground) self.table.item(0, 2).setToolTip("This column shows the price of the purchase") self.table.item(0, 2).setFont(titleFont) self.table.setItem(1, 2, SpreadSheetItem("150")) self.table.setItem(2, 2, SpreadSheetItem("2350")) self.table.setItem(3, 2, SpreadSheetItem("-14")) self.table.setItem(4, 2, SpreadSheetItem("980")) self.table.setItem(5, 2, SpreadSheetItem("5")) self.table.setItem(6, 2, SpreadSheetItem("120")) self.table.setItem(7, 2, SpreadSheetItem("300")) self.table.setItem(8, 2, SpreadSheetItem("1240")) self.table.setItem(9, 2, SpreadSheetItem()) self.table.item(9, 2).setBackground(Qt.lightGray) # column 3 self.table.setItem(0, 3, SpreadSheetItem("Currency")) self.table.item(0, 3).setBackgroundColor(titleBackground) self.table.item(0, 3).setToolTip("This column shows the currency") self.table.item(0, 3).setFont(titleFont) self.table.setItem(1, 3, SpreadSheetItem("NOK")) self.table.setItem(2, 3, SpreadSheetItem("NOK")) self.table.setItem(3, 3, SpreadSheetItem("EUR")) self.table.setItem(4, 3, SpreadSheetItem("EUR")) self.table.setItem(5, 3, SpreadSheetItem("USD")) self.table.setItem(6, 3, SpreadSheetItem("USD")) self.table.setItem(7, 3, SpreadSheetItem("USD")) self.table.setItem(8, 3, SpreadSheetItem("USD")) self.table.setItem(9, 3, SpreadSheetItem()) self.table.item(9,3).setBackground(Qt.lightGray) # column 4 self.table.setItem(0, 4, SpreadSheetItem("Ex. Rate")) self.table.item(0, 4).setBackground(titleBackground) self.table.item(0, 4).setToolTip("This column shows the exchange rate to NOK") self.table.item(0, 4).setFont(titleFont) self.table.setItem(1, 4, SpreadSheetItem("1")) self.table.setItem(2, 4, SpreadSheetItem("1")) self.table.setItem(3, 4, SpreadSheetItem("8")) self.table.setItem(4, 4, SpreadSheetItem("8")) self.table.setItem(5, 4, SpreadSheetItem("7")) self.table.setItem(6, 4, SpreadSheetItem("7")) self.table.setItem(7, 4, SpreadSheetItem("7")) self.table.setItem(8, 4, SpreadSheetItem("7")) self.table.setItem(9, 4, SpreadSheetItem()) self.table.item(9,4).setBackground(Qt.lightGray) # column 5 self.table.setItem(0, 5, SpreadSheetItem("NOK")) self.table.item(0, 5).setBackground(titleBackground) self.table.item(0, 5).setToolTip("This column shows the expenses in NOK") self.table.item(0, 5).setFont(titleFont) self.table.setItem(1, 5, SpreadSheetItem("* C2 E2")) self.table.setItem(2, 5, SpreadSheetItem("* C3 E3")) self.table.setItem(3, 5, SpreadSheetItem("* C4 E4")) self.table.setItem(4, 5, SpreadSheetItem("* C5 E5")) self.table.setItem(5, 5, SpreadSheetItem("* C6 E6")) self.table.setItem(6, 5, SpreadSheetItem("* C7 E7")) self.table.setItem(7, 5, SpreadSheetItem("* C8 E8")) self.table.setItem(8, 5, SpreadSheetItem("* C9 E9")) self.table.setItem(9, 5, SpreadSheetItem("sum F2 F9")) self.table.item(9,5).setBackground(Qt.lightGray) def showAbout(self): QMessageBox.about(self, "About Spreadsheet", """ <HTML> <p><b>This demo shows use of <c>QTableWidget</c> with custom handling for individual cells.</b></p> <p>Using a customized table item we make it possible to have dynamic output in different cells. The content that is implemented for this particular demo is: <ul> <li>Adding two cells.</li> <li>Subtracting one cell from another.</li> <li>Multiplying two cells.</li> <li>Dividing one cell with another.</li> <li>Summing the contents of an arbitrary number of cells.</li> </HTML> """) def print_(self): printer = QPrinter(QPrinter.ScreenResolution) dlg = QPrintPreviewDialog(printer) view = PrintView() view.setModel(self.table.model()) dlg.paintRequested.connect(view.print_) dlg.exec_()
def device_discovery(self, devs): """Called when new devices have been added""" for menu in self._all_role_menus: role_menu = menu["rolemenu"] mux_menu = menu["muxmenu"] dev_group = QActionGroup(role_menu, exclusive=True) for d in devs: dev_node = QAction(d.name, role_menu, checkable=True, enabled=True) role_menu.addAction(dev_node) dev_group.addAction(dev_node) dev_node.toggled.connect(self._inputdevice_selected) map_node = None if d.supports_mapping: map_node = QMenu(" Input map", role_menu, enabled=False) map_group = QActionGroup(role_menu, exclusive=True) # Connect device node to map node for easy # enabling/disabling when selection changes and device # to easily enable it dev_node.setData((map_node, d)) for c in ConfigManager().get_list_of_configs(): node = QAction(c, map_node, checkable=True, enabled=True) node.toggled.connect(self._inputconfig_selected) map_node.addAction(node) # Connect all the map nodes back to the device # action node where we can access the raw device node.setData(dev_node) map_group.addAction(node) # If this device hasn't been found before, then # select the default mapping for it. if d not in self._available_devices: last_map = Config().get("device_config_mapping") if d.name in last_map and last_map[d.name] == c: node.setChecked(True) role_menu.addMenu(map_node) dev_node.setData((map_node, d, mux_menu)) # Update the list of what devices we found # to avoid selecting default mapping for all devices when # a new one is inserted self._available_devices = () for d in devs: self._available_devices += (d,) # Only enable MUX nodes if we have enough devies to cover # the roles for mux_node in self._all_mux_nodes: (mux, sub_nodes) = mux_node.data() if len(mux.supported_roles()) <= len(self._available_devices): mux_node.setEnabled(True) # TODO: Currently only supports selecting default mux if self._all_mux_nodes[0].isEnabled(): self._all_mux_nodes[0].setChecked(True) # If the previous length of the available devies was 0, then select # the default on. If that's not available then select the first # on in the list. # TODO: This will only work for the "Normal" mux so this will be # selected by default if Config().get("input_device") in [d.name for d in devs]: for dev_menu in self._all_role_menus[0]["rolemenu"].actions(): if dev_menu.text() == Config().get("input_device"): dev_menu.setChecked(True) else: # Select the first device in the first mux (will always be "Normal" # mux) self._all_role_menus[0]["rolemenu"].actions()[0].setChecked(True) logger.info("Select first device") self._update_input_device_footer()
class ReTextWindow(QMainWindow): def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.resize(950, 700) screenRect = QDesktopWidget().screenGeometry() if globalSettings.windowGeometry: self.restoreGeometry(globalSettings.windowGeometry) else: self.move((screenRect.width()-self.width())/2, (screenRect.height()-self.height())/2) if not screenRect.contains(self.geometry()): self.showMaximized() if globalSettings.iconTheme: QIcon.setThemeName(globalSettings.iconTheme) if QIcon.themeName() in ('hicolor', ''): if not QFile.exists(icon_path + 'document-new.png'): QIcon.setThemeName(get_icon_theme()) if QFile.exists(icon_path+'retext.png'): self.setWindowIcon(QIcon(icon_path+'retext.png')) elif QFile.exists('/usr/share/pixmaps/retext.png'): self.setWindowIcon(QIcon('/usr/share/pixmaps/retext.png')) else: self.setWindowIcon(QIcon.fromTheme('retext', QIcon.fromTheme('accessories-text-editor'))) self.tabWidget = QTabWidget(self) self.initTabWidget() self.setCentralWidget(self.tabWidget) self.tabWidget.currentChanged.connect(self.changeIndex) self.tabWidget.tabCloseRequested.connect(self.closeTab) toolBar = QToolBar(self.tr('File toolbar'), self) self.addToolBar(Qt.TopToolBarArea, toolBar) self.editBar = QToolBar(self.tr('Edit toolbar'), self) self.addToolBar(Qt.TopToolBarArea, self.editBar) self.searchBar = QToolBar(self.tr('Search toolbar'), self) self.addToolBar(Qt.BottomToolBarArea, self.searchBar) toolBar.setVisible(not globalSettings.hideToolBar) self.editBar.setVisible(not globalSettings.hideToolBar) self.actionNew = self.act(self.tr('New'), 'document-new', self.createNew, shct=QKeySequence.New) self.actionNew.setPriority(QAction.LowPriority) self.actionOpen = self.act(self.tr('Open'), 'document-open', self.openFile, shct=QKeySequence.Open) self.actionOpen.setPriority(QAction.LowPriority) self.actionSetEncoding = self.act(self.tr('Set encoding'), trig=self.showEncodingDialog) self.actionSetEncoding.setEnabled(False) self.actionReload = self.act(self.tr('Reload'), 'view-refresh', lambda: self.currentTab.readTextFromFile()) self.actionReload.setEnabled(False) self.actionSave = self.act(self.tr('Save'), 'document-save', self.saveFile, shct=QKeySequence.Save) self.actionSave.setEnabled(False) self.actionSave.setPriority(QAction.LowPriority) self.actionSaveAs = self.act(self.tr('Save as'), 'document-save-as', self.saveFileAs, shct=QKeySequence.SaveAs) self.actionNextTab = self.act(self.tr('Next tab'), 'go-next', lambda: self.switchTab(1), shct=Qt.CTRL+Qt.Key_PageDown) self.actionPrevTab = self.act(self.tr('Previous tab'), 'go-previous', lambda: self.switchTab(-1), shct=Qt.CTRL+Qt.Key_PageUp) self.actionPrint = self.act(self.tr('Print'), 'document-print', self.printFile, shct=QKeySequence.Print) self.actionPrint.setPriority(QAction.LowPriority) self.actionPrintPreview = self.act(self.tr('Print preview'), 'document-print-preview', self.printPreview) self.actionViewHtml = self.act(self.tr('View HTML code'), 'text-html', self.viewHtml) self.actionChangeEditorFont = self.act(self.tr('Change editor font'), trig=self.changeEditorFont) self.actionChangePreviewFont = self.act(self.tr('Change preview font'), trig=self.changePreviewFont) self.actionSearch = self.act(self.tr('Find text'), 'edit-find', shct=QKeySequence.Find) self.actionSearch.setCheckable(True) self.actionSearch.triggered[bool].connect(self.searchBar.setVisible) self.searchBar.visibilityChanged.connect(self.searchBarVisibilityChanged) self.actionPreview = self.act(self.tr('Preview'), shct=Qt.CTRL+Qt.Key_E, trigbool=self.preview) if QIcon.hasThemeIcon('document-preview'): self.actionPreview.setIcon(QIcon.fromTheme('document-preview')) elif QIcon.hasThemeIcon('preview-file'): self.actionPreview.setIcon(QIcon.fromTheme('preview-file')) elif QIcon.hasThemeIcon('x-office-document'): self.actionPreview.setIcon(QIcon.fromTheme('x-office-document')) else: self.actionPreview.setIcon(QIcon(icon_path+'document-preview.png')) self.actionLivePreview = self.act(self.tr('Live preview'), shct=Qt.CTRL+Qt.Key_L, trigbool=self.enableLivePreview) menuPreview = QMenu() menuPreview.addAction(self.actionLivePreview) self.actionPreview.setMenu(menuPreview) self.actionTableMode = self.act(self.tr('Table mode'), shct=Qt.CTRL+Qt.Key_T, trigbool=lambda x: self.currentTab.editBox.enableTableMode(x)) if ReTextFakeVimHandler: self.actionFakeVimMode = self.act(self.tr('FakeVim mode'), shct=Qt.CTRL+Qt.ALT+Qt.Key_V, trigbool=self.enableFakeVimMode) if globalSettings.useFakeVim: self.actionFakeVimMode.setChecked(True) self.enableFakeVimMode(True) self.actionFullScreen = self.act(self.tr('Fullscreen mode'), 'view-fullscreen', shct=Qt.Key_F11, trigbool=self.enableFullScreen) self.actionFullScreen.setPriority(QAction.LowPriority) self.actionConfig = self.act(self.tr('Preferences'), icon='preferences-system', trig=self.openConfigDialog) self.actionConfig.setMenuRole(QAction.PreferencesRole) self.actionSaveHtml = self.act('HTML', 'text-html', self.saveFileHtml) self.actionPdf = self.act('PDF', 'application-pdf', self.savePdf) self.actionOdf = self.act('ODT', 'x-office-document', self.saveOdf) self.getExportExtensionsList() self.actionQuit = self.act(self.tr('Quit'), 'application-exit', shct=QKeySequence.Quit) self.actionQuit.setMenuRole(QAction.QuitRole) self.actionQuit.triggered.connect(self.close) self.actionUndo = self.act(self.tr('Undo'), 'edit-undo', lambda: self.currentTab.editBox.undo(), shct=QKeySequence.Undo) self.actionRedo = self.act(self.tr('Redo'), 'edit-redo', lambda: self.currentTab.editBox.redo(), shct=QKeySequence.Redo) self.actionCopy = self.act(self.tr('Copy'), 'edit-copy', lambda: self.currentTab.editBox.copy(), shct=QKeySequence.Copy) self.actionCut = self.act(self.tr('Cut'), 'edit-cut', lambda: self.currentTab.editBox.cut(), shct=QKeySequence.Cut) self.actionPaste = self.act(self.tr('Paste'), 'edit-paste', lambda: self.currentTab.editBox.paste(), shct=QKeySequence.Paste) self.actionUndo.setEnabled(False) self.actionRedo.setEnabled(False) self.actionCopy.setEnabled(False) self.actionCut.setEnabled(False) qApp = QApplication.instance() qApp.clipboard().dataChanged.connect(self.clipboardDataChanged) self.clipboardDataChanged() if enchant_available: self.actionEnableSC = self.act(self.tr('Enable'), trigbool=self.enableSpellCheck) self.actionSetLocale = self.act(self.tr('Set locale'), trig=self.changeLocale) self.actionWebKit = self.act(self.tr('Use WebKit renderer'), trigbool=self.enableWebKit) self.actionWebKit.setChecked(globalSettings.useWebKit) self.actionShow = self.act(self.tr('Show directory'), 'system-file-manager', self.showInDir) self.actionFind = self.act(self.tr('Next'), 'go-next', self.find, shct=QKeySequence.FindNext) self.actionFindPrev = self.act(self.tr('Previous'), 'go-previous', lambda: self.find(back=True), shct=QKeySequence.FindPrevious) self.actionCloseSearch = self.act(self.tr('Close'), 'window-close', lambda: self.searchBar.setVisible(False)) self.actionCloseSearch.setPriority(QAction.LowPriority) self.actionHelp = self.act(self.tr('Get help online'), 'help-contents', self.openHelp) self.aboutWindowTitle = self.tr('About ReText') self.actionAbout = self.act(self.aboutWindowTitle, 'help-about', self.aboutDialog) self.actionAbout.setMenuRole(QAction.AboutRole) self.actionAboutQt = self.act(self.tr('About Qt')) self.actionAboutQt.setMenuRole(QAction.AboutQtRole) self.actionAboutQt.triggered.connect(qApp.aboutQt) availableMarkups = markups.get_available_markups() if not availableMarkups: print('Warning: no markups are available!') self.defaultMarkup = availableMarkups[0] if availableMarkups else None if globalSettings.defaultMarkup: mc = markups.find_markup_class_by_name(globalSettings.defaultMarkup) if mc and mc.available(): self.defaultMarkup = mc if len(availableMarkups) > 1: self.chooseGroup = QActionGroup(self) markupActions = [] for markup in availableMarkups: markupAction = self.act(markup.name, trigbool=self.markupFunction(markup)) if markup == self.defaultMarkup: markupAction.setChecked(True) self.chooseGroup.addAction(markupAction) markupActions.append(markupAction) self.actionBold = self.act(self.tr('Bold'), shct=QKeySequence.Bold, trig=lambda: self.insertFormatting('bold')) self.actionItalic = self.act(self.tr('Italic'), shct=QKeySequence.Italic, trig=lambda: self.insertFormatting('italic')) self.actionUnderline = self.act(self.tr('Underline'), shct=QKeySequence.Underline, trig=lambda: self.insertFormatting('underline')) self.usefulTags = ('header', 'italic', 'bold', 'underline', 'numbering', 'bullets', 'image', 'link', 'inline code', 'code block', 'blockquote') self.usefulChars = ('deg', 'divide', 'dollar', 'hellip', 'laquo', 'larr', 'lsquo', 'mdash', 'middot', 'minus', 'nbsp', 'ndash', 'raquo', 'rarr', 'rsquo', 'times') self.formattingBox = QComboBox(self.editBar) self.formattingBox.addItem(self.tr('Formatting')) self.formattingBox.addItems(self.usefulTags) self.formattingBox.activated[str].connect(self.insertFormatting) self.symbolBox = QComboBox(self.editBar) self.symbolBox.addItem(self.tr('Symbols')) self.symbolBox.addItems(self.usefulChars) self.symbolBox.activated.connect(self.insertSymbol) self.updateStyleSheet() menubar = self.menuBar() menuFile = menubar.addMenu(self.tr('File')) menuEdit = menubar.addMenu(self.tr('Edit')) menuHelp = menubar.addMenu(self.tr('Help')) menuFile.addAction(self.actionNew) menuFile.addAction(self.actionOpen) self.menuRecentFiles = menuFile.addMenu(self.tr('Open recent')) self.menuRecentFiles.aboutToShow.connect(self.updateRecentFiles) menuFile.addAction(self.actionShow) menuFile.addAction(self.actionSetEncoding) menuFile.addAction(self.actionReload) menuFile.addSeparator() menuFile.addAction(self.actionSave) menuFile.addAction(self.actionSaveAs) menuFile.addSeparator() menuFile.addAction(self.actionNextTab) menuFile.addAction(self.actionPrevTab) menuFile.addSeparator() menuExport = menuFile.addMenu(self.tr('Export')) menuExport.addAction(self.actionSaveHtml) menuExport.addAction(self.actionOdf) menuExport.addAction(self.actionPdf) if self.extensionActions: menuExport.addSeparator() for action, mimetype in self.extensionActions: menuExport.addAction(action) menuExport.aboutToShow.connect(self.updateExtensionsVisibility) menuFile.addAction(self.actionPrint) menuFile.addAction(self.actionPrintPreview) menuFile.addSeparator() menuFile.addAction(self.actionQuit) menuEdit.addAction(self.actionUndo) menuEdit.addAction(self.actionRedo) menuEdit.addSeparator() menuEdit.addAction(self.actionCut) menuEdit.addAction(self.actionCopy) menuEdit.addAction(self.actionPaste) menuEdit.addSeparator() if enchant_available: menuSC = menuEdit.addMenu(self.tr('Spell check')) menuSC.addAction(self.actionEnableSC) menuSC.addAction(self.actionSetLocale) menuEdit.addAction(self.actionSearch) menuEdit.addAction(self.actionChangeEditorFont) menuEdit.addAction(self.actionChangePreviewFont) menuEdit.addSeparator() if len(availableMarkups) > 1: self.menuMode = menuEdit.addMenu(self.tr('Default markup')) for markupAction in markupActions: self.menuMode.addAction(markupAction) menuFormat = menuEdit.addMenu(self.tr('Formatting')) menuFormat.addAction(self.actionBold) menuFormat.addAction(self.actionItalic) menuFormat.addAction(self.actionUnderline) menuEdit.addAction(self.actionWebKit) menuEdit.addSeparator() menuEdit.addAction(self.actionViewHtml) menuEdit.addAction(self.actionPreview) menuEdit.addAction(self.actionTableMode) if ReTextFakeVimHandler: menuEdit.addAction(self.actionFakeVimMode) menuEdit.addSeparator() menuEdit.addAction(self.actionFullScreen) menuEdit.addAction(self.actionConfig) menuHelp.addAction(self.actionHelp) menuHelp.addSeparator() menuHelp.addAction(self.actionAbout) menuHelp.addAction(self.actionAboutQt) toolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) toolBar.addAction(self.actionNew) toolBar.addSeparator() toolBar.addAction(self.actionOpen) toolBar.addAction(self.actionSave) toolBar.addAction(self.actionPrint) toolBar.addSeparator() toolBar.addAction(self.actionPreview) toolBar.addAction(self.actionFullScreen) self.editBar.addAction(self.actionUndo) self.editBar.addAction(self.actionRedo) self.editBar.addSeparator() self.editBar.addAction(self.actionCut) self.editBar.addAction(self.actionCopy) self.editBar.addAction(self.actionPaste) self.editBar.addSeparator() self.editBar.addWidget(self.formattingBox) self.editBar.addWidget(self.symbolBox) self.searchEdit = QLineEdit(self.searchBar) self.searchEdit.setPlaceholderText(self.tr('Search')) self.searchEdit.returnPressed.connect(self.find) self.csBox = QCheckBox(self.tr('Case sensitively'), self.searchBar) self.searchBar.addWidget(self.searchEdit) self.searchBar.addSeparator() self.searchBar.addWidget(self.csBox) self.searchBar.addAction(self.actionFindPrev) self.searchBar.addAction(self.actionFind) self.searchBar.addAction(self.actionCloseSearch) self.searchBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.searchBar.setVisible(False) self.autoSaveEnabled = globalSettings.autoSave if self.autoSaveEnabled: timer = QTimer(self) timer.start(60000) timer.timeout.connect(self.saveAll) self.ind = None if enchant_available: self.sl = globalSettings.spellCheckLocale if self.sl: try: enchant.Dict(self.sl) except Exception as e: print(e, file=sys.stderr) self.sl = None if globalSettings.spellCheck: self.actionEnableSC.setChecked(True) self.fileSystemWatcher = QFileSystemWatcher() self.fileSystemWatcher.fileChanged.connect(self.fileChanged) def iterateTabs(self): for i in range(self.tabWidget.count()): yield self.tabWidget.widget(i).tab def updateStyleSheet(self): if globalSettings.styleSheet: sheetfile = QFile(globalSettings.styleSheet) sheetfile.open(QIODevice.ReadOnly) self.ss = QTextStream(sheetfile).readAll() sheetfile.close() else: palette = QApplication.palette() self.ss = 'html { color: %s; }\n' % palette.color(QPalette.WindowText).name() self.ss += 'td, th { border: 1px solid #c3c3c3; padding: 0 3px 0 3px; }\n' self.ss += 'table { border-collapse: collapse; }\n' def initTabWidget(self): def dragEnterEvent(e): e.acceptProposedAction() def dropEvent(e): fn = bytes(e.mimeData().data('text/plain')).decode().rstrip() if fn.startswith('file:'): fn = QUrl(fn).toLocalFile() self.openFileWrapper(fn) self.tabWidget.setTabsClosable(True) self.tabWidget.setAcceptDrops(True) self.tabWidget.setMovable(True) self.tabWidget.dragEnterEvent = dragEnterEvent self.tabWidget.dropEvent = dropEvent def act(self, name, icon=None, trig=None, trigbool=None, shct=None): if not isinstance(shct, QKeySequence): shct = QKeySequence(shct) if icon: action = QAction(self.actIcon(icon), name, self) else: action = QAction(name, self) if trig: action.triggered.connect(trig) elif trigbool: action.setCheckable(True) action.triggered[bool].connect(trigbool) if shct: action.setShortcut(shct) return action def actIcon(self, name): return QIcon.fromTheme(name, QIcon(icon_path+name+'.png')) def printError(self): import traceback print('Exception occured while parsing document:', file=sys.stderr) traceback.print_exc() def createTab(self, fileName): self.currentTab = ReTextTab(self, fileName, previewState=int(globalSettings.livePreviewByDefault)) self.tabWidget.addTab(self.currentTab.getSplitter(), self.tr("New document")) def closeTab(self, ind): if self.maybeSave(ind): if self.tabWidget.count() == 1: self.createTab("") currentWidget = self.tabWidget.widget(ind) if currentWidget.tab.fileName: self.fileSystemWatcher.removePath(currentWidget.tab.fileName) del currentWidget.tab self.tabWidget.removeTab(ind) def docTypeChanged(self): markupClass = self.currentTab.getMarkupClass() if type(self.currentTab.markup) != markupClass: self.currentTab.setMarkupClass(markupClass) self.currentTab.updatePreviewBox() dtMarkdown = (markupClass == markups.MarkdownMarkup) dtMkdOrReST = dtMarkdown or (markupClass == markups.ReStructuredTextMarkup) self.formattingBox.setEnabled(dtMarkdown) self.symbolBox.setEnabled(dtMarkdown) self.actionUnderline.setEnabled(dtMarkdown) self.actionBold.setEnabled(dtMkdOrReST) self.actionItalic.setEnabled(dtMkdOrReST) canReload = bool(self.currentTab.fileName) and not self.autoSaveActive() self.actionSetEncoding.setEnabled(canReload) self.actionReload.setEnabled(canReload) def changeIndex(self, ind): self.currentTab = self.tabWidget.currentWidget().tab editBox = self.currentTab.editBox previewState = self.currentTab.previewState self.actionUndo.setEnabled(editBox.document().isUndoAvailable()) self.actionRedo.setEnabled(editBox.document().isRedoAvailable()) self.actionCopy.setEnabled(editBox.textCursor().hasSelection()) self.actionCut.setEnabled(editBox.textCursor().hasSelection()) self.actionPreview.setChecked(previewState >= PreviewLive) self.actionLivePreview.setChecked(previewState == PreviewLive) self.actionTableMode.setChecked(editBox.tableModeEnabled) self.editBar.setEnabled(previewState < PreviewNormal) self.ind = ind if self.currentTab.fileName: self.setCurrentFile() else: self.setWindowTitle(self.tr('New document') + '[*]') self.docTypeChanged() self.modificationChanged(editBox.document().isModified()) editBox.setFocus(Qt.OtherFocusReason) def changeEditorFont(self): font, ok = QFontDialog.getFont(globalSettings.editorFont, self) if ok: globalSettings.editorFont = font for tab in self.iterateTabs(): tab.editBox.updateFont() def changePreviewFont(self): font, ok = QFontDialog.getFont(globalSettings.font, self) if ok: globalSettings.font = font for tab in self.iterateTabs(): tab.updatePreviewBox() def preview(self, viewmode): self.currentTab.previewState = viewmode * 2 self.actionLivePreview.setChecked(False) self.editBar.setDisabled(viewmode) self.currentTab.updateBoxesVisibility() if viewmode: self.currentTab.updatePreviewBox() def enableLivePreview(self, livemode): self.currentTab.previewState = int(livemode) self.actionPreview.setChecked(livemode) self.editBar.setEnabled(True) self.currentTab.updateBoxesVisibility() if livemode: self.currentTab.updatePreviewBox() def enableWebKit(self, enable): globalSettings.useWebKit = enable for i in range(self.tabWidget.count()): splitter = self.tabWidget.widget(i) tab = splitter.tab tab.previewBox.disconnectExternalSignals() tab.previewBox.setParent(None) tab.previewBox.deleteLater() tab.previewBox = tab.createPreviewBox(tab.editBox) tab.previewBox.setMinimumWidth(125) splitter.addWidget(tab.previewBox) splitter.setSizes((50, 50)) tab.updatePreviewBox() tab.updateBoxesVisibility() def enableCopy(self, copymode): self.actionCopy.setEnabled(copymode) self.actionCut.setEnabled(copymode) def enableFullScreen(self, yes): if yes: self.showFullScreen() else: self.showNormal() def openConfigDialog(self): dlg = ConfigDialog(self) dlg.setWindowTitle(self.tr('Preferences')) dlg.show() def enableFakeVimMode(self, yes): globalSettings.useFakeVim = yes if yes: FakeVimMode.init(self) for tab in self.iterateTabs(): tab.installFakeVimHandler() else: FakeVimMode.exit(self) def enableSpellCheck(self, yes): if yes: self.setAllDictionaries(enchant.Dict(self.sl or None)) else: self.setAllDictionaries(None) globalSettings.spellCheck = yes def setAllDictionaries(self, dictionary): for tab in self.iterateTabs(): hl = tab.highlighter hl.dictionary = dictionary hl.rehighlight() def changeLocale(self): if self.sl: localedlg = LocaleDialog(self, defaultText=self.sl) else: localedlg = LocaleDialog(self) if localedlg.exec() != QDialog.Accepted: return sl = localedlg.localeEdit.text() setdefault = localedlg.checkBox.isChecked() if sl: try: sl = str(sl) enchant.Dict(sl) except Exception as e: QMessageBox.warning(self, '', str(e)) else: self.sl = sl self.enableSpellCheck(self.actionEnableSC.isChecked()) else: self.sl = None self.enableSpellCheck(self.actionEnableSC.isChecked()) if setdefault: globalSettings.spellCheckLocale = sl def searchBarVisibilityChanged(self, visible): self.actionSearch.setChecked(visible) if visible: self.searchEdit.setFocus(Qt.ShortcutFocusReason) def find(self, back=False): flags = QTextDocument.FindFlags() if back: flags |= QTextDocument.FindBackward if self.csBox.isChecked(): flags |= QTextDocument.FindCaseSensitively text = self.searchEdit.text() editBox = self.currentTab.editBox cursor = editBox.textCursor() newCursor = editBox.document().find(text, cursor, flags) if not newCursor.isNull(): editBox.setTextCursor(newCursor) return self.setSearchEditColor(True) cursor.movePosition(QTextCursor.End if back else QTextCursor.Start) newCursor = editBox.document().find(text, cursor, flags) if not newCursor.isNull(): editBox.setTextCursor(newCursor) return self.setSearchEditColor(True) self.setSearchEditColor(False) def setSearchEditColor(self, found): palette = self.searchEdit.palette() palette.setColor(QPalette.Active, QPalette.Base, Qt.white if found else QColor(255, 102, 102)) self.searchEdit.setPalette(palette) def showInDir(self): if self.currentTab.fileName: path = QFileInfo(self.currentTab.fileName).path() QDesktopServices.openUrl(QUrl.fromLocalFile(path)) else: QMessageBox.warning(self, '', self.tr("Please, save the file somewhere.")) def setCurrentFile(self): self.setWindowTitle("") self.tabWidget.setTabText(self.ind, self.currentTab.getDocumentTitle(baseName=True)) self.tabWidget.setTabToolTip(self.ind, self.currentTab.fileName or '') self.setWindowFilePath(self.currentTab.fileName) files = readListFromSettings("recentFileList") while self.currentTab.fileName in files: files.remove(self.currentTab.fileName) files.insert(0, self.currentTab.fileName) if len(files) > 10: del files[10:] writeListToSettings("recentFileList", files) QDir.setCurrent(QFileInfo(self.currentTab.fileName).dir().path()) self.docTypeChanged() def createNew(self, text=None): self.createTab("") self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if text: self.currentTab.editBox.textCursor().insertText(text) def switchTab(self, shift=1): self.tabWidget.setCurrentIndex((self.ind + shift) % self.tabWidget.count()) def updateRecentFiles(self): self.menuRecentFiles.clear() self.recentFilesActions = [] filesOld = readListFromSettings("recentFileList") files = [] for f in filesOld: if QFile.exists(f): files.append(f) self.recentFilesActions.append(self.act(f, trig=self.openFunction(f))) writeListToSettings("recentFileList", files) for action in self.recentFilesActions: self.menuRecentFiles.addAction(action) def markupFunction(self, markup): return lambda: self.setDefaultMarkup(markup) def openFunction(self, fileName): return lambda: self.openFileWrapper(fileName) def extensionFunction(self, data): return lambda: \ self.runExtensionCommand(data['Exec'], data['FileFilter'], data['DefaultExtension']) def getExportExtensionsList(self): extensions = [] for extsprefix in datadirs: extsdir = QDir(extsprefix+'/export-extensions/') if extsdir.exists(): for fileInfo in extsdir.entryInfoList(['*.desktop', '*.ini'], QDir.Files | QDir.Readable): extensions.append(self.readExtension(fileInfo.filePath())) locale = QLocale.system().name() self.extensionActions = [] for extension in extensions: try: if ('Name[%s]' % locale) in extension: name = extension['Name[%s]' % locale] elif ('Name[%s]' % locale.split('_')[0]) in extension: name = extension['Name[%s]' % locale.split('_')[0]] else: name = extension['Name'] data = {} for prop in ('FileFilter', 'DefaultExtension', 'Exec'): if 'X-ReText-'+prop in extension: data[prop] = extension['X-ReText-'+prop] elif prop in extension: data[prop] = extension[prop] else: data[prop] = '' action = self.act(name, trig=self.extensionFunction(data)) if 'Icon' in extension: action.setIcon(self.actIcon(extension['Icon'])) mimetype = extension['MimeType'] if 'MimeType' in extension else None except KeyError: print('Failed to parse extension: Name is required', file=sys.stderr) else: self.extensionActions.append((action, mimetype)) def updateExtensionsVisibility(self): markupClass = self.currentTab.getMarkupClass() for action in self.extensionActions: if markupClass is None: action[0].setEnabled(False) continue mimetype = action[1] if mimetype == None: enabled = True elif markupClass == markups.MarkdownMarkup: enabled = (mimetype in ("text/x-retext-markdown", "text/x-markdown")) elif markupClass == markups.ReStructuredTextMarkup: enabled = (mimetype in ("text/x-retext-rst", "text/x-rst")) else: enabled = False action[0].setEnabled(enabled) def readExtension(self, fileName): extFile = QFile(fileName) extFile.open(QIODevice.ReadOnly) extension = {} stream = QTextStream(extFile) while not stream.atEnd(): line = stream.readLine() if '=' in line: index = line.index('=') extension[line[:index].rstrip()] = line[index+1:].lstrip() extFile.close() return extension def openFile(self): supportedExtensions = ['.txt'] for markup in markups.get_all_markups(): supportedExtensions += markup.file_extensions fileFilter = ' (' + str.join(' ', ['*'+ext for ext in supportedExtensions]) + ');;' fileNames = QFileDialog.getOpenFileNames(self, self.tr("Select one or several files to open"), "", self.tr("Supported files") + fileFilter + self.tr("All files (*)")) for fileName in fileNames[0]: self.openFileWrapper(fileName) def openFileWrapper(self, fileName): if not fileName: return fileName = QFileInfo(fileName).canonicalFilePath() exists = False for i, tab in enumerate(self.iterateTabs()): if tab.fileName == fileName: exists = True ex = i if exists: self.tabWidget.setCurrentIndex(ex) elif QFile.exists(fileName): noEmptyTab = ( (self.ind is None) or self.currentTab.fileName or self.currentTab.editBox.toPlainText() or self.currentTab.editBox.document().isModified() ) if noEmptyTab: self.createTab(fileName) self.ind = self.tabWidget.count()-1 self.tabWidget.setCurrentIndex(self.ind) if fileName: self.fileSystemWatcher.addPath(fileName) self.currentTab.fileName = fileName self.currentTab.readTextFromFile() editBox = self.currentTab.editBox self.setCurrentFile() self.setWindowModified(editBox.document().isModified()) def showEncodingDialog(self): if not self.maybeSave(self.ind): return encoding, ok = QInputDialog.getItem(self, '', self.tr('Select file encoding from the list:'), [bytes(b).decode() for b in QTextCodec.availableCodecs()], 0, False) if ok: self.currentTab.readTextFromFile(encoding) def saveFile(self): self.saveFileMain(dlg=False) def saveFileAs(self): self.saveFileMain(dlg=True) def saveAll(self): for tab in self.iterateTabs(): if tab.fileName and QFileInfo(tab.fileName).isWritable(): tab.saveTextToFile() tab.editBox.document().setModified(False) def saveFileMain(self, dlg): if (not self.currentTab.fileName) or dlg: markupClass = self.currentTab.getMarkupClass() if (markupClass is None) or not hasattr(markupClass, 'default_extension'): defaultExt = self.tr("Plain text (*.txt)") ext = ".txt" else: defaultExt = self.tr('%s files', 'Example of final string: Markdown files') \ % markupClass.name + ' (' + str.join(' ', ('*'+extension for extension in markupClass.file_extensions)) + ')' if markupClass == markups.MarkdownMarkup: ext = globalSettings.markdownDefaultFileExtension elif markupClass == markups.ReStructuredTextMarkup: ext = globalSettings.restDefaultFileExtension else: ext = markupClass.default_extension newFileName = QFileDialog.getSaveFileName(self, self.tr("Save file"), "", defaultExt)[0] if newFileName: if not QFileInfo(newFileName).suffix(): newFileName += ext if self.currentTab.fileName: self.fileSystemWatcher.removePath(self.currentTab.fileName) self.currentTab.fileName = newFileName self.actionSetEncoding.setDisabled(self.autoSaveActive()) if self.currentTab.fileName: if self.currentTab.saveTextToFile(): self.setCurrentFile() self.currentTab.editBox.document().setModified(False) self.setWindowModified(False) return True else: QMessageBox.warning(self, '', self.tr("Cannot save to file because it is read-only!")) return False def saveHtml(self, fileName): if not QFileInfo(fileName).suffix(): fileName += ".html" try: htmltext = self.currentTab.getHtml(includeStyleSheet=False, webenv=True) except Exception: return self.printError() htmlFile = QFile(fileName) htmlFile.open(QIODevice.WriteOnly) html = QTextStream(htmlFile) if globalSettings.defaultCodec: html.setCodec(globalSettings.defaultCodec) html << htmltext htmlFile.close() def textDocument(self): td = QTextDocument() td.setMetaInformation(QTextDocument.DocumentTitle, self.currentTab.getDocumentTitle()) if self.ss: td.setDefaultStyleSheet(self.ss) td.setHtml(self.currentTab.getHtml()) td.setDefaultFont(globalSettings.font) return td def saveOdf(self): try: document = self.textDocument() except Exception: return self.printError() fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to ODT"), "", self.tr("OpenDocument text files (*.odt)"))[0] if not QFileInfo(fileName).suffix(): fileName += ".odt" writer = QTextDocumentWriter(fileName) writer.setFormat(b"odf") writer.write(document) def saveFileHtml(self): fileName = QFileDialog.getSaveFileName(self, self.tr("Save file"), "", self.tr("HTML files (*.html *.htm)"))[0] if fileName: self.saveHtml(fileName) def getDocumentForPrint(self): if globalSettings.useWebKit: return self.currentTab.previewBox try: return self.textDocument() except Exception: self.printError() def standardPrinter(self): printer = QPrinter(QPrinter.HighResolution) printer.setDocName(self.currentTab.getDocumentTitle()) printer.setCreator('ReText %s' % app_version) return printer def savePdf(self): self.currentTab.updatePreviewBox() fileName = QFileDialog.getSaveFileName(self, self.tr("Export document to PDF"), "", self.tr("PDF files (*.pdf)"))[0] if fileName: if not QFileInfo(fileName).suffix(): fileName += ".pdf" printer = self.standardPrinter() printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(fileName) document = self.getDocumentForPrint() if document != None: document.print(printer) def printFile(self): self.currentTab.updatePreviewBox() printer = self.standardPrinter() dlg = QPrintDialog(printer, self) dlg.setWindowTitle(self.tr("Print document")) if (dlg.exec() == QDialog.Accepted): document = self.getDocumentForPrint() if document != None: document.print(printer) def printPreview(self): document = self.getDocumentForPrint() if document == None: return printer = self.standardPrinter() preview = QPrintPreviewDialog(printer, self) preview.paintRequested.connect(document.print) preview.exec() def runExtensionCommand(self, command, filefilter, defaultext): of = ('%of' in command) html = ('%html' in command) if of: if defaultext and not filefilter: filefilter = '*'+defaultext fileName = QFileDialog.getSaveFileName(self, self.tr('Export document'), '', filefilter)[0] if not fileName: return if defaultext and not QFileInfo(fileName).suffix(): fileName += defaultext basename = '.%s.retext-temp' % self.currentTab.getDocumentTitle(baseName=True) if html: tmpname = basename+'.html' self.saveHtml(tmpname) else: tmpname = basename + self.currentTab.getMarkupClass().default_extension self.currentTab.saveTextToFile(fileName=tmpname, addToWatcher=False) command = command.replace('%of', '"out'+defaultext+'"') command = command.replace('%html' if html else '%if', '"'+tmpname+'"') try: Popen(str(command), shell=True).wait() except Exception as error: errorstr = str(error) QMessageBox.warning(self, '', self.tr('Failed to execute the command:') + '\n' + errorstr) QFile(tmpname).remove() if of: QFile('out'+defaultext).rename(fileName) def autoSaveActive(self, ind=None): tab = self.currentTab if ind is None else self.tabWidget.widget(ind).tab return (self.autoSaveEnabled and tab.fileName and QFileInfo(tab.fileName).isWritable()) def modificationChanged(self, changed): if self.autoSaveActive(): changed = False self.actionSave.setEnabled(changed) self.setWindowModified(changed) def clipboardDataChanged(self): mimeData = QApplication.instance().clipboard().mimeData() if mimeData is not None: self.actionPaste.setEnabled(mimeData.hasText()) def insertFormatting(self, formatting): cursor = self.currentTab.editBox.textCursor() text = cursor.selectedText() moveCursorTo = None def c(cursor): nonlocal moveCursorTo moveCursorTo = cursor.position() def ensurenl(cursor): if not cursor.atBlockStart(): cursor.insertText('\n\n') toinsert = { 'header': (ensurenl, '# ', text), 'italic': ('*', text, c, '*'), 'bold': ('**', text, c, '**'), 'underline': ('<u>', text, c, '</u>'), 'numbering': (ensurenl, ' 1. ', text), 'bullets': (ensurenl, ' * ', text), 'image': ('![', text or self.tr('Alt text'), c, '](', self.tr('URL'), ')'), 'link': ('[', text or self.tr('Link text'), c, '](', self.tr('URL'), ')'), 'inline code': ('`', text, c, '`'), 'code block': (ensurenl, ' ', text), 'blockquote': (ensurenl, '> ', text), } if formatting not in toinsert: return cursor.beginEditBlock() for token in toinsert[formatting]: if callable(token): token(cursor) else: cursor.insertText(token) cursor.endEditBlock() self.formattingBox.setCurrentIndex(0) # Bring back the focus on the editor self.currentTab.editBox.setFocus(Qt.OtherFocusReason) if moveCursorTo: cursor.setPosition(moveCursorTo) self.currentTab.editBox.setTextCursor(cursor) def insertSymbol(self, num): if num: self.currentTab.editBox.insertPlainText('&'+self.usefulChars[num-1]+';') self.symbolBox.setCurrentIndex(0) def fileChanged(self, fileName): ind = None for testind, tab in enumerate(self.iterateTabs()): if tab.fileName == fileName: ind = testind if ind is None: self.fileSystemWatcher.removePath(fileName) self.tabWidget.setCurrentIndex(ind) if not QFile.exists(fileName): self.currentTab.editBox.document().setModified(True) QMessageBox.warning(self, '', self.tr( 'This file has been deleted by other application.\n' 'Please make sure you save the file before exit.')) elif not self.currentTab.editBox.document().isModified(): # File was not modified in ReText, reload silently self.currentTab.readTextFromFile() self.currentTab.updatePreviewBox() else: text = self.tr( 'This document has been modified by other application.\n' 'Do you want to reload the file (this will discard all ' 'your changes)?\n') if self.autoSaveEnabled: text += self.tr( 'If you choose to not reload the file, auto save mode will ' 'be disabled for this session to prevent data loss.') messageBox = QMessageBox(QMessageBox.Warning, '', text) reloadButton = messageBox.addButton(self.tr('Reload'), QMessageBox.YesRole) messageBox.addButton(QMessageBox.Cancel) messageBox.exec() if messageBox.clickedButton() is reloadButton: self.currentTab.readTextFromFile() self.currentTab.updatePreviewBox() else: self.autoSaveEnabled = False self.currentTab.editBox.document().setModified(True) if fileName not in self.fileSystemWatcher.files(): # https://github.com/retext-project/retext/issues/137 self.fileSystemWatcher.addPath(fileName) def maybeSave(self, ind): tab = self.tabWidget.widget(ind).tab if self.autoSaveActive(ind): tab.saveTextToFile() return True if not tab.editBox.document().isModified(): return True self.tabWidget.setCurrentIndex(ind) ret = QMessageBox.warning(self, '', self.tr("The document has been modified.\nDo you want to save your changes?"), QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) if ret == QMessageBox.Save: return self.saveFileMain(False) elif ret == QMessageBox.Cancel: return False return True def closeEvent(self, closeevent): for ind in range(self.tabWidget.count()): if not self.maybeSave(ind): return closeevent.ignore() if globalSettings.saveWindowGeometry and not self.isMaximized(): globalSettings.windowGeometry = self.saveGeometry() closeevent.accept() def viewHtml(self): htmlDlg = HtmlDialog(self) try: htmltext = self.currentTab.getHtml(includeStyleSheet=False) except Exception: return self.printError() winTitle = self.currentTab.getDocumentTitle(baseName=True) htmlDlg.setWindowTitle(winTitle+" ("+self.tr("HTML code")+")") htmlDlg.textEdit.setPlainText(htmltext.rstrip()) htmlDlg.hl.rehighlight() htmlDlg.show() htmlDlg.raise_() htmlDlg.activateWindow() def openHelp(self): QDesktopServices.openUrl(QUrl('https://github.com/retext-project/retext/wiki')) def aboutDialog(self): QMessageBox.about(self, self.aboutWindowTitle, '<p><b>' + (self.tr('ReText %s (using PyMarkups %s)') % (app_version, markups.__version__)) +'</b></p>' + self.tr('Simple but powerful editor' ' for Markdown and reStructuredText') +'</p><p>'+self.tr('Author: Dmitry Shachnev, 2011').replace('2011', '2011–2016') +'<br><a href="https://github.com/retext-project/retext">'+self.tr('Website') +'</a> | <a href="http://daringfireball.net/projects/markdown/syntax">' +self.tr('Markdown syntax') +'</a> | <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">' +self.tr('reStructuredText syntax')+'</a></p>') def setDefaultMarkup(self, markupClass): self.defaultMarkup = markupClass defaultName = markups.get_available_markups()[0].name writeToSettings('defaultMarkup', markupClass.name, defaultName) for tab in self.iterateTabs(): if not tab.fileName: tab.setMarkupClass(markupClass) tab.updatePreviewBox() self.docTypeChanged()
def create_format_actions(self): """actions for Format menu""" self.copy_format = Action(self.parent, "&Copy format", self.parent.workflows.format_copy_format, icon=Icon.copy_format, statustip='Copy format of selection to ' 'the clipboard') self.paste_format = \ Action(self.parent, "&Paste format", self.parent.workflows.format_paste_format, icon=Icon.paste_format, statustip='Apply format from the clipboard to the selected ' 'cells') self.font = Action(self.parent, "&Font...", self.parent.grid.on_font_dialog, icon=Icon.font_dialog, shortcut='Ctrl+n', statustip='Lauch font dialog') self.bold = Action(self.parent, "&Bold", self.parent.grid.on_bold_pressed, icon=Icon.bold, shortcut='Ctrl+b', checkable=True, statustip='Toggle bold font weight for the ' 'selected cells') self.italics = Action(self.parent, "&Italics", self.parent.grid.on_italics_pressed, icon=Icon.italics, shortcut='Ctrl+i', checkable=True, statustip='Toggle italics font style for the ' 'selected cells') self.underline = Action(self.parent, "&Underline", self.parent.grid.on_underline_pressed, icon=Icon.underline, shortcut='Ctrl+u', checkable=True, statustip='Toggle underline for the ' 'selected cells') self.strikethrough = Action(self.parent, "&Strikethrough", self.parent.grid.on_strikethrough_pressed, icon=Icon.strikethrough, checkable=True, statustip='Toggle strikethrough for the ' 'selected cells') self.text = Action(self.parent, "Text renderer", self.parent.grid.on_text_renderer_pressed, icon=Icon.text, checkable=True, statustip='Show cell results as text (default). ' 'Formats affect the whole cell') self.markup = Action(self.parent, "Markup renderer", self.parent.grid.on_markup_renderer_pressed, icon=Icon.markup, checkable=True, statustip='Show cell results as markup, which ' 'allows partly formatted output') self.image = Action(self.parent, "Image renderer", self.parent.grid.on_image_renderer_pressed, icon=Icon.image, checkable=True, statustip='Show cell results as image. A numpy ' 'array of shape (x, y, 3) ' 'is expected') if matplotlib_figure is not None: self.matplotlib = \ Action(self.parent, "Matplotlib chart renderer", self.parent.grid.on_matplotlib_renderer_pressed, icon=Icon.matplotlib, checkable=True, statustip='Show cell results as matplotlib chart. A ' 'numpy array of shape (x, y, 3) is expected') renderer_group = QActionGroup(self.parent) renderer_group.addAction(self.text) renderer_group.addAction(self.markup) renderer_group.addAction(self.image) if matplotlib_figure is not None: renderer_group.addAction(self.matplotlib) self.text_color = Action( self.parent, "Text color...", self.parent.widgets.text_color_button.on_pressed, icon=Icon.text_color, statustip='Lauch text color dialog') self.line_color = Action( self.parent, "Line color...", self.parent.widgets.line_color_button.on_pressed, icon=Icon.line_color, statustip='Lauch line color dialog') self.background_color = Action( self.parent, "Background color...", self.parent.widgets.background_color_button.on_pressed, icon=Icon.background_color, statustip='Lauch background color dialog') self.freeze_cell = Action(self.parent, "Freeze cell", self.parent.grid.on_freeze_pressed, icon=Icon.freeze, checkable=True, statustip='Freeze the selected cell so that ' 'is is only updated when <F5> is ' 'pressed') self.lock_cell = Action(self.parent, "Lock cell", self.parent.grid.on_lock_pressed, icon=Icon.lock, checkable=True, statustip='Lock cell so that its code ' 'cannot be changed') self.button_cell = Action(self.parent, "Button cell", self.parent.grid.on_button_cell_pressed, icon=Icon.button, checkable=True, statustip='Make cell a button cell that is ' 'executed only when pressed') self.merge_cells = Action(self.parent, "Merge cells", self.parent.grid.on_merge_pressed, icon=Icon.merge_cells, checkable=True, statustip='Merge/unmerge selected cells') self.rotate_0 = Action(self.parent, "0°", self.parent.grid.on_rotate_0, icon=Icon.rotate_0, checkable=True, statustip='Set text rotation to 0°') self.rotate_90 = Action(self.parent, "90°", self.parent.grid.on_rotate_90, icon=Icon.rotate_90, checkable=True, statustip='Set text rotation to 90°') self.rotate_180 = Action(self.parent, "180°", self.parent.grid.on_rotate_180, icon=Icon.rotate_180, checkable=True, statustip='Set text rotation to 180°') self.rotate_270 = Action(self.parent, "270°", self.parent.grid.on_rotate_270, icon=Icon.rotate_270, checkable=True, statustip='Set text rotation to 270°') rotate_group = QActionGroup(self.parent) rotate_group.addAction(self.rotate_0) rotate_group.addAction(self.rotate_90) rotate_group.addAction(self.rotate_180) rotate_group.addAction(self.rotate_270) self.justify_left = Action(self.parent, "Left", self.parent.grid.on_justify_left, icon=Icon.justify_left, checkable=True, statustip='Display cell result text ' 'left justified') self.justify_center = Action(self.parent, "Center", self.parent.grid.on_justify_center, checkable=True, icon=Icon.justify_center, statustip='Display cell result text ' 'centered') self.justify_right = Action(self.parent, "Right", self.parent.grid.on_justify_right, checkable=True, icon=Icon.justify_right, statustip='Display cell result text ' 'right justified') self.justify_fill = Action(self.parent, "Fill", self.parent.grid.on_justify_fill, icon=Icon.justify_fill, checkable=True, statustip='Display cell result text ' 'filled into the cell') justify_group = QActionGroup(self.parent) justify_group.addAction(self.justify_left) justify_group.addAction(self.justify_center) justify_group.addAction(self.justify_right) justify_group.addAction(self.justify_fill) self.align_top = Action(self.parent, "Top", self.parent.grid.on_align_top, icon=Icon.align_top, checkable=True, statustip='Align cell result at the top of ' 'the cell') self.align_center = Action(self.parent, "Center", self.parent.grid.on_align_middle, icon=Icon.align_center, checkable=True, statustip='Center cell result within ' 'the cell') self.align_bottom = Action(self.parent, "Bottom", self.parent.grid.on_align_bottom, icon=Icon.align_bottom, checkable=True, statustip='Align cell result at the ' 'bottom of the cell') align_group = QActionGroup(self.parent) align_group.addAction(self.align_top) align_group.addAction(self.align_center) align_group.addAction(self.align_bottom) self.format_borders_all = \ Action(self.parent, "All borders", self.parent.grid.on_border_choice, icon=Icon.format_borders_all, checkable=True, statustip='Format all borders of selection') self.format_borders_top = \ Action(self.parent, "Top border", self.parent.grid.on_border_choice, icon=Icon.format_borders_top, checkable=True, statustip='Format top border of selection') self.format_borders_bottom = \ Action(self.parent, "Bottom border", self.parent.grid.on_border_choice, icon=Icon.format_borders_bottom, checkable=True, statustip='Format bottom border of selection') self.format_borders_left = \ Action(self.parent, "Left border", self.parent.grid.on_border_choice, icon=Icon.format_borders_left, checkable=True, statustip='Format left border of selection') self.format_borders_right = \ Action(self.parent, "Right border", self.parent.grid.on_border_choice, icon=Icon.format_borders_right, checkable=True, statustip='Format right border of selection') self.format_borders_outer = \ Action(self.parent, "Outer borders", self.parent.grid.on_border_choice, icon=Icon.format_borders_outer, checkable=True, statustip='Format outer borders of selection') self.format_borders_inner = \ Action(self.parent, "Inner borders", self.parent.grid.on_border_choice, icon=Icon.format_borders_inner, checkable=True, statustip='Format inner borders of selection') self.format_borders_top_bottom = \ Action(self.parent, "Top and bottom borders", self.parent.grid.on_border_choice, icon=Icon.format_borders_top_bottom, checkable=True, statustip='Format top and bottom borders of selection') self.border_group = QActionGroup(self.parent) self.border_group.addAction(self.format_borders_all) self.border_group.addAction(self.format_borders_top) self.border_group.addAction(self.format_borders_bottom) self.border_group.addAction(self.format_borders_left) self.border_group.addAction(self.format_borders_right) self.border_group.addAction(self.format_borders_outer) self.border_group.addAction(self.format_borders_inner) self.border_group.addAction(self.format_borders_top_bottom) self.format_borders_all.setChecked(True) self.format_borders_0 = Action(self.parent, "Border width 0", self.parent.grid.on_borderwidth, icon=Icon.format_borders_0, statustip='Set border width to 0') self.format_borders_1 = Action(self.parent, "Border width 1", self.parent.grid.on_borderwidth, icon=Icon.format_borders_1, statustip='Set border width to 1') self.format_borders_2 = Action(self.parent, "Border width 2", self.parent.grid.on_borderwidth, icon=Icon.format_borders_2, statustip='Set border width to 2') self.format_borders_4 = Action(self.parent, "Border width 4", self.parent.grid.on_borderwidth, icon=Icon.format_borders_4, statustip='Set border width to 4') self.format_borders_8 = Action(self.parent, "Border width 8", self.parent.grid.on_borderwidth, icon=Icon.format_borders_8, statustip='Set border width to 8') self.format_borders_16 = Action(self.parent, "Border width 16", self.parent.grid.on_borderwidth, icon=Icon.format_borders_16, statustip='Set border width to 16') self.format_borders_32 = Action(self.parent, "Border width 32", self.parent.grid.on_borderwidth, icon=Icon.format_borders_32, statustip='Set border width to 32') self.format_borders_64 = Action(self.parent, "Border width 64", self.parent.grid.on_borderwidth, icon=Icon.format_borders_64, statustip='Set border width to 64') self.border_width_group = QActionGroup(self.parent) self.border_width_group.addAction(self.format_borders_0) self.border_width_group.addAction(self.format_borders_1) self.border_width_group.addAction(self.format_borders_2) self.border_width_group.addAction(self.format_borders_4) self.border_width_group.addAction(self.format_borders_8) self.border_width_group.addAction(self.format_borders_16) self.border_width_group.addAction(self.format_borders_32) self.border_width_group.addAction(self.format_borders_64) self.format_borders_1.setChecked(True)
def set_menus(self): menubar = self.menuBar() file_menu = menubar.addMenu("&File") for d in ({ "name": u"&Open...", "shct": "Ctrl+O", "func": self.open_file }, { "name": u"&Save HTML...", "shct": "Ctrl+S", "func": self.save_html }, { "name": u"&Find...", "shct": "Ctrl+F", "func": self.show_search_bar }, { "name": u"&Print...", "shct": "Ctrl+P", "func": self.print_doc }, { "name": u"&Quit", "shct": "Ctrl+Q", "func": self.quit }): action = QAction(d["name"], self) action.setShortcut(QKeySequence(d["shct"])) action.triggered.connect(d["func"]) file_menu.addAction(action) view_menu = menubar.addMenu("&View") for d in ({ "name": u"Zoom &In", "shct": "Ctrl++", "func": self.zoom_in }, { "name": u"Zoom &Out", "shct": "Ctrl+-", "func": self.zoom_out }, { "name": u"&Actual Size", "shct": "Ctrl+0", "func": self.zoom_reset }): action = QAction(d["name"], self) action.setShortcut(QKeySequence(d["shct"])) action.triggered.connect(d["func"]) view_menu.addAction(action) style_menu = menubar.addMenu("&Style") style_menu.setStyleSheet("menu-scrollable: 1") style_menu.setDisabled(True) if os.path.exists(css_dir): files = sorted(os.listdir(css_dir)) files = [f for f in files if f.endswith(".css")] if len(files) > 0: style_menu.setDisabled(False) group = QActionGroup(self, exclusive=True) for i, f in enumerate(files, start=1): name = os.path.splitext(f)[0].replace("&", "&&") action = group.addAction(QtWidgets.QAction(name, self)) action.triggered.connect(lambda x, stylesheet=f: self. set_stylesheet(self, stylesheet)) if i < 10: action.setShortcut(QKeySequence("Ctrl+%d" % i)) action.setCheckable(True) style_menu.addAction(action) if f == self.stylesheet: action.trigger() help_menu = menubar.addMenu("&Help") for d in ( { "name": u"&About...", "func": self.about }, { "name": u"&Report an Issue", "func": self.report_issue }, ): action = QAction(d["name"], self) action.triggered.connect(d["func"]) help_menu.addAction(action) # Redefine reload action reload_action = self.web_view.page().action(QWebPage.Reload) reload_action.setShortcut(QKeySequence.Refresh) reload_action.triggered.connect(self.thread1.run) self.web_view.addAction(reload_action) # Define additional shortcuts QShortcut(QKeySequence("j"), self, activated=self.scroll_down) QShortcut(QKeySequence("k"), self, activated=self.scroll_up) QShortcut(QKeySequence("t"), self, activated=self.toggle_toc)
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) 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 idx == 0: act.setChecked(True) keymap_group.addAction(act) keyboard_layout_menu.addAction(act) 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.settings.value("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)
def setupLanguageActions(self): languageActionGroup = QActionGroup(self) languageActionGroup.addAction(self.actionEnglish) languageActionGroup.addAction(self.actionGerman) languageActionGroup.setExclusive(True)
def __init__(self, parent=None): super().__init__(parent) self.pyPath = '' self.pyName = '' self.icoPath = '' self.icoName = '' self.specName = '' self.MoveTo = '' self.PassWord = '******' self.SettingName = 'Settings.json' self.ColorW = [240, 240, 240, 255] self.ColorT = [0, 0, 0, 255] self.argv = sys.argv self.stayPath = os.path.dirname(os.path.realpath(sys.argv[0])) self.ui = Ui_MainWindow() self.ui.setupUi(self) self.ui.linePath.setReadOnly(True) self.ui.pBtnOpen.setEnabled(False) self.ui.linePath.setFocusPolicy(Qt.NoFocus) self.ui.pBtnOK.setShortcut(Qt.Key_Return) self.BarLable = QLabel('注意:在使用本程序之前,请确认已经配置好Python的环境变量' '以及安装pyinstaller。') self.ui.statusBar.addWidget(self.BarLable, stretch=0) actGroup = QActionGroup(self) actGroup.addAction(self.ui.actDall) actGroup.addAction(self.ui.actDbootloader) actGroup.addAction(self.ui.actDimports) actGroup.addAction(self.ui.actDnoarchive) actGroup.setExclusive(True) try: self.LoadSettings() except KeyError: pass if not os.path.exists(self.stayPath+'\\Temporary.json'): if self.ui.actRigthMenu.isChecked(): if len(self.argv) == 2: Path = self.argv[1] if os.path.isfile(Path): if os.path.splitext(Path)[1].lower() == '.py': self.pyName = Path.split('\\')[-1] self.ui.linePath.setText(Path) else: self.pyName = '' self.ui.linePath.clear() self.pyPath = os.path.dirname(Path) else: self.pyPath = Path self.pyName = '' self.ui.linePath.clear() else: self.pyPath = os.getcwd() self.pyName = '' self.ui.linePath.clear() self.ui.pBtnOpen.setEnabled(True) if self.pyPath != "": if os.path.exists(self.pyPath): self.ui.actCleanPyo.setEnabled(True) self.ui.actCleanSpec.setEnabled(True) self.Thread = QmyTread() self.Thread.finish.connect(self.do_Finish)
class PyScientificSpinBox(QDoubleSpinBox): # This is emited like valueChanged, but not when using setValue_quiet and not within valueChange_gui_time valueChanged_gui = pyqtSignal(float) # For PyQt < 5.11: only a class with attributes using integer can be made an enum # (so simple object subclass) # from 5.11: Enum class can also be used. (also Q_ENUMS, Q_FLAGS are depracated for Q_ENUM and Q_FLAG) # Version can be obtain as: # import PyQt5.QtCore # PyQt5.QtCore.PYQT_VERSION_STR #class Sci_Mode(Enum): class Sci_Mode(object): ENG_UNIT, ENG, SCI = range(3) Q_ENUMS(Sci_Mode) class Enabled_Controls(object): none = 0x00 minimum = 0x01 maximum = 0x02 min_increment = 0x04 precision = 0x08 display_mode = 0x10 log_increment_mode = 0x20 fixed_exponent = 0x40 all = 0x7f minmax = 0x03 no_minmax = all & ~minmax Q_FLAGS(Enabled_Controls) def __init__(self, *args, **kwargs): """ display_mode can be any of the Sci_Mode enum: ENG_UNIT (default), ENG or SCI To have a unit, set the suffix. keyboardTracking is disabled by default. accelerated is enabled by default decimals is overriden and redirected to precision. A new signal, valueChanged_gui, is emitted when the value is updated but at a restricted rate, valueChange_gui_time, and also when pressing enter. It is not emitted when using setValue_quiet option disable_context_menu can be used to disable the context menu. It is False by default. enabled_controls property can be used to disable some entries in the context_menu """ self._initializing = True self._in_setValue_quiet = False key_track = kwargs.pop('keyboardTracking', False) kwargs['keyboardTracking'] = key_track # need to handle min/max before decimal self._disable_context_menu = kwargs.pop('disable_context_menu', False) maximum = kwargs.pop('maximum', np.inf) minimum = kwargs.pop('minimum', -np.inf) accelerated = kwargs.pop('accelerated', True) decimals = kwargs.pop('decimals', 5) precision = kwargs.pop('precision', decimals) self.text_formatter = Format_Float() self.text_formatter.precision = precision self.validator = FloatValidator() self._min_increment = 1e-15 self._log_increment_mode = False self._log_increment = 0.01 self._display_mode = self.Sci_Mode.ENG_UNIT self._enabled_controls = self.Enabled_Controls.all self._last_enabled_controls = None self._valueChange_timer = QTimer() self._valueChange_timer.setInterval(100) # ms # We override decimals. This should set the parent value. kwargs[ 'decimals'] = 1000 # this get adjusted to max double precision (323) kwargs['accelerated'] = accelerated kwargs['minimum'] = minimum kwargs['maximum'] = maximum # Note that this will call all properties that are left in kwargs super(PyScientificSpinBox, self).__init__(*args, **kwargs) # decimals is used when changing min/max, on setValue and on converting value to Text (internally, it is overriden here) #super(PyScientificSpinBox, self).setDecimals(1000) # this get adjusted to max double precision (323) # make sure parent class decimals is set properly if super(PyScientificSpinBox, self).decimals() < 323: raise RuntimeError('Unexpected parent decimals value.') # The suffix/prefix change in the above QDoubleSpinBox.__init__ did not call our override # lets update them now self.setSuffix(self.suffix()) self.setPrefix(self.prefix()) # pyqt5 5.6 on windows (at least) QAction requires the parent value. self.copyAction = QAction('&Copy', None, shortcut=QKeySequence(QKeySequence.Copy), triggered=self.handle_copy) self.pasteAction = QAction('&Paste', None, shortcut=QKeySequence(QKeySequence.Paste), triggered=self.handle_paste) self.addActions((self.copyAction, self.pasteAction)) self._create_menu_init() self.clipboard = QApplication.instance().clipboard() self._last_valueChanged_gui = self.value() self._current_valueChanged_gui = self._last_valueChanged_gui self.valueChanged.connect(self._valueChanged_helper) self._valueChange_timer.timeout.connect(self._do_valueChanged_gui_emit) self._last_focus_reason_mouse = False self.lineEdit().installEventFilter(self) self.text_is_being_changed_state = False self.lineEdit().textEdited.connect(self.text_is_being_changed) self._last_key_pressed_is_enter = False self.editingFinished.connect(self.editFinished_slot) self._in_config_menu = False self._initializing = False def _create_menu_init(self): if self._disable_context_menu: return # pyqt5 5.6 on windows (at least) QAction requires the parent value. self.accel_action = QAction('&Acceleration', None, checkable=True, checked=self.isAccelerated(), triggered=self.setAccelerated) self.log_increment_mode_action = QAction( '&Log increment', None, checkable=True, checked=self.log_increment_mode, triggered=self.log_increment_mode_change) self.help_action = QAction('&Help', None, triggered=self.help_dialog) self.stepUpAction = QAction('Step &up', None) self.stepUpAction.triggered.connect(self.stepUp) self.stepDownAction = QAction('Step &down', None) self.stepDownAction.triggered.connect(self.stepDown) self.display_mode_grp = QActionGroup(None) self.display_mode_eng_unit = QAction('Engineering with &units', None, checkable=True) self.display_mode_eng = QAction('&Engineering', None, checkable=True) self.display_mode_sci = QAction('&Scientific', None, checkable=True) self.display_mode_option = { self.Sci_Mode.ENG_UNIT: self.display_mode_eng_unit, self.Sci_Mode.ENG: self.display_mode_eng, self.Sci_Mode.SCI: self.display_mode_sci } self.display_mode_grp.addAction(self.display_mode_eng_unit) self.display_mode_grp.addAction(self.display_mode_eng) self.display_mode_grp.addAction(self.display_mode_sci) self.display_mode_grp.triggered.connect(self.handle_display_mode) # fixed exponent self.fixed_exponent_menu = submenu = QMenu('&Fixed exponent') self.fixed_exponent_group = subgroup = QActionGroup(None) self.fixed_exponent_actions = actions = {} for i in range(-15, 9 + 1, 3)[::-1]: if i == 0: act = QAction('%i: %s' % (i, ''), None, checkable=True) else: act = QAction('%i: %s' % (i, units_inverse[i]), None, checkable=True) submenu.addAction(act) subgroup.addAction(act) actions[i] = act act = QAction('&Custom', None, checkable=True) submenu.addAction(act) subgroup.addAction(act) actions['custom'] = act self.fixed_exponent_custom_action = QWidgetAction(None) self.fixed_exponent_custom_widget = QSpinBox(value=0, minimum=-MAX_FIXED_EXP, maximum=MAX_FIXED_EXP) self.fixed_exponent_custom_action.setDefaultWidget( self.fixed_exponent_custom_widget) submenu.addAction(self.fixed_exponent_custom_action) submenu.addSeparator() act = QAction('&Disabled', None, checkable=True) submenu.addAction(act) subgroup.addAction(act) actions['disabled'] = act subgroup.triggered.connect(self.handle_fixed_exponent) self.fixed_exponent_custom_widget.valueChanged.connect( self.handle_fixed_exponent) # precision self.precision_menu = submenu = QMenu('&Precision') self.precision_group = subgroup = QActionGroup(None) self.precision_actions = actions = {} for i in range(0, 10): act = QAction('&%i' % i, None, checkable=True) submenu.addAction(act) subgroup.addAction(act) actions[i] = act act = QAction('&Custom', None, checkable=True) submenu.addAction(act) subgroup.addAction(act) actions['custom'] = act self.precision_custom_action = QWidgetAction(None) self.precision_custom_widget = QSpinBox(value=0, minimum=0, maximum=MAX_PRECISION) self.precision_custom_action.setDefaultWidget( self.precision_custom_widget) submenu.addAction(self.precision_custom_action) subgroup.triggered.connect(self.handle_precision) self.precision_custom_widget.valueChanged.connect( self.handle_precision) # min, max, min_increment self.min_custom_action = QWidgetAction(None) self.min_custom_widget = PyScientificSpinBox(prefix='min: ', disable_context_menu=True) self.min_custom_action.setDefaultWidget(self.min_custom_widget) self.min_custom_widget.valueChanged.connect( self.handle_min_custom_widget) self.max_custom_action = QWidgetAction(None) self.max_custom_widget = PyScientificSpinBox(prefix='max: ', disable_context_menu=True) self.max_custom_action.setDefaultWidget(self.max_custom_widget) self.max_custom_widget.valueChanged.connect( self.handle_max_custom_widget) self.min_incr_custom_action = QWidgetAction(None) self.min_incr_custom_widget = PyScientificSpinBox( prefix='min incr: ', disable_context_menu=True) self.min_incr_custom_action.setDefaultWidget( self.min_incr_custom_widget) self.min_incr_custom_widget.valueChanged.connect( self.handle_min_incr_custom_widget) self.display_mode_sep = QAction('Display mode', None) self.display_mode_sep.setSeparator(True) def text_is_being_changed(self): self.text_is_being_changed_state = True def _create_menu(self): ec = self.enabled_controls EC = self.Enabled_Controls if ec == self._last_enabled_controls: return self.modified_context_menu self._last_enabled_controls = ec #self.modified_context_menu = menu = self.lineEdit().createStandardContextMenu() self.modified_context_menu = menu = QMenu('Main Menu') menu.addActions((self.copyAction, self.pasteAction)) menu.addSeparator() menu.addActions((self.stepUpAction, self.stepDownAction)) menu.addSeparator() if ec & EC.display_mode: menu.addActions((self.display_mode_sep, self.display_mode_eng_unit, self.display_mode_eng, self.display_mode_sci)) menu.addSeparator() menu.addAction(self.accel_action) if ec & EC.log_increment_mode: menu.addAction(self.log_increment_mode_action) if ec & EC.fixed_exponent: menu.addMenu(self.fixed_exponent_menu) if ec & EC.precision: menu.addMenu(self.precision_menu) menu.addActions((self.min_custom_action, self.max_custom_action, self.min_incr_custom_action)) self.min_custom_action.setEnabled(ec & EC.minimum) self.max_custom_action.setEnabled(ec & EC.maximum) self.min_incr_custom_action.setEnabled(ec & EC.min_increment) menu.addSeparator() menu.addAction(self.help_action) return menu #def __del__(self): # print('Deleting up PyScientificSpinBox') def pyqtConfigure(self, **kwargs): # this is necessary to override pyqtConfigure decimals # otherwise it calls the parent one directly. decimals = kwargs.pop('decimals', None) if decimals is not None: self.setDecimals(decimals) # same thing for suffix/prefix prefix = kwargs.pop('prefix', None) if prefix is not None: self.setPrefix(prefix) suffix = kwargs.pop('suffix', None) if suffix is not None: self.setSuffix(suffix) if len(kwargs): super(PyScientificSpinBox, self).pyqtConfigure(**kwargs) def get_config(self): """ The return value can by used in pyqtConfigure. """ return dict(display_mode=self.display_mode, precision=self.precision, min_increment=self.min_increment, minimum=self.minimum(), maximum=self.maximum(), log_increment_mode=self.log_increment_mode, log_increment=self.log_increment, fixed_exponent=self.fixed_exponent, suffix=self.suffix(), prefix=self.prefix(), singleStep=self.singleStep(), accelerated=self.isAccelerated(), wrapping=self.wrapping(), keyboardTracking=self.keyboardTracking()) def force_update(self): val = self.value() self.setValue(val) self.selectAll() def decimals(self): return self.precision def setDecimals(self, val): self.precision = val @pyqtProperty(Enabled_Controls) def enabled_controls(self): return self._enabled_controls @enabled_controls.setter def enabled_controls(self, val): self._enabled_controls = val @pyqtProperty(Sci_Mode) #@pyqtProperty(int) def display_mode(self): return self._display_mode @display_mode.setter def display_mode(self, val): self._display_mode = val self.force_update() self.updateGeometry() @pyqtProperty(int) def min_increment(self): return self._min_increment @min_increment.setter def min_increment(self, val): self._min_increment = val if self.singleStep() < val: self.setSingleStep(val) @pyqtProperty(int) def precision(self): return self.text_formatter.precision @precision.setter def precision(self, val): val = min(max(val, 0), MAX_PRECISION) self.text_formatter.precision = val self.force_update() self.updateGeometry() @pyqtProperty(bool) def log_increment_mode(self): return self._log_increment_mode @log_increment_mode.setter def log_increment_mode(self, val): self._log_increment_mode = val @pyqtProperty(float) def log_increment(self): return self._log_increment @log_increment.setter def log_increment(self, val): self._log_increment = val @pyqtProperty(int) def fixed_exponent(self): """ 9999 disables fixed exponent mode """ val = self.text_formatter.fixed_exponent if val is None: return 9999 return val @fixed_exponent.setter def fixed_exponent(self, val): if val == 9999: val = None else: # keep within -308 to 308: MAX_FIXED_EXP = 308 val = max(val, -MAX_FIXED_EXP) val = min(val, MAX_FIXED_EXP) self.text_formatter.fixed_exponent = val self.force_update() self.updateGeometry() @pyqtProperty(float) def valueChange_gui_time(self): return self._valueChange_timer.interval() / 1e3 @valueChange_gui_time.setter def valueChange_gui_time(self, val): val = int(val * 1e3) if val <= 0: self._valueChange_timer.stop() self._valueChange_timer.setInterval(val) def keyPressEvent(self, event): key = event.key() self._last_key_pressed_is_enter = False if event.matches(QKeySequence.Copy): #print('doing copy') self.copyAction.trigger() elif event.matches(QKeySequence.Paste): #print('doing paste') self.pasteAction.trigger() elif key == QtCore.Qt.Key_Escape: if self.text_is_being_changed_state: #print('doing escape') self.setValue(self.value()) else: #print('letting escape propagate') event.ignore() elif key in [QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return]: self._last_key_pressed_is_enter = True text_was_being_changed_state = self.text_is_being_changed_state super(PyScientificSpinBox, self).keyPressEvent(event) # default handling does event.ignore() so event, after doing something here # propagate. only do that if test is not being changed. if text_was_being_changed_state: event.accept() else: super(PyScientificSpinBox, self).keyPressEvent(event) # Can't seem to do the same thing (stop short timer) for wheelEvent def keyReleaseEvent(self, event): if not event.isAutoRepeat() and event.key() in [ QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown ]: self._valueChange_timer.stop() self._do_valueChanged_gui_emit() super(PyScientificSpinBox, self).keyReleaseEvent(event) def mouseReleaseEvent(self, event): if event.button() == QtCore.Qt.LeftButton: self._valueChange_timer.stop() self._do_valueChanged_gui_emit() super(PyScientificSpinBox, self).mouseReleaseEvent(event) def mousePressEvent(self, event): self._last_focus_reason_mouse = False super(PyScientificSpinBox, self).mousePressEvent(event) def focusInEvent(self, event): if event.reason() == QtCore.Qt.MouseFocusReason: self._last_focus_reason_mouse = True l = self.lineEdit() super(PyScientificSpinBox, self).focusInEvent(event) # Using tab to activate widget already does this. # It is necessary so that pressing the up/down buttons directly from another widget # does not increment before the unit (because position is 0, selection is False) self.selectAll() def editFinished_slot(self): self.text_is_being_changed_state = False if self._last_key_pressed_is_enter: self._last_key_pressed_is_enter = False self._do_valueChanged_gui_emit(force=True) def eventFilter(self, watched_obj, event): if event.type( ) == QEvent.MouseButtonPress and self._last_focus_reason_mouse: self._last_focus_reason_mouse = False return True return False def handle_copy(self): #print('handling copy') self.clipboard.setText('%r' % self.value()) def handle_paste(self): #print('handling paste') text = self.clipboard.text() # let user decide if it is ok by pressing ok (not esc) self.lineEdit().setText(text) self.text_is_being_changed_state = True # update value immediately #try: # self.setValue(float(text)) #except ValueError: # maybe text is not pure number (contains units) # self.lineEdit().setText(text) # self.interpretText() def handle_display_mode(self, action): if self._in_config_menu: return mode = [k for k, v in self.display_mode_option.items() if v == action][0] self.display_mode = mode def handle_fixed_exponent(self, action_or_val): if self._in_config_menu: return if isinstance(action_or_val, QAction): enable = False action = [ k for k, v in self.fixed_exponent_actions.items() if v == action_or_val ][0] if action == 'disabled': self.fixed_exponent = 9999 elif action == 'custom': enable = True self.fixed_exponent = self.fixed_exponent_custom_widget.value() else: self.fixed_exponent = action self.fixed_exponent_custom_action.setEnabled(enable) else: self.fixed_exponent = action_or_val def handle_precision(self, action_or_val): if self._in_config_menu: return if isinstance(action_or_val, QAction): enable = False action = [ k for k, v in self.precision_actions.items() if v == action_or_val ][0] if action == 'custom': enable = True self.precision = self.precision_custom_widget.value() else: self.precision = action self.precision_custom_action.setEnabled(enable) else: self.precision = action_or_val def handle_min_custom_widget(self, val): if self._in_config_menu: return self.setMinimum(val) def handle_max_custom_widget(self, val): if self._in_config_menu: return self.setMaximum(val) def handle_min_incr_custom_widget(self, val): if self._in_config_menu: return self.min_increment = val def setSuffix(self, string): self.validator.suffix = string super(PyScientificSpinBox, self).setSuffix(string) self.updateGeometry() def setPrefix(self, string): self.validator.prefix = string super(PyScientificSpinBox, self).setPrefix(string) self.updateGeometry() def contextMenuEvent(self, event): if self._disable_context_menu: return self._in_config_menu = True menu = self._create_menu() se = self.stepEnabled() self.stepUpAction.setEnabled(se & QAbstractSpinBox.StepUpEnabled) self.stepDownAction.setEnabled(se & QAbstractSpinBox.StepDownEnabled) mode = self.display_mode self.display_mode_option[mode].setChecked(True) self.log_increment_mode_action.setChecked(self.log_increment_mode) self.accel_action.setChecked(self.isAccelerated()) # fixed_exponent fe = self.fixed_exponent if fe == 9999: fe = 'disabled' action = self.fixed_exponent_actions.get(fe, None) fe_action_custom = self.fixed_exponent_actions['custom'] if fe_action_custom.isChecked() or action is None: action = fe_action_custom self.fixed_exponent_custom_action.setEnabled(True) self.fixed_exponent_custom_widget.setValue(fe) else: self.fixed_exponent_custom_action.setEnabled(False) action.setChecked(True) # precision prec = self.precision action = self.precision_actions.get(prec, None) prec_action_custom = self.precision_actions['custom'] if prec_action_custom.isChecked() or action is None: action = prec_action_custom self.precision_custom_action.setEnabled(True) self.precision_custom_widget.setValue(prec) else: self.precision_custom_action.setEnabled(False) action.setChecked(True) #min conf = self.get_config() del conf['prefix'], conf['maximum'], conf['minimum'], conf[ 'min_increment'] conf['keyboardTracking'] = False self.min_custom_widget.pyqtConfigure(value=self.minimum(), **conf) self.max_custom_widget.pyqtConfigure(value=self.maximum(), **conf) self.min_incr_custom_widget.pyqtConfigure(value=self.min_increment, minimum=0., **conf) self._in_config_menu = False menu.exec_(event.globalPos()) def validate(self, text, position): if self._initializing: return QValidator.Invalid, text, position return self.validator.validate(text, position) def fixup(self, text): return self.validator.fixup(text) def valueFromText(self, text): val, unit, scale = self.validator.valueFromText(text) if unit: if val == 0: self.text_formatter.decode_eng(scale) # update last_exp return val def textFromValue(self, value, tmp=False): #mode = self.display_mode_grp.checkedAction() mode = self.display_mode fmt = self.text_formatter fmt_func_d = { self.Sci_Mode.ENG_UNIT: fmt.to_eng_unit, self.Sci_Mode.ENG: fmt.to_eng, self.Sci_Mode.SCI: fmt.to_float } suffix = self.suffix() ret = fmt_func_d[mode](value, suffix, tmp) return ret # this is needed because setAccelerated is not a slot, which makes # using it connect block deletes. wrapping it in python makes it work properly. def setAccelerated(self, val): if self._in_config_menu: return super(PyScientificSpinBox, self).setAccelerated(val) def log_increment_mode_change(self, val): if self._in_config_menu: return self.log_increment_mode = val def setSingleStep_bounded(self, incr): incr = max(incr, self.min_increment) self.setSingleStep(incr) @pyqtSlot(int) def stepBy(self, steps): pos = self.lineEdit().cursorPosition() if not self.lineEdit().hasSelectedText(): text = self.lineEdit().text() special = self.specialValueText() if not (special and special == text): incr, log_incr = self.validator.find_increment(text, pos) self.setSingleStep_bounded(incr) if log_incr is not None: self.log_increment = log_incr if self.log_increment_mode: #frac = self._adapative_step_frac #frac = .01 frac = self.log_increment frac_exp = np.floor(np.log10(frac)) cval = self.value() step_dir = np.sign(steps) val_dir = np.sign(cval) min_increment = self.min_increment for i in range(np.abs(steps)): cval = self.value() val_dir = np.sign(cval) abs_cval = np.abs(cval) abs_increase = val_dir == step_dir if cval == 0: incr = min_increment elif abs_cval < min_increment: if abs_increase: incr = min_increment - cval else: #incr = abs_cval self.setValue(0) continue else: incr = abs_cval * frac cval_exp = np.log10(abs_cval) incr_exp = np.floor(cval_exp + frac_exp) incr = 10.**incr_exp if not abs_increase: incr_1 = 10.**(incr_exp - 1) if np.log10(np.abs(cval) - incr_1) < np.floor(cval_exp): incr = incr_1 self.setSingleStep_bounded(incr) super(PyScientificSpinBox, self).stepBy(step_dir) else: super(PyScientificSpinBox, self).stepBy(steps) def _do_valueChanged_gui_emit(self, val=None, force=False): if val is None: val = self._current_valueChanged_gui if val != self._last_valueChanged_gui or force: self._last_valueChanged_gui = val self.valueChanged_gui.emit(val) else: self._valueChange_timer.stop() @pyqtSlot(float) def _valueChanged_helper(self, val): if not self._in_setValue_quiet: self._current_valueChanged_gui = val if not self._valueChange_timer.isActive(): self._do_valueChanged_gui_emit(val) if self.valueChange_gui_time > 0: self._valueChange_timer.start() def setValue_quiet(self, val): self._in_setValue_quiet = True self.setValue(val) self._in_setValue_quiet = False def _sizeHint_helper(self, minimum=False): self.ensurePolished() # code from widgets/qabstractspinbox.cpp fm = self.fontMetrics() # migth need a copy try: fmw = fm.horizontalAdvance except AttributeError: fmw = fm.width h = self.lineEdit().sizeHint().height() w = 0 if minimum: fixedContent = self.prefix() + " " else: fixedContent = self.prefix() + self.suffix() + " " val = -988.888e-99 if self.fixed_exponent != 9999: if self.maximum() != np.inf: val = self.maximum() else: val = -999888. * 10.**self.fixed_exponent s = self.textFromValue(val, tmp=True) s += fixedContent w = fmw(s) special = self.specialValueText() if special: w = max(w, fmw(special)) w += 2 # cursor blinking space hint = QSize(w, h) opt = QStyleOptionSpinBox() self.initStyleOption(opt) style = self.style() hint = style.sizeFromContents(style.CT_SpinBox, opt, QSize(w, h), self) qapp = QApplication.instance() hint = hint.expandedTo(qapp.globalStrut()) return hint def minimumSizeHint(self): return self._sizeHint_helper(minimum=True) def sizeHint(self): return self._sizeHint_helper() # Observations from QDoubleSpinBox code: # minimumSizeHint is the same as SizeHint less the space for suffix # sizeHint # # internal commands is space of prefix+suffix+max(min, max, special)+graphical elements # # note that text is limited to 18 char. # sizeHint is recalculated and advertised when calling setSuffix (doubleSpinBox.setPrefix seems to only update text) # aslo setMinimum, setMaximum, setRange all update the geometry # setSpecialValueText, setValue, setSingleStep and setSuffix all update the text help_text = u""" For entry you can use scientific entry (like 1e-3) or units. You can add spaces. The available units are (they are case sensitive): Y(1e24), Z, E, P, T, G, M, k(1e3), h(100), da(10), d(0.1), c, m, µ, n, p, f, a, z, y(1e-24) you can use u instead of µ, and b for no unit (1) The arrows, or the mouse wheel steps the value. Page up/down steps the value 10 times faster. Using the CTRL key also goes 10 times faster but only for wheel events. If you change the cursor position (left/right keys or mouse click) before stepping, the number to the left of the cursor is incremented by 1. Adapatative option will increase in a logarithmic way. Acceleration is a speed up of the steps when pressing the GUI button. """ def help_dialog(self): dialog = QMessageBox.information(self, 'Entry Help', self.help_text)
def __init__(self, pointSize, parent=None): super(FontToolBar, self).__init__(parent) auxiliaryWidth = self.fontMetrics().width('0') * 8 self.leftTextField = QLineEdit(self) self.leftTextField.setMaximumWidth(auxiliaryWidth) self.textField = QComboBox(self) self.textField.setEditable(True) completer = self.textField.completer() completer.setCaseSensitivity(Qt.CaseSensitive) self.textField.setCompleter(completer) # XXX: had to use Maximum because Preferred did entend the widget(?) self.textField.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) items = QSettings().value("metricsWindow/comboBoxItems", comboBoxItems, str) self.textField.addItems(items) self.rightTextField = QLineEdit(self) self.rightTextField.setMaximumWidth(auxiliaryWidth) self.leftTextField.textEdited.connect(self.textField.editTextChanged) self.rightTextField.textEdited.connect(self.textField.editTextChanged) self.comboBox = QComboBox(self) self.comboBox.setEditable(True) self.comboBox.setValidator(QIntValidator(self)) for p in pointSizes: self.comboBox.addItem(str(p)) self.comboBox.setEditText(str(pointSize)) self.configBar = QPushButton(self) self.configBar.setFlat(True) self.configBar.setIcon(QIcon(":/resources/settings.svg")) self.configBar.setStyleSheet("padding: 2px 0px; padding-right: 10px") self.toolsMenu = QMenu(self) showKerning = self.toolsMenu.addAction( "Show Kerning", self.showKerning) showKerning.setCheckable(True) showMetrics = self.toolsMenu.addAction( "Show Metrics", self.showMetrics) showMetrics.setCheckable(True) self.toolsMenu.addSeparator() wrapLines = self.toolsMenu.addAction("Wrap lines", self.wrapLines) wrapLines.setCheckable(True) noWrapLines = self.toolsMenu.addAction("No wrap", self.noWrapLines) noWrapLines.setCheckable(True) self.toolsMenu.addSeparator() verticalFlip = self.toolsMenu.addAction( "Vertical flip", self.verticalFlip) verticalFlip.setCheckable(True) """ lineHeight = QWidgetAction(self.toolsMenu) lineHeight.setText("Line height:") lineHeightSlider = QSlider(Qt.Horizontal, self) # QSlider works with integers so we'll just divide by 100 what comes # out of it lineHeightSlider.setMinimum(80) lineHeightSlider.setMaximum(160) lineHeightSlider.setValue(100) #lineHeightSlider.setContentsMargins(30, 0, 30, 0) lineHeightSlider.valueChanged.connect(self.lineHeight) lineHeight.setDefaultWidget(lineHeightSlider) self.toolsMenu.addAction(lineHeight) """ wrapLinesGroup = QActionGroup(self) wrapLinesGroup.addAction(wrapLines) wrapLinesGroup.addAction(noWrapLines) wrapLines.setChecked(True) # self.toolsMenu.setActiveAction(wrapLines) self.configBar.setMenu(self.toolsMenu) self.addWidget(self.leftTextField) self.addWidget(self.textField) self.addWidget(self.rightTextField) self.addWidget(self.comboBox) self.addWidget(self.configBar)
class MainGlyphWindow(QMainWindow): def __init__(self, glyph, parent=None): super().__init__(parent) menuBar = self.menuBar() fileMenu = QMenu("&File", self) fileMenu.addAction("E&xit", self.close, QKeySequence.Quit) menuBar.addMenu(fileMenu) editMenu = QMenu("&Edit", self) self._undoAction = editMenu.addAction( "&Undo", self.undo, QKeySequence.Undo) self._redoAction = editMenu.addAction( "&Redo", self.redo, QKeySequence.Redo) editMenu.addSeparator() # XXX action = editMenu.addAction("C&ut", self.cutOutlines, QKeySequence.Cut) action.setEnabled(False) self._copyAction = editMenu.addAction( "&Copy", self.copyOutlines, QKeySequence.Copy) editMenu.addAction("&Paste", self.pasteOutlines, QKeySequence.Paste) editMenu.addAction( "Select &All", self.selectAll, QKeySequence.SelectAll) editMenu.addAction("&Deselect", self.deselect, "Ctrl+D") menuBar.addMenu(editMenu) glyphMenu = QMenu("&Glyph", self) glyphMenu.addAction("&Next Glyph", lambda: self.glyphOffset(1), "End") glyphMenu.addAction( "&Previous Glyph", lambda: self.glyphOffset(-1), "Home") glyphMenu.addAction("&Go To…", self.changeGlyph, "G") glyphMenu.addSeparator() self._layerAction = glyphMenu.addAction( "&Layer Actions…", self.layerActions, "L") menuBar.addMenu(glyphMenu) # create tools and buttons toolBars self._tools = [] self._toolsActionGroup = QActionGroup(self) self._toolsToolBar = QToolBar("Tools", self) self._toolsToolBar.setMovable(False) self._buttons = [] self._buttonsToolBar = QToolBar("Buttons", self) self._buttonsToolBar.setMovable(False) self.addToolBar(self._toolsToolBar) self.addToolBar(self._buttonsToolBar) # http://www.setnode.com/blog/right-aligning-a-button-in-a-qtoolbar/ self._layersToolBar = QToolBar("Layers", self) spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._currentLayerBox = QComboBox(self) self._currentLayerBox.currentIndexChanged.connect( self._layerChanged) self._layersToolBar.addWidget(spacer) self._layersToolBar.addWidget(self._currentLayerBox) self._layersToolBar.setContentsMargins(0, 0, 2, 0) self._layersToolBar.setMovable(False) self.addToolBar(self._layersToolBar) viewMenu = self.createPopupMenu() viewMenu.setTitle("View") viewMenu.addSeparator() action = viewMenu.addAction("Lock Toolbars", self.lockToolBars) action.setCheckable(True) action.setChecked(True) menuBar.addMenu(viewMenu) self.view = GlyphView(self) self.setGlyph(glyph) selectionTool = self.installTool(SelectionTool) selectionTool.trigger() self.installTool(PenTool) self.installTool(RulerTool) self.installTool(KnifeTool) self.installButton(RemoveOverlapButton) self.setCentralWidget(self.view.scrollArea()) self.resize(900, 700) self.view.setFocus(True) # ---------- # Menu items # ---------- def glyphOffset(self, offset): currentGlyph = self.view.glyph() font = currentGlyph.font glyphOrder = font.glyphOrder # should be enforced in fontView already if not (glyphOrder and len(glyphOrder)): return index = glyphOrder.index(currentGlyph.name) newIndex = (index + offset) % len(glyphOrder) glyph = font[glyphOrder[newIndex]] self.setGlyph(glyph) def changeGlyph(self): glyph = self.view.glyph() newGlyph, ok = GotoDialog.getNewGlyph(self, glyph) if ok and newGlyph is not None: self.setGlyph(newGlyph) def layerActions(self): glyph = self.view.glyph() newLayer, action, ok = LayerActionsDialog.getLayerAndAction( self, glyph) if ok and newLayer is not None: # TODO: whole glyph for now, but consider selection too if not glyph.name in newLayer: newLayer.newGlyph(glyph.name) otherGlyph = newLayer[glyph.name] otherGlyph.disableNotifications() if action == "Swap": tempGlyph = TGlyph() otherGlyph.drawPoints(tempGlyph.getPointPen()) tempGlyph.width = otherGlyph.width otherGlyph.clearContours() glyph.drawPoints(otherGlyph.getPointPen()) otherGlyph.width = glyph.width if action != "Copy": glyph.disableNotifications() glyph.clearContours() if action == "Swap": tempGlyph.drawPoints(glyph.getPointPen()) glyph.width = tempGlyph.width glyph.enableNotifications() otherGlyph.enableNotifications() def undo(self): glyph = self.view.glyph() glyph.undo() def redo(self): glyph = self.view.glyph() glyph.redo() def cutOutlines(self): pass def copyOutlines(self): glyph = self.view.glyph() clipboard = QApplication.clipboard() mimeData = QMimeData() copyGlyph = glyph.getRepresentation("defconQt.FilterSelection") mimeData.setData("application/x-defconQt-glyph-data", pickle.dumps([copyGlyph.serialize( blacklist=("name", "unicode") )])) clipboard.setMimeData(mimeData) def pasteOutlines(self): glyph = self.view.glyph() clipboard = QApplication.clipboard() mimeData = clipboard.mimeData() if mimeData.hasFormat("application/x-defconQt-glyph-data"): data = pickle.loads(mimeData.data( "application/x-defconQt-glyph-data")) if len(data) == 1: pen = glyph.getPointPen() pasteGlyph = TGlyph() pasteGlyph.deserialize(data[0]) # TODO: if we serialize selected state, we don't need to do # this pasteGlyph.selected = True if len(pasteGlyph) or len(pasteGlyph.components) or \ len(pasteGlyph.anchors): glyph.prepareUndo() pasteGlyph.drawPoints(pen) def selectAll(self): glyph = self.view.glyph() glyph.selected = True if not len(glyph): for component in glyph.components: component.selected = True def deselect(self): glyph = self.view.glyph() for anchor in glyph.anchors: anchor.selected = False for component in glyph.components: component.selected = False glyph.selected = False def lockToolBars(self): action = self.sender() movable = not action.isChecked() for toolBar in ( self._toolsToolBar, self._buttonsToolBar, self._layersToolBar): toolBar.setMovable(movable) # -------------------------- # Tools & buttons management # -------------------------- def installTool(self, tool): # TODO: add shortcut with number action = self._toolsToolBar.addAction( QIcon(tool.iconPath), tool.name, self._setViewTool) action.setCheckable(True) action.setData(len(self._tools)) self._toolsActionGroup.addAction(action) self._tools.append(tool(parent=self.view)) return action def uninstallTool(self, tool): pass # XXX def _setViewTool(self): action = self.sender() action.setChecked(True) index = action.data() self.view.currentTool = self._tools[index] def installButton(self, button): action = self._buttonsToolBar.addAction( QIcon(button.iconPath), button.name, self._buttonAction) action.setData(len(self._buttons)) self._buttons.append(button(parent=self.view)) return action def uninstallButton(self, button): pass # XXX def _buttonAction(self): action = self.sender() index = action.data() button = self._buttons[index] button.clicked() # -------------------- # Notification support # -------------------- # glyph def _subscribeToGlyph(self, glyph): if glyph is not None: glyph.addObserver(self, "_glyphChanged", "Glyph.Changed") glyph.addObserver(self, "_glyphNameChanged", "Glyph.NameChanged") glyph.addObserver( self, "_glyphSelectionChanged", "Glyph.SelectionChanged") undoManager = glyph.undoManager undoManager.canUndoChanged.connect(self._undoAction.setEnabled) undoManager.canRedoChanged.connect(self._redoAction.setEnabled) self._subscribeToFontAndLayerSet(glyph.font) def _unsubscribeFromGlyph(self, glyph): if glyph is not None: glyph.removeObserver(self, "Glyph.Changed") glyph.removeObserver(self, "Glyph.NameChanged") glyph.removeObserver(self, "Glyph.SelectionChanged") undoManager = glyph.undoManager undoManager.canUndoChanged.disconnect(self._undoAction.setEnabled) undoManager.canRedoChanged.disconnect(self._redoAction.setEnabled) self._unsubscribeFromFontAndLayerSet(glyph.font) def _glyphChanged(self, notification): self.view.glyphChanged() def _glyphNameChanged(self, notification): glyph = self.view.glyph() self.setWindowTitle(glyph.name, glyph.font) def _glyphSelectionChanged(self, notification): self._updateSelection() self.view.glyphChanged() def _fontInfoChanged(self, notification): self.view.fontInfoChanged() glyph = self.view.glyph() self.setWindowTitle(glyph.name, glyph.font) # layers & font def _subscribeToFontAndLayerSet(self, font): """Note: called by _subscribeToGlyph.""" if font is None: return font.info.addObserver(self, "_fontInfoChanged", "Info.Changed") layerSet = font.layers if layerSet is None: return layerSet.addObserver(self, '_layerSetLayerDeleted', 'LayerSet.LayerDeleted') for event in ('LayerSet.LayerAdded', 'LayerSet.LayerChanged', 'LayerSet.LayerOrderChanged'): layerSet.addObserver(self, '_layerSetEvents', event) def _unsubscribeFromFontAndLayerSet(self, font): """Note: called by _unsubscribeFromGlyph.""" if font is None: return font.info.removeObserver(self, "Info.Changed") layerSet = font.layers if layerSet is None: return for event in ('LayerSet.LayerAdded', 'LayerSet.LayerChanged', 'LayerSet.LayerOrderChanged', 'LayerSet.LayerDeleted'): layerSet.removeObserver(self, event) def _layerSetEvents(self, notification): self._updateLayerControls() def _layerSetLayerDeleted(self, notification): self._layerSetEvents(notification) self._currentLayerBox.setCurrentIndex(0) # other updaters def _updateUndoRedo(self): glyph = self.view.glyph() self._undoAction.setEnabled(glyph.canUndo()) self._redoAction.setEnabled(glyph.canRedo()) def _updateSelection(self): def hasSelection(): glyph = self.view.glyph() for contour in glyph: if len(contour.selection): return True for anchor in glyph.anchors: if anchor.selected: return True for component in glyph.components: if component.selected: return True return False self._copyAction.setEnabled(hasSelection()) # -------------- # Public Methods # -------------- def setGlyph(self, glyph): currentGlyph = self.view.glyph() self._unsubscribeFromGlyph(currentGlyph) self._subscribeToGlyph(glyph) self.view.setGlyph(glyph) self._updateLayerControls() self._updateUndoRedo() self._updateSelection() self.setWindowTitle(glyph.name, glyph.font) def setDrawingAttribute(self, attr, value, layerName=None): self.view.setDrawingAttribute(attr, value, layerName) def drawingAttribute(self, attr, layerName=None): return self.view.drawingAttribute(attr, layerName) # ----------------- # Layers management # ----------------- def _layerChanged(self, newLayerIndex): glyph = self.view.glyph() layer = self._currentLayerBox.itemData(newLayerIndex) if layer is None: layer = self._makeLayer() if layer is None: # restore comboBox to active index layerSet = glyph.layerSet index = layerSet.layerOrder.index(glyph.layer.name) self._setLayerBoxIndex(index) return if glyph.name in layer: newGlyph = layer[glyph.name] else: # TODO: make sure we mimic defcon ufo3 APIs for that newGlyph = self._makeLayerGlyph(layer, glyph) self.setGlyph(newGlyph) # setting the layer-glyph here app = QApplication.instance() app.setCurrentGlyph(newGlyph) def _makeLayer(self): # TODO: what with duplicate names? glyph = self.view.glyph() newLayerName, ok = AddLayerDialog.getNewLayerName(self) if ok: layerSet = glyph.layerSet # TODO: this should return the layer layerSet.newLayer(newLayerName) return layerSet[newLayerName] else: return None def _makeLayerGlyph(self, layer, currentGlyph): glyph = layer.newGlyph(currentGlyph.name) glyph.width = currentGlyph.width glyph.template = True return glyph def _updateLayerControls(self): comboBox = self._currentLayerBox glyph = self.view.glyph() comboBox.blockSignals(True) comboBox.clear() for layer in glyph.layerSet: comboBox.addItem(layer.name, layer) comboBox.setCurrentText(glyph.layer.name) comboBox.addItem("New layer…", None) comboBox.blockSignals(False) self._layerAction.setEnabled(len(glyph.layerSet) > 1) def _setLayerBoxIndex(self, index): comboBox = self._currentLayerBox comboBox.blockSignals(True) comboBox.setCurrentIndex(index) comboBox.blockSignals(False) # --------------------- # QMainWindow functions # --------------------- def event(self, event): if event.type() == QEvent.WindowActivate: app = QApplication.instance() app.setCurrentGlyph(self.view.glyph()) return super().event(event) def closeEvent(self, event): glyph = self.view.glyph() self._unsubscribeFromGlyph(glyph) event.accept() def setWindowTitle(self, title, font=None): if font is not None: title = "%s – %s %s" % ( title, font.info.familyName, font.info.styleName) super().setWindowTitle(title)
def __init__(self): super(MainWindow, self).__init__() self.currentPath = '' self.view = SvgView() fileMenu = QMenu("&File", self) openAction = fileMenu.addAction("&Open...") openAction.setShortcut("Ctrl+O") quitAction = fileMenu.addAction("E&xit") quitAction.setShortcut("Ctrl+Q") self.menuBar().addMenu(fileMenu) viewMenu = QMenu("&View", self) self.backgroundAction = viewMenu.addAction("&Background") self.backgroundAction.setEnabled(False) self.backgroundAction.setCheckable(True) self.backgroundAction.setChecked(False) self.backgroundAction.toggled.connect(self.view.setViewBackground) self.outlineAction = viewMenu.addAction("&Outline") self.outlineAction.setEnabled(False) self.outlineAction.setCheckable(True) self.outlineAction.setChecked(True) self.outlineAction.toggled.connect(self.view.setViewOutline) self.menuBar().addMenu(viewMenu) rendererMenu = QMenu("&Renderer", self) self.nativeAction = rendererMenu.addAction("&Native") self.nativeAction.setCheckable(True) self.nativeAction.setChecked(True) if QGLFormat.hasOpenGL(): self.glAction = rendererMenu.addAction("&OpenGL") self.glAction.setCheckable(True) self.imageAction = rendererMenu.addAction("&Image") self.imageAction.setCheckable(True) if QGLFormat.hasOpenGL(): rendererMenu.addSeparator() self.highQualityAntialiasingAction = rendererMenu.addAction("&High Quality Antialiasing") self.highQualityAntialiasingAction.setEnabled(False) self.highQualityAntialiasingAction.setCheckable(True) self.highQualityAntialiasingAction.setChecked(False) self.highQualityAntialiasingAction.toggled.connect(self.view.setHighQualityAntialiasing) rendererGroup = QActionGroup(self) rendererGroup.addAction(self.nativeAction) if QGLFormat.hasOpenGL(): rendererGroup.addAction(self.glAction) rendererGroup.addAction(self.imageAction) self.menuBar().addMenu(rendererMenu) openAction.triggered.connect(self.openFile) quitAction.triggered.connect(QApplication.instance().quit) rendererGroup.triggered.connect(self.setRenderer) self.setCentralWidget(self.view) self.setWindowTitle("SVG Viewer")
def initUI(self): # TODO: initCanvas()? self.canvas = PlotCanvas(self) self.canvas.setFocusPolicy(Qt.ClickFocus) self.canvas.setFocus() # self.canvas.mpl_connect('button_press_event', self.on_press) # self.canvas.mpl_connect('button_release_event', self.on_release) # self.canvas.mpl_connect('motion_notify_event', self.on_motion) # self.canvas.mpl_connect('scroll_event', self.on_scroll) # self.canvas.mpl_connect('key_release_event', self.on_key_release) menuBar = self.menuBar() openMenu = menuBar.addMenu('Open') showMenu = menuBar.addMenu('Show') allAct = QAction("All", self) allAct.setCheckable(True) allAct.setChecked(True) allAct.triggered.connect(lambda: self.showPts(0)) missAct = QAction("Miss Clf", self) missAct.setCheckable(True) missAct.setChecked(False) missAct.triggered.connect(lambda: self.showPts(1)) noneAct = QAction("None", self) noneAct.setCheckable(True) noneAct.setChecked(False) noneAct.triggered.connect(lambda: self.showPts(2)) showMenu.addAction(allAct) showMenu.addAction(missAct) showMenu.addAction(noneAct) showMenuGroup = QActionGroup(self) showMenuGroup.addAction(allAct) showMenuGroup.addAction(missAct) showMenuGroup.addAction(noneAct) showMenuGroup.setExclusive(True) showMenu.addSeparator() # highlight action hlAct = QAction('Highlight Neighbors', self) hlAct.setShortcut("Ctrl+H") hlAct.setCheckable(True) hlAct.setChecked(False) hlAct.triggered.connect(self.setHLNeighbors) showMenu.addAction(hlAct) freezeAct = QAction('Freeze', self) freezeAct.setShortcut("Ctrl+F") freezeAct.setCheckable(True) freezeAct.setChecked(False) freezeAct.triggered.connect(self.freeze_act) showMenu.addAction(freezeAct) boundaryMapAct = QAction('Show boundary map') boundaryMapAct.setShortcut("Ctrl+B") boundaryMapAct.setCheckable(True) boundaryMapAct.setChecked(True) boundaryMapAct.triggered.connect(lambda: self.show_map(self.BMAP)) showMenu.addAction(boundaryMapAct) distMapAct = QAction('Show dist map', self) distMapAct.setShortcut("Ctrl+D") distMapAct.setCheckable(True) distMapAct.setChecked(False) distMapAct.triggered.connect(lambda: self.show_map(self.DMAP2D)) showMenu.addAction(distMapAct) distMapNDAct = QAction('Show dist map nD', self) distMapNDAct.setCheckable(True) distMapNDAct.setChecked(False) distMapNDAct.triggered.connect(lambda: self.show_map(self.DMAPND)) showMenu.addAction(distMapNDAct) distMapND2Act = QAction('Show dist map nD2', self) distMapND2Act.setCheckable(True) distMapND2Act.setChecked(False) distMapND2Act.triggered.connect(lambda: self.show_map(self.DMAPND2)) showMenu.addAction(distMapND2Act) distMapND3Act = QAction('Show dist map nD3', self) distMapND3Act.setCheckable(True) distMapND3Act.setChecked(False) distMapND3Act.triggered.connect(lambda: self.show_map(self.DMAPND3)) showMenu.addAction(distMapND3Act) distMenuGroup = QActionGroup(self) distMenuGroup.addAction(boundaryMapAct) distMenuGroup.addAction(distMapAct) distMenuGroup.addAction(distMapNDAct) distMenuGroup.addAction(distMapND2Act) distMenuGroup.addAction(distMapND3Act) openProjAct = QAction("Projection", self) openProjAct.setShortcut("Ctrl+O") openProjAct.triggered.connect(self.openProjection) saveAct = QAction('Save', self) saveAct.setShortcut("Ctrl+S") saveAct.triggered.connect(self.save_new_samples) openMenu.addAction(openProjAct) openMenu.addAction(saveAct) self.errUI = ErrUI() splitter = QSplitter(self) splitter.addWidget(self.errUI) splitter.addWidget(self.canvas) splitter.addWidget(self.newsamplesUI) self.setCentralWidget(splitter)
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() widget = QWidget() self.setCentralWidget(widget) topFiller = QWidget() topFiller.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.infoLabel = QLabel( "<i>Choose a menu option, or right-click to invoke a context menu</i>", alignment=Qt.AlignCenter) self.infoLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) bottomFiller = QWidget() bottomFiller.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) vbox = QVBoxLayout() vbox.setContentsMargins(5, 5, 5, 5) vbox.addWidget(topFiller) vbox.addWidget(self.infoLabel) vbox.addWidget(bottomFiller) widget.setLayout(vbox) self.createActions() self.createMenus() message = "A context menu is available by right-clicking" self.statusBar().showMessage(message) self.setWindowTitle("Menus") self.setMinimumSize(160,160) self.resize(480,320) def contextMenuEvent(self, event): menu = QMenu(self) menu.addAction(self.cutAct) menu.addAction(self.copyAct) menu.addAction(self.pasteAct) menu.exec_(event.globalPos()) def newFile(self): self.infoLabel.setText("Invoked <b>File|New</b>") def open(self): self.infoLabel.setText("Invoked <b>File|Open</b>") def save(self): self.infoLabel.setText("Invoked <b>File|Save</b>") def print_(self): self.infoLabel.setText("Invoked <b>File|Print</b>") def undo(self): self.infoLabel.setText("Invoked <b>Edit|Undo</b>") def redo(self): self.infoLabel.setText("Invoked <b>Edit|Redo</b>") def cut(self): self.infoLabel.setText("Invoked <b>Edit|Cut</b>") def copy(self): self.infoLabel.setText("Invoked <b>Edit|Copy</b>") def paste(self): self.infoLabel.setText("Invoked <b>Edit|Paste</b>") def bold(self): self.infoLabel.setText("Invoked <b>Edit|Format|Bold</b>") def italic(self): self.infoLabel.setText("Invoked <b>Edit|Format|Italic</b>") def leftAlign(self): self.infoLabel.setText("Invoked <b>Edit|Format|Left Align</b>") def rightAlign(self): self.infoLabel.setText("Invoked <b>Edit|Format|Right Align</b>") def justify(self): self.infoLabel.setText("Invoked <b>Edit|Format|Justify</b>") def center(self): self.infoLabel.setText("Invoked <b>Edit|Format|Center</b>") def setLineSpacing(self): self.infoLabel.setText("Invoked <b>Edit|Format|Set Line Spacing</b>") def setParagraphSpacing(self): self.infoLabel.setText("Invoked <b>Edit|Format|Set Paragraph Spacing</b>") def about(self): self.infoLabel.setText("Invoked <b>Help|About</b>") QMessageBox.about(self, "About Menu", "The <b>Menu</b> example shows how to create menu-bar menus " "and context menus.") def aboutQt(self): self.infoLabel.setText("Invoked <b>Help|About Qt</b>") def createActions(self): self.newAct = QAction("&New", self, shortcut=QKeySequence.New, statusTip="Create a new file", triggered=self.newFile) self.openAct = QAction("&Open...", self, shortcut=QKeySequence.Open, statusTip="Open an existing file", triggered=self.open) self.saveAct = QAction("&Save", self, shortcut=QKeySequence.Save, statusTip="Save the document to disk", triggered=self.save) self.printAct = QAction("&Print...", self, shortcut=QKeySequence.Print, statusTip="Print the document", triggered=self.print_) self.exitAct = QAction("E&xit", self, shortcut="Ctrl+Q", statusTip="Exit the application", triggered=self.close) self.undoAct = QAction("&Undo", self, shortcut=QKeySequence.Undo, statusTip="Undo the last operation", triggered=self.undo) self.redoAct = QAction("&Redo", self, shortcut=QKeySequence.Redo, statusTip="Redo the last operation", triggered=self.redo) self.cutAct = QAction("Cu&t", self, shortcut=QKeySequence.Cut, statusTip="Cut the current selection's contents to the clipboard", triggered=self.cut) self.copyAct = QAction("&Copy", self, shortcut=QKeySequence.Copy, statusTip="Copy the current selection's contents to the clipboard", triggered=self.copy) self.pasteAct = QAction("&Paste", self, shortcut=QKeySequence.Paste, statusTip="Paste the clipboard's contents into the current selection", triggered=self.paste) self.boldAct = QAction("&Bold", self, checkable=True, shortcut="Ctrl+B", statusTip="Make the text bold", triggered=self.bold) boldFont = self.boldAct.font() boldFont.setBold(True) self.boldAct.setFont(boldFont) self.italicAct = QAction("&Italic", self, checkable=True, shortcut="Ctrl+I", statusTip="Make the text italic", triggered=self.italic) italicFont = self.italicAct.font() italicFont.setItalic(True) self.italicAct.setFont(italicFont) self.setLineSpacingAct = QAction("Set &Line Spacing...", self, statusTip="Change the gap between the lines of a paragraph", triggered=self.setLineSpacing) self.setParagraphSpacingAct = QAction("Set &Paragraph Spacing...", self, statusTip="Change the gap between paragraphs", triggered=self.setParagraphSpacing) self.aboutAct = QAction("&About", self, statusTip="Show the application's About box", triggered=self.about) self.aboutQtAct = QAction("About &Qt", self, statusTip="Show the Qt library's About box", triggered=self.aboutQt) self.aboutQtAct.triggered.connect(QApplication.instance().aboutQt) self.leftAlignAct = QAction("&Left Align", self, checkable=True, shortcut="Ctrl+L", statusTip="Left align the selected text", triggered=self.leftAlign) self.rightAlignAct = QAction("&Right Align", self, checkable=True, shortcut="Ctrl+R", statusTip="Right align the selected text", triggered=self.rightAlign) self.justifyAct = QAction("&Justify", self, checkable=True, shortcut="Ctrl+J", statusTip="Justify the selected text", triggered=self.justify) self.centerAct = QAction("&Center", self, checkable=True, shortcut="Ctrl+C", statusTip="Center the selected text", triggered=self.center) self.alignmentGroup = QActionGroup(self) self.alignmentGroup.addAction(self.leftAlignAct) self.alignmentGroup.addAction(self.rightAlignAct) self.alignmentGroup.addAction(self.justifyAct) self.alignmentGroup.addAction(self.centerAct) self.leftAlignAct.setChecked(True) def createMenus(self): self.fileMenu = self.menuBar().addMenu("&File") self.fileMenu.addAction(self.newAct) self.fileMenu.addAction(self.openAct) self.fileMenu.addAction(self.saveAct) self.fileMenu.addAction(self.printAct) self.fileMenu.addSeparator() self.fileMenu.addAction(self.exitAct) self.editMenu = self.menuBar().addMenu("&Edit") self.editMenu.addAction(self.undoAct) self.editMenu.addAction(self.redoAct) self.editMenu.addSeparator() self.editMenu.addAction(self.cutAct) self.editMenu.addAction(self.copyAct) self.editMenu.addAction(self.pasteAct) self.editMenu.addSeparator() self.helpMenu = self.menuBar().addMenu("&Help") self.helpMenu.addAction(self.aboutAct) self.helpMenu.addAction(self.aboutQtAct) self.formatMenu = self.editMenu.addMenu("&Format") self.formatMenu.addAction(self.boldAct) self.formatMenu.addAction(self.italicAct) self.formatMenu.addSeparator().setText("Alignment") self.formatMenu.addAction(self.leftAlignAct) self.formatMenu.addAction(self.rightAlignAct) self.formatMenu.addAction(self.justifyAct) self.formatMenu.addAction(self.centerAct) self.formatMenu.addSeparator() self.formatMenu.addAction(self.setLineSpacingAct) self.formatMenu.addAction(self.setParagraphSpacingAct)
class GuiNovelToolBar(QWidget): def __init__(self, novelView): QTreeWidget.__init__(self, novelView) logger.debug("Initialising GuiNovelToolBar ...") self.mainConf = novelwriter.CONFIG self.novelView = novelView self.theProject = novelView.mainGui.theProject self.mainTheme = novelView.mainGui.mainTheme iPx = self.mainTheme.baseIconSize mPx = self.mainConf.pxInt(3) self.setContentsMargins(0, 0, 0, 0) self.setAutoFillBackground(True) qPalette = self.palette() qPalette.setBrush(QPalette.Window, qPalette.base()) self.setPalette(qPalette) fadeCol = qPalette.text().color() buttonStyle = ( "QToolButton {{padding: {0}px; border: none; background: transparent;}} " "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}" ).format(mPx, fadeCol.red(), fadeCol.green(), fadeCol.blue()) # Widget Label self.viewLabel = QLabel("<b>%s</b>" % self.tr("Novel Outline")) self.viewLabel.setContentsMargins(0, 0, 0, 0) self.viewLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # Refresh Button self.tbRefresh = QToolButton(self) self.tbRefresh.setToolTip(self.tr("Refresh")) self.tbRefresh.setIcon(self.mainTheme.getIcon("refresh")) self.tbRefresh.setIconSize(QSize(iPx, iPx)) self.tbRefresh.setStyleSheet(buttonStyle) self.tbRefresh.clicked.connect(self._refreshNovelTree) # Novel Root Menu self.mRoot = QMenu() self.gRoot = QActionGroup(self.mRoot) self.aRoot = {} self.tbRoot = QToolButton(self) self.tbRoot.setToolTip(self.tr("Novel Root")) self.tbRoot.setIcon( self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])) self.tbRoot.setIconSize(QSize(iPx, iPx)) self.tbRoot.setStyleSheet(buttonStyle) self.tbRoot.setMenu(self.mRoot) self.tbRoot.setPopupMode(QToolButton.InstantPopup) # More Options Menu self.mMore = QMenu() self.mLastCol = self.mMore.addMenu(self.tr("Last Column")) self.gLastCol = QActionGroup(self.mMore) self.aLastCol = {} self._addLastColAction(NovelTreeColumn.HIDDEN, self.tr("Hidden")) self._addLastColAction(NovelTreeColumn.POV, self.tr("Point of View Character")) self._addLastColAction(NovelTreeColumn.FOCUS, self.tr("Focus Character")) self._addLastColAction(NovelTreeColumn.PLOT, self.tr("Novel Plot")) self.tbMore = QToolButton(self) self.tbMore.setToolTip(self.tr("More Options")) self.tbMore.setIcon(self.mainTheme.getIcon("menu")) self.tbMore.setIconSize(QSize(iPx, iPx)) self.tbMore.setStyleSheet(buttonStyle) self.tbMore.setMenu(self.mMore) self.tbMore.setPopupMode(QToolButton.InstantPopup) # Assemble self.outerBox = QHBoxLayout() self.outerBox.addWidget(self.viewLabel) self.outerBox.addWidget(self.tbRefresh) self.outerBox.addWidget(self.tbRoot) self.outerBox.addWidget(self.tbMore) self.outerBox.setContentsMargins(mPx, mPx, 0, mPx) self.outerBox.setSpacing(0) self.setLayout(self.outerBox) logger.debug("GuiNovelToolBar initialisation complete") return ## # Methods ## def clearContent(self): """Run clearing project tasks. """ self.mRoot.clear() self.aRoot = {} return def buildNovelRootMenu(self): """Build the novel root menu. """ self.mRoot.clear() self.aRoot = {} for n, (tHandle, nwItem) in enumerate( self.theProject.tree.iterRoots(nwItemClass.NOVEL)): aRoot = self.mRoot.addAction(nwItem.itemName) aRoot.setData(tHandle) aRoot.setCheckable(True) aRoot.triggered.connect( lambda n, tHandle=tHandle: self.setCurrentRoot(tHandle)) self.gRoot.addAction(aRoot) self.aRoot[tHandle] = aRoot return def setCurrentRoot(self, rootHandle): """Set the current active root handle. """ if rootHandle in self.aRoot: self.aRoot[rootHandle].setChecked(True) self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) return def setLastColType(self, colType, doRefresh=True): """Set the last column type. """ self.aLastCol[colType].setChecked(True) self.novelView.novelTree.setLastColType(colType, doRefresh=doRefresh) return ## # Private Slots ## @pyqtSlot() def _refreshNovelTree(self): """Rebuild the current tree. """ rootHandle = self.theProject.lastNovel self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) return ## # Internal Functions ## def _addLastColAction(self, colType, actionLabel): """Add a column selection entry to the last column menu. """ aLast = self.mLastCol.addAction(actionLabel) aLast.setCheckable(True) aLast.setActionGroup(self.gLastCol) aLast.triggered.connect(lambda: self.setLastColType(colType)) self.aLastCol[colType] = aLast return