#!/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 import os import re from qtpy.QtCore import Property, Signal, QSize from qtpy.QtGui import QFont, QFontMetrics, QColor, QIcon from qtpy.QtWidgets import QWidget, QAction,\ QVBoxLayout, QToolBar, QLineEdit, QHBoxLayout, QMessageBox, \ QFrame, QLabel, QStyle 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__) # Force the log level for this module # LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL # load this after Logging set up so we get a nice dialog. import qtpy try: from qtpy.Qsci import QsciScintilla, QsciLexerCustom, QsciLexerPython except ImportError: if qtpy.PYQT5: try: from PyQt5.Qsci import QsciScintilla, QsciLexerCustom, QsciLexerPython except ImportError as e: LOG.critical("Can't import QsciScintilla - is package python3-pyqt5.qsci installed?", exc_info=e) sys.exit(1) else: try: from PyQt6.Qsci import QsciScintilla, QsciLexerCustom, QsciLexerPython except ImportError as e: LOG.critical("Can't import QsciScintilla - is package python3-pyqt6.qsci installed?", exc_info=e) sys.exit(1) class GcodeLexer(QsciLexerCustom): """ QSciLexer for parsing and highlighting G-code """ def __init__(self, parent): super(GcodeLexer, self).__init__(parent) # Style/Token Types (Used for Styling & Regex) self._styles = { 0: 'Default', 1: 'Comment', 2: 'Gcode', 3: 'Mcode', 4: 'Axis', 5: 'Other', 6: 'AxisValue', 7: 'OtherValue', } for key, value in self._styles.items(): setattr(self, value, key) def language(self): return "G-code" def description(self, style): return self._styles.get(style, "") def styleText(self, start, end): editor = self.editor() if editor is None: return self.startStyling(start) # scintilla works with encoded bytes, not decoded characters. # this matters if the source contains non-ascii characters and # a multi-byte encoding is used (e.g. utf-8) source = '' if end > editor.length(): end = editor.length() if end > start: if sys.hexversion >= 0x02060000: # faster when styling big files, but needs python 2.6 source = bytearray(end - start) editor.SendScintilla( editor.SCI_GETTEXTRANGE, start, end, source) source = source.decode("utf-8", "ignore") else: source = str(editor.text())[start:end] if not source: return re_tokens = { 1: r"(?:[N]\d+|\(.*?\)|;.*)", # LineNo and Comment 2: r"[G]\s*\d{1,2}\.\d\s|[G]\s*\d{1,2}", # Gcode 3: r"[M]\s*\d{1,3}", # Mcode 4: r"[XYZABCUVW]{1}(?:[+-]?[\d\.\s\][+-]+|\#\<.*\>|\[.*\]|\#\d+)", # Axis 5: r"[EFHIJKDQLRPST$]{1}(?:[+-]?[\d\.\s]+|\#\<.*\>|\[.*\]|\#\d+)", # Other (feed,rpm,radius,etc) 0: r"\s+|\w+|\W", # Default (fallback) } re_comment_cmd = r"(?:\(\s*(?:print,|debug,|msg,|logopen,|logappend,|logclose|log,|pyrun,|pyreload|abort,|probeopen|probeclose)|^\s*\;py,)" re_string = "|".join(re_tokens.values()) p = re.compile(re_string, re.IGNORECASE) for line in source.splitlines(True): token_list = [(token, len(bytearray(token, "utf-8"))) for token in p.findall(line)] num_comment_cmds = len(re.findall(re_comment_cmd, line, re.IGNORECASE)) for token in token_list: if re.match(re_tokens[self.Comment], token[0], re.IGNORECASE): m = re.search(re_comment_cmd, token[0], re.IGNORECASE) if m: num_comment_cmds -= 1 if m and num_comment_cmds == 0: # Only highlight last comment_cmd on line self.setStyling(1, self.Comment) self.setStyling(m.span()[1] - 1, self.Other) self.setStyling(len(token[0]) - m.span()[1], self.Comment) else: self.setStyling(token[1], self.Comment) elif re.match(re_tokens[self.Gcode], token[0], re.IGNORECASE): self.setStyling(token[1], self.Gcode) elif re.match(re_tokens[self.Mcode], token[0], re.IGNORECASE): self.setStyling(token[1], self.Mcode) elif re.match(re_tokens[self.Axis], token[0], re.IGNORECASE): self.setStyling(1, self.Axis) self.setStyling(token[1] - 1, self.AxisValue) elif re.match(re_tokens[self.Other], token[0], re.IGNORECASE): self.setStyling(1, self.Other) self.setStyling(token[1] - 1, self.OtherValue) else: self.setStyling(token[1], self.Default) ########################################################## # Base editor class ########################################################## class EditorBase(QsciScintilla): CURRENT_MARKER_NUM = 0 USER_MARKER_NUM = 1 # Default Styles # get/set function for font and colors styles will return # styleFont[0] and _styleColor[0] as defaults when they are not # set in the dict manually, or by CSS properties. _styleFont = { 0: QFont("Courier", 11), # Default Font # 2: QFont("Courier", 20, weight=QFont.Bold), # Gcode # 5: QFont("Courier", 11, weight=QFont.Bold), # Other # "Margins": QFont("Courier", 9), # Margins } _styleColor = { 0: QColor("#000000"), # Color0 (Default) 1: QColor("#434d3f"), # Color1 (Lexer Comment) 2: QColor("#ba220b"), # Color2 (Lexer Gcode) 3: QColor("#f56b1b"), # Color3 (Lexer Mcode) 4: QColor("#1883c9"), # Color4 (Lexer Axis) 5: QColor("#dd30f0"), # Color5 (Lexer Other) 6: QColor("#0e5482"), # Color6 (Lexer Axis Value) 7: QColor("#a420b3"), # Color7 (Lexer Other Value) "Margins": QColor("#666769"), # Margins } # keep track of the property value _styleBackgroundColor = QColor("#c0c0c0") _styleCursorColor = QColor("white") _styleMarginsBackgroundColor = QColor("#cccccc") _styleMarkerBackgroundColor = QColor("#a5a526") _styleSelectionBackgroundColor = QColor("#001111") _styleSelectionForegroundColor = QColor("#ffffff") _styleSyntaxHighlightEnabled = True def __init__(self, parent=None): super(EditorBase, self).__init__(parent) self.lexer = None self.lexer_num_styles = 0 # updated when lexer is loaded self._lastUserLine = 0 self.setReadOnly(True) # don't allow editing by default self.set_lexer("g-code") self._marginWidth = '00000' # Margin 0 is used for line numbers self.setMarginWidth(0, self._marginWidth) self.linesChanged.connect(self.on_lines_changed) self.setMarginLineNumbers(0, True) # Clickable margin for showing markers self.marginClicked.connect(self.on_margin_clicked) self.setMarginMarkerMask(0, 0b1111) self.setMarginSensitivity(0, True) # setting _marker_ margin width self.setMarginWidth(1, 0) # Gcode highlight current line self.currentHandle = self.markerDefine(QsciScintilla.MarkerSymbol.Background, self.CURRENT_MARKER_NUM) self.setColorMarkerBackground(self.getColorMarkerBackground()) # User Highlight line (when clicking margin) self.userHandle = self.markerDefine(QsciScintilla.MarkerSymbol.Background, self.USER_MARKER_NUM) self.setMarkerBackgroundColor(QColor("#ffc0c0"), self.USER_MARKER_NUM) # Brace matching: enable for a brace immediately before or after # the current position self.setBraceMatching(QsciScintilla.BraceMatch.SloppyBraceMatch) # Current line visible with special background color self.setCaretLineVisible(False) self.SendScintilla(QsciScintilla.SCI_GETCARETLINEVISIBLEALWAYS, True) self.setCaretLineBackgroundColor(QColor("#ffe4e4")) self.ensureLineVisible(True) # Don't want to see the horizontal scrollbar at all # Use raw message to Scintilla here (all messages are documented # here: http://www.scintilla.org/ScintillaDoc.html) #self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0) self.SendScintilla(QsciScintilla.SCI_SETSCROLLWIDTH, 700) self.SendScintilla(QsciScintilla.SCI_SETSCROLLWIDTHTRACKING) # not too small self.setMinimumSize(200, 100) self.filepath = None def set_lexer(self, lexer_type=None): self.lexer = None self.lexer_num_styles = 0 if lexer_type is None or not self._styleSyntaxHighlightEnabled: self.lexer = None elif lexer_type.lower() == "g-code": self.lexer = GcodeLexer(self) elif lexer_type.lower() == "python": self.lexer = QsciLexerPython(self) # Get Number of lexer styles if self.lexer is not None: while self.lexer.description(self.lexer_num_styles) != "": self.lexer_num_styles += 1 self.setLexer(self.lexer) self.refresh_styles() #print("Loaded Lexer {} with {} Styles".format(lexer_type, self.lexer_num_styles)) def refresh_styles(self): self.setDefaultFont(self.getDefaultFont()) self.set_font_colors() self.setColorBackground(self.getColorBackground()) self.setColorMarginsBackground(self.getColorMarginsBackground()) self.setSelectionBackgroundColor(self.getColorSelectionBackground()) self.setSelectionForegroundColor(self.getColorSelectionForeground()) def set_font_colors(self): self.setColor(self.getColor0()) self.setColorMarginsForeground(self.getColorMarginsForeground()) if self.lexer is not None: for i in range(0, self.lexer_num_styles): self.lexer.setColor(self._styleColor.get(i, self._styleColor[0]), i) def set_margin_width(self): self.setMarginWidth(0, self._marginWidth) def set_margin_metric(self, width): fontmetrics = QFontMetrics(self.getFontMargins()) self.setMarginWidth(0, fontmetrics.width("0" * width) + 6) # reset margin width when number of lines change def on_lines_changed(self): if len(str(self.lines())) < 3: self._marginWidth = '0000' else: self._marginWidth = str(self.lines())+'0' self.setMarginWidth(0, self._marginWidth) def on_margin_clicked(self, nmargin, nline, modifiers): # Toggle marker for the line the margin was clicked on # 2 means it's already there if self.markersAtLine(nline) != 2: self.markerDelete(self._lastUserLine, self.USER_MARKER_NUM) self.markerAdd(nline, self.USER_MARKER_NUM) elif self._lastUserLine != nline: self.markerAdd(nline, self.USER_MARKER_NUM) self.markerDelete(self._lastUserLine, self.USER_MARKER_NUM) else: self.markerDelete(self._lastUserLine, self.USER_MARKER_NUM) self._lastUserLine = 0 return self._lastUserLine = nline def mouseDoubleClickEvent(self, event): pass def new_text(self): self.setText('') def load_text(self, filepath): self.filepath = filepath if filepath is None: return try: fp = os.path.expanduser(filepath) with open(fp) as f: self.setText(f.read()) except OSError as e: LOG.error("load_text(): {}".format(e)) self.setText('') return except Exception as e: LOG.error("load_text(): {}".format(e)) self.setText('') return self.ensureCursorVisible() self.SendScintilla(QsciScintilla.SCI_VERTICALCENTRECARET) self.setModified(False) def save_text(self): try: with open(self.filepath + 'text', "w") as f: f.write(self.text()) except OSError as e: LOG.error("save_text(): {}".format(e)) def replace_text(self, text): self.replace(text) def search(self, text, re=False, case=False, word=False, wrap=False, fwd=True): self.findFirst(text, re, case, word, wrap, fwd) def search_Next(self): self.SendScintilla(QsciScintilla.SCI_SEARCHANCHOR) self.findNext() # Style get/set functions # Default Color def getColor0(self): return self._styleColor[0] def setColor0(self, value): self._styleColor[0] = value self.setColor(value) if self.lexer is not None: self.lexer.setColor(value, 0) styleColor0 = Property(QColor, getColor0, setColor0) # Lexer Colors def getColor1(self): return self._styleColor.get(1, self._styleColor[0]) def setColor1(self, value): self._styleColor[1] = value if self.lexer is not None: self.lexer.setColor(value, 1) styleColor1 = Property(QColor, getColor1, setColor1) def getColor2(self): return self._styleColor.get(2, self._styleColor[0]) def setColor2(self, value): self._styleColor[2] = value if self.lexer is not None: self.lexer.setColor(value, 2) styleColor2 = Property(QColor, getColor2, setColor2) def getColor3(self): return self._styleColor.get(3, self._styleColor[0]) def setColor3(self, value): self._styleColor[3] = value if self.lexer is not None: self.lexer.setColor(value, 3) styleColor3 = Property(QColor, getColor3, setColor3) def getColor4(self): return self._styleColor.get(4, self._styleColor[0]) def setColor4(self, value): self._styleColor[4] = value if self.lexer is not None: self.lexer.setColor(value, 4) styleColor4 = Property(QColor, getColor4, setColor4) def getColor5(self): return self._styleColor.get(5, self._styleColor[0]) def setColor5(self, value): self._styleColor[5] = value if self.lexer is not None: self.lexer.setColor(value, 5) styleColor5 = Property(QColor, getColor5, setColor5) def getColor6(self): return self._styleColor.get(6, self._styleColor[0]) def setColor6(self, value): self._styleColor[6] = value if self.lexer is not None: self.lexer.setColor(value, 6) styleColor6 = Property(QColor, getColor6, setColor6) def getColor7(self): return self._styleColor.get(7, self._styleColor[0]) def setColor7(self, value): self._styleColor[7] = value if self.lexer is not None: self.lexer.setColor(value, 7) styleColor7 = Property(QColor, getColor7, setColor7) # Margins Text Color def getColorMarginsForeground(self): return self._styleColor.get("Margins", self._styleColor[0]) def setColorMarginsForeground(self, value): super(EditorBase, self).setMarginsForegroundColor(value) self._styleColor["Margins"] = value styleColorMarginText = Property(QColor, getColorMarginsForeground, setColorMarginsForeground) # Cursor Color def getColorCursor(self): return self._styleCursorColor def setColorCursor(self, color): self._styleCursorColor = color super(EditorBase, self).setCaretForegroundColor(color) styleColorCursor = Property(QColor, getColorCursor, setColorCursor) # Backgrounds def getColorBackground(self): return self._styleBackgroundColor def setColorBackground(self, color): self._styleBackgroundColor = color #self.SendScintilla(QsciScintilla.SCI_STYLESETBACK, QsciScintilla.STYLE_DEFAULT, color) self.setPaper(color) if self.lexer is not None: self.lexer.setDefaultPaper(color) for i in range(0, self.lexer_num_styles): self.lexer.setPaper(color, i) styleColorBackground = Property(QColor, getColorBackground, setColorBackground) # Margins Background def setColorMarginsBackground(self, color): super(EditorBase, self).setMarginsBackgroundColor(color) self._styleMarginsBackgroundColor = color def getColorMarginsBackground(self): return self._styleMarginsBackgroundColor styleColorMarginBackground = Property(QColor, getColorMarginsBackground, setColorMarginsBackground) # Selection Highlight Background & Foreground def getColorSelectionBackground(self): return self._styleSelectionBackgroundColor def setColorSelectionBackground(self, value): self._styleSelectionBackgroundColor = value self.setSelectionBackgroundColor(value) styleColorSelectionBackground = Property(QColor, getColorSelectionBackground, setColorSelectionBackground) def getColorSelectionForeground(self): return self._styleSelectionForegroundColor def setColorSelectionForeground(self, value): self._styleSelectionForegroundColor = value self.setSelectionForegroundColor(value) styleColorSelectionText = Property(QColor, getColorSelectionForeground, setColorSelectionForeground) # Current Line Marker Background def getColorMarkerBackground(self): return self._styleMarkerBackgroundColor def setColorMarkerBackground(self, value): self._styleMarkerBackgroundColor = value self.setMarkerBackgroundColor(value, self.CURRENT_MARKER_NUM) styleColorMarkerBackground = Property(QColor, getColorMarkerBackground, setColorMarkerBackground) # Fonts def setDefaultFont(self, value): self._styleFont[0] = value self.setFont(value) self.setFontMargins(self.getFontMargins()) if self.lexer is not None: self.lexer.setFont(value) for i in range(0, self.lexer_num_styles): self.lexer.setFont(self._styleFont.get(i, self._styleFont[0]), i) def getDefaultFont(self): return self._styleFont[0] styleFont = Property(QFont, getDefaultFont, setDefaultFont) def getFont0(self): return self._styleFont[0] def setFont0(self, value): self._styleFont[0] = value self.setFont(value) if self.lexer is not None: self.lexer.setFont(value, 0) styleFont0 = Property(QFont, getFont0, setFont0) def getFont1(self): return self._styleFont.get(1, self._styleFont[0]) def setFont1(self, value): self._styleFont[1] = value if self.lexer is not None: self.lexer.setFont(value, 1) styleFont1 = Property(QFont, getFont1, setFont1) def getFont2(self): return self._styleFont.get(2, self._styleFont[0]) def setFont2(self, value): self._styleFont[2] = value if self.lexer is not None: self.lexer.setFont(value, 2) styleFont2 = Property(QFont, getFont2, setFont2) def getFont3(self): return self._styleFont.get(3, self._styleFont[0]) def setFont3(self, value): self._styleFont[3] = value if self.lexer is not None: self.lexer.setFont(value, 3) styleFont3 = Property(QFont, getFont3, setFont3) def getFont4(self): return self._styleFont.get(4, self._styleFont[0]) def setFont4(self, value): self._styleFont[4] = value if self.lexer is not None: self.lexer.setFont(value, 4) styleFont4 = Property(QFont, getFont4, setFont4) def getFont5(self): return self._styleFont.get(5, self._styleFont[0]) def setFont5(self, value): self._styleFont[5] = value if self.lexer is not None: self.lexer.setFont(value, 5) styleFont5 = Property(QFont, getFont5, setFont5) def getFont6(self): return self._styleFont.get(6, self._styleFont[0]) def setFont6(self, value): self._styleFont[6] = value if self.lexer is not None: self.lexer.setFont(value, 6) styleFont6 = Property(QFont, getFont6, setFont6) def getFont7(self): return self._styleFont.get(7, self._styleFont[0]) def setFont7(self, value): self._styleFont[7] = value if self.lexer is not None: self.lexer.setFont(value, 7) styleFont7 = Property(QFont, getFont7, setFont7) def getFontMargins(self): return self._styleFont.get("Margins", self._styleFont[0]) def setFontMargins(self, value): self._styleFont["Margins"] = value self.setMarginsFont(value) styleFontMargin = Property(QFont, getFontMargins, setFontMargins) # Syntax Highlighting Bool def getSyntaxHighlightEnabled(self): return self._styleSyntaxHighlightEnabled def setSyntaxHighlightEnabled(self, value): if value is not self._styleSyntaxHighlightEnabled: self._styleSyntaxHighlightEnabled = value if not value: self.set_lexer(None) else: self.set_lexer("g-code") styleSyntaxHighlightEnabled = Property(bool, getSyntaxHighlightEnabled, setSyntaxHighlightEnabled) ########################################################## # Gcode display widget (intended read-only) ########################################################## class GcodeDisplay(EditorBase, _HalWidgetBase): CURRENT_MARKER_NUM = 0 USER_MARKER_NUM = 1 def __init__(self, parent=None): super(GcodeDisplay, self).__init__(parent) # linuxcnc defaults self.idle_line_reset = False self._last_filename = None self.auto_show_mdi = True self.auto_show_manual = False self.auto_show_preference = True self.last_line = 0 # keep track of vertical scroll setting in auto mode self._last_auto_scroll = 0 def _hal_init(self): self.cursorPositionChanged.connect(self.line_changed) if self.auto_show_mdi: STATUS.connect('mode-mdi', self.load_mdi) STATUS.connect('mdi-history-changed', self.load_mdi) STATUS.connect('mode-auto', self.reload_last) STATUS.connect('move-text-lineup', self.select_lineup) STATUS.connect('move-text-linedown', self.select_linedown) STATUS.connect('mode-manual', self.load_manual) if self.auto_show_manual: STATUS.connect('mode-manual', self.load_manual) STATUS.connect('machine-log-changed', self.load_manual) if self.auto_show_preference: STATUS.connect('show-preference', self.load_preference) STATUS.connect('file-loaded', self.load_program) STATUS.connect('reload-display', self.load_program) STATUS.connect('line-changed', self.external_highlight_request) STATUS.connect('graphics-line-selected', self.external_highlight_request) STATUS.connect('command-stopped', lambda w: self.run_stopped()) if self.idle_line_reset: STATUS.connect('interp_idle', lambda w: self.set_line_number(0)) self.markerDeleteHandle(self.currentHandle) def load_program(self, w, filename=None): if filename is None: filename = self._last_filename elif 'file_clear.ngc' in filename: self._last_filename = None else: self._last_filename = filename self.load_text(filename) #self.zoomTo(6) self.setCursorPosition(0, 0) self.markerDeleteHandle(self.currentHandle) self.setModified(False) self._lastUserLine = 0 # when switching from MDI to AUTO we need to reload the # last (linuxcnc loaded) program. def reload_last(self, w): self.load_text(STATUS.old['file']) self.setCursorPosition(0, 0) # keep track of vertical scroll bar setting self.verticalScrollBar().setValue(self._last_auto_scroll) # and margin marker if it is showing if self._lastUserLine >0: self.markerAdd(self._lastUserLine, self.USER_MARKER_NUM) # With the auto_show__mdi option, MDI history is shown def load_mdi(self, w): # record scroll position in auto mode's gcode if STATUS.get_previous_mode() == STATUS.AUTO: self._last_auto_scroll = self.verticalScrollBar().value() self.load_text(INFO.MDI_HISTORY_PATH) self._last_filename = INFO.MDI_HISTORY_PATH self.setCursorPosition(self.lines()-1, 0) self.moveMarker(self.lines()-1) # With the auto_show__mdi option, MDI history is shown def load_manual(self, w): # record scroll position in auto mode's gcode if STATUS.get_previous_mode() == STATUS.AUTO: self._last_auto_scroll = self.verticalScrollBar().value() if self.auto_show_manual and STATUS.is_man_mode(): self.load_text(INFO.MACHINE_LOG_HISTORY_PATH) self.setCursorPosition(self.lines()-1, 0) def load_preference(self, w): self.load_text(self.PATHS_.PREFS_FILENAME) self.setCursorPosition(self.lines()-1, 0) # external line numbers start at 1 - convert that to start at 0 def external_highlight_request(self, w, line): if line in (-1, None): return if STATUS.is_auto_running(): self.highlight_line(None, line-1) return LOG.verbose('editor: got external highlight {}'.format(line)) #self.highlight_line(None, line-1) self.ensureLineVisible(line-1) #self.setSelection(line-1,0,line-1,self.lineLength(line-1)-1) self.moveMarker(line-1) self.selectAll(False) def moveMarker(self, line): if STATUS.is_auto_running() and STATUS.stat.file == '': self.last_line = 0 return self.markerDeleteHandle(self.currentHandle) self.currentHandle = self.markerAdd(line, self.CURRENT_MARKER_NUM) self.last_line = line def highlight_line(self, w, line): LOG.verbose('editor: highlight line {}'.format(line)) if STATUS.is_auto_running(): if not STATUS.old['file'] == self._last_filename: LOG.debug('should reload the display') self.load_text(STATUS.old['file']) self._last_filename = STATUS.old['file'] self.emit_percent(round(line*100/self.lines())) self.moveMarker(line) self.setCursorPosition(line, 0) self.ensureCursorVisible() self.SendScintilla(QsciScintilla.SCI_VERTICALCENTRECARET) def emit_percent(self, percent): pass def run_stopped(self): self.emit_percent(-1) def set_line_number(self, line): STATUS.emit('gcode-line-selected', line+1) def line_changed(self, line, index): LOG.verbose('Line changed: {}'.format(line)) if STATUS.is_auto_running() is False: if STATUS.is_mdi_mode(): line_text = str(self.text(line)).strip() STATUS.emit('mdi-line-selected', line_text, self._last_filename) else: self.moveMarker(line) self.set_line_number(line) def select_lineup(self, w): line, col = self.getCursorPosition() LOG.verbose(line) self.setCursorPosition(line-1, 0) self.highlight_line(None, line-1) def select_linedown(self, w): line, col = self.getCursorPosition() LOG.verbose(line) self.setCursorPosition(line+1, 0) self.highlight_line(None, line+1) def jump_line(self, jump): line, col = self.getCursorPosition() line = line + jump LOG.verbose(line) if line <0: line = 0 elif line > self.lines()-1: line = self.lines()-1 self.setCursorPosition(line, 0) self.highlight_line(None, line) # overridden functions # ##################################### def zoomIn(self): super().zoomIn() self.set_margin_width() def zoomOut(self): super().zoomOut() self.set_margin_width() ##################################### # designer recognized getter/setters # auto_show_mdi status def set_auto_show_mdi(self, data): self.auto_show_mdi = data def get_auto_show_mdi(self): return self.auto_show_mdi def reset_auto_show_mdi(self): self.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.auto_show_manual = data def get_auto_show_manual(self): return self.auto_show_manual def reset_auto_show_manual(self): self.auto_show_manual = True auto_show_manual_status = Property(bool, get_auto_show_manual, set_auto_show_manual, reset_auto_show_manual) ############################################# # For Editing Gcode ############################################# class GcodeEditor(QWidget, _HalWidgetBase): percentDone = Signal(int) def __init__(self, parent=None): super(GcodeEditor, self).__init__(parent) self.load_dialog_code = 'LOAD' self.save_dialog_code = 'SAVE' STATUS.connect('general',self.returnFromDialog) self.isCaseSensitive = 0 self.setMinimumSize(QSize(300, 200)) self.setWindowTitle("PyQt5 editor test example") lay = QVBoxLayout() lay.setContentsMargins(0,0,0,0) self.setLayout(lay) # make editor self.editor = GcodeDisplay(self) # 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) ################################ # add menubar actions ################################ # Create new action icon = self.style().standardIcon( QStyle.SP_FileIcon) self.newAction = QAction(icon, 'New', self) self.newAction.setShortcut('Ctrl+N') self.newAction.setStatusTip('New document') self.newAction.triggered.connect(self.newCall) # Create open action icon = self.style().standardIcon( QStyle.SP_DirOpenIcon) self.openAction = QAction(icon, '&Open', self) self.openAction.setShortcut('Ctrl+O') self.openAction.setStatusTip('Open document') self.openAction.triggered.connect(self.openCall) # Create save action icon = self.style().standardIcon( QStyle.SP_DialogSaveButton) self.saveAction = QAction(icon, '&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.setStatusTip('Exit application') 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) # Create toolbar and add action self.toolBar = QToolBar('File') self.toolBar.addAction(self.newAction) self.toolBar.addAction(self.openAction) self.toolBar.addAction(self.saveAction) self.toolBar.addAction(self.exitAction) self.toolBar.addSeparator() # add lexer actions self.toolBar.addAction(self.gCodeLexerAction) self.toolBar.addAction(self.pythonLexerAction) self.toolBar.addSeparator() self.label = QLabel('''

Edit Mode

''') self.toolBar.addWidget(self.label) # create a frame for buttons self.topBox = QHBoxLayout() self.topBox.addWidget(self.toolBar) self.topMenu = QFrame() self.topMenu.setLayout(self.topBox) # add widgets lay.addWidget(self.topMenu) lay.addWidget(self.editor) lay.addWidget(self.createGroup()) self.readOnlyMode() def createGroup(self): self.bottomMenu = QFrame() self.searchText = QLineEdit(self) self.searchText.setStatusTip('Text to search for') self.replaceText = QLineEdit(self) self.replaceText.setStatusTip('Replace search text with this text') toolBar = QToolBar() # Create new action undoAction = QAction(QIcon.fromTheme('edit-undo'), 'Undo', self) undoAction.setStatusTip('Undo') undoAction.triggered.connect(self.undoCall) toolBar.addAction(undoAction) # create redo action redoAction = QAction(QIcon.fromTheme('edit-redo'), 'Redo', self) redoAction.setStatusTip('Redo') redoAction.triggered.connect(self.redoCall) toolBar.addAction(redoAction) toolBar.addSeparator() # create replace action replaceAction = QAction(QIcon.fromTheme('edit-find-replace'), 'Replace', self) replaceAction.setStatusTip('Replace text') replaceAction.triggered.connect(self.replaceCall) toolBar.addAction(replaceAction) # create find action findAction = QAction(QIcon.fromTheme('edit-find'), 'Find', self) findAction.setStatusTip('Find next occurrence of text') findAction.triggered.connect(self.findCall) toolBar.addAction(findAction) # create next action previousAction = QAction(QIcon.fromTheme('go-previous'), 'Find Previous', self) previousAction.setStatusTip('Find previous occurrence of text') previousAction.triggered.connect(self.previousCall) toolBar.addAction(previousAction) toolBar.addSeparator() # create case action caseAction = QAction(QIcon.fromTheme('edit-case'), 'Aa', self) caseAction.setStatusTip('Toggle between any case and match case') caseAction.setCheckable(1) caseAction.triggered.connect(self.caseCall) toolBar.addAction(caseAction) self.bottomBox = QHBoxLayout() self.bottomBox.addWidget(toolBar) self.bottomBox.addWidget(self.searchText) self.bottomBox.addWidget(self.replaceText) self.bottomBox.addStretch(1) self.bottomMenu.setLayout(self.bottomBox) return self.bottomMenu # 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: self.readOnlyMode() 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 _hal_init(self): # name the top and bottom frames so it's easier to style self.bottomMenu.setObjectName('%sBottomButtonFrame'% self.objectName()) self.topMenu.setObjectName('%sTopButtonFrame'% self.objectName()) self.editor.setObjectName('{}_display'.format( self.objectName())) def editMode(self): self.topMenu.show() self.bottomMenu.show() self.editor.setReadOnly(False) def readOnlyMode(self): self.topMenu.hide() self.bottomMenu.hide() self.editor.setReadOnly(True) 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', 'FILENAME':self.editor._last_filename} STATUS.emit('dialog-request', 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) def killCheck(self): choice = QMessageBox.question(self, 'Warning!!', "This file has changed since loading...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(int(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_metric(self,width): self.editor.set_margin_metric(width) def set_font(self, font): self.editor.setDefaultFont(font) 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) # for direct testing if __name__ == "__main__": from qtpy.QtWidgets import * from qtpy.QtCore import * from qtpy.QtGui import * sample_text = r"""( ----- Simple G-Code -----) G17 G20 G40 G49 G54 G80 G90 G94 M7 G0 Z10 F400 G1 Z1 G3 X50 Y50 Z1 I-7.5 J0 G0 Z30 G5.2 X3.53 Y-1.50 P2 X5.33 Y-11.01 P1 X3.52 Y-24.00 P1 X0.0 Y-29.56 P1 G5.3 G0 Z2 G1 Z-5 F100 F600 ( ----- Line Numbers ----- ) N1 N20 N300 N030 (insert numbers in place of .) N000 G1 X20 N98765 G0 Z30""" app = QApplication(sys.argv) w = GcodeEditor() w.editMode() if len(sys.argv) > 1: w.editor.load_text(sys.argv[1]) else: w.editor.setText(sample_text) if 0: w.toolBar.hide() if 1: w.pythonLexerAction.setVisible(True) w.gCodeLexerAction.setVisible(True) if 1: w.openAction.setVisible(False) w.newAction.setVisible(False) if 0: w.saveAction.setVisible(False) w.exitAction.setVisible(False) if 1: w.label.setText('Edit mode title label') w.show() sys.exit( app.exec() )