Files
LorenEteval_Furious/Furious/Window/TextEditorWindow.py
T
2026-08-12 12:20:14 +08:00

435 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (C) 2024present Loren Eteval & contributors <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/>.
"""Provide the application window for text editor window."""
from __future__ import annotations
from Furious.Frozenlib import *
from Furious.Library import *
from Furious.Qt import *
from Furious.Qt import gettext as _
from Furious.Widget.IndentSpinBox import *
from PySide6 import QtCore
from PySide6.QtGui import *
from PySide6.QtWidgets import *
import functools
__all__ = ['TextEditorWindow']
registerAppSettings('ServerWidgetPointSize')
class MBoxQuestionSave(AppQMessageBox):
"""Represent m box question save."""
def __init__(self, *args, **kwargs):
"""Initialize the MBoxQuestionSave."""
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 MBoxJSONDecodeError(AppQMessageBox):
"""Represent m box JSON decode error."""
def __init__(self, *args, **kwargs):
"""Initialize the MBoxJSONDecodeError."""
super().__init__(*args, **kwargs)
self.error = ''
def customText(self):
"""Return the user-facing message text for the m box JSON decode error."""
return (
_('Please check if the configuration is in valid JSON format')
+ f'\n\n{self.error}'
)
def retranslate(self):
"""Refresh translated text for the m box JSON decode error."""
self.setWindowTitle(_(self.windowTitle()))
self.setText(self.customText())
# Ignore informative text, buttons
self.moveToCenter()
class TextEditorWindow(AppQMainWindow):
"""Present the text editor window."""
def __init__(self, *args, **kwargs):
"""Initialize the TextEditorWindow."""
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():
"""Handle the modification callback."""
self.markAsModified()
def cursorChangedCallback(cursor: QTextCursor):
"""Handle the cursor changed callback."""
self.lineColumnLabel.setText(
f'{cursor.blockNumber() + 1}:{cursor.columnNumber() + 1} {cursor.position()}'
)
self.jsonEditor = DraculaJSONTextEditor(
fontFamily=AppFontName(),
pointSizeSettingsName='ServerWidgetPointSize',
)
self.jsonEditor.setLineWrapMode(DraculaJSONTextEditor.LineWrapMode.NoWrap)
self.jsonEditor.registerModificationChangedCb(modificationCallback)
self.jsonEditor.registerCursorPositionChangedCb(cursorChangedCallback)
self.setCentralWidget(self.jsonEditor)
if PLATFORM == 'Darwin':
self.toolbar = AppQToolBar(
AppQAction(
_('Close'),
icon=bootstrapIcon('window-x.svg'),
callback=lambda: self.close(),
),
parent=self,
)
self.addToolBar(self.toolbar)
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.close(),
),
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):
"""Handle mark as modified for the text editor window."""
self.modified = True
self.setWindowTitle(self.customWindowTitle + self.modifiedMark)
def markAsSaved(self):
"""Handle mark as saved for the text editor window."""
self.modified = False
self.setWindowTitle(self.customWindowTitle)
def setPlainText(self, text: str, blockSignals: bool):
"""Set plain text."""
if blockSignals:
with Mixins.QBlockSignalContext(self.jsonEditor):
self.jsonEditor.setPlainText(text)
else:
self.jsonEditor.setPlainText(text)
def save(self, showChangesMethod='open') -> bool:
"""Save the text editor window."""
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 = MBoxJSONDecodeError(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 = Storage.UserServers()[index]
new = configFactoryFromDict(jsonObject, **old.kwargs)
old.deleted = True
Storage.UserServers()[index] = new
try:
APP().mainWindow.flushRow(index, new)
except Exception:
# Any non-exit exceptions
pass
if index == Storage.UserActivatedItemIndex():
showMBoxNewChangesNextTime(parent=self, method=showChangesMethod)
self.markAsSaved()
return True
def saveAsFile(self):
"""Save as file."""
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):
"""Set indent."""
def handleResultCode(_indentSpinBox, code):
"""Handle result code."""
if code == PySide6Legacy.enumValueWrapper(AppQDialog.DialogCode.Accepted):
plain = self.jsonEditor.toPlainText()
try:
jsonObject = JSONEncoder.decode(plain)
except Exception as ex:
# Any non-exit exceptions
mbox = MBoxJSONDecodeError(
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)
)
# Show the MessageBox asynchronously
indentSpinBox.open()
def showTabAndSpaces(self):
"""Show tab and spaces."""
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):
"""Hide tab and spaces."""
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):
"""Handle closure of the text editor window."""
if self.modified:
mbox = MBoxQuestionSave(icon=AppQMessageBox.Icon.Question, parent=self)
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
def handleButtonClicked(button):
"""Handle button clicked."""
if button == mbox.button0:
# Save
if self.save(showChangesMethod='exec'):
mbox.close()
event.accept()
else:
event.ignore()
elif button == mbox.button1:
# Discard
self.markAsSaved()
mbox.close()
event.accept()
elif button == mbox.button2:
# Cancel. Do nothing
event.ignore()
mbox.buttonClicked.connect(functools.partial(handleButtonClicked))
# Show the MessageBox and wait for the user to close it
mbox.exec()
else:
event.accept()
def retranslate(self):
# Do nothing
"""Refresh translated text for the text editor window."""
pass