Files
LorenEteval_Furious/Furious/Window/TextEditorWindow.py
2025-03-20 00:22:32 +08:00

403 lines
13 KiB
Python

# Copyright (C) 2024 Loren Eteval <loren.eteval@proton.me>
#
# This file is part of Furious.
#
# 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 3 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, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from Furious.PyFramework import *
from Furious.QtFramework import *
from Furious.QtFramework import gettext as _
from Furious.Utility import *
from Furious.Library import *
from Furious.Widget.IndentSpinBox import *
from PySide6 import QtCore
from PySide6.QtGui import *
from PySide6.QtWidgets import *
import functools
__all__ = ['TextEditorWindow']
registerAppSettings('ServerWidgetPointSize')
class QuestionSaveMBox(AppQMessageBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWindowTitle(_('Save Changes'))
self.setText(_('The content has been modified. Save changes?'))
self.button0 = self.addButton(_('Save'), AppQMessageBox.ButtonRole.AcceptRole)
self.button1 = self.addButton(
_('Discard'), AppQMessageBox.ButtonRole.DestructiveRole
)
self.button2 = self.addButton(_('Cancel'), AppQMessageBox.ButtonRole.RejectRole)
self.setDefaultButton(self.button0)
class JSONDecodeErrorMBox(AppQMessageBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.error = ''
def customText(self):
return (
_('Please check if the configuration is in valid JSON format')
+ f'\n\n{self.error}'
)
def retranslate(self):
self.setWindowTitle(_(self.windowTitle()))
self.setText(self.customText())
# Ignore informative text, buttons
self.moveToCenter()
class TextEditorWindow(AppQMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.customWindowTitle = ''
self.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
self.setFixedSize(450, int(450 * GOLDEN_RATIO))
# Current editing index
self.currentIndex = -1
self.modified = False
self.modifiedMark = ' *'
self.lineColumnLabel = QLabel('1:1 0')
self.statusBar().addPermanentWidget(self.lineColumnLabel)
def modificationCallback():
self.markAsModified()
def cursorChangedCallback(cursor: QTextCursor):
self.lineColumnLabel.setText(
f'{cursor.blockNumber() + 1}:{cursor.columnNumber() + 1} {cursor.position()}'
)
self.jsonEditor = DraculaJSONTextEditor(
fontFamily=APP().customFontName,
pointSizeSettingsName='ServerWidgetPointSize',
)
self.jsonEditor.setLineWrapMode(DraculaJSONTextEditor.LineWrapMode.NoWrap)
self.jsonEditor.registerModificationChangedCb(modificationCallback)
self.jsonEditor.registerCursorPositionChangedCb(cursorChangedCallback)
self.setCentralWidget(self.jsonEditor)
self.fileMenu = AppQMenu(
AppQAction(
_('Save'),
icon=bootstrapIcon('save.svg'),
callback=lambda: self.save(),
shortcut=QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier.ControlModifier,
QtCore.Qt.Key.Key_S,
),
),
AppQAction(
_('Save As...'),
callback=lambda: self.saveAsFile(),
),
AppQSeperator(),
AppQAction(
_('Close Window'),
callback=lambda: self.questionSave(),
),
title=_('File'),
parent=self.menuBar(),
)
self.editMenu = AppQMenu(
AppQAction(
_('Undo'),
icon=bootstrapIcon('arrow-return-left.svg'),
callback=lambda: self.jsonEditor.undo(),
shortcut=QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier.ControlModifier,
QtCore.Qt.Key.Key_Z,
),
),
AppQAction(
_('Redo'),
icon=bootstrapIcon('arrow-return-right.svg'),
callback=lambda: self.jsonEditor.redo(),
shortcut=QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier.ControlModifier
| QtCore.Qt.KeyboardModifier.ShiftModifier,
QtCore.Qt.Key.Key_Z,
),
),
AppQSeperator(),
AppQAction(
_('Cut'),
icon=bootstrapIcon('scissors.svg'),
callback=lambda: self.jsonEditor.cut(),
shortcut=QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier.ControlModifier,
QtCore.Qt.Key.Key_X,
),
),
AppQAction(
_('Copy'),
icon=bootstrapIcon('files.svg'),
callback=lambda: self.jsonEditor.copy(),
shortcut=QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier.ControlModifier,
QtCore.Qt.Key.Key_C,
),
),
AppQAction(
_('Paste'),
callback=lambda: self.jsonEditor.paste(),
shortcut=QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier.ControlModifier,
QtCore.Qt.Key.Key_V,
),
),
AppQSeperator(),
AppQAction(
_('Select All'),
callback=lambda: self.jsonEditor.selectAll(),
shortcut=QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier.ControlModifier,
QtCore.Qt.Key.Key_A,
),
),
AppQSeperator(),
AppQAction(
_('Indent...'),
callback=lambda: self.setIndent(),
),
title=_('Edit'),
parent=self.menuBar(),
)
self.viewMenu = AppQMenu(
AppQAction(
_('Zoom In'),
callback=lambda: self.jsonEditor.zoomIn(),
shortcut=QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier.ControlModifier,
QtCore.Qt.Key.Key_Plus,
),
),
AppQAction(
_('Zoom Out'),
callback=lambda: self.jsonEditor.zoomOut(),
shortcut=QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier.ControlModifier,
QtCore.Qt.Key.Key_Minus,
),
),
title=_('View'),
parent=self.menuBar(),
)
self.menuBar().addMenu(self.fileMenu)
self.menuBar().addMenu(self.editMenu)
self.menuBar().addMenu(self.viewMenu)
def markAsModified(self):
self.modified = True
self.setWindowTitle(self.customWindowTitle + self.modifiedMark)
def markAsSaved(self):
self.modified = False
self.setWindowTitle(self.customWindowTitle)
def questionSave(self):
if self.modified:
def handleButtonClicked(saveMBox, button):
assert isinstance(saveMBox, QuestionSaveMBox)
if saveMBox.button0 == button:
# Save
if self.save():
saveMBox.close()
self.hide()
elif saveMBox.button1 == button:
# Discard
self.markAsSaved()
saveMBox.close()
self.hide()
elif saveMBox.button2 == button:
# Cancel. Do nothing
pass
mbox = QuestionSaveMBox(icon=AppQMessageBox.Icon.Question, parent=self)
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
mbox.buttonClicked.connect(functools.partial(handleButtonClicked, mbox))
# dummy ref
setattr(self, '_questionSaveMBox', mbox)
# Show the MessageBox asynchronously
mbox.open()
else:
self.hide()
def setPlainText(self, text: str, blockSignals: bool):
if blockSignals:
with QBlockSignals(self.jsonEditor):
self.jsonEditor.setPlainText(text)
else:
self.jsonEditor.setPlainText(text)
def save(self) -> bool:
index = self.currentIndex
if index < 0:
# Should not reach here. Do nothing
self.markAsSaved()
return True
plain = self.jsonEditor.toPlainText()
try:
jsonObject = JSONEncoder.decode(plain)
except Exception as ex:
# Any non-exit exceptions
mbox = JSONDecodeErrorMBox(icon=AppQMessageBox.Icon.Critical, parent=self)
mbox.error = str(ex)
mbox.setWindowTitle(_('Error saving configuration'))
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
mbox.setText(mbox.customText())
# Show the MessageBox asynchronously
mbox.open()
return False
else:
old = AS_UserServers()[index]
new = constructFromDict(jsonObject, **old.kwargs)
FastItemDeletionSearch.moveToTrash(old)
AS_UserServers()[index] = new
try:
APP().mainWindow.flushRow(index, new)
except Exception:
# Any non-exit exceptions
pass
if index == AS_UserActivatedItemIndex():
showNewChangesNextTimeMBox(parent=self)
self.markAsSaved()
return True
def saveAsFile(self):
filename, selectedFilter = QFileDialog.getSaveFileName(
None, _('Save File'), filter=_('Text files (*.json);;All files (*)')
)
if filename:
try:
with open(filename, 'w', encoding='utf-8') as file:
file.write(self.jsonEditor.toPlainText())
except Exception as ex:
# Any non-exit exceptions
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Critical, parent=self)
mbox.setWindowTitle(_('Error Saving File'))
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
mbox.setText(_('Invalid server configuration'))
mbox.setInformativeText(str(ex))
# Show the MessageBox asynchronously
mbox.open()
def setIndent(self):
def handleResultCode(_indentSpinBox, code):
if code == PySide6LegacyEnumValueWrapper(AppQDialog.DialogCode.Accepted):
plain = self.jsonEditor.toPlainText()
try:
jsonObject = JSONEncoder.decode(plain)
except Exception as ex:
# Any non-exit exceptions
mbox = JSONDecodeErrorMBox(
icon=AppQMessageBox.Icon.Critical, parent=self
)
mbox.error = str(ex)
mbox.setWindowTitle(_('Error setting indent'))
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
mbox.setText(mbox.customText())
# Show the MessageBox asynchronously
mbox.open()
else:
text = JSONEncoder.encode(jsonObject, indent=_indentSpinBox.value())
self.setPlainText(text, False)
else:
# Do nothing
pass
indentSpinBox = IndentSpinBox(parent=self)
indentSpinBox.finished.connect(
functools.partial(handleResultCode, indentSpinBox)
)
# dummy ref
setattr(self, '_IndentSpinBox', indentSpinBox)
# Show the MessageBox asynchronously
indentSpinBox.open()
def showTabAndSpaces(self):
textOption = QTextOption()
textOption.setFlags(QTextOption.Flag.ShowTabsAndSpaces)
self.jsonEditor.document().setDefaultTextOption(textOption)
# Reset. Set
self.jsonEditor.setLineWrapMode(QPlainTextEdit.LineWrapMode.WidgetWidth)
self.jsonEditor.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
def hideTabAndSpaces(self):
textOption = QTextOption()
self.jsonEditor.document().setDefaultTextOption(textOption)
# Reset. Set
self.jsonEditor.setLineWrapMode(QPlainTextEdit.LineWrapMode.WidgetWidth)
self.jsonEditor.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
def closeEvent(self, event: QtCore.QEvent):
event.ignore()
self.questionSave()
def retranslate(self):
# Do nothing
pass