x01.DiamondIDE
虽然一直在用 vscode,但自己写一个也不错。通过比较,选择 Spyder 来学习。代码: x01.DiamondIDE
效果图
1 Edit
1.1 hello DiamondIDE
使用 pip 安装所需模块 pyqt 等,自不待言。hello.py 代码如下:
from qtpy.QtWidgets import QApplication, QPlainTextEdit
app = QApplication(['x01.DiamondIDE'])
edit = QPlainTextEdit('hello DiamondIDE')
edit.show()
app.exec_()
终端输入: python3 hello.py
运行一下,OK!
1.2 添加测试
删除 hello.py, 添加 widgets/edit.py 如下:
# widgets/edit.py (c) 2021 by x01
from qtpy.QtWidgets import QPlainTextEdit, QApplication
class Edit(QPlainTextEdit):
def __init__(self, parent=None):
super().__init__(parent=parent)
def test_edit():
app = QApplication(['x01.DiamondIDE'])
edit = Edit()
edit.setPlainText('Hello IDE!')
edit.show()
app.exec()
if __name__ == "__main__":
test_edit()
添加 tests/test_edit.py 如下:
import os, sys
RootDir = os.path.dirname(os.path.dirname(__file__))
sys.path.append(RootDir)
import widgets.edit as edit
def test_edit():
edit.test_edit()
先安装 pytest: python3 -m pip install -U pytest
, 然后在终端运行测试: pytest
, OK!
顺便添加 main.py,代码如下:
import os, sys
CurrDir = os.path.dirname(__file__)
sys.path.append(CurrDir)
from widgets.edit import test_edit
def main():
test_edit()
if __name__ == "__main__":
main()
运行一下,OK!
注释 tests/test_edit.py 的 RootDir,添加 test.py 以在测试时统一添加路径,代码如下:
import os, sys
RootDir = os.path.dirname(__file__)
sys.path.append(RootDir)
import pytest
if __name__ == "__main__":
pytest.main()
运行一下,OK!
1.3 切入点
在 Python 的 site-packages 目录下新建 mypath.pth 文件,添加 x01.DiamondIDE 所在路径,以便导入。
Spyder 太大,还是以 CodeEditor 作为切入点。
widgets/edit.py 更改如下:
# widgets/edit.py (c) 2021 by x01
from PyQt5.QtGui import QColor, QFont, QPaintEvent, QPainter, QSyntaxHighlighter, QTextBlock, QTextCharFormat, QTextDocument, QTextFormat
from PyQt5.QtCore import QRect, Qt, QSize
from PyQt5.QtWidgets import QMainWindow, QTextEdit, QPlainTextEdit, QApplication, QWidget
from functools import namedtuple
import re
def get_span(match, key=None):
if key is not None:
start, end = match.span(key)
else:
start, end = match.span()
start = len(match.string[:start])
end = len(match.string[:end])
return start, end
class Highlighter(QSyntaxHighlighter):
HighlightingRule = namedtuple('HighlightingRule', ['pattern', 'format'])
def __init__(self, parent: QTextDocument=None):
super().__init__(parent)
self.keywordFormat = QTextCharFormat()
self.keywordFormat.setForeground(Qt.red)
self.keywordFormat.setFontWeight(QFont.Bold)
self.keywords = r'\b' + '(?P<keyword>' + '|'.join("class int char".split()) + ')' + r'\b'
def highlightBlock(self, text:str):
patterns = re.compile(self.keywords, re.S)
match = patterns.search(text)
index = 0
while match:
for key, value in list(match.groupdict().items()):
if value:
start, end = get_span(match, key)
index += end - start
self.setFormat(start, end-start, self.keywordFormat)
match = patterns.search(text, match.end())
class LineNumberArea(QWidget):
def __init__(self, editor=None):
super().__init__(editor)
self.editor = editor
self.left_padding = 3
self.right_padding = 6
# override
def sizeHint(self):
return QSize(self.editor.LineNumberAreaWidth(), 0)
def paintEvent(self, event):
self.editor.LineNumberAreaPaintEvent(event)
class CodeEditor(QPlainTextEdit):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.line_number_area = LineNumberArea(self)
self.line_number_enabled = True
#event
self.blockCountChanged.connect(self.UpdateLineNumberAreaWidth)
self.updateRequest.connect(self.UpdateLineNumberArea)
self.cursorPositionChanged.connect(self.HighlightCurrentLine)
self.UpdateLineNumberAreaWidth(0)
self.HighlightCurrentLine()
self.highlighting = Highlighter(self.document())
def resizeEvent(self, event):
super().resizeEvent(event)
cr:QRect = self.contentsRect()
self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.LineNumberAreaWidth(), cr.height()))
def LineNumberAreaWidth(self):
width = 0
if self.line_number_enabled:
digits = 1
count = max(1, self.blockCount())
while count >= 10:
count /= 10
digits += 1
fm = self.fontMetrics()
width = fm.width('9') * digits + self.line_number_area.left_padding + self.line_number_area.right_padding
return width
def LineNumberAreaPaintEvent(self, event:QPaintEvent):
if self.line_number_enabled:
painter = QPainter(self.line_number_area)
painter.fillRect(event.rect(), Qt.lightGray)
block:QTextBlock = self.firstVisibleBlock()
block_number = block.blockNumber()
top = round(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
bottom = top + round(self.blockBoundingRect(block).height())
while block.isValid() and top <= event.rect().bottom():
if block.isVisible() and bottom >= event.rect().top():
number = block_number + 1
painter.setPen(Qt.black)
painter.drawText(0, top, self.line_number_area.width() - self.line_number_area.right_padding,
self.fontMetrics().height(), Qt.AlignRight, str(number))
block = block.next()
top = bottom
bottom = top + round(self.blockBoundingRect(block).height())
block_number += 1
def UpdateLineNumberAreaWidth(self, new_block_count=None):
self.setViewportMargins(self.LineNumberAreaWidth(),0,0,0)
def UpdateLineNumberArea(self, rect, dy):
if self.line_number_enabled:
if dy:
self.line_number_area.scroll(0, dy)
else:
self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height())
if rect.contains(self.viewport().rect()):
self.UpdateLineNumberAreaWidth(0)
def HighlightCurrentLine(self):
extra = []
if not self.isReadOnly():
lineColor = QColor(Qt.yellow).lighter(160)
selection = QTextEdit.ExtraSelection()
selection.format.setBackground(lineColor)
selection.format.setProperty(QTextFormat.FullWidthSelection, True)
selection.cursor = self.textCursor()
selection.cursor.clearSelection()
extra.append(selection)
self.setExtraSelections(extra)
def test_edit():
app = QApplication(['x01.DiamondIDE'])
ed = CodeEditor()
ed.setPlainText('Hello IDE!')
ed.show()
app.exec_()
if __name__ == "__main__":
test_edit()
运行一下,OK!现在已经可以显示行号和语法高亮了。
2 PyQode
运行 spyder 时 ipython console 出错:extra_extension 不能为 1。 在 ~/.ipython/profile_default/ipython_kernel_config.py
中,注释掉如下行即可:
# c.InteractiveShellApp.extra_extension = 1
发现 pyqode 基本上能够满足自动完成,代码折叠,语法高亮,智能提示等主要功能,故搬运之,运行一下,比想象的效果要好。
2.1 FoldPanel
QPlainTextEdit 的内容为 QTextDocument, 而 QTextDocument 由 QTextBlock 组成,处理语法高亮,代码折叠,实则是处理 QTextBlock。在 core/edit.py 中添加类 R 和 FoldPanel 如下:
class R:
TabSize = 4
'''
QPlainTextEdit 的内容为 QDocument, 而 QDocument 由 QTextBlock 组成,
处理语法高亮,代码折叠,实则是处理 QTextBlock
'''
# QTextBlock .userState
# bit0-15: syntax highlighter
# bit16-25: fold level
# bit26: fold trigger flag (折叠箭头所在行)
# bit27: fold trigger state (expanded or collapsed 折叠内容)
@staticmethod
def GetBlockState(block:QTextBlock):
if block is None: return -1
state = block.userState()
if state == -1: return state
return state & 0x0000FFFF
@staticmethod
def SetBlockState(block:QTextBlock, state:int):
if block is None: return
user_state = block.userState()
if user_state == -1:
user_state = 0
high = user_state & 0x7FFF0000
state &= 0x0000FFFF
state |= high
block.setUserState(state)
@staticmethod
def GetBlockFoldLevel(block:QTextBlock):
if block is None: return 0
state = block.userState()
if state == -1: state = 0
return (state & 0x03FF0000) >> 16 # bit16-25
@staticmethod
def SetBlockFoldLevel(block:QTextBlock, level:int):
if block is None: return
state = block.userState()
if state == -1: state = 0
if level >= 0x3FF: level = 0x3FF
state &= 0x7C00FFFF
state |= level << 16
block.setUserState(state)
@staticmethod
def IsBlockFoldTrigger(block:QTextBlock):
if block is None: return False
state = block.userState()
if state == -1: state = 0
return bool(state & 0x04000000)
@staticmethod
def SetBlockFoldTrigger(block:QTextBlock, trigger:bool):
if block is None: return
state = block.userState()
if state == -1: state = 0
state &= 0x7BFFFFFF
state |= int(trigger) << 26
block.setUserState(state)
@staticmethod
def IsBlockCollapsed(block:QTextBlock):
if block is None: return False
state = block.userState()
if state == -1: state = 0
return bool(state & 0x08000000)
@staticmethod
def SetBlockCollapsed(block:QTextBlock, collapsed:bool):
if block is None: return
state = block.userState()
if state == -1: state = 0
state &= 0x77FFFFFF
state |= int(collapsed) << 27
block.setUserState(state)
class FoldPanel():
def __init__(self, edit:QPlainTextEdit=None):
self.edit = edit
self.document: QTextDocument = edit.document()
def UpdateBlocks(self):
block = self.document.firstBlock()
if block is None: return
last_block = self.document.lastBlock()
prev_block = block
block = block.next()
while block and block != last_block :
self.ProcessBlock(prev_block, block)
prev_block = block
block = block.next()
def ProcessBlock(self, prev_block:QTextBlock, current_block:QTextBlock):
text = current_block.text()
prev_fold_level = R.GetBlockFoldLevel(prev_block)
if text.strip() == '':
fold_level = prev_fold_level
else:
fold_level = self.DetectFoldLevel(prev_block, current_block)
if fold_level > 0x03FF: fold_level = 0x03FF
prev_fold_level = R.GetBlockFoldLevel(prev_block)
if fold_level > prev_fold_level:
block = current_block.previous()
while block.isValid() and block.text().strip() == '':
R.SetBlockFoldLevel(block, fold_level)
block = block.previous()
R.SetBlockFoldTrigger(block, True) # 上一层非空行为触发行
if text.strip():
R.SetBlockFoldTrigger(prev_block, fold_level > prev_fold_level)
R.SetBlockFoldLevel(current_block, fold_level)
prev :QTextBlock= current_block.previous()
if (prev and prev.isValid() and prev.text().strip() == '' and
R.IsBlockFoldTrigger(prev)):
R.SetBlockCollapsed(current_block, R.IsBlockCollapsed(prev))
R.SetBlockFoldTrigger(prev, False)
R.SetBlockCollapsed(prev, False)
def DetectFoldLevel(self, prev_block:QTextBlock, current_block:QTextBlock):
text = current_block.text()
curr_level = (len(text) - len(text.lstrip())) // R.TabSize
prev_level = R.GetBlockFoldLevel(prev_block)
if prev_level and curr_level > prev_level and not(self.StripComment(prev_block).endswith(':')):
curr_level = prev_level
# curr_level = self.ProcessDocstring(prev_block, current_block, curr_level)
# curr_level = self.ProcessImport(prev_block, current_block, curr_level)
return curr_level
def StripComment(self, block:QTextBlock):
text = block.text().strip() if block else ''
i = text.find('#')
if i != -1:
text = text[:i].strip()
return text
测试一下:
def print_tree(ed):
block:QTextBlock = ed.document().firstBlock()
while block.isValid():
trigger = R.IsBlockFoldTrigger(block)
collapsed = R.IsBlockCollapsed(block)
level = R.GetBlockFoldLevel(block)
visible = 'V' if block.isVisible() else 'I'
if trigger:
flag = '+' if collapsed else '-'
print(f'{block.blockNumber()+1}: {level}{flag}{visible}')
# else:
# print(f'{block.blockNumber()+1}: {level}{visible}')
block = block.next()
def test_edit():
app = QApplication(['x01.DiamondIDE'])
ed = CodeEdit()
text = ''
with open(__file__, 'r') as f:
text = f.read()
ed.setPlainText(text)
ed.foldPanel.UpdateBlocks()
print_tree(ed)
ed.show()
app.exec_()
if __name__ == "__main__":
test_edit()
基本上可以达到预期的目的。
2.1.1 实现代码折叠与展开
参考 LineNumberArea, 初步实现 FoldPanel, 但主要折叠功能在 CodeEdit 中实现, 关键代码如下。
FoldBlockModel = namedtuple('FoldBlockModel', ['trigger', 'collapsed', 'level', 'block'])
class FoldPanel(QWidget):
def __init__(self, editor: QPlainTextEdit = None):
super().__init__(editor)
self.editor = editor
self.document: QTextDocument = editor.document()
# override
def sizeHint(self):
return QSize(R.FoldPanelWidth, 0)
def paintEvent(self, event: QPaintEvent):
self.editor.FoldPanelPaintEvent(event)
class CodeEdit(QPlainTextEdit):
# ContextMenu
def InitContextMenu(self):
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.ContextMenuShow)
self.context_menu = QtWidgets.QMenu('Folding', self)
action = QtWidgets.QAction('Toggle Collapsed', self.context_menu)
action.setShortcut('Shift+-')
action.triggered.connect(self.ToggleCollapsed)
self.context_menu.addAction(action)
def ContextMenuShow(self):
self.context_menu.exec(QCursor.pos())
def ToggleCollapsed(self):
self.UpdateBlocks()
doc:QTextDocument = self.document()
first = doc.firstBlock()
line = self.textCursor().blockNumber()
curr = doc.findBlockByNumber(line)
if not curr.isValid(): return
if R.IsBlockFoldTrigger(curr):
models = self.GetCollapsedBlocks(curr)
for m in models:
isCollapsed = R.IsBlockCollapsed(m.block)
m.block.setVisible(isCollapsed)
R.SetBlockCollapsed(m.block, not isCollapsed)
doc.adjustSize()
def GetCollapsedBlocks(self, trigger_block):
i = trigger_block.blockNumber()
level = self.fold_models[i].level
end = i
for m in self.fold_models[i+1:]:
end += 1
if m.level <= level: break
if end > len(self.fold_models): end = len(self.fold_models)
return self.fold_models[i+1:end]
# FoldPanel
def FoldPanelPaintEvent(self, event):
painter = QPainter(self.foldPanel)
painter.fillRect(event.rect(), Qt.green)
def UpdateFoldPanel(self, rect, dy):
self.UpdateBlocks()
def UpdateBlocks(self):
self.fold_models.clear()
self.foldPanel.UpdateBlocks()
block: QTextBlock = self.document().firstBlock()
while block.isValid():
trigger = R.IsBlockFoldTrigger(block)
collapsed = R.IsBlockCollapsed(block)
level = R.GetBlockFoldLevel(block)
self.fold_models.append(FoldBlockModel(trigger, collapsed, level, block))
block = block.next()
OK!现在已经可以在快捷菜单中实现代码折叠与展开了。
2.1.2 画上箭头
在 FoldPanel 中添加上箭头,貌似还不错。core/edit.py 主要更改如下:
FoldBlockModel = namedtuple('FoldBlockModel', ['trigger', 'collapsed', 'level', 'block', 'triggered'])
FoldBlockModel.__new__.__defaults__ = (False, False, -1, None, False)
class FoldPanel:
def mousePressEvent(self, e:QMouseEvent):
self.editor.mousePressEvent(e)
class CodeEdit:
def mousePressEvent(self, e: QMouseEvent):
super(CodeEdit, self).mousePressEvent(e)
if e.pos().x() > self.LineNumberAreaWidth()+R.FoldPanelWidth: return
block = self.GetTriggerBlock(e.pos())
if block is None: return
i = 0
for m in self.fold_models[:]:
if block == m.block and e.buttons() == Qt.LeftButton:
self.ToggleFold(block)
break
i += 1
self.foldPanel.scroll(0,1)
self.foldPanel.scroll(0,-1)
def GetTriggerBlock(self, pos):
height = self.fontMetrics().height()
for m in self.fold_models[:]:
if m.trigger:
top = self.blockBoundingGeometry(m.block).translated(self.contentOffset()).top()
if top <= pos.y() <= top + height:
return m.block
def FoldPanelPaintEvent(self, event):
painter = QPainter(self.foldPanel)
for m in self.fold_models:
if m.trigger:
top = self.blockBoundingGeometry(m.block).translated(self.contentOffset()).top()
height = self.fontMetrics().height()
path = R.RightOffIconPath if m.triggered else R.DownOffIconPath
QIcon(path).paint(painter,0,top,R.FoldPanelWidth, height)
self.document().adjustSize()
def InitFoldBlockModels(self):
self.fold_models.clear()
self.foldPanel.UpdateBlocks()
block: QTextBlock = self.document().firstBlock()
while block.isValid():
trigger = R.IsBlockFoldTrigger(block)
collapsed = R.IsBlockCollapsed(block)
level = R.GetBlockFoldLevel(block)
self.fold_models.append(FoldBlockModel(trigger, collapsed, level, block, False))
block = block.next()
def UpdateBlocks(self, n:int=0):
models = self.fold_models[:]
limit = len(models)
i = 0
self.fold_models.clear()
self.foldPanel.UpdateBlocks()
block: QTextBlock = self.document().firstBlock()
while block.isValid():
trigger = R.IsBlockFoldTrigger(block)
collapsed = R.IsBlockCollapsed(block)
level = R.GetBlockFoldLevel(block)
triggered = models[i].triggered if i < limit and models[i].block == block and trigger else False
self.fold_models.append(FoldBlockModel(trigger, collapsed, level, block, triggered))
block = block.next()
i += 1
self.foldPanel.scroll(0,1)
self.foldPanel.scroll(0,-1)
为了更新箭头,采取 self.foldPanel.scroll()
的方式,多少显得笨拙。 凡此种种,不及细言。
2.2 AutoComplete
采取字典的方式,初步实现"",'',(),{},[]
的自动完成。
class AutoComplete:
def __init__(self, editor):
self.editor:CodeEdit = editor
self.MAPPING = {'"': '"', "'": "'", "(": ")", "{": "}", "[": "]"}
class CodeEdit:
def KeyPressed(self, event):
text = event.text()
if text in self.MAPPING.keys():
self.editor.InsertText(self.MAPPING[text])
def InsertText(self, text, keep_pos=True):
cursor:QTextCursor = self.textCursor()
QTextCursor
if keep_pos:
s = cursor.selectionStart()
e = cursor.selectionEnd()
cursor.insertText(text)
if keep_pos:
cursor.setPosition(s)
cursor.setPosition(e, cursor.KeepAnchor)
self.setTextCursor(cursor)
2.3 Intellisense
根据文档的内容,初步实现智能提示。
class Intellisense:
def __init__(self, editor):
self.editor:CodeEdit = editor
self.key =''
self.results = {}
def UpdateSource(self):
source = self.editor.toPlainText().split('\n')
for line in source:
words = re.split(r'\W+', line)
for word in words:
if word == '': continue
key = ''
for c in word:
key += c
if self.results.get(key) is None:
self.results[key] =[word]
elif word in self.results[key] :
continue
else:
self.results[key].append(word)
def ShowTips(self, event:QKeyEvent):
if self.key == '': self.UpdateSource()
key = event.text()
if key in [' ', '(', ')', '[', ']', ',', '.', ';', '"', "'", ':', '+', '=']:
self.key = ''
QtWidgets.QToolTip.hideText()
return
self.key += key
results = self.results
if self.key in results.keys():
print(self.key)
tip = ''
for word in results[self.key]:
tip += '<p>'+word+'</p>'
pos = QPoint(self.editor.cursorRect().x(), self.editor.cursorRect().y())
pos = self.editor.mapToGlobal(pos)
QtWidgets.QToolTip.showText(pos, tip, self.editor)
3. 添加主窗口
在 main.py 中添加 MainWindow, 代码如下:
import os
import platform
import sys
from PyQt5.QtWidgets import QAction, QApplication, QDesktopWidget, QFileDialog, QFrame, QLabel, QMainWindow, QMessageBox
from PyQt5.QtGui import QIcon, QKeySequence
from PyQt5.QtCore import QByteArray, QFile, QFileInfo, QSettings
from core.edit import CodeEdit
CurrDir = os.path.dirname(__file__)
sys.path.append(CurrDir)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.filename = None
self.printer = None
self.editor = CodeEdit()
self.setCentralWidget(self.editor)
self.sizeLabel = QLabel()
self.sizeLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
status = self.statusBar()
status.setSizeGripEnabled(False)
status.addPermanentWidget(self.sizeLabel)
status.showMessage("Ready")
fileNewAction = self.createAction("&New", self.FileNew,
QKeySequence.New, "filenew", "Create a file")
fileOpenAction = self.createAction("&Open", self.FileOpen,
QKeySequence.Open, "fileopen", "Open a exist file")
fileSaveAction = self.createAction("&Save", self.FileSave,
QKeySequence.Save, "filesave", "Save the file")
fileSaveAsAction = self.createAction("Save &as", self.FileSaveAs,
icon="filesaveas", tip="Save file using a new filename")
fileQuitAction = self.createAction("&Quit", self.close,
"Ctrl+Q", "filequit", "Close the application")
self.fileMenu = self.menuBar().addMenu("&File")
self.fileMenuActions = (fileNewAction, fileOpenAction,
fileSaveAction, fileSaveAsAction, None, fileQuitAction)
settings = QSettings()
self.recentFiles = []
if settings.value("RecentFiles"):
self.recentFiles = settings.value("RecentFiles")
if settings.value("MainWindow/Geometry"):
self.restoreGeometry( QByteArray(settings.value("MainWindow/Geometry")) )
if settings.value("MainWindow/State"):
self.restoreState( QByteArray(settings.value("MainWindow/State")) )
self.UpdateFileMenu()
self.setWindowTitle("Image changer")
self.resize(900,600)
self.SetCenter()
def SetCenter(self):
screen = QDesktopWidget().screenGeometry()
size = self.geometry()
self.move((screen.width()-size.width())/2, (screen.height()-size.height())/4)
def createAction(self, text, slot=None, shortcut=None, icon=None,
tip=None, checkable=False, signal="triggered"):
action = QAction(text, self)
actSignal = None
if signal == "triggered":
actSignal = action.triggered
elif signal == "toggled":
actSignal = action.toggled
elif signal == "changed":
actSignal = action.changed
elif signal == "hovered":
actSignal = action.hovered
else:
actSignal = action.triggered
if icon is not None:
iconpath = os.path.join(CurrDir, 'core/images/'+icon+'.png')
action.setIcon(QIcon(iconpath))
if shortcut is not None:
action.setShortcut(shortcut)
if tip is not None:
action.setToolTip(tip)
if slot is not None:
actSignal.connect(slot)
if checkable:
action.setCheckable(True)
return action
def closeEvent(self, e):
if self.IsSave():
settings = QSettings()
settings.setValue("LastFile", self.filename)
settings.setValue("RecentFiles", self.recentFiles)
settings.setValue("MainWindow/Geometry", self.saveGeometry())
settings.setValue("MainWindow/State", self.saveState())
else:
e.ignore()
def IsSave(self):
if self.editor.dirty:
reply = QMessageBox.question(self, "x01.DiamondIDE", "Save changed file?",
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
if reply == QMessageBox.Yes:
return self.FileSave()
elif reply == QMessageBox.No:
return True
else:
return False
return True
# file actions
def FileNew(self):
if not self.IsSave(): return
self.editor.setPlainText('')
self.editor.dirty = False
def FileOpen(self):
if not self.IsSave():
return
fname, ext = QFileDialog.getOpenFileName( self, "x01.DiamondIDE - Choose File", CurrDir,
"Python files (*.py);;All files (*.*)" )
self.LoadFile(filename=fname)
def FileSave(self):
if self.filename is None:
return self.FileSaveAs()
else:
with open(self.filename, 'w') as f:
text = self.editor.toPlainText()
f.write(text)
self.addRecentFile(self.filename)
return True
def FileSaveAs(self):
fname, ext = QFileDialog.getSaveFileName( self, "x01.DiamondIDE", CurrDir,
"Python files (*.py);;All files (*.*)" )
with open(fname, 'w') as f:
text = self.editor.toPlainText()
f.write(text)
self.recentFiles.append(fname)
def addRecentFile(self, fname):
if fname is None:
return
if fname not in self.recentFiles:
self.recentFiles.insert(0, fname)
if len(self.recentFiles) > 9:
self.recentFiles = self.recentFiles[:9]
def updateStatus(self, message):
self.statusBar().showMessage(message)
def UpdateFileMenu(self):
self.fileMenu.clear()
self.AddActions(self.fileMenu, self.fileMenuActions[:-1])
recentFiles = []
for fname in self.recentFiles:
if QFile.exists(fname):
recentFiles.append(fname)
if recentFiles:
self.fileMenu.addSeparator()
iconpath = os.path.join(CurrDir, 'core/images/icon.png')
for i, fname in enumerate(recentFiles):
action = QAction(QIcon(iconpath),
"&{0} {1}".format(i+1, QFileInfo(fname).fileName()), self)
action.setData(fname)
action.triggered.connect(self.LoadFile)
self.fileMenu.addAction(action)
self.fileMenu.addSeparator()
self.fileMenu.addAction(self.fileMenuActions[-1])
def LoadFile(self, trigger=False, filename=None):
if not self.IsSave(): return
if filename is None:
action = self.sender()
if isinstance(action, QAction):
filename = str(action.data())
if filename in self.recentFiles:
self.recentFiles.remove(filename)
self.recentFiles.insert(0,filename)
else:
return
if filename:
with open(filename, 'r') as f:
text = f.read()
self.editor.setPlainText(text)
self.filename = filename
self.addRecentFile(filename)
self.UpdateFileMenu()
def AddActions(self, target, actions):
for action in actions:
if action is None:
target.addSeparator()
else:
target.addAction(action)
if __name__ == "__main__":
app = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(app.exec())
运行一下,基本可用了.顺便把多余的文件删除掉,OK!
3.1 完善 AutoComplete
遇到自动完成字典中的值[')', '}', ']']
时,光标移到下一字符,再删除该字符即可。改动代码如下:
class AutoComplete:
def __init__(self, editor):
self.editor: CodeEdit = editor
self.map = {'"': '"', "'": "'", "(": ")", "{": "}", "[": "]"}
self.avoid_duplicates = [')', '}', ']']
self.next_chars = []
self.curr_block_number = None
def KeyPressed(self, event:QKeyEvent):
text = event.text()
if text in self.map.keys():
self.editor.InsertText(self.map[text])
if self.map[text] in self.avoid_duplicates:
self.next_chars.append(self.map[text])
self.curr_block_number = self.editor.textCursor().blockNumber()
class CodeEdit:
def keyPressEvent(self, e: QKeyEvent):
super(CodeEdit, self).keyPressEvent(e)
if self.autoComplete.curr_block_number != self.textCursor().blockNumber():
self.autoComplete.next_chars.clear()
if e.text() in self.autoComplete.next_chars and e.text() == self.GetRightChar():
cursor:QTextCursor = self.textCursor()
cursor.movePosition(cursor.NextCharacter,cursor.MoveAnchor)
cursor.deletePreviousChar()
self.setTextCursor(cursor)
self.autoComplete.next_chars.remove(e.text())
self.autoComplete.KeyPressed(e)
self.intellisense.ShowTips(e)
self.update()
self.dirty = True