from Furious.Action.Import import ImportLinkAction, ImportJSONAction from Furious.Action.Export import ExportLinkAction, ExportJSONAction, ExportQRCodeAction from Furious.Core.Intellisense import Intellisense from Furious.Core.Configuration import Configuration from Furious.Gui.Action import Action, Seperator from Furious.Widget.Widget import ( Menu, MessageBox, PushButton, TabWidget, ZoomablePlainTextEdit, ) from Furious.Widget.IndentSpinBox import IndentSpinBox from Furious.Utility.Constants import ( PLATFORM, APPLICATION_NAME, APPLICATION_VERSION, APPLICATION_ABOUT_PAGE, APPLICATION_REPO_OWNER_NAME, APPLICATION_REPO_NAME, GOLDEN_RATIO, ) from Furious.Utility.Utility import ( Base64Encoder, StateContext, Storage, Switch, SupportConnectedCallback, bootstrapIcon, swapListItem, moveToCenter, ) from Furious.Utility.Translator import Translatable, gettext as _ from Furious.Utility.Theme import DraculaTheme from PySide6 import QtCore from PySide6.QtGui import QBrush, QColor, QDesktopServices, QFont, QTextOption from PySide6.QtWidgets import ( QAbstractItemView, QApplication, QDialog, QFileDialog, QGridLayout, QHBoxLayout, QHeaderView, QLabel, QLineEdit, QListWidget, QListWidgetItem, QMainWindow, QPlainTextEdit, QSplitter, QStyledItemDelegate, QTableWidget, QTableWidgetItem, QTabWidget, QVBoxLayout, QWidget, ) from PySide6.QtNetwork import ( QNetworkAccessManager, QNetworkReply, QNetworkRequest, QNetworkProxy, ) import os import copy import ujson import logging logger = logging.getLogger(__name__) class NewBlankServerAction(Action): def __init__(self, **kwargs): super().__init__(_('New...'), **kwargs) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier, QtCore.Qt.Key.Key_N, ) ) def triggeredCallback(self, checked): self.parent().importServer(_('Untitled'), '', syncStorage=True) class ImportFileAction(Action): def __init__(self, **kwargs): super().__init__( _('Import From File...'), icon=bootstrapIcon('folder2-open.svg'), **kwargs, ) self.openErrorBox = MessageBox( icon=MessageBox.Icon.Critical, parent=self.parent() ) def triggeredCallback(self, checked): filename, selectedFilter = QFileDialog.getOpenFileName( self.parent(), _('Import File'), filter=_('Text files (*.json);;All files (*)'), ) if filename: try: with open(filename, 'r', encoding='utf-8') as file: plainText = file.read() except Exception: # Any non-exit exceptions self.openErrorBox.setWindowTitle(_('Error opening file')) self.openErrorBox.setText(_('Invalid configuration file.')) # Show the MessageBox and wait for user to close it self.openErrorBox.exec() else: self.parent().importServer( os.path.basename(filename), plainText, syncStorage=True ) class ImportLinkActionWithHotKey(ImportLinkAction): def __init__(self, **kwargs): # Cloned. With hot key super().__init__(**kwargs) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier | QtCore.Qt.KeyboardModifier.ShiftModifier, QtCore.Qt.Key.Key_V, ) ) class ImportJSONActionWithHotKey(ImportJSONAction): def __init__(self, **kwargs): # Cloned. With hot key super().__init__(**kwargs) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier, QtCore.Qt.Key.Key_J, ) ) class ExportLinkActionWithHotKey(ExportLinkAction): def __init__(self, **kwargs): # Cloned. With hot key super().__init__(**kwargs) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier | QtCore.Qt.KeyboardModifier.ShiftModifier, QtCore.Qt.Key.Key_C, ) ) class ExportQRCodeActionWithHotKey(ExportQRCodeAction): def __init__(self, **kwargs): # Cloned. With hot key super().__init__(**kwargs) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier | QtCore.Qt.KeyboardModifier.ShiftModifier, QtCore.Qt.Key.Key_Q, ) ) class ExportJSONActionWithHotKey(ExportJSONAction): def __init__(self, **kwargs): # Cloned. With hot key super().__init__(**kwargs) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier | QtCore.Qt.KeyboardModifier.ShiftModifier, QtCore.Qt.Key.Key_J, ) ) class SelectAllServerAction(Action): def __init__(self, **kwargs): super().__init__(_('Select All'), **kwargs) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier | QtCore.Qt.KeyboardModifier.ShiftModifier, QtCore.Qt.Key.Key_A, ) ) class ScrollToActivatedServerAction(Action): def __init__(self, **kwargs): super().__init__(_('Scroll To Activated Server'), **kwargs) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier, QtCore.Qt.Key.Key_G, ) ) def triggeredCallback(self, checked): if QApplication.instance().MainWidget.modified: self.parent().saveChangeFirst.exec() return activatedItem = self.parent().activatedItem self.parent().setCurrentItem(activatedItem) self.parent().scrollToItem(activatedItem) class JSONErrorBox(MessageBox): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.decodeError = '' def getText(self): return ( _('Please check your configuration is in valid JSON format:') + f'\n\n{self.decodeError}' ) def retranslate(self): with StateContext(self): self.setWindowTitle(_(self.windowTitle())) self.setText(self.getText()) # Ignore informative text, buttons self.moveToCenter() class SaveConfInfo(MessageBox): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.addButton(_('Reconnect'), MessageBox.ButtonRole.AcceptRole) self.addButton(_('OK'), MessageBox.ButtonRole.RejectRole) def questionReconnect(saveConfInfo): saveConfInfo.setWindowTitle(_('Hint')) saveConfInfo.setText( _( f'{APPLICATION_NAME} is currently connected.\n\n' f'The new configuration will take effect the next time you connect.' ) ) # Show the MessageBox and wait for user to close it choice = saveConfInfo.exec() if choice == MessageBox.ButtonRole.AcceptRole.value: # Reconnect QApplication.instance().tray.ConnectAction.reconnectAction() else: # OK. Do nothing pass class SaveAsServerAction(Action): def __init__(self, **kwargs): super().__init__(_('Save'), icon=bootstrapIcon('save.svg'), **kwargs) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier, QtCore.Qt.Key.Key_S ) ) self.saveErrorBox = JSONErrorBox(icon=MessageBox.Icon.Critical) self.saveConfInfo = SaveConfInfo(icon=MessageBox.Icon.Information) def save(self, successCallback=None): def saveSuccess(server): myServerList = self.parent().ServerList myFocusIndex = self.parent().currentFocus # Assert to be valid assert 0 <= myFocusIndex < len(myServerList) # Unchecked? myServerList[myFocusIndex]['config'] = server # Flush row self.parent().normalServerWidget.flushRow( myFocusIndex, myServerList[myFocusIndex] ) # Sync it Storage.sync() self.parent().markAsSaved() if callable(successCallback): successCallback() if myFocusIndex == self.parent().activatedItemIndex: # Activated configuration modified if server and QApplication.instance().tray.ConnectAction.isConnected(): questionReconnect(self.saveConfInfo) return True if not self.parent().modified: return True text = self.parent().plainTextEdit.toPlainText() if text == '': # Text is empty. Allowed. return saveSuccess('') try: myJSON = Configuration.toJSON(text) except ujson.JSONDecodeError as decodeError: self.saveErrorBox.decodeError = str(decodeError) self.saveErrorBox.setWindowTitle(_('Error saving configuration')) self.saveErrorBox.setText(self.saveErrorBox.getText()) # Show the MessageBox and wait for user to close it self.saveErrorBox.exec() return False except Exception: # Any non-exit exceptions self.saveErrorBox.setWindowTitle(_('Error saving configuration')) self.saveErrorBox.setText(_('Invalid server configuration.')) # Show the MessageBox and wait for user to close it self.saveErrorBox.exec() return False else: return saveSuccess(text) def triggeredCallback(self, checked): self.save() class SaveErrorBox(MessageBox): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.saveError = '' def getText(self): if self.saveError: return _('Invalid server configuration.') + f'\n\n{self.saveError}' else: return _('Invalid server configuration.') def retranslate(self): with StateContext(self): self.setWindowTitle(_(self.windowTitle())) self.setText(self.getText()) # Ignore informative text, buttons self.moveToCenter() class SaveAsFileAction(Action): def __init__(self, **kwargs): super().__init__(_('Save As...'), **kwargs) self.saveErrorBox = SaveErrorBox( icon=MessageBox.Icon.Critical, parent=self.parent() ) def triggeredCallback(self, checked): filename, selectedFilter = QFileDialog.getSaveFileName( self.parent(), _('Save File'), filter=_('Text files (*.json);;All files (*)'), ) if filename: try: with open(filename, 'w', encoding='utf-8') as file: file.write(self.parent().plainTextEdit.toPlainText()) except Exception as ex: # Any non-exit exceptions self.saveErrorBox.saveError = str(ex) self.saveErrorBox.setWindowTitle(_('Error saving file')) self.saveErrorBox.setText(self.saveErrorBox.getText()) # Show the MessageBox and wait for user to close it self.saveErrorBox.exec() class ExitAction(Action): def __init__(self, **kwargs): super().__init__(_('Exit'), **kwargs) def triggeredCallback(self, checked): self.parent().syncSettings() self.parent().questionSave() class UndoAction(Action): def __init__(self, **kwargs): super().__init__( _('Undo'), icon=bootstrapIcon('arrow-return-left.svg'), **kwargs, ) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier, QtCore.Qt.Key.Key_Z ) ) def triggeredCallback(self, checked): self.parent().plainTextEdit.undo() class RedoAction(Action): def __init__(self, **kwargs): super().__init__( _('Redo'), icon=bootstrapIcon('arrow-return-right.svg'), **kwargs, ) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier | QtCore.Qt.KeyboardModifier.ShiftModifier, QtCore.Qt.Key.Key_Z, ) ) def triggeredCallback(self, checked): self.parent().plainTextEdit.redo() class CutAction(Action): def __init__(self, **kwargs): super().__init__(_('Cut'), icon=bootstrapIcon('scissors.svg'), **kwargs) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier, QtCore.Qt.Key.Key_X, ) ) def triggeredCallback(self, checked): self.parent().plainTextEdit.cut() class CopyAction(Action): def __init__(self, **kwargs): super().__init__(_('Copy'), icon=bootstrapIcon('files.svg'), **kwargs) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier, QtCore.Qt.Key.Key_C, ) ) def triggeredCallback(self, checked): self.parent().plainTextEdit.copy() class PasteAction(Action): def __init__(self, **kwargs): super().__init__(_('Paste'), **kwargs) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier, QtCore.Qt.Key.Key_V, ) ) def triggeredCallback(self, checked): self.parent().plainTextEdit.paste() class SelectAllTextAction(Action): def __init__(self, **kwargs): super().__init__(_('Select All'), **kwargs) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier, QtCore.Qt.Key.Key_A, ) ) def triggeredCallback(self, checked): self.parent().plainTextEdit.selectAll() class IndentAction(Action): def __init__(self, **kwargs): super().__init__(_('Indent...'), **kwargs) self.indentSpinBox = IndentSpinBox() self.indentFailure = JSONErrorBox(icon=MessageBox.Icon.Critical) def triggeredCallback(self, checked): choice = self.indentSpinBox.exec() if choice == QDialog.DialogCode.Accepted.value: myText = self.parent().plainTextEdit.toPlainText() if myText == '': # Empty. Do nothing return try: myJSON = ujson.loads(myText) myText = ujson.dumps( myJSON, indent=self.indentSpinBox.value(), ensure_ascii=False, escape_forward_slashes=False, ) except ujson.JSONDecodeError as decodeError: self.indentFailure.decodeError = str(decodeError) self.indentFailure.setWindowTitle(_('Error setting indent')) self.indentFailure.setText(self.indentFailure.getText()) # Show the MessageBox and wait for user to close it self.indentFailure.exec() except Exception: # Any non-exit exceptions self.indentFailure.setWindowTitle(_('Error setting indent')) self.indentFailure.setText(_('Invalid server configuration.')) # Show the MessageBox and wait for user to close it self.indentFailure.exec() else: self.parent().plainTextEdit.setPlainText(myText) else: # Do nothing pass class ZoomInAction(Action): def __init__(self, **kwargs): super().__init__(_('Zoom In'), **kwargs) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier, QtCore.Qt.Key.Key_Plus ) ) def triggeredCallback(self, checked): self.parent().plainTextEdit.zoomIn() class ZoomOutAction(Action): def __init__(self, **kwargs): super().__init__(_('Zoom Out'), **kwargs) self.setShortcut( QtCore.QKeyCombination( QtCore.Qt.KeyboardModifier.ControlModifier, QtCore.Qt.Key.Key_Minus ) ) def triggeredCallback(self, checked): self.parent().plainTextEdit.zoomOut() class ShowLogAction(Action): def __init__(self, **kwargs): super().__init__(_('Show Log...'), **kwargs) def triggeredCallback(self, checked): QApplication.instance().logViewerWidget.showMaximized() class WhetherToUpdateInfoBox(MessageBox): def __init__(self, version='0.0.0', *args, **kwargs): super().__init__(*args, **kwargs) self.version = version def retranslate(self): with StateContext(self): self.setText(_('New version available: ') + self.version) self.setWindowTitle(_(self.windowTitle())) self.setInformativeText(_(self.informativeText())) # Ignore button text self.moveToCenter() class CheckForUpdatesAction(Action): def __init__(self, **kwargs): super().__init__(_('Check For Updates'), **kwargs) self.networkAccessManager = QNetworkAccessManager(parent=self) self.networkReply = None self.checkForUpdateErrorBox = MessageBox( icon=MessageBox.Icon.Critical, parent=self.parent() ) self.whetherToUpdateInfoBox = WhetherToUpdateInfoBox( icon=MessageBox.Icon.Information, parent=self.parent() ) self.isLatestVersionInfoBox = MessageBox( icon=MessageBox.Icon.Information, parent=self.parent() ) def triggeredCallback(self, checked): proxyServer = QApplication.instance().tray.ConnectAction.proxyServer if proxyServer: logger.info(f'check for updates uses proxy server {proxyServer}') # Checked. split should not throw exceptions proxyHost, proxyPort = proxyServer.split(':') # Checked. int(proxyPort) should not throw exceptions self.networkAccessManager.setProxy( QNetworkProxy( QNetworkProxy.ProxyType.HttpProxy, proxyHost, int(proxyPort) ) ) else: self.networkAccessManager.setProxy( QNetworkProxy(QNetworkProxy.ProxyType.DefaultProxy) ) apiUrl = ( f'https://api.github.com/repos/' f'{APPLICATION_REPO_OWNER_NAME}/{APPLICATION_REPO_NAME}/releases/latest' ) self.networkReply = self.networkAccessManager.get( QNetworkRequest(QtCore.QUrl(apiUrl)) ) @QtCore.Slot() def finishedCallback(): if self.networkReply.error() != QNetworkReply.NetworkError.NoError: logger.error( f'check for updates failed. {self.networkReply.errorString()}' ) self.checkForUpdateErrorBox.setWindowTitle(_(APPLICATION_NAME)) self.checkForUpdateErrorBox.setText(_('Check for updates failed.')) # Show the MessageBox and wait for user to close it self.checkForUpdateErrorBox.exec() else: logger.info('check for updates success') # b'...' data = ( str(self.networkReply.readAll())[2:-1] .encode('utf-8') .decode('unicode_escape') ) info = ujson.loads(data) if info['tag_name'] > APPLICATION_VERSION: self.whetherToUpdateInfoBox.version = info['tag_name'] self.whetherToUpdateInfoBox.setWindowTitle(_(APPLICATION_NAME)) self.whetherToUpdateInfoBox.setStandardButtons( MessageBox.StandardButton.Yes | MessageBox.StandardButton.No ) self.whetherToUpdateInfoBox.setText( _('New version available: ') + info['tag_name'] ) self.whetherToUpdateInfoBox.setInformativeText( _('Go to download page?') ) # Show the MessageBox and wait for user to close it if ( self.whetherToUpdateInfoBox.exec() == MessageBox.StandardButton.Yes.value ): if QDesktopServices.openUrl(QtCore.QUrl(info['html_url'])): logger.info('open download page success') else: logger.error('open download page failed') else: # Do nothing pass else: self.isLatestVersionInfoBox.setWindowTitle(_(APPLICATION_NAME)) self.isLatestVersionInfoBox.setText( _(f'{APPLICATION_NAME} is already the latest version.') ) self.isLatestVersionInfoBox.setInformativeText( _(f'Thank you for using {APPLICATION_NAME}.') ) # Show the MessageBox and wait for user to close it self.isLatestVersionInfoBox.exec() self.networkReply.finished.connect(finishedCallback) class AboutAction(Action): def __init__(self, **kwargs): super().__init__(_('About'), **kwargs) def triggeredCallback(self, checked): if QDesktopServices.openUrl(QtCore.QUrl(APPLICATION_ABOUT_PAGE)): logger.info('open about page success') else: logger.error('open about page failed') class QuestionSaveBox(MessageBox): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setWindowTitle(_('Save Changes')) self.setText( _('The content has been modified. Do you want to save the changes?') ) self.button0 = self.addButton( _('Save'), MessageBox.ButtonRole.AcceptRole, ) self.button1 = self.addButton( _('Cancel'), MessageBox.ButtonRole.RejectRole, ) self.button2 = self.addButton( _('Discard'), MessageBox.ButtonRole.DestructiveRole, ) self.setDefaultButton(self.button0) class QuestionDeleteBox(MessageBox): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.isMulti = False self.possibleRemark = '' self.setWindowTitle(_('Delete')) self.setStandardButtons( MessageBox.StandardButton.Yes | MessageBox.StandardButton.No ) def getText(self): if self.isMulti: return _('Delete these configuration?') else: return _('Delete this configuration?') + f'\n\n{self.possibleRemark}' def retranslate(self): with StateContext(self): self.setWindowTitle(_(self.windowTitle())) self.setText(self.getText()) # Ignore informative text, buttons self.moveToCenter() class ProgressWaitBox(MessageBox): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setWindowTitle(_('Connecting')) self.setText(_('Connecting. Please wait.')) def retranslate(self): with StateContext(self): self.setWindowTitle(_(self.windowTitle())) self.setText(_(self.text())) # Ignore informative text, buttons self.moveToCenter() class SaveChangeFirst(MessageBox): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setWindowTitle(_('Hint')) self.setText(_('Please save any changes to continue.')) def retranslate(self): with StateContext(self): self.setWindowTitle(_(self.windowTitle())) self.setText(_(self.text())) # Ignore informative text, buttons self.moveToCenter() class NormalServerHorizontalHeader(QHeaderView): class SortOrder: Ascending_ = False Descending = True def __init__(self, *args, **kwargs): super().__init__(QtCore.Qt.Orientation.Horizontal, *args, **kwargs) self.setSectionsClickable(True) self.sectionClicked.connect(self.handleSectionClicked) self.sectionResized.connect(self.handleSectionResized) self.sortOrderTable = list( NormalServerHorizontalHeader.SortOrder.Ascending_ for i in range(self.parent().columnCount()) ) @QtCore.Slot(int) def handleSectionClicked(self, clickedIndex): if QApplication.instance().MainWidget.modified: self.parent().saveChangeFirst.exec() return activatedIndex = self.parent().activatedItemIndex if activatedIndex < 0: activatedServerId = None else: # Object is activated activatedServerId = id(self.parent().ServerList[activatedIndex]) # De-activated temporarily for sorting self.parent().activateItemByIndex(activatedIndex, activate=False) # Assertions assert len(self.parent().ServerList) == len(self.parent().ScrollList) # Save focus scroll bar value currentFocus = self.parent().currentFocus if currentFocus >= 0: self.parent().saveScrollBarValue(currentFocus) else: # No item selected. Save nothing pass # Prepare for scroll bar value restoration scrollBarValueTable = {} for index, server in enumerate(self.parent().ServerList): # Object id -> v, h scrollBarValueTable[id(server)] = self.parent().ScrollList[index] # Sort it self.parent().ServerList.sort( key=lambda x: NormalServerWidget.HEADER_LABEL_GET_FUNC[clickedIndex](x), reverse=self.sortOrderTable[clickedIndex], ) # Sync it Storage.sync() # Flush it self.parent().flushAll() # Toggle. Sorting done self.sortOrderTable[clickedIndex] = not self.sortOrderTable[clickedIndex] # Restore corresponding scroll bar value. Later clicking will use them for index, server in enumerate(self.parent().ServerList): # Get v, h by Object id self.parent().ScrollList[index] = scrollBarValueTable[id(server)] if activatedServerId is not None: foundActivatedItem = False for index, server in enumerate(self.parent().ServerList): if activatedServerId == id(server): foundActivatedItem = True # Restore scroll bar value. Later clicking will use it if currentFocus >= 0: self.parent().restoreScrollBarValue(currentFocus) # Focus on focus self.parent().setCurrentItemByIndex(index) self.parent().activateItemByIndex(index, activate=True) break # Object id should be found assert foundActivatedItem else: # Restore scroll bar value. Later clicking will use it if currentFocus >= 0: self.parent().restoreScrollBarValue(currentFocus) # Focus on focus self.parent().setCurrentItemByIndex(currentFocus) @QtCore.Slot(int, int, int) def handleSectionResized(self, index, oldSize, newSize): # Keys are string when loaded from json self.parent().sectionSizeTable[str(index)] = newSize class NormalServerVerticalHeader(SupportConnectedCallback, QHeaderView): def __init__(self, *args, **kwargs): super().__init__(QtCore.Qt.Orientation.Vertical, *args, **kwargs) self.setSectionsClickable(True) self.setStyleSheet( f'QHeaderView::section:hover {{ background-color: #008AE1; }}' ) def connectedCallback(self): self.setStyleSheet( f'QHeaderView::section:hover {{ background-color: #EB212E; }}' ) def disconnectedCallback(self): self.setStyleSheet( f'QHeaderView::section:hover {{ background-color: #008AE1; }}' ) class NormalServerWidgetItemDelegate(QStyledItemDelegate): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def createEditor(self, parent, option, index): editor = QLineEdit(parent) editor.setFont(QFont(QApplication.instance().customFontName)) return editor def toJSONMayBeNull(text): try: return ujson.loads(text) except Exception: # Any non-exit exceptions return {} class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget): # Might be extended in the future HEADER_LABEL = [ 'Remark', 'Protocol', 'Address', 'Port', 'Transport', 'TLS', ] # Corresponds to header label HEADER_LABEL_GET_FUNC = [ lambda server: server['remark'], lambda server: Intellisense.getCoreProtocol(toJSONMayBeNull(server['config'])), lambda server: Intellisense.getCoreAddr(toJSONMayBeNull(server['config'])), lambda server: Intellisense.getCorePort(toJSONMayBeNull(server['config'])), lambda server: Intellisense.getCoreTransport(toJSONMayBeNull(server['config'])), lambda server: Intellisense.getCoreTLS(toJSONMayBeNull(server['config'])), ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # MessageBox self.questionSaveBox = QuestionSaveBox( icon=MessageBox.Icon.Question, parent=self.parent() ) self.questionDel0Box = QuestionDeleteBox( icon=MessageBox.Icon.Question, parent=self.parent() ) self.saveConfInfoBox = SaveConfInfo( icon=MessageBox.Icon.Information, parent=self.parent() ) self.progressWaitBox = ProgressWaitBox( icon=MessageBox.Icon.Information, parent=self.parent() ) self.saveChangeFirst = SaveChangeFirst( icon=MessageBox.Icon.Information, parent=self.parent() ) # Shallow copy. Checked self.ServerList = self.parent().ServerList # Handy reference self.plainTextEdit = self.parent().plainTextEdit # Handy reference self.editorTab = self.parent().editorTab # Delegate self.delegate = NormalServerWidgetItemDelegate(parent=self) self.setItemDelegate(self.delegate) # Scroll bar value list. v, h self.ScrollList = list([0, 0] for i in range(len(self.ServerList))) # Must set before flush all self.setColumnCount(len(NormalServerWidget.HEADER_LABEL)) # Flush all data to table self.flushAll() # Install custom header self.setHorizontalHeader(NormalServerHorizontalHeader(self)) # Horizontal header resize mode for index in range(self.columnCount()): if index < self.columnCount() - 1: self.horizontalHeader().setSectionResizeMode( index, QHeaderView.ResizeMode.Interactive ) else: self.horizontalHeader().setSectionResizeMode( index, QHeaderView.ResizeMode.Stretch ) try: # Restore horizontal section size self.sectionSizeTable = ujson.loads( QApplication.instance().ServerWidgetSectionSizeTable ) # Block resize callback self.horizontalHeader().blockSignals(True) for key, value in self.sectionSizeTable.items(): self.horizontalHeader().resizeSection(int(key), value) # Unblock resize callback self.horizontalHeader().blockSignals(False) except Exception: # Any non-exit exceptions # Leave keys as strings since they will be # loaded as string from json self.sectionSizeTable = { str(row): self.horizontalHeader().defaultSectionSize() for row in range(self.columnCount()) } self.setHorizontalHeaderLabels( list(_(label) for label in NormalServerWidget.HEADER_LABEL) ) for column in range(self.horizontalHeader().count()): self.horizontalHeaderItem(column).setFont( QFont(QApplication.instance().customFontName) ) # Install custom header self.setVerticalHeader(NormalServerVerticalHeader(self)) # Selection self.setSelectionColor('#008AE1') self.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self.setSelectionMode(QTableWidget.SelectionMode.ExtendedSelection) # No drag and drop self.setDragEnabled(False) self.setDragDropMode(QAbstractItemView.DragDropMode.NoDragDrop) self.setDropIndicatorShown(False) self.setDefaultDropAction(QtCore.Qt.DropAction.IgnoreAction) contextMenuActions = [ Action( _('Move Up'), callback=lambda: self.moveUpSelectedItem(), parent=self, ), Action( _('Move Down'), callback=lambda: self.moveDownSelectedItem(), parent=self, ), Action( _('Duplicate'), callback=lambda: self.duplicateSelectedItem(), parent=self, ), Action( _('Delete'), callback=lambda: self.deleteSelectedItem(), parent=self, ), Seperator(), SelectAllServerAction( callback=lambda: self.selectAll(), parent=self, ), Seperator(), ScrollToActivatedServerAction(parent=self), Seperator(), ImportFileAction(parent=self), ImportLinkActionWithHotKey(parent=self), ImportJSONActionWithHotKey(parent=self), Seperator(), ExportLinkActionWithHotKey(parent=self), ExportQRCodeActionWithHotKey(parent=self), ExportJSONActionWithHotKey(parent=self), ] self.contextMenu = Menu(*contextMenuActions) self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect(self.handleCustomContextMenuRequested) # Signals self.currentItemChanged.connect(self.handleCurrentItemChanged) self.itemChanged.connect(self.handleItemChanged) self.itemSelectionChanged.connect(self.handleItemSelectionChanged) self.itemActivated.connect(self.handleItemActivated) self.doNotSwitchCallback = None if self.activatedItem is not None: self.setCurrentItem(self.activatedItem) # Activate it from settings self.activateItemByIndex(self.activatedItemIndex, activate=True) @property def activatedItemIndex(self): try: return int(QApplication.instance().ActivatedItemIndex) except Exception: # Any non-exit exceptions return -1 @property def activatedItem(self): return self.item(self.activatedItemIndex, 0) @property def selectedIndex(self): return sorted(list(set(index.row() for index in self.selectedIndexes()))) @property def currentFocus(self): try: # Get current index from tab return int(self.editorTab.tabText(0).split(' - ')[0]) - 1 except Exception: # Any non-exit exceptions return -1 def flushRow(self, row, server): for column in range(self.columnCount()): oldItem = self.item(row, column) newItem = QTableWidgetItem( NormalServerWidget.HEADER_LABEL_GET_FUNC[column](server) ) if oldItem is None: # Item does not exists newItem.setFont(QFont(QApplication.instance().customFontName)) else: # Use existing newItem.setFont(oldItem.font()) newItem.setForeground(oldItem.foreground()) if column == 0: # Remark is editable newItem.setFlags( QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEditable ) else: newItem.setFlags( QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable ) self.setItem(row, column, newItem) def flushAll(self): if self.rowCount() == 0: shouldInsertRow = True else: shouldInsertRow = False for row, server in enumerate(self.ServerList): if shouldInsertRow: self.insertRow(row) self.flushRow(row, server) def saveScrollBarValue(self, index): assert len(self.ScrollList) == len(self.ServerList) if 0 <= index < len(self.ScrollList): self.ScrollList[index] = [ self.plainTextEdit.verticalScrollBar().value(), self.plainTextEdit.horizontalScrollBar().value(), ] def restoreScrollBarValue(self, index): assert len(self.ScrollList) == len(self.ServerList) if 0 <= index < len(self.ScrollList): self.plainTextEdit.verticalScrollBar().setValue(self.ScrollList[index][0]) self.plainTextEdit.horizontalScrollBar().setValue(self.ScrollList[index][1]) def appendScrollBarEntry(self): self.ScrollList.append([0, 0]) def setPlainText(self, text, block=False, disabled=False): if block: self.plainTextEdit.blockSignals(True) self.plainTextEdit.setPlainText(text) self.plainTextEdit.blockSignals(False) else: self.plainTextEdit.setPlainText(text) self.plainTextEdit.setDisabled(disabled) def syncRemarkByIndex(self, index): assert self.rowCount() == len(self.ServerList) if self.ServerList[index]['remark'] != self.item(index, 0).text(): self.ServerList[index]['remark'] = self.item(index, 0).text() # Only one tab currently. Refresh tab title self.editorTab.setTabText( 0, f'{index + 1} - {self.ServerList[index]["remark"]}' ) remarkModified = True else: remarkModified = False # Avoid unnecessary encode/write, etc. if remarkModified: Storage.sync() def setSelectionColor(self, color): self.setStyleSheet(f'QTableWidget {{ selection-background-color: {color}; }}') def activateItemByIndex(self, index, activate=True): if activate: for column in range(self.columnCount()): serverItem = self.item(int(index), column) if serverItem is None: # Do nothing return serverFont = serverItem.font() serverFont.setBold(True) serverItem.setFont(serverFont) if QApplication.instance().tray is None: # Initializing serverItem.setForeground(QColor('#008AE1')) else: if QApplication.instance().tray.ConnectAction.isConnected(): serverItem.setForeground(QColor('#EB212E')) else: serverItem.setForeground(QColor('#008AE1')) QApplication.instance().ActivatedItemIndex = str(index) else: for column in range(self.columnCount()): serverItem = self.item(int(index), column) if serverItem is None: # Do nothing return serverFont = serverItem.font() serverFont.setBold(False) serverItem.setFont(serverFont) serverItem.setForeground(QBrush()) def setCurrentItemByIndex(self, index): self.setCurrentItem(self.item(index, 0)) def swapItem(self, index0, index1): swapListItem(self.ServerList, index0, index1) swapListItem(self.ScrollList, index0, index1) # Storage sync is done by caller!!! self.flushRow(index0, self.ServerList[index0]) self.flushRow(index1, self.ServerList[index1]) if index0 == self.activatedItemIndex: # De-activate self.activateItemByIndex(index0, activate=False) # Activate self.activateItemByIndex(index1, activate=True) elif index1 == self.activatedItemIndex: # De-activate self.activateItemByIndex(index1, activate=False) # Activate self.activateItemByIndex(index0, activate=True) def selectMultipleRow(self, indexes, clearCurrentSelection): if clearCurrentSelection: self.selectionModel().clearSelection() selection = self.selectionModel().selection() for index in indexes: selection.select( self.model().index(index, 0), self.model().index(index, self.columnCount() - 1), ) self.selectionModel().select( selection, QtCore.QItemSelectionModel.SelectionFlag.Select ) def moveUpItemByIndex(self, index): if index <= 0 or index >= len(self.ServerList): # The top item, or does not exist. Do nothing return self.swapItem(index, index - 1) def moveUpSelectedItem(self): if QApplication.instance().MainWidget.modified: self.saveChangeFirst.exec() return indexes = self.selectedIndex if len(indexes) == 0: # Nothing selected. Do nothing return currentFocus = self.currentFocus if currentFocus >= 0: self.saveScrollBarValue(currentFocus) for index in indexes: self.moveUpItemByIndex(index) # Sync it Storage.sync() # Don't use setCurrentItemXXX since it will # trigger handleCurrentItemChanged. Just switch context # and change index self.switchContext(indexes[-1] - 1) self.blockSignals(True) self.setCurrentIndex(self.indexFromItem(self.item(indexes[-1] - 1, 0))) self.blockSignals(False) self.selectMultipleRow( list(index - 1 for index in indexes), clearCurrentSelection=True ) def moveDownItemByIndex(self, index): if index < 0 or index >= len(self.ServerList) - 1: # The bottom item, or does not exist. Do nothing return self.swapItem(index, index + 1) def moveDownSelectedItem(self): if QApplication.instance().MainWidget.modified: self.saveChangeFirst.exec() return indexes = self.selectedIndex if len(indexes) == 0: # Nothing selected. Do nothing return currentFocus = self.currentFocus if currentFocus >= 0: self.saveScrollBarValue(currentFocus) for index in indexes[::-1]: self.moveDownItemByIndex(index) # Sync it Storage.sync() # Don't use setCurrentItemXXX since it will # trigger handleCurrentItemChanged. Just switch context # and change index self.switchContext(indexes[0] + 1) self.blockSignals(True) self.setCurrentIndex(self.indexFromItem(self.item(indexes[0] + 1, 0))) self.blockSignals(False) self.selectMultipleRow( list(index + 1 for index in indexes), clearCurrentSelection=True ) def duplicateSelectedItem(self): if QApplication.instance().MainWidget.modified: self.saveChangeFirst.exec() return indexes = self.selectedIndex if len(indexes) == 0: # Nothing selected. Do nothing return for index in indexes: if 0 <= index < len(self.ServerList): QApplication.instance().MainWidget.importServer( **self.ServerList[index] ) # Sync it Storage.sync() def deleteSelectedItem(self): if QApplication.instance().MainWidget.modified: self.saveChangeFirst.exec() return indexes = self.selectedIndex if len(indexes) == 0: # Nothing selected. Do nothing return self.questionDel0Box.isMulti = bool(len(indexes) > 1) self.questionDel0Box.possibleRemark = ( f'{indexes[0] + 1} - {self.ServerList[indexes[0]]["remark"]}' ) self.questionDel0Box.setText(self.questionDel0Box.getText()) if self.questionDel0Box.exec() == MessageBox.StandardButton.No.value: # Do not delete return if self.activatedItemIndex in indexes: takedActivated = True else: takedActivated = False for i in range(len(indexes)): takedRow = indexes[i] - i self.blockSignals(True) self.removeRow(takedRow) self.blockSignals(False) self.ServerList.pop(takedRow) self.ScrollList.pop(takedRow) if not takedActivated and takedRow < self.activatedItemIndex: QApplication.instance().ActivatedItemIndex = str( self.activatedItemIndex - 1 ) # Sync it Storage.sync() if takedActivated: if QApplication.instance().tray.ConnectAction.isConnected(): # Trigger disconnect QApplication.instance().tray.ConnectAction.trigger() QApplication.instance().ActivatedItemIndex = str(-1) # Don't use setCurrentItemXXX since the # selection may not be changed. Just switch context # and change index # Don't use currentFocus since it may have been deleted. Get # current row from self self.switchContext(self.currentRow()) self.blockSignals(True) self.setCurrentIndex(self.indexFromItem(self.item(self.currentRow(), 0))) self.blockSignals(False) def setEmptyItem(self): # Click empty. self.setPlainText('', block=True, disabled=True) # Only one tab currently. Clear tab title self.editorTab.setTabText(0, '') self.blockSignals(True) self.setCurrentIndex(self.indexFromItem(None)) self.blockSignals(False) def switchContext(self, index): self.doNotSwitchCallback = None if index < 0 or index >= len(self.ServerList): # Click empty. self.setEmptyItem() else: self.setPlainText(self.ServerList[index]['config'], block=True) # Only one tab currently. Refresh tab title self.editorTab.setTabText( 0, f'{index + 1} - {self.ServerList[index]["remark"]}' ) # Restore scroll bar self.restoreScrollBarValue(index) @QtCore.Slot(QTableWidgetItem, QTableWidgetItem) def handleCurrentItemChanged(self, currItem, prevItem): currRow = -1 if currItem is None else currItem.row() prevRow = -1 if prevItem is None else prevItem.row() def doNotSwitchCallback(): self.blockSignals(True) self.setCurrentIndex(self.indexFromItem(prevItem)) self.blockSignals(False) def doNotSwitch(): self.doNotSwitchCallback = doNotSwitchCallback self.saveScrollBarValue(prevRow) if ( QApplication.instance().MainWidget is not None and QApplication.instance().MainWidget.modified ): choice = self.questionSaveBox.exec() if choice == MessageBox.ButtonRole.AcceptRole.value: # Save if QApplication.instance().MainWidget.SaveAsServerAction.save( successCallback=lambda: self.switchContext(currRow), ): pass else: # Do not switch doNotSwitch() elif choice == MessageBox.ButtonRole.DestructiveRole.value: # Discard self.switchContext(currRow) QApplication.instance().MainWidget.markAsSaved() # Fake saved elif choice == MessageBox.ButtonRole.RejectRole.value: # Cancel. Do not switch doNotSwitch() else: self.switchContext(currRow) @QtCore.Slot(QTableWidgetItem) def handleItemChanged(self, item): self.syncRemarkByIndex(item.row()) @QtCore.Slot() def handleItemSelectionChanged(self): if callable(self.doNotSwitchCallback): self.doNotSwitchCallback() @QtCore.Slot(QTableWidgetItem) def handleItemActivated(self, item): if self.activatedItemIndex == item.row(): # Same item activated. Do nothing return if ( QApplication.instance().MainWidget is not None and QApplication.instance().MainWidget.modified ): self.saveChangeFirst.exec() return if QApplication.instance().tray.ConnectAction.isConnecting(): # Show the MessageBox asynchronously self.progressWaitBox.open() return if self.activatedItemIndex >= 0: # De-activate self.activateItemByIndex(self.activatedItemIndex, activate=False) newIndex = item.row() # Activate self.activateItemByIndex(newIndex, activate=True) if QApplication.instance().tray.ConnectAction.isConnected(): QApplication.instance().tray.ConnectAction.reconnectAction() @QtCore.Slot(QtCore.QPoint) def handleCustomContextMenuRequested(self, point): self.contextMenu.exec(self.mapToGlobal(point)) def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key.Key_Delete: self.deleteSelectedItem() else: super().keyPressEvent(event) def connectedCallback(self): self.setSelectionColor('#EB212E') # Reactivate with possible color self.activateItemByIndex(self.activatedItemIndex) def disconnectedCallback(self): self.setSelectionColor('#008AE1') # Reactivate with possible color self.activateItemByIndex(self.activatedItemIndex) def retranslate(self): self.setHorizontalHeaderLabels( list(_(label) for label in NormalServerWidget.HEADER_LABEL) ) class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindow): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setWindowTitle(_('Edit Server Configuration')) self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg')) # MessageBox self.questionSaveBox = QuestionSaveBox( icon=MessageBox.Icon.Question, parent=self ) self.restoreErrorBox = MessageBox(parent=self) try: self.StorageObj = Storage.toObject(QApplication.instance().Configuration) # Check model. Shallow copy self.ServerList = self.StorageObj['model'] except Exception: # Any non-exit exceptions self.StorageObj = Storage.init() # Shallow copy self.ServerList = self.StorageObj['model'] # Clear it Storage.clear() Configuration.corruptedError(self.restoreErrorBox) # Modified flag self.modified = False self.modifiedMark = ' *' # Initialize PlainTextEdit @QtCore.Slot(bool) def handleModification(changed): if changed: self.plainTextEdit.document().setModified(False) self.markAsModified() self.plainTextEdit = ZoomablePlainTextEdit(parent=self) self.plainTextEdit.modificationChanged.connect(handleModification) self.plainTextEdit.setDisabled(True) self.showTabAndSpacesIfNecessary() self.lineColumnLabel = QLabel('1:1 0') self.statusBar().addPermanentWidget(self.lineColumnLabel) @QtCore.Slot() def handleCursorPositionChanged(): cursor = self.plainTextEdit.textCursor() self.lineColumnLabel.setText( f'{cursor.blockNumber() + 1}:{cursor.columnNumber() + 1} {cursor.position()}' ) self.plainTextEdit.cursorPositionChanged.connect(handleCursorPositionChanged) # Theme self.plainTextEdit.setStyleSheet( DraculaTheme.getStyleSheet( widgetName='QPlainTextEdit', fontFamily=QApplication.instance().customFontName, ) ) self.theme = DraculaTheme(self.plainTextEdit.document()) try: font = self.plainTextEdit.font() font.setPointSize(int(QApplication.instance().EditorWidgetPointSize)) self.plainTextEdit.setFont(font) except Exception: # Any non-exit exceptions pass self.serverEditorTabWidget = QWidget() self.serverEditorTabWidgetLayout = QVBoxLayout(self.serverEditorTabWidget) self.serverEditorTabWidgetLayout.addWidget(self.plainTextEdit) # Advance for Normal Server Widget self.editorTab = QTabWidget(self) # Note: Set tab text later self.editorTab.addTab(self.serverEditorTabWidget, '') self.normalServerWidget = NormalServerWidget(parent=self) # Buttons self.moveUpButton = PushButton(_('Move Up')) self.moveUpButton.clicked.connect( lambda: self.normalServerWidget.moveUpSelectedItem() ) self.moveDownButton = PushButton(_('Move Down')) self.moveDownButton.clicked.connect( lambda: self.normalServerWidget.moveDownSelectedItem() ) self.duplicateButton = PushButton(_('Duplicate')) self.duplicateButton.clicked.connect( lambda: self.normalServerWidget.duplicateSelectedItem() ) self.deleteButton = PushButton(_('Delete')) self.deleteButton.clicked.connect( lambda: self.normalServerWidget.deleteSelectedItem() ) # Button Layout self.buttonWidget = QWidget() self.buttonWidgetLayout = QGridLayout(parent=self.buttonWidget) self.buttonWidgetLayout.addWidget(self.moveUpButton, 0, 0) self.buttonWidgetLayout.addWidget(self.moveDownButton, 0, 1) self.buttonWidgetLayout.addWidget(self.duplicateButton, 1, 0) self.buttonWidgetLayout.addWidget(self.deleteButton, 1, 1) self.normalServerTabWidget = QWidget() self.normalServerTabWidgetLayout = QVBoxLayout(self.normalServerTabWidget) self.normalServerTabWidgetLayout.addWidget(self.normalServerWidget) self.normalServerTabWidgetLayout.addWidget(self.buttonWidget) # Note: Reserved for future extension # self.subscriptionWidget = QWidget(parent=self) # # self.subscriptionTabWidget = QWidget() # self.subscriptionTabWidgetLayout = QVBoxLayout(self.subscriptionTabWidget) # self.subscriptionTabWidgetLayout.addWidget(self.subscriptionWidget) self.serverTab = TabWidget(self) self.serverTab.addTab(self.normalServerTabWidget, _('Server')) # Note: Reserved for future extension # self.serverTab.addTab(self.subscriptionTabWidget, '') self.splitter = QSplitter(QtCore.Qt.Orientation.Horizontal) self.splitter.addWidget(self.serverTab) self.splitter.addWidget(self.editorTab) self.splitter.setStretchFactor(0, 1) self.splitter.setStretchFactor(1, 1) self.fakeCentralWidget = QWidget() self.fakeCentralWidgetLayout = QHBoxLayout(self.fakeCentralWidget) self.fakeCentralWidgetLayout.addWidget(self.splitter) self.setCentralWidget(self.fakeCentralWidget) # Menu actions fileMenu = { 'name': 'File', 'actions': [ NewBlankServerAction(parent=self), Seperator(), SelectAllServerAction( callback=lambda: self.normalServerWidget.selectAll(), parent=self, ), Seperator(), ScrollToActivatedServerAction(parent=self.normalServerWidget), Seperator(), ImportFileAction(parent=self), ImportLinkActionWithHotKey(parent=self), ImportJSONActionWithHotKey(parent=self), Seperator(), ExportLinkActionWithHotKey(parent=self), ExportQRCodeActionWithHotKey(parent=self), ExportJSONActionWithHotKey(parent=self), Seperator(), SaveAsServerAction(parent=self), SaveAsFileAction(parent=self), Seperator(), ExitAction(parent=self), ], } editMenu = { 'name': 'Edit', 'actions': [ UndoAction(parent=self), RedoAction(parent=self), Seperator(), CutAction(parent=self), CopyAction(parent=self), PasteAction(parent=self), Seperator(), SelectAllTextAction(parent=self), Seperator(), IndentAction(parent=self), ], } viewMenu = { 'name': 'View', 'actions': [ ZoomInAction(parent=self), ZoomOutAction(parent=self), ], } helpMenu = { 'name': 'Help', 'actions': [ ShowLogAction(parent=self), CheckForUpdatesAction(parent=self), AboutAction(parent=self), ], } # Menus for menuDict in (fileMenu, editMenu, viewMenu, helpMenu): menuName = menuDict['name'] menuObjName = f'_{menuName}Menu' menu = Menu(*menuDict['actions'], title=_(menuName), parent=self.menuBar()) for action in menuDict['actions']: if isinstance(action, Action): setattr(self, f'{action}', action) # Set reference setattr(self, menuObjName, menu) self.menuBar().addMenu(menu) try: self.setGeometry( 100, 100, *list( int(size) for size in QApplication.instance().MainWidgetWindowSize.split(',') ), ) except Exception: # Any non-exit exceptions self.setGeometry(100, 100, 1440, 1440 * GOLDEN_RATIO) moveToCenter(self) def importServer(self, remark, config, syncStorage=False): server = {'remark': remark, 'config': config} self.ServerList.append(server) if syncStorage: # Sync it Storage.sync() row = self.normalServerWidget.rowCount() self.normalServerWidget.insertRow(row) self.normalServerWidget.appendScrollBarEntry() self.normalServerWidget.flushRow(row, server) item = QTableWidgetItem(str(row + 1)) item.setFont(QFont(QApplication.instance().customFontName)) if len(self.ServerList) == 1: # The first one. Click it self.normalServerWidget.setCurrentItemByIndex(0) # Try to be user-friendly in some extreme cases if not QApplication.instance().tray.ConnectAction.isConnected(): # Activate automatically self.normalServerWidget.activateItemByIndex(0) def showTabAndSpacesIfNecessary(self): textOption = QTextOption() if QApplication.instance().ShowTabAndSpacesInEditor == Switch.ON_: textOption.setFlags(QTextOption.Flag.ShowTabsAndSpaces) self.plainTextEdit.document().setDefaultTextOption(textOption) # Reset. Set self.plainTextEdit.setLineWrapMode(QPlainTextEdit.LineWrapMode.WidgetWidth) self.plainTextEdit.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap) @property def activatedItemIndex(self): return self.normalServerWidget.activatedItemIndex @property def selectedIndex(self): return self.normalServerWidget.selectedIndex @property def rowCount(self): return self.normalServerWidget.rowCount() @property def currentFocus(self): return self.normalServerWidget.currentFocus def saveScrollBarValue(self, index): self.normalServerWidget.saveScrollBarValue(index) def restoreScrollBarValue(self, index): self.normalServerWidget.restoreScrollBarValue(index) def questionSave(self): # Save current scroll bar value currentFocus = self.currentFocus if currentFocus >= 0: self.saveScrollBarValue(currentFocus) else: # No item selected. Save nothing pass if self.modified: choice = self.questionSaveBox.exec() if choice == MessageBox.ButtonRole.AcceptRole.value: # Save # If save success, close the window. return self.SaveAsServerAction.save(successCallback=lambda: self.hide()) if choice == MessageBox.ButtonRole.DestructiveRole.value: # Discard self.hide() self.normalServerWidget.setEmptyItem() self.markAsSaved() # Fake saved return True if choice == MessageBox.ButtonRole.RejectRole.value: # Cancel. Do nothing return False else: self.hide() return True def markAsModified(self): self.modified = True self.setWindowTitle(_('Edit Server Configuration') + self.modifiedMark) def markAsSaved(self): self.modified = False self.setWindowTitle(_('Edit Server Configuration')) def closeEvent(self, event): event.ignore() self.syncSettings() self.questionSave() def syncSettings(self): QApplication.instance().MainWidgetWindowSize = ( f'{self.geometry().width()},{self.geometry().height()}' ) QApplication.instance().ServerWidgetSectionSizeTable = ujson.dumps( self.normalServerWidget.sectionSizeTable, ensure_ascii=False, escape_forward_slashes=False, ) QApplication.instance().EditorWidgetPointSize = str( self.plainTextEdit.font().pointSize() ) def connectedCallback(self): self.setWindowIcon(bootstrapIcon('rocket-takeoff-connected-dark.svg')) def disconnectedCallback(self): self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg')) def retranslate(self): with StateContext(self): if self.modified: # Keep modified mark self.setWindowTitle(_('Edit Server Configuration') + self.modifiedMark) else: self.setWindowTitle(_(self.windowTitle()))