Files
linuxcncAllWasm/lib/python/qtvcp/widgets/geditor.py
2026-08-20 06:17:39 +08:00

560 lines
20 KiB
Python

#!/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('''<html><head/><body><p><span style=" font-size:20pt;
font-weight:600;">Edit Mode</span></p></body></html>''')
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('<b>Edit mode title label</b>')
w.show()
sys.exit( app.exec() )