560 lines
20 KiB
Python
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() )
|
|
|
|
|