diff --git a/Furious/Utility/Settings.py b/Furious/Utility/Settings.py index f517c6e..4a6d8fb 100644 --- a/Furious/Utility/Settings.py +++ b/Furious/Utility/Settings.py @@ -66,8 +66,6 @@ SUPPORTED_SETTINGS = ( Settings('MainWidgetWindowSize'), # Edit Routing Widget Window Size Settings('RoutesWidgetWindowSize'), - # Edit Routing Widget Editor Window Size - Settings('RoutesEditorWindowSize'), # Main Widget Section Size Settings('ServerWidgetSectionSizeTable'), # Edit Routing Widget Section Size diff --git a/Furious/Utility/Utility.py b/Furious/Utility/Utility.py index 1fae4f9..167637f 100644 --- a/Furious/Utility/Utility.py +++ b/Furious/Utility/Utility.py @@ -156,6 +156,25 @@ class SupportConnectedCallback: ob.disconnectedCallback() +class NeedSyncSettings: + Object = list() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + NeedSyncSettings.Object.append(self) + + def syncSettings(self): + raise NotImplementedError + + @staticmethod + def syncAll(): + for ob in NeedSyncSettings.Object: + assert isinstance(ob, NeedSyncSettings) + + ob.syncSettings() + + class SupportThemeChangedCallback: Object = list() diff --git a/Furious/Widget/Application.py b/Furious/Widget/Application.py index 50effb5..5d4706a 100644 --- a/Furious/Widget/Application.py +++ b/Furious/Widget/Application.py @@ -12,7 +12,7 @@ from Furious.Utility.Constants import ( SYSTEM_LANGUAGE, DATA_DIR, ) -from Furious.Utility.Utility import SupportThemeChangedCallback +from Furious.Utility.Utility import SupportThemeChangedCallback, NeedSyncSettings from Furious.Utility.Proxy import Proxy from Furious.Utility.Settings import Settings from Furious.Utility.Translator import gettext as _ @@ -265,14 +265,7 @@ class Application(SingletonApplication): # if self.tray is not None: # self.tray.ConnectAction.stopCore() - if self.MainWidget is not None: - self.MainWidget.syncSettings() - - if self.editRoutingWidget is not None: - self.editRoutingWidget.syncSettings() - - if self.logViewerWidget is not None: - self.logViewerWidget.syncSettings() + NeedSyncSettings.syncAll() def exit(self, exitcode=0): if self.MainWidget is not None: diff --git a/Furious/Widget/AssetViewer.py b/Furious/Widget/AssetViewer.py index 55a4a73..06b07f9 100644 --- a/Furious/Widget/AssetViewer.py +++ b/Furious/Widget/AssetViewer.py @@ -1,5 +1,5 @@ from Furious.Gui.Action import Action, Seperator -from Furious.Widget.Widget import ListWidget, Menu, MessageBox +from Furious.Widget.Widget import ListWidget, MainWindow, Menu, MessageBox from Furious.Utility.Constants import APPLICATION_NAME, PLATFORM, GOLDEN_RATIO, ROOT_DIR from Furious.Utility.Utility import ( StateContext, @@ -14,7 +14,7 @@ from Furious.Utility.Translator import Translatable, gettext as _ from PySide6 import QtCore from PySide6.QtGui import QFont -from PySide6.QtWidgets import QFileDialog, QListWidget, QListWidgetItem, QMainWindow +from PySide6.QtWidgets import QFileDialog, QListWidget, QListWidgetItem import os import shutil @@ -136,9 +136,7 @@ class ExitAction(Action): self.parent().hide() -class AssetViewerWidget( - Translatable, SupportConnectedCallback, SupportThemeChangedCallback, QMainWindow -): +class AssetViewerWidget(SupportThemeChangedCallback, MainWindow): AssetDir = ROOT_DIR / APPLICATION_NAME / 'Data' / 'xray' def __init__(self, *args, **kwargs): @@ -251,17 +249,6 @@ class AssetViewerWidget( else: self.flushItemByTheme(darkdetect.theme()) - def closeEvent(self, event): - event.ignore() - - self.hide() - - def connectedCallback(self): - self.setWindowIcon(bootstrapIcon('rocket-takeoff-connected-dark.svg')) - - def disconnectedCallback(self): - self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg')) - def themeChangedCallback(self, theme): if PLATFORM == 'Linux' and getUbuntuRelease() == '20.04': # Ubuntu 20.04 system dark theme does not @@ -269,7 +256,3 @@ class AssetViewerWidget( pass else: self.flushItemByTheme(theme) - - def retranslate(self): - with StateContext(self): - self.setWindowTitle(_(self.windowTitle())) diff --git a/Furious/Widget/EditConfiguration.py b/Furious/Widget/EditConfiguration.py index 5d71c3c..e80efd1 100644 --- a/Furious/Widget/EditConfiguration.py +++ b/Furious/Widget/EditConfiguration.py @@ -5,6 +5,7 @@ from Furious.Core.Configuration import Configuration from Furious.Gui.Action import Action, Seperator from Furious.Widget.Widget import ( HeaderView, + MainWindow, Menu, MessageBox, PushButton, @@ -47,10 +48,8 @@ from PySide6.QtWidgets import ( QHBoxLayout, QHeaderView, QLabel, - QLineEdit, QListWidget, QListWidgetItem, - QMainWindow, QPlainTextEdit, QSplitter, QStyledItemDelegate, @@ -1607,7 +1606,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, TableWidget): ) -class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindow): +class EditConfigurationWidget(MainWindow): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1975,12 +1974,6 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo ) APP().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: diff --git a/Furious/Widget/EditRouting.py b/Furious/Widget/EditRouting.py index 767f759..08e2947 100644 --- a/Furious/Widget/EditRouting.py +++ b/Furious/Widget/EditRouting.py @@ -9,6 +9,7 @@ from Furious.Gui.Action import Action, Seperator from Furious.Widget.Widget import ( HeaderView, Menu, + MainWindow, MessageBox, PushButton, StyledItemDelegate, @@ -39,14 +40,15 @@ from PySide6 import QtCore from PySide6.QtGui import QFont from PySide6.QtWidgets import ( QAbstractItemView, + QDialog, + QDialogButtonBox, QFileDialog, + QFormLayout, QGridLayout, QHBoxLayout, QHeaderView, - QInputDialog, QLabel, QLineEdit, - QMainWindow, QPlainTextEdit, QSplitter, QStyledItemDelegate, @@ -204,7 +206,7 @@ BUILTIN_ROUTING_TEXT = { f'# \n' f'# This rule is read-only.\n' f'# \n' - f'# If no rule file or mmdb file is provided, hysteria\n' + f'# If no rule file or mmdb file is provided, {APPLICATION_NAME}\n' f'# will fall back to proxy all traffic.\n\n' ) + ujson.dumps( @@ -289,7 +291,7 @@ USER_ROUTING_TEXT = { f'# by {APPLICATION_NAME} forever.\n' f'# \n' f'# Note: If any one of filepath is given but\n' - f'# does not exist, hysteria will fall back to\n' + f'# does not exist, {APPLICATION_NAME} will fall back to\n' f'# proxy all traffic.\n\n' ) + ujson.dumps( @@ -515,7 +517,7 @@ class RoutingPlainTextEdit(ZoomablePlainTextEdit): super().wheelEvent(event) -class RoutingEditor(Translatable, SupportConnectedCallback, QMainWindow): +class RoutingEditor(MainWindow): def __init__(self, isBuiltin, *args, **kwargs): super().__init__(*args, **kwargs) @@ -669,12 +671,6 @@ class RoutingEditor(Translatable, SupportConnectedCallback, QMainWindow): self.questionSave() - def connectedCallback(self): - self.setWindowIcon(bootstrapIcon('rocket-takeoff-connected-dark.svg')) - - def disconnectedCallback(self): - self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg')) - def retranslate(self): if self.isBuiltin: # Built-in routing is translatable @@ -852,17 +848,7 @@ class EditRoutingTableWidget(Translatable, SupportConnectedCallback, TableWidget routingEditor = RoutingEditor(isBuiltin) routingEditor.title = title routingEditor.setWindowTitle(title) - - try: - routingEditor.setGeometry( - 100, - 100, - *list(int(size) for size in APP().RoutesEditorWindowSize.split(',')), - ) - except Exception: - # Any non-exit exceptions - - routingEditor.setGeometry(100, 100, 728 * GOLDEN_RATIO, 728) + routingEditor.setGeometry(100, 100, 656, 856) moveToCenter(routingEditor) @@ -970,7 +956,52 @@ class EditRoutingTableWidget(Translatable, SupportConnectedCallback, TableWidget item.setText(_(item.text())) -class EditRoutingWidget(Translatable, SupportConnectedCallback, QMainWindow): +class AddRoutingDialog(Translatable, SupportConnectedCallback, QDialog): + def __init__(self, parent=None): + super().__init__(parent) + + self.setWindowTitle(_('Add routing')) + self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg')) + + self.remarkText = QLabel(_('Enter routing remark:')) + self.remarkEdit = QLineEdit() + + self.dialogBtns = QDialogButtonBox(QtCore.Qt.Orientation.Horizontal) + + self.dialogBtns.addButton(_('OK'), QDialogButtonBox.ButtonRole.AcceptRole) + self.dialogBtns.addButton(_('Cancel'), QDialogButtonBox.ButtonRole.RejectRole) + self.dialogBtns.accepted.connect(self.accept) + self.dialogBtns.rejected.connect(self.reject) + + layout = QFormLayout() + + layout.addRow(self.remarkText) + layout.addRow(self.remarkEdit) + layout.addRow(self.dialogBtns) + layout.setFormAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.setLayout(layout) + + def routingRemark(self): + return self.remarkEdit.text() + + 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): + self.setWindowTitle(_(self.windowTitle())) + + self.remarkText.setText(_(self.remarkText.text())) + + for button in self.dialogBtns.buttons(): + button.setText(_(button.text())) + + +class EditRoutingWidget(MainWindow): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -978,6 +1009,7 @@ class EditRoutingWidget(Translatable, SupportConnectedCallback, QMainWindow): self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg')) self.assetViewer = AssetViewerWidget() + self.addRoutingDialog = AddRoutingDialog() try: self.StorageObj = RoutesStorage.toObject(APP().CustomRouting) @@ -1048,36 +1080,44 @@ class EditRoutingWidget(Translatable, SupportConnectedCallback, QMainWindow): moveToCenter(self) def addRoute(self): - # Get text for a new item from the user - text, ok = QInputDialog.getText( - self, _('Add routing'), _('Enter routing remark:') - ) + choice = self.addRoutingDialog.exec() - if text and ok: - self.editRoutingTableWidget.appendDataByColumn( - lambda column: EditRoutingTableWidget.USER_ITEM_GET_FUNC[column](text) - ) + if choice == QDialog.DialogCode.Accepted.value: + routingRemark = self.addRoutingDialog.routingRemark() - routingEditor = self.editRoutingTableWidget.appendRoutingEditor( - text, isBuiltin=False - ) - - for core in [XrayCore.name(), Hysteria.name()]: - routingEditor.addTabWithData( - core, - USER_ROUTING_TEXT[core], + if routingRemark: + self.editRoutingTableWidget.appendDataByColumn( + lambda column: EditRoutingTableWidget.USER_ITEM_GET_FUNC[column]( + routingRemark + ) ) - self.RoutesList.append({'remark': text, **DEFAULT_USER_ROUTING}) + routingEditor = self.editRoutingTableWidget.appendRoutingEditor( + routingRemark, isBuiltin=False + ) - # Sync it - RoutesStorage.sync() + for core in [XrayCore.name(), Hysteria.name()]: + routingEditor.addTabWithData( + core, + USER_ROUTING_TEXT[core], + ) - routingAction = APP().tray.RoutingAction - # Add entry in routing menu - routingAction.addAction( - RoutingChildAction(text, checkable=True, checked=False) - ) + self.RoutesList.append( + {'remark': routingRemark, **DEFAULT_USER_ROUTING} + ) + + # Sync it + RoutesStorage.sync() + + routingAction = APP().tray.RoutingAction + # Add entry in routing menu + routingAction.addAction( + RoutingChildAction(routingRemark, checkable=True, checked=False) + ) + + else: + # Do nothing + pass def deleteSelectedItem(self): self.editRoutingTableWidget.deleteSelectedItem() @@ -1090,12 +1130,6 @@ class EditRoutingWidget(Translatable, SupportConnectedCallback, QMainWindow): f'{self.geometry().width()},{self.geometry().height()}' ) - if len(self.editRoutingTableWidget.routingEditorRef): - APP().RoutesEditorWindowSize = ( - f'{self.editRoutingTableWidget.routingEditorRef[0].geometry().width()},' - f'{self.editRoutingTableWidget.routingEditorRef[0].geometry().height()}' - ) - APP().RoutesWidgetSectionSizeTable = ujson.dumps( self.editRoutingTableWidget.sectionSizeTable, ensure_ascii=False, @@ -1107,19 +1141,3 @@ class EditRoutingWidget(Translatable, SupportConnectedCallback, QMainWindow): self.deleteSelectedItem() else: super().keyPressEvent(event) - - def closeEvent(self, event): - event.ignore() - - self.syncSettings() - self.hide() - - 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): - self.setWindowTitle(_(self.windowTitle())) diff --git a/Furious/Widget/ExportQRCode.py b/Furious/Widget/ExportQRCode.py index 6126854..dca08a8 100644 --- a/Furious/Widget/ExportQRCode.py +++ b/Furious/Widget/ExportQRCode.py @@ -1,16 +1,18 @@ +from Furious.Widget.Widget import MainWindow + from Furious.Utility.Constants import APPLICATION_NAME from Furious.Utility.Utility import SupportConnectedCallback, bootstrapIcon from Furious.Utility.Translator import gettext as _ from PySide6 import QtCore from PySide6.QtGui import QPixmap -from PySide6.QtWidgets import QLabel, QMainWindow, QTabWidget +from PySide6.QtWidgets import QLabel, QTabWidget import io import pyqrcode -class ExportQRCode(SupportConnectedCallback, QMainWindow): +class ExportQRCode(MainWindow): def __init__(self, parent=None): super().__init__(parent) @@ -47,9 +49,3 @@ class ExportQRCode(SupportConnectedCallback, QMainWindow): if self.editorTab.count() == 0: self.hide() - - def connectedCallback(self): - self.setWindowIcon(bootstrapIcon('rocket-takeoff-connected-dark.svg')) - - def disconnectedCallback(self): - self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg')) diff --git a/Furious/Widget/LogViewer.py b/Furious/Widget/LogViewer.py index 6438511..50316a9 100644 --- a/Furious/Widget/LogViewer.py +++ b/Furious/Widget/LogViewer.py @@ -1,5 +1,5 @@ from Furious.Gui.Action import Action, Seperator -from Furious.Widget.Widget import Menu, MessageBox, ZoomableTextBrowser +from Furious.Widget.Widget import MainWindow, Menu, MessageBox, ZoomableTextBrowser from Furious.Utility.Constants import APP from Furious.Utility.Utility import ( StateContext, @@ -10,7 +10,7 @@ from Furious.Utility.Translator import Translatable, gettext as _ from Furious.Utility.Theme import DraculaTheme from PySide6 import QtCore -from PySide6.QtWidgets import QFileDialog, QMainWindow, QTextBrowser +from PySide6.QtWidgets import QFileDialog, QTextBrowser import logging @@ -134,7 +134,7 @@ class ZoomOutAction(Action): self.parent().textBrowser.zoomOut() -class LogViewerWidget(Translatable, SupportConnectedCallback, QMainWindow): +class LogViewerWidget(MainWindow): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -201,19 +201,3 @@ class LogViewerWidget(Translatable, SupportConnectedCallback, QMainWindow): def syncSettings(self): APP().ViewerWidgetPointSize = str(self.textBrowser.font().pointSize()) - - def closeEvent(self, event): - event.ignore() - - self.syncSettings() - self.hide() - - 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): - self.setWindowTitle(_(self.windowTitle())) diff --git a/Furious/Widget/Widget.py b/Furious/Widget/Widget.py index 6d37158..3a7f747 100644 --- a/Furious/Widget/Widget.py +++ b/Furious/Widget/Widget.py @@ -3,6 +3,7 @@ from Furious.Utility.Constants import APP from Furious.Utility.Utility import ( StateContext, SupportConnectedCallback, + NeedSyncSettings, bootstrapIcon, moveToCenter, ) @@ -15,6 +16,7 @@ from PySide6.QtWidgets import ( QInputDialog, QLineEdit, QListWidget, + QMainWindow, QMenu, QMessageBox, QPlainTextEdit, @@ -56,6 +58,31 @@ class ListWidget(QListWidget): return sorted(list(set(index.row() for index in self.selectedIndexes()))) +class MainWindow(Translatable, SupportConnectedCallback, NeedSyncSettings, QMainWindow): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def syncSettings(self): + pass + + def closeEvent(self, event): + event.ignore() + + # Sync partial + self.syncSettings() + self.hide() + + 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): + self.setWindowTitle(_(self.windowTitle())) + + class Menu(Translatable, QMenu): def __init__(self, *actions, **kwargs): super().__init__(**kwargs)