#!/usr/bin/env python3 # -*- encoding: utf-8 -*- # Gcode display / edit widget for QT_VCP # Copyright 2016 Chris Morley # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This was based on # QScintilla sample with PyQt # Eli Bendersky (eliben@gmail.com) # Which is code in the public domain # # See also: # http://pyqt.sourceforge.net/Docs/QScintilla2/index.html # https://qscintilla.com/ # https://qscintilla.com/simple-example/ import sys from qtpy.QtCore import Property, Signal, QSize, QEvent, Qt, QByteArray from qtpy.QtGui import QIcon from qtpy.QtWidgets import QMainWindow, QAction,\ QToolBar, QLineEdit, QHBoxLayout, QMessageBox, \ QFrame, QLabel from qtvcp.widgets.gcode_editor import GcodeDisplay from qtvcp.widgets.widget_baseclass import _HalWidgetBase from qtvcp.core import Status, Info, Action from qtvcp import logger # Instantiate the libraries with global reference # INFO holds INI file details # STATUS gives us status messages from linuxcnc # LOG is for running code logging STATUS = Status() INFO = Info() ACTION = Action() LOG = logger.getLogger(__name__) ############################################# # For Editing Gcode ############################################# class GEditor(QMainWindow, _HalWidgetBase): percentDone = Signal(int) def __init__(self, parent=None, designer=False): if not designer: parent = None super().__init__(parent) self._show_editor = True self.load_dialog_code = 'LOAD' self.save_dialog_code = 'SAVE' self.dialog_code = 'KEYBOARD' STATUS.connect('general',self.returnFromDialog) self.isCaseSensitive = 0 self.setMinimumSize(QSize(300, 200)) self.setWindowTitle("PyQt5 editor test example") # make editor self.editor = GcodeDisplay(self) self.setCentralWidget(self.editor) # class patch editor's function to ours # so we get the lines percent update self.editor.emit_percent = self.emit_percent self.editor.setReadOnly(True) self.editor.setModified(False) self.createActions() # Create toolbar and add action self.toolBar = QToolBar(self) self.toolBar.setObjectName('{}_toolbarfile'.format( self.objectName())) self.toolBar.addAction(self.newAction) self.toolBar.addAction(self.openAction) self.toolBar.addAction(self.saveAction) self.toolBar.addAction(self.exitAction) self.addToolBar(Qt.TopToolBarArea, self.toolBar) #self.toolBar.addSeparator() self.toolBarLexer = QToolBar('Lexer') self.toolBarLexer.setObjectName('{}_toolbarlexer'.format( self.objectName())) # add lexer actions self.toolBarLexer.addAction(self.gCodeLexerAction) self.toolBarLexer.addAction(self.pythonLexerAction) self.toolBarLexer.addSeparator() self.label = QLabel('''
Edit Mode
''') self.toolBarLexer.addWidget(self.label) self.addToolBar(Qt.TopToolBarArea, self.toolBarLexer) # Create toolbar and add action self.toolBarEdit = QToolBar('Edit') self.toolBarEdit.setObjectName('{}_toolbaredit'.format( self.objectName())) self.toolBarEdit.addAction(self.undoAction) self.toolBarEdit.addAction(self.redoAction) self.toolBarEdit.addSeparator() self.toolBarEdit.addAction(self.replaceAction) self.toolBarEdit.addAction(self.findAction) self.toolBarEdit.addAction(self.previousAction) self.toolBarEdit.addSeparator() self.toolBarEdit.addAction(self.caseAction) self.addToolBar(Qt.BottomToolBarArea, self.toolBarEdit) self.toolBarEntry = QToolBar('entry') self.toolBarEntry.setObjectName('{}_toolbarentry'.format( self.objectName())) frame = QFrame() box = QHBoxLayout() box.addWidget(self.searchText) box.addWidget(self.replaceText) box.addStretch(1) frame.setLayout(box) self.toolBarEntry.addWidget(frame) self.addToolBar(Qt.BottomToolBarArea, self.toolBarEntry) self.readOnlyMode(save = False) def createActions(self): # Create new action self.newAction = QAction(QIcon.fromTheme('document-new'), 'New', self) self.newAction.setShortcut('Ctrl+N') self.newAction.setStatusTip('New document') self.newAction.triggered.connect(self.newCall) # Create open action self.openAction = QAction(QIcon.fromTheme('document-open'), '&Open', self) self.openAction.setShortcut('Ctrl+O') self.openAction.setStatusTip('Open document') self.openAction.triggered.connect(self.openCall) # Create save action self.saveAction = QAction(QIcon.fromTheme('document-save'), '&Save', self) self.saveAction.setShortcut('Ctrl+S') self.saveAction.setStatusTip('Save document') self.saveAction.triggered.connect(self.saveCall) # Create exit action self.exitAction = QAction(QIcon.fromTheme('application-exit'), '&Exit', self) self.exitAction.setShortcut('Ctrl+Q') self.exitAction.setToolTip('Exit Edit Mode') self.exitAction.setStatusTip('Exit Edit Mode') self.exitAction.triggered.connect(self.exitCall) # Create gcode lexer action self.gCodeLexerAction = QAction(QIcon.fromTheme('lexer.png'), '&Gcode\nLexer', self) self.gCodeLexerAction.setCheckable(1) self.gCodeLexerAction.setShortcut('Ctrl+G') self.gCodeLexerAction.setStatusTip('Set Gcode highlighting') self.gCodeLexerAction.triggered.connect(self.gcodeLexerCall) # Create gcode lexer action self.pythonLexerAction = QAction(QIcon.fromTheme('lexer.png'), '&Python\nLexer', self) self.pythonLexerAction.setShortcut('Ctrl+P') self.pythonLexerAction.setStatusTip('Set Python highlighting') self.pythonLexerAction.triggered.connect(self.pythonLexerCall) self.searchText = QLineEdit(self) self.searchText.setToolTip('Search Text') self.searchText.setStatusTip('Text to search for') self.searchText.installEventFilter(self) self.replaceText = QLineEdit(self) self.replaceText.setToolTip('Replace Text') self.replaceText.setStatusTip('Replace search text with this text') self.replaceText.installEventFilter(self) # Create new action self.undoAction = QAction(QIcon.fromTheme('edit-undo'), 'Undo', self) self.undoAction.setStatusTip('Undo') self.undoAction.triggered.connect(self.undoCall) # create redo action self.redoAction = QAction(QIcon.fromTheme('edit-redo'), 'Redo', self) self.redoAction.setStatusTip('Redo') self.redoAction.triggered.connect(self.redoCall) # create replace action self.replaceAction = QAction(QIcon.fromTheme('edit-find-replace'), 'Replace', self) self.replaceAction.setStatusTip('Replace text') self.replaceAction.triggered.connect(self.replaceCall) # create find action self.findAction = QAction(QIcon.fromTheme('edit-find'), 'Find', self) self.findAction.setStatusTip('Find next occurrence of text') self.findAction.triggered.connect(self.findCall) # create next action self.previousAction = QAction(QIcon.fromTheme('go-previous'), 'Find Previous', self) self.previousAction.setStatusTip('Find previous occurrence of text') self.previousAction.triggered.connect(self.previousCall) # create case action self.caseAction = QAction(QIcon.fromTheme('edit-case'), 'Aa', self) self.caseAction.setToolTip('Toggle Match Case') self.caseAction.setStatusTip('Toggle between any case and match case') self.caseAction.setCheckable(1) self.caseAction.triggered.connect(self.caseCall) # catch focusIn event to pop keyboard dialog def eventFilter(self, obj, event): if event.type() == QEvent.FocusIn: if isinstance(obj, QLineEdit): # only if mouse selected if event.reason () == 0: self.popEntry(obj) return super().eventFilter(obj, event) # callback functions built for easy class patching ########## # want to refrain from renaming these functions as it will break # any class patch user's use # we split them like this so a user can intercept the callback # but still call the original functionality def caseCall(self): self.case() def case(self): self.isCaseSensitive -=1 self.isCaseSensitive *=-1 def exitCall(self): self.exit() def exit(self): if self.editor.isModified(): result = self.killCheck() if result: try: self.editor.reload_last(None) except Exception as e: print (e) self.readOnlyMode() return result return True def findCall(self): self.find() def find(self): self.editor.search(str(self.searchText.text()), re=False, case=self.isCaseSensitive, word=False, wrap= True, fwd=True) def previousCall(self): self.previous() def previous(self): self.editor.setCursorPosition(self.editor.getSelection()[0], self.editor.getSelection()[1]) self.editor.search(str(self.searchText.text()), re=False, case=self.isCaseSensitive, word=False, wrap=True, fwd=False) def gcodeLexerCall(self): self.gcodeLexer() def gcodeLexer(self): self.editor.set_lexer("g-code") def nextCall(self): self.next() def next(self): self.editor.search(str(self.searchText.text()), re=False, case=self.isCaseSensitive, word=False, wrap=True, fwd=False) self.editor.search_Next() def newCall(self): self.new() def new(self): if self.editor.isModified(): result = self.killCheck() if result: self.editor.new_text() else: self.editor.new_text() def openCall(self): self.open() def open(self): self.getFileName() def openReturn(self,f): ACTION.OPEN_PROGRAM(f) self.editor.setModified(False) def pythonLexerCall(self): self.pythonLexer() def pythonLexer(self): self.editor.set_lexer("python") def redoCall(self): self.redo() def redo(self): self.editor.redo() def replaceCall(self): self.replace() def replace(self): self.editor.replace_text(str(self.replaceText.text())) self.editor.search(str(self.searchText.text()), re=False, case=self.isCaseSensitive, word=False, wrap=True, fwd=True) def saveCall(self): self.save() def save(self): self.getSaveFileName() def saveReturn(self, fname): saved = ACTION.SAVE_PROGRAM(self.editor.text(), fname) if saved is not None: self.editor.setModified(False) ACTION.OPEN_PROGRAM(saved) def undoCall(self): self.undo() def undo(self): self.editor.undo() # helper functions ############################################ def saveSettings(self): self.SETTINGS_.beginGroup("geditor-{}".format(self.objectName())) self.SETTINGS_.setValue('state', self.saveState().data()) self.SETTINGS_.endGroup() def restoreSettings(self): # set recorded toolbar settings self.SETTINGS_.beginGroup("geditor-{}".format(self.objectName())) state = self.SETTINGS_.value('state') self.SETTINGS_.endGroup() if not state is None: try: self.restoreState(QByteArray(state)) except Exception as e: print(e) else: return True return False def editMode(self): self.editor.setReadOnly(False) result = self.restoreSettings() check = (self.toolBar.toggleViewAction().isChecked() and self.toolBarEdit.toggleViewAction().isChecked() and self.toolBarEntry.toggleViewAction().isChecked() and self.toolBarLexer.toggleViewAction().isChecked()) if not check: self.toolBar.toggleViewAction().setChecked(False) self.toolBar.toggleViewAction().trigger() if not result: self.toolBarEdit.toggleViewAction().setChecked(False) self.toolBarEdit.toggleViewAction().trigger() self.toolBarEntry.toggleViewAction().setChecked(False) self.toolBarEntry.toggleViewAction().trigger() self.toolBarLexer.toggleViewAction().setChecked(False) self.toolBarLexer.toggleViewAction().trigger() def readOnlyMode(self, save = True): if save: self.saveSettings() self.editor.setReadOnly(True) self.toolBar.toggleViewAction().setChecked(True) self.toolBar.toggleViewAction().trigger() self.toolBarEdit.toggleViewAction().setChecked(True) self.toolBarEdit.toggleViewAction().trigger() self.toolBarEntry.toggleViewAction().setChecked(True) self.toolBarEntry.toggleViewAction().trigger() self.toolBarLexer.toggleViewAction().setChecked(True) self.toolBarLexer.toggleViewAction().trigger() def getFileName(self): mess = {'NAME':self.load_dialog_code,'ID':'%s__' % self.objectName(), 'TITLE':'Load Editor'} STATUS.emit('dialog-request', mess) def getSaveFileName(self): mess = {'NAME':self.save_dialog_code,'ID':'%s__' % self.objectName(), 'TITLE':'Save Editor'} STATUS.emit('dialog-request', mess) def popEntry(self, obj): mess = {'NAME':self.dialog_code, 'ID':'%s__' % self.objectName(), 'OVERLAY':False, 'OBJECT':obj, 'TITLE':'Set Entry for {}'.format(obj.objectName().upper()), 'GEONAME':'_{}'.format(self.dialog_code) } STATUS.emit('dialog-request', mess) LOG.debug('message sent:{}'.format (mess)) # process the STATUS return message def returnFromDialog(self, w, message): if message.get('NAME') == self.load_dialog_code: path = message.get('RETURN') code = bool(message.get('ID') == '%s__'% self.objectName()) if path and code: self.openReturn(path) elif message.get('NAME') == self.save_dialog_code: path = message.get('RETURN') code = bool(message.get('ID') == '%s__'% self.objectName()) if path and code: self.saveReturn(path) elif message.get('NAME') == self.dialog_code: txt = message['RETURN'] code = bool(message.get('ID') == '%s__'% self.objectName()) if code and txt is not None: LOG.debug('message return:{}'.format (message)) obj = message.get('OBJECT') obj.setText(str(txt)) def killCheck(self): choice = QMessageBox.question(self, 'Warning!', """This file has changed since loading and has not been saved. You will lose your changes. Still want to proceed?""", QMessageBox.Yes | QMessageBox.No) if choice == QMessageBox.Yes: return True else: return False def emit_percent(self, percent): self.percentDone.emit(percent) def select_lineup(self): self.editor.select_lineup(None) def select_linedown(self): self.editor.select_linedown(None) def select_line(self, line): self.editor.highlight_line(None, line) def jump_line(self, jump): self.editor.jump_line(jump) def get_line(self): return self.editor.getCursorPosition()[0] +1 def set_margin_width(self,width): self.editor.set_margin_width(width) def set_font(self, font): self.editor.font = font for i in range(0,8): self.editor.lexer.setFont(font,i) def set_background_color(self, color): self.editor.set_background_color(color) def isReadOnly(self): return self.editor.isReadOnly() # designer recognized getter/setters # auto_show_mdi status # These adjust the self.editor instance def set_auto_show_mdi(self, data): self.editor.auto_show_mdi = data def get_auto_show_mdi(self): return self.editor.auto_show_mdi def reset_auto_show_mdi(self): self.editor.auto_show_mdi = True auto_show_mdi_status = Property(bool, get_auto_show_mdi, set_auto_show_mdi, reset_auto_show_mdi) # designer recognized getter/setters # auto_show_manual status def set_auto_show_manual(self, data): self.editor.auto_show_manual = data def get_auto_show_manual(self): return self.editor.auto_show_manual def reset_auto_show_manual(self): self.editor.auto_show_manual = True auto_show_manual_status = Property(bool, get_auto_show_manual, set_auto_show_manual, reset_auto_show_manual) # designer recognized getter/setters # show_editor status def set_show_editor(self, data): self._show_editor = data if data: self.toolBar.show() self.toolBarLexer.show() self.toolBarEntry.show() self.toolBarEdit.show() else: self.toolBar.hide() self.toolBarLexer.hide() self.toolBarEntry.hide() self.toolBarEdit.hide() def get_show_editor(self): return self._show_editor def reset_show_editor(self): self._show_editor = True self.toolBar.show() self.toolBarLexer.show() self.toolBarEntry.show() self.toolBarEdit.show() show_editor = Property(bool, get_show_editor, set_show_editor, reset_show_editor) # for direct testing if __name__ == "__main__": from qtpy.QtWidgets import * from qtpy.QtCore import * from qtpy.QtGui import * app = QApplication(sys.argv) w = GcodeEditor() w.editMode() w.editor.setText(''' This is test text a a a B b n C C C This is the end of the test text.''') if 0: w.toolBar.hide() if 0: w.pythonLexerAction.setVisible(False) w.gCodeLexerAction.setVisible(False) if 0: w.openAction.setVisible(False) w.newAction.setVisible(False) if 0: w.saveAction.setVisible(False) w.exitAction.setVisible(False) if 0: w.label.setText('Edit mode title label') w.show() sys.exit( app.exec() )