Import LinuxCNC source tree
This commit is contained in:
559
lib/python/qtvcp/widgets/geditor.py
Normal file
559
lib/python/qtvcp/widgets/geditor.py
Normal file
@@ -0,0 +1,559 @@
|
||||
#!/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() )
|
||||
|
||||
|
||||
Reference in New Issue
Block a user