Add Edit Routing feature

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2023-08-11 12:59:46 +08:00
parent ad80a02c5c
commit e92c6a9e85
25 changed files with 2270 additions and 377 deletions
+102 -99
View File
@@ -1,11 +1,11 @@
from Furious.Core.Core import XrayCore, Hysteria
from Furious.Core.Intellisense import Intellisense
from Furious.Core.Configuration import Configuration
from Furious.Action.Routing import SUPPORTED_ROUTING_DETAIL
from Furious.Action.Routing import BUILTIN_ROUTING_TABLE, BUILTIN_ROUTING
from Furious.Gui.Action import Action
from Furious.Widget.ConnectingProgressBar import ConnectingProgressBar
from Furious.Widget.Widget import MessageBox
from Furious.Utility.Constants import APPLICATION_NAME, PROXY_SERVER_BYPASS
from Furious.Utility.Constants import APP, APPLICATION_NAME, PROXY_SERVER_BYPASS
from Furious.Utility.Utility import (
Switch,
SupportConnectedCallback,
@@ -16,7 +16,6 @@ from Furious.Utility.Translator import gettext as _
from Furious.Utility.Proxy import Proxy
from PySide6 import QtCore
from PySide6.QtWidgets import QApplication
from PySide6.QtNetwork import (
QNetworkAccessManager,
QNetworkReply,
@@ -180,7 +179,7 @@ class ConnectAction(Action):
self.Hysteria.stop()
def showConnectingProgressBar(self):
if QApplication.instance().ShowProgressBarWhenConnecting == Switch.ON_:
if APP().ShowProgressBarWhenConnecting == Switch.ON_:
self.connectingProgressBar.progressBar.setValue(0)
# Update the progress bar every 50ms
self.connectingProgressBar.timer.start(50)
@@ -209,7 +208,7 @@ class ConnectAction(Action):
def setDisabledAction(self, value):
self.setDisabled(value)
QApplication.instance().tray.RoutingAction.setDisabled(value)
APP().tray.RoutingAction.setDisabled(value)
def setConnectingStatus(self, showProgressBar=True):
if showProgressBar:
@@ -220,13 +219,13 @@ class ConnectAction(Action):
self.setText(_('Connecting'))
self.setIcon(bootstrapIcon('lock-fill.svg'))
QApplication.instance().tray.setPlainIcon()
APP().tray.setPlainIcon()
def setConnectedStatus(self):
self.hideConnectingProgressBar(done=True)
self.setDisabledAction(False)
QApplication.instance().tray.setConnectedIcon()
APP().tray.setConnectedIcon()
# Finished. Reset connecting flag
self.connectingFlag = False
@@ -255,8 +254,8 @@ class ConnectAction(Action):
self.setIcon(bootstrapIcon('unlock-fill.svg'))
self.setChecked(False)
QApplication.instance().Connect = Switch.OFF
QApplication.instance().tray.setPlainIcon()
APP().Connect = Switch.OFF
APP().tray.setPlainIcon()
self.proxyServer = ''
@@ -276,12 +275,12 @@ class ConnectAction(Action):
@property
def MainWidget(self):
# Handy reference
return QApplication.instance().MainWidget
return APP().MainWidget
@property
def activatedServer(self):
try:
activatedIndex = int(QApplication.instance().ActivatedItemIndex)
activatedIndex = int(APP().ActivatedItemIndex)
if activatedIndex < 0:
return None
@@ -395,10 +394,9 @@ class ConnectAction(Action):
if validateProxyServer(proxyServer):
self.coreRunning = True
routing = QApplication.instance().Routing
routing = APP().Routing
logger.info(f'Core {XrayCore.name()} configured')
logger.info(f'Routing is {routing}')
logger.info(f'core {XrayCore.name()} configured')
def fixLoggingRelativePath(attr):
# Relative path fails if booting on start up
@@ -426,53 +424,35 @@ class ConnectAction(Action):
fixLoggingRelativePath('access')
fixLoggingRelativePath('error')
if routing == 'Bypass':
self.coreJSON['routing'] = {
'domainStrategy': 'IPIfNonMatch',
'domainMatcher': 'hybrid',
'rules': (
# ads
{
'type': 'field',
'outboundTag': 'block',
'domain': ['geosite:category-ads-all'],
},
# geosite
{
'type': 'field',
'outboundTag': 'direct',
'domain': ['geosite:cn'],
},
# geoip
{
'type': 'field',
'outboundTag': 'direct',
'ip': ['geoip:private', 'geoip:cn'],
},
# Proxy everything
{
'type': 'field',
'port': '0-65535',
'outboundTag': 'proxy',
},
),
}
if routing == 'Global':
self.coreJSON['routing'] = {
'domainStrategy': 'IPIfNonMatch',
'domainMatcher': 'hybrid',
'rules': (
# Proxy everything
{
'type': 'field',
'port': '0-65535',
'outboundTag': 'proxy',
},
),
}
if routing == 'Custom':
# Filter Custom
if routing in BUILTIN_ROUTING[:-1]:
routingObject = BUILTIN_ROUTING_TABLE[routing][XrayCore.name()]
logger.info(f'routing is {routing}')
logger.info(f'RoutingObject: {routingObject}')
self.coreJSON['routing'] = routingObject
elif routing == 'Custom':
logger.info(f'routing is {routing}')
logger.info(f'RoutingObject: {self.XrayRouting}')
# Assign user routing
self.coreJSON['routing'] = self.XrayRouting
else:
try:
routingWidget = APP().editRoutingWidget
route = routingWidget.RoutesList[int(routing)]
logger.info(f'routing is {route["remark"]}')
logger.info(f'RoutingObject: {route[XrayCore.name()]}')
self.coreJSON['routing'] = route[XrayCore.name()]
except Exception:
# Any non-exit exceptions
# Fast fail
self.coreJSON = {}
# Refresh configuration modified before. User cannot feel
self.coreText = ujson.dumps(
@@ -498,33 +478,52 @@ class ConnectAction(Action):
if validateProxyServer(proxyServer):
self.coreRunning = True
routing = QApplication.instance().Routing
routing = APP().Routing
logger.info(f'Core {Hysteria.name()} configured')
logger.info(f'Routing is {routing}')
logger.info(f'core {Hysteria.name()} configured')
# Filter Global, Custom
if routing in BUILTIN_ROUTING[:-2]:
logger.info(f'routing is {routing}')
routingObject = BUILTIN_ROUTING_TABLE[routing][Hysteria.name()]
if routing == 'Bypass':
self.Hysteria.start(
self.coreText, Hysteria.rule(), Hysteria.mmdb()
self.coreText,
Hysteria.rule(routingObject.get('acl')),
Hysteria.mmdb(routingObject.get('mmdb')),
)
if routing == 'Global':
elif routing == 'Global':
logger.info(f'routing is {routing}')
self.Hysteria.start(self.coreText, '', '')
if routing == 'Custom':
if self.coreJSON.get('acl') is not None:
# acl specified
if self.coreJSON.get('mmdb') is not None:
# mmdb specified. Start "as is"
self.Hysteria.start(
self.coreText,
Hysteria.rule(self.coreJSON['acl']),
Hysteria.mmdb(self.coreJSON['mmdb']),
)
else:
# mmdb not specified. Route as global
self.Hysteria.start(self.coreText, '', '')
else:
# acl not specified. Route as global
self.Hysteria.start(self.coreText, '', '')
elif routing == 'Custom':
logger.info(f'routing is {routing}')
self.Hysteria.start(
self.coreText,
Hysteria.rule(self.coreJSON.get('acl')),
Hysteria.mmdb(self.coreJSON.get('mmdb')),
)
else:
try:
routingWidget = APP().editRoutingWidget
route = routingWidget.RoutesList[int(routing)]
logger.info(f'routing is {route["remark"]}')
logger.info(f'RoutingObject: {route[Hysteria.name()]}')
self.Hysteria.start(
self.coreText,
Hysteria.rule(route[Hysteria.name()].get('acl')),
Hysteria.mmdb(route[Hysteria.name()].get('mmdb')),
)
except Exception:
# Any non-exit exceptions
# Fast fail
self.Hysteria.start('', '', '')
return Hysteria.name()
@@ -572,24 +571,28 @@ class ConnectAction(Action):
else:
logger.info(f'{self.coreName}: connection test success. Connected')
QApplication.instance().Connect = Switch.ON_
APP().Connect = Switch.ON_
# Connected status
self.setConnectedStatus()
if showRoutingChangedMessage:
# Routing changed
QApplication.instance().tray.showMessage(
_('Routing changed: ')
+ _(
f'{SUPPORTED_ROUTING_DETAIL[QApplication.instance().Routing]}'
try:
routingWidget = APP().editRoutingWidget
route = routingWidget.RoutesList[int(APP().Routing)]
APP().tray.showMessage(
_('Routing changed: ') + f'{route["remark"]}'
)
except ValueError:
APP().tray.showMessage(
_('Routing changed: ') + _(f'{APP().Routing}')
)
)
else:
# Connected
QApplication.instance().tray.showMessage(
f'{self.coreName}: {_("Connected")}'
)
APP().tray.showMessage(f'{self.coreName}: {_("Connected")}')
self.networkReply.finished.connect(finishedCallback)
@@ -600,11 +603,8 @@ class ConnectAction(Action):
# Connecting
self.connectingFlag = True
if (
not QApplication.instance().Configuration
or len(self.MainWidget.ServerList) == 0
):
QApplication.instance().Connect = Switch.OFF
if not APP().Configuration or len(self.MainWidget.ServerList) == 0:
APP().Connect = Switch.OFF
self.setChecked(False)
self.connectingFlag = False
@@ -615,7 +615,7 @@ class ConnectAction(Action):
myText = self.activatedServer
if myText is None:
QApplication.instance().Connect = Switch.OFF
APP().Connect = Switch.OFF
self.setChecked(False)
self.connectingFlag = False
@@ -624,7 +624,7 @@ class ConnectAction(Action):
return
if myText == '':
QApplication.instance().Connect = Switch.OFF
APP().Connect = Switch.OFF
self.setChecked(False)
self.connectingFlag = False
@@ -637,7 +637,7 @@ class ConnectAction(Action):
except Exception:
# Any non-exit exceptions
QApplication.instance().Connect = Switch.OFF
APP().Connect = Switch.OFF
self.setChecked(False)
self.connectingFlag = False
@@ -657,6 +657,9 @@ class ConnectAction(Action):
self.connectingAction()
def connectingAction(self, showProgressBar=True, showRoutingChangedMessage=False):
# Connecting. Redefined
self.connectingFlag = True
# Connecting status
self.setConnectingStatus(showProgressBar)
@@ -703,7 +706,7 @@ class ConnectAction(Action):
SupportConnectedCallback.callDisconnectedCallback()
QApplication.instance().tray.showMessage(reason)
APP().tray.showMessage(reason)
def reconnectAction(self, reason=''):
self.disconnectAction(reason)
+2 -3
View File
@@ -1,9 +1,8 @@
from Furious.Gui.Action import Action
from Furious.Utility.Constants import APP
from Furious.Utility.Utility import bootstrapIcon
from Furious.Utility.Translator import gettext as _
from PySide6.QtWidgets import QApplication
class EditConfigurationAction(Action):
def __init__(self):
@@ -13,4 +12,4 @@ class EditConfigurationAction(Action):
)
def triggeredCallback(self, checked):
QApplication.instance().MainWidget.show()
APP().MainWidget.show()
+2 -3
View File
@@ -1,9 +1,8 @@
from Furious.Gui.Action import Action
from Furious.Utility.Constants import APP
from Furious.Utility.Utility import bootstrapIcon
from Furious.Utility.Translator import gettext as _
from PySide6.QtWidgets import QApplication
class ExitAction(Action):
def __init__(self):
@@ -13,4 +12,4 @@ class ExitAction(Action):
)
def triggeredCallback(self, checked):
QApplication.instance().exit()
APP().exit()
+6 -5
View File
@@ -2,6 +2,7 @@ from Furious.Core.Configuration import Configuration
from Furious.Gui.Action import Action
from Furious.Widget.Widget import Menu, MessageBox
from Furious.Widget.ExportQRCode import ExportQRCode
from Furious.Utility.Constants import APP
from Furious.Utility.Utility import StateContext, bootstrapIcon
from Furious.Utility.Translator import gettext as _
@@ -110,7 +111,7 @@ class ExportJSONResultBox(MessageBox):
def exportLink(selectedIndex):
serverList = QApplication.instance().MainWidget.ServerList
serverList = APP().MainWidget.ServerList
serverLink = []
successStr = []
failureStr = []
@@ -140,7 +141,7 @@ class ExportLinkAction(Action):
self.exportLinkResult = ExportLinkResultBox('', '', isQRCodeExport=False)
def triggeredCallback(self, checked):
selectedIndex = QApplication.instance().MainWidget.selectedIndex
selectedIndex = APP().MainWidget.selectedIndex
if len(selectedIndex) == 0:
# Nothing selected. Do nothing
@@ -166,7 +167,7 @@ class ExportQRCodeAction(Action):
self.exportLinkResult = ExportLinkResultBox('', '', isQRCodeExport=True)
def triggeredCallback(self, checked):
selectedIndex = QApplication.instance().MainWidget.selectedIndex
selectedIndex = APP().MainWidget.selectedIndex
if len(selectedIndex) == 0:
# Nothing selected. Do nothing
@@ -194,7 +195,7 @@ class ExportJSONAction(Action):
self.exportJSONResult = ExportJSONResultBox()
def triggeredCallback(self, checked):
selectedIndex = QApplication.instance().MainWidget.selectedIndex
selectedIndex = APP().MainWidget.selectedIndex
if len(selectedIndex) == 0:
# Nothing selected. Do nothing
@@ -203,7 +204,7 @@ class ExportJSONAction(Action):
try:
QApplication.clipboard().setText(
'\n'.join(
QApplication.instance().MainWidget.ServerList[index]['config']
APP().MainWidget.ServerList[index]['config']
for index in selectedIndex
)
)
+12 -11
View File
@@ -6,11 +6,12 @@ from Furious.Core.Configuration import (
)
from Furious.Gui.Action import Action
from Furious.Widget.Widget import Menu, MessageBox
from Furious.Utility.Constants import APP
from Furious.Utility.Utility import (
Base64Encoder,
Protocol,
StateContext,
Storage,
ServerStorage,
bootstrapIcon,
protocolRepr,
)
@@ -145,7 +146,7 @@ class ImportLinkAction(Action):
if choice == MessageBox.ButtonRole.AcceptRole.value:
# Go to edit
QApplication.instance().MainWidget.show()
APP().MainWidget.show()
else:
# OK. Do nothing
pass
@@ -186,7 +187,7 @@ class ImportLinkAction(Action):
remark = urllib.parse.unquote(getOrDefault('ps'))
QApplication.instance().MainWidget.importServer(
APP().MainWidget.importServer(
remark,
ujson.dumps(
myJSON,
@@ -246,7 +247,7 @@ class ImportLinkAction(Action):
)
)
QApplication.instance().MainWidget.importServer(
APP().MainWidget.importServer(
remark,
ujson.dumps(
myJSON, indent=2, ensure_ascii=False, escape_forward_slashes=False
@@ -297,7 +298,7 @@ class ImportLinkAction(Action):
)
)
QApplication.instance().MainWidget.importServer(
APP().MainWidget.importServer(
remark,
ujson.dumps(
myJSON, indent=2, ensure_ascii=False, escape_forward_slashes=False
@@ -323,7 +324,7 @@ class ImportLinkAction(Action):
ProxyOutboundObjectSS(*re.split(r'[@:]', myData))
)
QApplication.instance().MainWidget.importServer(
APP().MainWidget.importServer(
'sslegacy',
ujson.dumps(
myJSON, indent=2, ensure_ascii=False, escape_forward_slashes=False
@@ -380,7 +381,7 @@ class ImportLinkAction(Action):
if result:
# Sync it
Storage.sync()
ServerStorage.sync()
self.showLinkImportOK(remark)
else:
@@ -388,7 +389,7 @@ class ImportLinkAction(Action):
else:
successRemark = []
rowCount = QApplication.instance().MainWidget.rowCount
rowCount = APP().MainWidget.rowCount
for shareLink in splitByNewLine:
remark, result = self.parseShareLink(shareLink)
@@ -402,7 +403,7 @@ class ImportLinkAction(Action):
return
# At least one server's been imported. Sync it
Storage.sync()
ServerStorage.sync()
if len(successRemark) == 1:
# Fall back to single
@@ -445,7 +446,7 @@ class ImportJSONAction(Action):
if choice == MessageBox.ButtonRole.AcceptRole.value:
# Go to edit
QApplication.instance().MainWidget.show()
APP().MainWidget.show()
else:
# OK. Do nothing
pass
@@ -460,7 +461,7 @@ class ImportJSONAction(Action):
self.showJSONErrorBox()
else:
QApplication.instance().MainWidget.importServer(
APP().MainWidget.importServer(
_('Untitled'), self.clipboard, syncStorage=True
)
+4 -6
View File
@@ -1,5 +1,6 @@
from Furious.Gui.Action import Action
from Furious.Widget.Widget import Menu
from Furious.Utility.Constants import APP
from Furious.Utility.Utility import bootstrapIcon
from Furious.Utility.Translator import (
Translatable,
@@ -8,8 +9,6 @@ from Furious.Utility.Translator import (
LANGUAGE_TO_ABBR,
)
from PySide6.QtWidgets import QApplication
import logging
logger = logging.getLogger(__name__)
@@ -24,10 +23,10 @@ class LanguageChildAction(Action):
def triggeredCallback(self, checked):
checkedLanguage = LANGUAGE_TO_ABBR[self.text()]
if QApplication.instance().Language != checkedLanguage:
if APP().Language != checkedLanguage:
logger.info(f'set language to \'{self.text()}\'')
QApplication.instance().Language = checkedLanguage
APP().Language = checkedLanguage
Translatable.retranslateAll()
@@ -43,8 +42,7 @@ class LanguageAction(Action):
# Language representation
text,
checkable=True,
checked=text
== ABBR_TO_LANGUAGE[QApplication.instance().Language],
checked=text == ABBR_TO_LANGUAGE[APP().Language],
)
for text in list(LANGUAGE_TO_ABBR.keys())
),
+202 -20
View File
@@ -1,50 +1,232 @@
from Furious.Gui.Action import Action
from Furious.Core.Core import XrayCore, Hysteria
from Furious.Gui.Action import Action, Seperator
from Furious.Widget.Widget import Menu
from Furious.Utility.Constants import APP, DATA_DIR
from Furious.Utility.Utility import Switch, bootstrapIcon
from Furious.Utility.Translator import gettext as _
from PySide6.QtWidgets import QApplication
SUPPORTED_ROUTING = ('Bypass', 'Global', 'Custom')
SUPPORTED_ROUTING_DETAIL = {
'Bypass': 'Bypass Mainland China',
'Global': 'Global',
'Custom': 'Custom',
BUILTIN_ROUTING_TABLE = {
'Bypass Mainland China': {
XrayCore.name(): {
'domainStrategy': 'IPIfNonMatch',
'domainMatcher': 'hybrid',
'rules': [
# ads
{
'type': 'field',
'domain': [
'geosite:category-ads-all',
],
'outboundTag': 'block',
},
# geosite
{
'type': 'field',
'domain': [
'geosite:cn',
],
'outboundTag': 'direct',
},
# geoip
{
'type': 'field',
'ip': [
'geoip:private',
'geoip:cn',
],
'outboundTag': 'direct',
},
# Proxy everything
{
'type': 'field',
'port': '0-65535',
'outboundTag': 'proxy',
},
],
},
Hysteria.name(): {
'acl': str(DATA_DIR / 'hysteria' / 'bypass-mainland-China.acl'),
'mmdb': str(DATA_DIR / 'hysteria' / 'country.mmdb'),
},
},
'Bypass Iran': {
XrayCore.name(): {
"domainStrategy": "IPIfNonMatch",
"domainMatcher": "hybrid",
"rules": [
# ads
{
"type": "field",
"domain": [
"geosite:category-ads-all",
"iran:ads",
],
"outboundTag": "block",
},
# Iran sites
{
"type": "field",
"domain": [
"iran:ir",
"iran:other",
],
"outboundTag": "direct",
},
# Iran IP
{
"type": "field",
"ip": [
"geoip:private",
"geoip:ir",
],
"outboundTag": "direct",
},
# Proxy everything
{
"type": "field",
"port": "0-65535",
"outboundTag": "proxy",
},
],
},
Hysteria.name(): {
'acl': str(DATA_DIR / 'hysteria' / 'bypass-Iran.acl'),
'mmdb': str(DATA_DIR / 'hysteria' / 'country.mmdb'),
},
},
'Global': {
XrayCore.name(): {
'domainStrategy': 'IPIfNonMatch',
'domainMatcher': 'hybrid',
'rules': [
# Proxy everything
{
'type': 'field',
'port': '0-65535',
'outboundTag': 'proxy',
},
],
},
Hysteria.name(): {},
},
'Custom': {
XrayCore.name(): {},
Hysteria.name(): {},
},
}
BUILTIN_ROUTING = list(BUILTIN_ROUTING_TABLE.keys())
def routingToIndex():
currentRouting = APP().Routing
try:
index = int(currentRouting)
except ValueError:
for i, routing in enumerate(BUILTIN_ROUTING):
if routing == currentRouting:
return i
return -1
else:
return index + len(BUILTIN_ROUTING)
class BuiltinRoutingChildAction(Action):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def triggeredCallback(self, checked):
textEnglish = self.textEnglish
if APP().Routing != textEnglish:
# De-activate
APP().editRoutingWidget.activateItemByIndex(
routingToIndex(), activate=False
)
APP().Routing = textEnglish
# Activate
APP().editRoutingWidget.activateItemByIndex(routingToIndex(), activate=True)
if APP().tray.ConnectAction.isConnected():
# Connected. Re-configure connection
APP().tray.ConnectAction.connectingAction(
showProgressBar=True,
showRoutingChangedMessage=True,
)
class RoutingChildAction(Action):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def triggeredCallback(self, checked):
for key, value in SUPPORTED_ROUTING_DETAIL.items():
if self.textCompare(value):
if QApplication.instance().Routing != key:
QApplication.instance().Routing = key
self.translatable = False
if QApplication.instance().tray.ConnectAction.isConnected():
def triggeredCallback(self, checked):
routingAction = APP().tray.RoutingAction
for index, action in enumerate(
# Skip built-in actions.
routingAction.menu().actions()[len(BUILTIN_ROUTING) :]
):
if id(self) == id(action):
# Found action
if APP().Routing != str(index):
# De-activate
APP().editRoutingWidget.activateItemByIndex(
routingToIndex(), activate=False
)
APP().Routing = str(index)
# Activate
APP().editRoutingWidget.activateItemByIndex(
routingToIndex(), activate=True
)
if APP().tray.ConnectAction.isConnected():
# Connected. Re-configure connection
QApplication.instance().tray.ConnectAction.connectingAction(
showProgressBar=False,
APP().tray.ConnectAction.connectingAction(
showProgressBar=True,
showRoutingChangedMessage=True,
)
# Select routing action done
return
raise Exception('Fatal error occurred')
@property
def textEnglish(self):
return self.text()
class RoutingAction(Action):
def __init__(self):
if APP().Routing == 'Bypass':
# Update value for backward compatibility
APP().Routing = 'Bypass Mainland China'
super().__init__(
_('Routing'),
icon=bootstrapIcon('shuffle.svg'),
menu=Menu(
*list(
RoutingChildAction(
_(SUPPORTED_ROUTING_DETAIL[mode]),
BuiltinRoutingChildAction(
_(routing),
checkable=True,
checked=mode == QApplication.instance().Routing,
checked=APP().Routing == routing,
)
for mode in SUPPORTED_ROUTING
for routing in BUILTIN_ROUTING
),
*list(
RoutingChildAction(
route['remark'],
checkable=True,
checked=APP().Routing == str(index),
)
for index, route in enumerate(APP().editRoutingWidget.RoutesList)
),
),
)
+11 -14
View File
@@ -1,11 +1,10 @@
from Furious.Gui.Action import Action
from Furious.Widget.Widget import Menu
from Furious.Utility.Constants import APP
from Furious.Utility.Utility import bootstrapIcon, Switch
from Furious.Utility.Translator import gettext as _
from Furious.Utility.StartupOnBoot import StartupOnBoot
from PySide6.QtWidgets import QApplication
class SettingsChildAction(Action):
def __init__(self, *args, **kwargs):
@@ -16,23 +15,23 @@ class SettingsChildAction(Action):
if checked:
StartupOnBoot.on_()
QApplication.instance().StartupOnBoot = Switch.ON_
APP().StartupOnBoot = Switch.ON_
else:
StartupOnBoot.off()
QApplication.instance().StartupOnBoot = Switch.OFF
APP().StartupOnBoot = Switch.OFF
if self.textCompare('Show Progress Bar When Connecting'):
if checked:
QApplication.instance().ShowProgressBarWhenConnecting = Switch.ON_
APP().ShowProgressBarWhenConnecting = Switch.ON_
else:
QApplication.instance().ShowProgressBarWhenConnecting = Switch.OFF
APP().ShowProgressBarWhenConnecting = Switch.OFF
if self.textCompare('Show Tab And Spaces In Editor'):
if checked:
QApplication.instance().ShowTabAndSpacesInEditor = Switch.ON_
APP().ShowTabAndSpacesInEditor = Switch.ON_
else:
QApplication.instance().ShowTabAndSpacesInEditor = Switch.OFF
APP().ShowTabAndSpacesInEditor = Switch.OFF
QApplication.instance().MainWidget.showTabAndSpacesIfNecessary()
APP().MainWidget.showTabAndSpacesIfNecessary()
class SettingsAction(Action):
@@ -44,19 +43,17 @@ class SettingsAction(Action):
SettingsChildAction(
_('Startup On Boot'),
checkable=True,
checked=QApplication.instance().StartupOnBoot == Switch.ON_,
checked=APP().StartupOnBoot == Switch.ON_,
),
SettingsChildAction(
_('Show Progress Bar When Connecting'),
checkable=True,
checked=QApplication.instance().ShowProgressBarWhenConnecting
== Switch.ON_,
checked=APP().ShowProgressBarWhenConnecting == Switch.ON_,
),
SettingsChildAction(
_('Show Tab And Spaces In Editor'),
checkable=True,
checked=QApplication.instance().ShowTabAndSpacesInEditor
== Switch.ON_,
checked=APP().ShowTabAndSpacesInEditor == Switch.ON_,
),
),
useActionGroup=False,
+20 -14
View File
@@ -142,33 +142,39 @@ class Hysteria(Core):
super().__init__(*args, **kwargs)
@staticmethod
def rule(rulePath=''):
if rulePath == '':
path = DATA_DIR / 'hysteria' / 'bypass-mainland-China.acl'
else:
path = getAbsolutePath(rulePath)
def rule(rulePath):
path = getAbsolutePath(str(rulePath))
try:
with open(path, 'rb') as file:
return file.read()
except Exception:
data = file.read()
logger.info(f'hysteria rule \'{path}\' load success')
return data
except Exception as ex:
# Any non-exit exceptions
logger.error(f'hysteria rule \'{path}\' load failed. {ex}')
return ''
@staticmethod
def mmdb(mmdbPath=''):
if mmdbPath == '':
path = DATA_DIR / 'hysteria' / 'country.mmdb'
else:
path = getAbsolutePath(mmdbPath)
def mmdb(mmdbPath):
path = getAbsolutePath(str(mmdbPath))
try:
with open(path, 'rb') as file:
return file.read()
except Exception:
data = file.read()
logger.info(f'hysteria mmdb \'{path}\' load success')
return data
except Exception as ex:
# Any non-exit exceptions
logger.error(f'hysteria mmdb \'{path}\' load failed. {ex}')
return ''
@staticmethod
+20 -2
View File
@@ -9,7 +9,6 @@ from Furious.Utility.Translator import Translatable, gettext as _
from PySide6 import QtCore
from PySide6.QtGui import QAction, QActionGroup
from PySide6.QtWidgets import QApplication
import logging
import darkdetect
@@ -47,6 +46,11 @@ class Action(Translatable, SupportThemeChangedCallback, QAction):
self._actionGroup = ActionGroup(self, *menu.actions())
self.setActionGroup(self._actionGroup)
else:
self._actionGroup = None
else:
self._menu = None
self._actionGroup = None
self.setCheckable(checkable)
self.setChecked(checked)
@@ -65,6 +69,20 @@ class Action(Translatable, SupportThemeChangedCallback, QAction):
self.triggered.connect(triggerSignal)
def addAction(self, action):
if self._menu is not None:
self._menu.addAction(action)
if self._actionGroup is not None:
self._actionGroup.addAction(action)
def removeAction(self, action):
if self._menu is not None:
self._menu.removeAction(action)
if self._actionGroup is not None:
self._actionGroup.removeAction(action)
@property
def textEnglish(self):
return _(self.text(), 'EN')
@@ -127,7 +145,7 @@ class Action(Translatable, SupportThemeChangedCallback, QAction):
def retranslate(self):
def recursiveTranslate(action, memo):
if action not in memo and not action.isSeparator():
if action not in memo and not action.isSeparator() and action.translatable:
action.setText(_(action.text()))
memo[action] = True
+7 -3
View File
@@ -1,10 +1,14 @@
from Furious.Version import __version__
from PySide6 import QtCore
from PySide6.QtWidgets import QApplication
import math
import pathlib
import platform
import functools
APP = functools.partial(QApplication.instance)
APPLICATION_NAME = 'Furious'
APPLICATION_VERSION = __version__
@@ -24,9 +28,9 @@ GOLDEN_RATIO = (math.sqrt(5) - 1) / 2
SYSTEM_LANGUAGE = QtCore.QLocale().name()[:2].upper()
ROOT_DIR = pathlib.Path(__file__).resolve().parent.parent
DATA_DIR = ROOT_DIR / 'Data'
CRASH_LOG_DIR = ROOT_DIR / 'CrashLog'
ROOT_DIR = pathlib.Path(__file__).resolve().parent.parent.parent
DATA_DIR = ROOT_DIR / APPLICATION_NAME / 'Data'
CRASH_LOG_DIR = ROOT_DIR / APPLICATION_NAME / 'CrashLog'
PROXY_SERVER_BYPASS = (
'localhost;*.local;127.*;10.*;172.16.*;172.17.*;'
+5 -7
View File
@@ -1,7 +1,5 @@
from Furious.Widget.Application import Application
from Furious.Utility.Constants import PLATFORM, CRASH_LOG_DIR
from PySide6.QtWidgets import QApplication
from Furious.Utility.Constants import APP, PLATFORM, CRASH_LOG_DIR
import os
import sys
@@ -41,8 +39,8 @@ class Process(ProcessContext.Process):
self.saveCrashLog(exceptionType, exceptionValue, tb)
if QApplication.instance() is not None:
QApplication.instance().exit(error)
if APP() is not None:
APP().exit(error)
else:
sys.exit(error)
@@ -63,10 +61,10 @@ class Process(ProcessContext.Process):
traceback.format_exception(exceptionType, exceptionValue, tb),
)
if QApplication.instance() is None:
if APP() is None:
crashLog = f'{stackLog}'
else:
crashLog = f'{QApplication.instance().log()}\n{stackLog}'
crashLog = f'{APP().log()}\n{stackLog}'
with open(CRASH_LOG_DIR / self.logFileName, 'w', encoding='utf-8') as file:
file.write(crashLog)
+21 -2
View File
@@ -1,4 +1,3 @@
from Furious.Action.Routing import SUPPORTED_ROUTING
from Furious.Action.Language import SUPPORTED_LANGUAGE
from Furious.Utility.Constants import SYSTEM_LANGUAGE
from Furious.Utility.Utility import Switch
@@ -53,22 +52,42 @@ SMART_CHOSEN_LANGUAGE = (
)
SUPPORTED_SETTINGS = (
# Connected last time or not
Settings('Connect', Switch.RANGE),
Settings('Routing', SUPPORTED_ROUTING),
# User Routing option
Settings('Routing'),
# User Custom Routing object
Settings('CustomRouting'),
# User Configuration
Settings('Configuration'),
# User Activated Server Index
Settings('ActivatedItemIndex'),
# Main Widget Window Size
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
Settings('RoutesWidgetSectionSizeTable'),
# Main Widget Font Point Size
Settings('EditorWidgetPointSize'),
# Log Viewer Widget Font Point Size
Settings('ViewerWidgetPointSize'),
# User selected language
Settings('Language', SUPPORTED_LANGUAGE, SMART_CHOSEN_LANGUAGE),
# Startup On Boot
Settings('StartupOnBoot', Switch.RANGE, Switch.ON_), # On by default
# Show Progressbar
Settings(
# For user experience: On by default
'ShowProgressBarWhenConnecting',
Switch.RANGE,
Switch.ON_,
),
# Show Tab and spaces
Settings('ShowTabAndSpacesInEditor', Switch.RANGE),
)
-1
View File
@@ -6,7 +6,6 @@ from Furious.Utility.Constants import (
)
from PySide6 import QtCore
from PySide6.QtWidgets import QApplication
import os
import sys
+2
View File
@@ -40,6 +40,8 @@ class DraculaTheme(Theme):
HighlightRules(r'"[^"\\]*(\\.[^"\\]*)*"', '#F1FA8C'),
# JSON keys. Green
HighlightRules(r'"([^"\\]*(\\.[^"\\]*)*)"\s*:', '#50FA7B', isJSONKey=True),
# Comments(only for hints on display). Grey
HighlightRules(r'^#.*', '#6272a4'),
]
def highlightBlock(self, text):
+112 -11
View File
@@ -1,6 +1,5 @@
from Furious.Utility.Constants import APPLICATION_NAME
from PySide6.QtWidgets import QApplication
from Furious.Core.Core import XrayCore, Hysteria
from Furious.Utility.Constants import APP, APPLICATION_NAME
class Translatable:
@@ -9,6 +8,8 @@ class Translatable:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.translatable = True
Translatable.Object.append(self)
def retranslate(self):
@@ -19,7 +20,8 @@ class Translatable:
for ob in Translatable.Object:
assert isinstance(ob, Translatable)
ob.retranslate()
if ob.translatable:
ob.retranslate()
class Translator:
@@ -58,9 +60,9 @@ def installTranslation(translation):
def gettext(source, locale=None):
if locale is None:
assert QApplication.instance() is not None
assert APP() is not None
return _trans.translate(source, QApplication.instance().Language)
return _trans.translate(source, APP().Language)
else:
assert locale in list(LANGUAGE_TO_ABBR.values())
@@ -83,6 +85,8 @@ NO_TRANSLATION = [
APPLICATION_NAME,
*LANGUAGE_TO_ABBR.keys(),
'TLS',
XrayCore.name(),
Hysteria.name(),
]
NO_TRANSLATION_DICT = {key: True for key in NO_TRANSLATION}
@@ -124,6 +128,11 @@ TRANSLATION = {
'ZH': '绕过中国大陆',
'TW': '繞過中國大陸',
},
'Bypass Iran': {
'ES': 'Evitar Irán',
'ZH': '绕过伊朗',
'TW': '繞過伊朗',
},
'Global': {
'ES': 'Global',
'ZH': '全球',
@@ -134,6 +143,11 @@ TRANSLATION = {
'ZH': '自定义',
'TW': '自定義',
},
'Edit Routing...': {
'ES': 'Editar Enrutamiento...',
'ZH': '编辑路由...',
'TW': '編輯路由...',
},
'Import': {
'ES': 'Importar',
'ZH': '导入',
@@ -496,16 +510,16 @@ TRANSLATION = {
'ZH': '删除',
'TW': '刪除',
},
'Delete this configuration?': {
'ES': '¿Eliminar esta configuración?',
'ZH': '删除此配置?',
'TW': '刪除此配置?',
},
'Delete these configuration?': {
'ES': '¿Eliminar esta configuración?',
'ZH': '删除这些配置?',
'TW': '刪除這些配置?',
},
'Delete this configuration?': {
'ES': '¿Eliminar esta configuración?',
'ZH': '删除此配置?',
'TW': '刪除此配置?',
},
'Connecting. Please wait.': {
'ES': 'Conectando. Espere por favor.',
'ZH': '正在连接,请稍候。',
@@ -637,6 +651,93 @@ TRANSLATION = {
'ZH': '滚动到激活的服务器',
'TW': '滾動到激活的服務器',
},
# Edit Routing widget
'Edit Routing': {
'ES': 'Editar Enrutamiento',
'ZH': '编辑路由',
'TW': '編輯路由',
},
'Type': {
'ES': 'Tipo',
'ZH': '类型',
'TW': '類型',
},
'Built-in': {
'ES': 'Incorporado',
'ZH': '内置',
'TW': '内置',
},
'Routing Rules': {
'ES': 'Reglas de Enrutamiento',
'ZH': '路由规则',
'TW': '路由規則',
},
'User Defined': {
'ES': 'Usuario Definido',
'ZH': '用户定义',
'TW': '用戶定義',
},
'Add routing': {
'ES': 'Agregar enrutamiento',
'ZH': '添加路由',
'TW': '添加路由',
},
'Enter routing remark:': {
'ES': 'Introducir comentario de enrutamiento:',
'ZH': '输入路由名称:',
'TW': '輸入路由名稱:',
},
'Add': {
'ES': 'Agregar',
'ZH': '添加',
'TW': '添加',
},
'Import Asset File...': {
'ES': 'Importar Archivo de Activos...',
'ZH': '导入资源文件...',
'TW': '導入資源文件...',
},
'Error saving routing configuration': {
'ES': 'Error al guardar la configuración de enrutamiento',
'ZH': '保存路由配置时出错',
'TW': '保存路由配置時出錯',
},
# Asset Viewer
'Asset File': {
'ES': 'Activos',
'ZH': '资源文件',
'TW': '資源文件',
},
'Asset file already exists. Overwrite?': {
'ES': 'Archivo de activos ya existe. ¿Sobrescribir?',
'ZH': '资源文件已存在。是否覆盖?',
'TW': '資源文件已存在。是否覆蓋?',
},
'Error import asset file.': {
'ES': 'Error al importar el archivo de activos.',
'ZH': '导入资源文件出错。',
'TW': '導入資源文件出錯。',
},
'Import asset file success.': {
'ES': 'Importar archivo de activos con éxito.',
'ZH': '导入资源文件成功。',
'TW': '導入資源文件成功。',
},
'Delete these asset files?': {
'ES': '¿Eliminar estos archivos de activos?',
'ZH': '删除这些资源文件?',
'TW': '刪除這些資源文件?',
},
'Delete this asset file?': {
'ES': '¿Eliminar este archivo de activos?',
'ZH': '删除该资源文件?',
'TW': '刪除該資源文件?',
},
'All files (*)': {
'ES': 'Todos los archivos (*)',
'ZH': '所有文件 (*)',
'TW': '所有文件 (*)',
},
# Log Viewer
'Log Viewer': {
'ES': 'Visor de Registro',
+47 -11
View File
@@ -1,5 +1,5 @@
from Furious.Gui.Icon import Icon
from Furious.Utility.Constants import PLATFORM, ROOT_DIR
from Furious.Utility.Constants import APP, PLATFORM, ROOT_DIR
from PySide6.QtWidgets import QApplication
@@ -46,29 +46,27 @@ class StateContext:
self._ob.setDisabled(False)
class Storage:
class ServerStorage:
EMPTY_OBJECT = {'model': []}
@staticmethod
def init():
return copy.deepcopy(Storage.EMPTY_OBJECT)
return copy.deepcopy(ServerStorage.EMPTY_OBJECT)
@staticmethod
def sync(ob=None):
if ob is None:
# Object is up-to-date
QApplication.instance().Configuration = Storage.toStorage(
QApplication.instance().MainWidget.StorageObj
)
APP().Configuration = ServerStorage.toStorage(APP().MainWidget.StorageObj)
else:
# Object is up-to-date
QApplication.instance().Configuration = Storage.toStorage(ob)
APP().Configuration = ServerStorage.toStorage(ob)
@staticmethod
def toObject(st):
if not st:
# Storage does not exist, or is empty
return Storage.init()
# Server storage does not exist, or is empty
return ServerStorage.init()
return ujson.loads(Base64Encoder.decode(st))
@@ -80,8 +78,46 @@ class Storage:
@staticmethod
def clear():
QApplication.instance().Configuration = ''
QApplication.instance().ActivatedItemIndex = str(-1)
APP().Configuration = ''
APP().ActivatedItemIndex = str(-1)
class RoutesStorage:
# remark, corename, routes
EMPTY_OBJECT = {'model': []}
@staticmethod
def init():
return copy.deepcopy(RoutesStorage.EMPTY_OBJECT)
@staticmethod
def sync(ob=None):
if ob is None:
# Object is up-to-date
APP().CustomRouting = RoutesStorage.toStorage(
APP().editRoutingWidget.StorageObj
)
else:
# Object is up-to-date
APP().CustomRouting = RoutesStorage.toStorage(ob)
@staticmethod
def toObject(st):
if not st:
# Server storage does not exist, or is empty
return RoutesStorage.init()
return ujson.loads(Base64Encoder.decode(st))
@staticmethod
def toStorage(ob):
return Base64Encoder.encode(
ujson.dumps(ob, ensure_ascii=False, escape_forward_slashes=False).encode()
)
@staticmethod
def clear():
APP().CustomRouting = ''
class Switch:
+11 -3
View File
@@ -1,5 +1,6 @@
from Furious.Widget.SystemTrayIcon import SystemTrayIcon
from Furious.Widget.EditConfiguration import EditConfigurationWidget
from Furious.Widget.EditRouting import EditRoutingWidget
from Furious.Widget.LogViewer import LogViewerWidget
from Furious.Utility.Constants import (
APPLICATION_NAME,
@@ -140,6 +141,8 @@ class Application(SingletonApplication):
self.customFontEnabled = False
self.customFontName = ''
self.editRoutingWidget = None
self.logViewerWidget = None
self.logViewerHandle = None
self.logStreamHandle = None
@@ -181,7 +184,7 @@ class Application(SingletonApplication):
logging.basicConfig(
format='[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s',
level=logging.DEBUG,
level=logging.INFO,
handlers=(self.logViewerHandle, self.logStreamHandle),
)
logging.raiseExceptions = False
@@ -244,6 +247,7 @@ class Application(SingletonApplication):
self.aboutToQuit.connect(self.cleanup)
self.MainWidget = EditConfigurationWidget()
self.editRoutingWidget = EditRoutingWidget()
self.tray = SystemTrayIcon()
self.tray.show()
@@ -257,12 +261,16 @@ class Application(SingletonApplication):
Proxy.off()
Proxy.daemonOff()
if self.tray is not None:
self.tray.ConnectAction.stopCore()
# Avoid calling core exit callback. Cores are cleaned by OS.
# 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()
+276
View File
@@ -0,0 +1,276 @@
from Furious.Gui.Action import Action, Seperator
from Furious.Widget.Widget import ListWidget, Menu, MessageBox
from Furious.Utility.Constants import APPLICATION_NAME, PLATFORM, GOLDEN_RATIO, ROOT_DIR
from Furious.Utility.Utility import (
StateContext,
SupportConnectedCallback,
SupportThemeChangedCallback,
bootstrapIcon,
bootstrapIconWhite,
moveToCenter,
)
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
import os
import shutil
import logging
import threading
import watchfiles
import darkdetect
logger = logging.getLogger(__name__)
def watchFileChanges(callback):
for changes in watchfiles.watch(AssetViewerWidget.AssetDir):
if callable(callback):
callback()
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 asset files?')
else:
return _('Delete this asset file?') + 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 AssetExistsBox(MessageBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWindowTitle(_('Import'))
self.setStandardButtons(
MessageBox.StandardButton.Yes | MessageBox.StandardButton.No
)
def retranslate(self):
with StateContext(self):
self.setWindowTitle(_(self.windowTitle()))
self.setText(_(self.text()))
# Ignore informative text, buttons
self.moveToCenter()
class ImportAction(Action):
def __init__(self, **kwargs):
super().__init__(_('Import From File...'), **kwargs)
self.assetExistsBox = AssetExistsBox(
icon=MessageBox.Icon.Question, parent=self.parent()
)
self.importErrorBox = MessageBox(
icon=MessageBox.Icon.Critical, parent=self.parent()
)
self.importSuccessBox = MessageBox(
icon=MessageBox.Icon.Information, parent=self.parent()
)
def triggeredCallback(self, checked):
filename, selectedFilter = QFileDialog.getOpenFileName(
self.parent(), _('Import File'), filter=_('All files (*)')
)
if filename:
basename = os.path.basename(filename)
if os.path.isfile(AssetViewerWidget.AssetDir / os.path.basename(filename)):
self.assetExistsBox.setWindowTitle(_('Import'))
self.assetExistsBox.setText(_('Asset file already exists. Overwrite?'))
self.assetExistsBox.setInformativeText(basename)
if self.assetExistsBox.exec() == MessageBox.StandardButton.No.value:
# Do not overwrite
return
try:
shutil.copy(filename, AssetViewerWidget.AssetDir)
except shutil.SameFileError:
# Same file imported. Do nothing
pass
except Exception as ex:
# Any non-exit exception
self.importErrorBox.setWindowTitle(_('Import'))
self.importErrorBox.setText(_('Error import asset file.'))
self.importErrorBox.setInformativeText(str(ex))
# Show the MessageBox and wait for user to close it
self.importErrorBox.exec()
else:
self.importSuccessBox.setWindowTitle(_('Import'))
self.importSuccessBox.setText(_('Import asset file success.'))
# Show the MessageBox and wait for user to close it
self.importSuccessBox.exec()
class ExitAction(Action):
def __init__(self, **kwargs):
super().__init__(_('Exit'), **kwargs)
def triggeredCallback(self, checked):
self.parent().hide()
class WatchFiles(QtCore.QObject):
filesChanged = QtCore.Signal()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class AssetViewerWidget(
Translatable, SupportConnectedCallback, SupportThemeChangedCallback, QMainWindow
):
AssetDir = ROOT_DIR / APPLICATION_NAME / 'Data' / 'xray'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWindowTitle(_('Asset File'))
self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg'))
self.questionDeleteBox = QuestionDeleteBox(
icon=MessageBox.Icon.Question, parent=self
)
self.listWidget = ListWidget(parent=self)
self.listWidget.setSelectionBehavior(QListWidget.SelectionBehavior.SelectRows)
self.listWidget.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
self.listWidget.setIconSize(QtCore.QSize(64, 64))
self.flushItem()
self.setCentralWidget(self.listWidget)
# Check filesystem events
self.watchFiles = WatchFiles()
self.watchFiles.filesChanged.connect(lambda: self.flushItem())
self.watchFilesListenerThread = threading.Thread(
target=watchFileChanges,
args=(self.watchFiles.filesChanged.emit,),
daemon=True,
)
self.watchFilesListenerThread.start()
contextMenuActions = [
Action(
_('Delete'), callback=lambda: self.deleteSelectedItem(), parent=self
),
]
self.contextMenu = Menu(*contextMenuActions)
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self.handleCustomContextMenuRequested)
fileMenuActions = [
ImportAction(parent=self),
Seperator(),
ExitAction(parent=self),
]
for menu in (fileMenuActions,):
for action in menu:
if isinstance(action, Action):
if hasattr(self, f'{action}'):
logger.warning(f'{self} already has action {action}')
setattr(self, f'{action}', action)
self._fileMenu = Menu(*fileMenuActions, title=_('File'), parent=self)
self.menuBar().addMenu(self._fileMenu)
self.setGeometry(100, 100, 512 * GOLDEN_RATIO, 512)
moveToCenter(self)
@QtCore.Slot(QtCore.QPoint)
def handleCustomContextMenuRequested(self, point):
self.contextMenu.exec(self.mapToGlobal(point))
def deleteSelectedItem(self):
indexes = self.listWidget.selectedIndex
if len(indexes) == 0:
# Nothing selected
return
self.questionDeleteBox.isMulti = bool(len(indexes) > 1)
self.questionDeleteBox.possibleRemark = (
f'{self.listWidget.item(indexes[0]).text()}'
)
self.questionDeleteBox.setText(self.questionDeleteBox.getText())
if self.questionDeleteBox.exec() == MessageBox.StandardButton.No.value:
# Do not delete
return
for index in indexes:
os.remove(AssetViewerWidget.AssetDir / self.listWidget.item(index).text())
def flushItem(self):
self.listWidget.clear()
for filename in os.listdir(AssetViewerWidget.AssetDir):
if os.path.isfile(AssetViewerWidget.AssetDir / filename):
item = QListWidgetItem(filename)
if darkdetect.theme() == 'Dark':
if PLATFORM == 'Windows':
# Windows. Always use black icon
item.setIcon(bootstrapIcon('file-earmark.svg'))
else:
item.setIcon(bootstrapIconWhite('file-earmark.svg'))
else:
item.setIcon(bootstrapIcon('file-earmark.svg'))
self.listWidget.addItem(item)
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):
# TODO
pass
def retranslate(self):
with StateContext(self):
self.setWindowTitle(_(self.windowTitle()))
+38 -2
View File
@@ -1,12 +1,16 @@
from Furious.Utility.Constants import APPLICATION_NAME, GOLDEN_RATIO
from Furious.Utility.Utility import bootstrapIcon, StateContext
from Furious.Utility.Utility import (
bootstrapIcon,
StateContext,
SupportConnectedCallback,
)
from Furious.Utility.Translator import Translatable, gettext as _
from PySide6 import QtCore
from PySide6.QtWidgets import QHBoxLayout, QProgressBar, QWidget
class ConnectingProgressBar(Translatable, QWidget):
class ConnectingProgressBar(Translatable, SupportConnectedCallback, QWidget):
def __init__(self, parent=None):
super().__init__(parent)
@@ -54,6 +58,38 @@ class ConnectingProgressBar(Translatable, QWidget):
self.hide()
def connectedCallback(self):
self.setWindowIcon(bootstrapIcon('rocket-takeoff-connected-dark.svg'))
self.progressBar.setStyleSheet(
f'QProgressBar {{'
f' border-radius: 2px;'
f' text-align: center;'
f'}}'
f''
f'QProgressBar::chunk {{'
f' background-color: #EB212E;'
f' width: 10px;'
f' margin: 0.5px;'
f'}}'
)
def disconnectedCallback(self):
self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg'))
self.progressBar.setStyleSheet(
f'QProgressBar {{'
f' border-radius: 2px;'
f' text-align: center;'
f'}}'
f''
f'QProgressBar::chunk {{'
f' background-color: #43ACED;'
f' width: 10px;'
f' margin: 0.5px;'
f'}}'
)
def retranslate(self):
with StateContext(self):
self.setWindowTitle(_(self.windowTitle()))
+82 -147
View File
@@ -4,26 +4,30 @@ from Furious.Core.Intellisense import Intellisense
from Furious.Core.Configuration import Configuration
from Furious.Gui.Action import Action, Seperator
from Furious.Widget.Widget import (
HeaderView,
Menu,
MessageBox,
PushButton,
StyledItemDelegate,
TableWidget,
TabWidget,
ZoomablePlainTextEdit,
)
from Furious.Widget.IndentSpinBox import IndentSpinBox
from Furious.Utility.Constants import (
PLATFORM,
APP,
APPLICATION_NAME,
APPLICATION_VERSION,
APPLICATION_ABOUT_PAGE,
APPLICATION_REPO_OWNER_NAME,
APPLICATION_REPO_NAME,
PLATFORM,
GOLDEN_RATIO,
)
from Furious.Utility.Utility import (
Base64Encoder,
StateContext,
Storage,
ServerStorage,
Switch,
SupportConnectedCallback,
bootstrapIcon,
@@ -34,10 +38,9 @@ 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.QtGui import QDesktopServices, QFont, QTextOption
from PySide6.QtWidgets import (
QAbstractItemView,
QApplication,
QDialog,
QFileDialog,
QGridLayout,
@@ -110,11 +113,12 @@ class ImportFileAction(Action):
try:
with open(filename, 'r', encoding='utf-8') as file:
plainText = file.read()
except Exception:
except Exception as ex:
# Any non-exit exceptions
self.openErrorBox.setWindowTitle(_('Error opening file'))
self.openErrorBox.setText(_('Invalid configuration file.'))
self.openErrorBox.setInformativeText(str(ex))
# Show the MessageBox and wait for user to close it
self.openErrorBox.exec()
@@ -218,7 +222,7 @@ class ScrollToActivatedServerAction(Action):
)
def triggeredCallback(self, checked):
if QApplication.instance().MainWidget.modified:
if APP().MainWidget.modified:
self.parent().saveChangeFirst.exec()
return
@@ -273,7 +277,7 @@ def questionReconnect(saveConfInfo):
if choice == MessageBox.ButtonRole.AcceptRole.value:
# Reconnect
QApplication.instance().tray.ConnectAction.reconnectAction()
APP().tray.ConnectAction.reconnectAction()
else:
# OK. Do nothing
pass
@@ -308,7 +312,7 @@ class SaveAsServerAction(Action):
)
# Sync it
Storage.sync()
ServerStorage.sync()
self.parent().markAsSaved()
@@ -318,7 +322,7 @@ class SaveAsServerAction(Action):
if myFocusIndex == self.parent().activatedItemIndex:
# Activated configuration modified
if server and QApplication.instance().tray.ConnectAction.isConnected():
if server and APP().tray.ConnectAction.isConnected():
questionReconnect(self.saveConfInfo)
return True
@@ -594,12 +598,20 @@ class ZoomOutAction(Action):
self.parent().plainTextEdit.zoomOut()
class RoutingAction(Action):
def __init__(self, **kwargs):
super().__init__(_('Edit Routing...'), **kwargs)
def triggeredCallback(self, checked):
APP().editRoutingWidget.show()
class ShowLogAction(Action):
def __init__(self, **kwargs):
super().__init__(_('Show Log...'), **kwargs)
def triggeredCallback(self, checked):
QApplication.instance().logViewerWidget.showMaximized()
APP().logViewerWidget.showMaximized()
class WhetherToUpdateInfoBox(MessageBox):
@@ -637,7 +649,7 @@ class CheckForUpdatesAction(Action):
)
def triggeredCallback(self, checked):
proxyServer = QApplication.instance().tray.ConnectAction.proxyServer
proxyServer = APP().tray.ConnectAction.proxyServer
if proxyServer:
logger.info(f'check for updates uses proxy server {proxyServer}')
@@ -828,7 +840,7 @@ class SaveChangeFirst(MessageBox):
self.moveToCenter()
class NormalServerHorizontalHeader(QHeaderView):
class NormalServerHorizontalHeader(HeaderView):
class SortOrder:
Ascending_ = False
Descending = True
@@ -836,8 +848,6 @@ class NormalServerHorizontalHeader(QHeaderView):
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)
@@ -848,7 +858,7 @@ class NormalServerHorizontalHeader(QHeaderView):
@QtCore.Slot(int)
def handleSectionClicked(self, clickedIndex):
if QApplication.instance().MainWidget.modified:
if APP().MainWidget.modified:
self.parent().saveChangeFirst.exec()
return
@@ -890,7 +900,7 @@ class NormalServerHorizontalHeader(QHeaderView):
)
# Sync it
Storage.sync()
ServerStorage.sync()
# Flush it
self.parent().flushAll()
@@ -936,37 +946,10 @@ class NormalServerHorizontalHeader(QHeaderView):
self.parent().sectionSizeTable[str(index)] = newSize
class NormalServerVerticalHeader(SupportConnectedCallback, QHeaderView):
class NormalServerVerticalHeader(HeaderView):
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:
@@ -977,7 +960,7 @@ def toJSONMayBeNull(text):
return {}
class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
class NormalServerWidget(Translatable, SupportConnectedCallback, TableWidget):
# Might be extended in the future
HEADER_LABEL = [
'Remark',
@@ -1028,7 +1011,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
self.editorTab = self.parent().editorTab
# Delegate
self.delegate = NormalServerWidgetItemDelegate(parent=self)
self.delegate = StyledItemDelegate(parent=self)
self.setItemDelegate(self.delegate)
# Scroll bar value list. v, h
@@ -1056,9 +1039,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
try:
# Restore horizontal section size
self.sectionSizeTable = ujson.loads(
QApplication.instance().ServerWidgetSectionSizeTable
)
self.sectionSizeTable = ujson.loads(APP().ServerWidgetSectionSizeTable)
# Block resize callback
self.horizontalHeader().blockSignals(True)
@@ -1083,9 +1064,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
)
for column in range(self.horizontalHeader().count()):
self.horizontalHeaderItem(column).setFont(
QFont(QApplication.instance().customFontName)
)
self.horizontalHeaderItem(column).setFont(QFont(APP().customFontName))
# Install custom header
self.setVerticalHeader(NormalServerVerticalHeader(self))
@@ -1160,7 +1139,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
@property
def activatedItemIndex(self):
try:
return int(QApplication.instance().ActivatedItemIndex)
return int(APP().ActivatedItemIndex)
except Exception:
# Any non-exit exceptions
@@ -1170,10 +1149,6 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
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:
@@ -1193,7 +1168,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
if oldItem is None:
# Item does not exists
newItem.setFont(QFont(QApplication.instance().customFontName))
newItem.setFont(QFont(APP().customFontName))
else:
# Use existing
newItem.setFont(oldItem.font())
@@ -1272,48 +1247,13 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
# Avoid unnecessary encode/write, etc.
if remarkModified:
Storage.sync()
def setSelectionColor(self, color):
self.setStyleSheet(f'QTableWidget {{ selection-background-color: {color}; }}')
ServerStorage.sync()
def activateItemByIndex(self, index, activate=True):
super().activateItemByIndex(index, activate)
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())
APP().ActivatedItemIndex = str(index)
def setCurrentItemByIndex(self, index):
self.setCurrentItem(self.item(index, 0))
@@ -1322,7 +1262,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
swapListItem(self.ServerList, index0, index1)
swapListItem(self.ScrollList, index0, index1)
# Storage sync is done by caller!!!
# Server storage sync is done by caller!!!
self.flushRow(index0, self.ServerList[index0])
self.flushRow(index1, self.ServerList[index1])
@@ -1362,7 +1302,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
self.swapItem(index, index - 1)
def moveUpSelectedItem(self):
if QApplication.instance().MainWidget.modified:
if APP().MainWidget.modified:
self.saveChangeFirst.exec()
return
@@ -1382,7 +1322,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
self.moveUpItemByIndex(index)
# Sync it
Storage.sync()
ServerStorage.sync()
# Don't use setCurrentItemXXX since it will
# trigger handleCurrentItemChanged. Just switch context
@@ -1405,7 +1345,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
self.swapItem(index, index + 1)
def moveDownSelectedItem(self):
if QApplication.instance().MainWidget.modified:
if APP().MainWidget.modified:
self.saveChangeFirst.exec()
return
@@ -1425,7 +1365,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
self.moveDownItemByIndex(index)
# Sync it
Storage.sync()
ServerStorage.sync()
# Don't use setCurrentItemXXX since it will
# trigger handleCurrentItemChanged. Just switch context
@@ -1441,7 +1381,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
)
def duplicateSelectedItem(self):
if QApplication.instance().MainWidget.modified:
if APP().MainWidget.modified:
self.saveChangeFirst.exec()
return
@@ -1454,15 +1394,13 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
for index in indexes:
if 0 <= index < len(self.ServerList):
QApplication.instance().MainWidget.importServer(
**self.ServerList[index]
)
APP().MainWidget.importServer(**self.ServerList[index])
# Sync it
Storage.sync()
ServerStorage.sync()
def deleteSelectedItem(self):
if QApplication.instance().MainWidget.modified:
if APP().MainWidget.modified:
self.saveChangeFirst.exec()
return
@@ -1499,18 +1437,16 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
self.ScrollList.pop(takedRow)
if not takedActivated and takedRow < self.activatedItemIndex:
QApplication.instance().ActivatedItemIndex = str(
self.activatedItemIndex - 1
)
APP().ActivatedItemIndex = str(self.activatedItemIndex - 1)
# Sync it
Storage.sync()
ServerStorage.sync()
if takedActivated:
if QApplication.instance().tray.ConnectAction.isConnected():
if APP().tray.ConnectAction.isConnected():
# Trigger disconnect
QApplication.instance().tray.ConnectAction.trigger()
QApplication.instance().ActivatedItemIndex = str(-1)
APP().tray.ConnectAction.trigger()
APP().ActivatedItemIndex = str(-1)
# Don't use setCurrentItemXXX since the
# selection may not be changed. Just switch context
@@ -1563,15 +1499,12 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
self.saveScrollBarValue(prevRow)
if (
QApplication.instance().MainWidget is not None
and QApplication.instance().MainWidget.modified
):
if APP().MainWidget is not None and APP().MainWidget.modified:
choice = self.questionSaveBox.exec()
if choice == MessageBox.ButtonRole.AcceptRole.value:
# Save
if QApplication.instance().MainWidget.SaveAsServerAction.save(
if APP().MainWidget.SaveAsServerAction.save(
successCallback=lambda: self.switchContext(currRow),
):
pass
@@ -1583,7 +1516,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
# Discard
self.switchContext(currRow)
QApplication.instance().MainWidget.markAsSaved() # Fake saved
APP().MainWidget.markAsSaved() # Fake saved
elif choice == MessageBox.ButtonRole.RejectRole.value:
# Cancel. Do not switch
@@ -1606,15 +1539,12 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
# Same item activated. Do nothing
return
if (
QApplication.instance().MainWidget is not None
and QApplication.instance().MainWidget.modified
):
if APP().MainWidget is not None and APP().MainWidget.modified:
self.saveChangeFirst.exec()
return
if QApplication.instance().tray.ConnectAction.isConnecting():
if APP().tray.ConnectAction.isConnecting():
# Show the MessageBox asynchronously
self.progressWaitBox.open()
@@ -1629,8 +1559,8 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
# Activate
self.activateItemByIndex(newIndex, activate=True)
if QApplication.instance().tray.ConnectAction.isConnected():
QApplication.instance().tray.ConnectAction.reconnectAction()
if APP().tray.ConnectAction.isConnected():
APP().tray.ConnectAction.reconnectAction()
@QtCore.Slot(QtCore.QPoint)
def handleCustomContextMenuRequested(self, point):
@@ -1672,18 +1602,18 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
self.restoreErrorBox = MessageBox(parent=self)
try:
self.StorageObj = Storage.toObject(QApplication.instance().Configuration)
self.StorageObj = ServerStorage.toObject(APP().Configuration)
# Check model. Shallow copy
self.ServerList = self.StorageObj['model']
except Exception:
# Any non-exit exceptions
self.StorageObj = Storage.init()
self.StorageObj = ServerStorage.init()
# Shallow copy
self.ServerList = self.StorageObj['model']
# Clear it
Storage.clear()
ServerStorage.clear()
Configuration.corruptedError(self.restoreErrorBox)
@@ -1722,7 +1652,7 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
self.plainTextEdit.setStyleSheet(
DraculaTheme.getStyleSheet(
widgetName='QPlainTextEdit',
fontFamily=QApplication.instance().customFontName,
fontFamily=APP().customFontName,
)
)
@@ -1730,7 +1660,7 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
try:
font = self.plainTextEdit.font()
font.setPointSize(int(QApplication.instance().EditorWidgetPointSize))
font.setPointSize(int(APP().EditorWidgetPointSize))
self.plainTextEdit.setFont(font)
except Exception:
@@ -1849,6 +1779,13 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
],
}
routingMenu = {
'name': 'Routing',
'actions': [
RoutingAction(parent=self),
],
}
viewMenu = {
'name': 'View',
'actions': [
@@ -1868,13 +1805,16 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
}
# Menus
for menuDict in (fileMenu, editMenu, viewMenu, helpMenu):
for menuDict in (fileMenu, editMenu, routingMenu, 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):
if hasattr(self, f'{action}'):
logger.warning(f'{self} already has action {action}')
setattr(self, f'{action}', action)
# Set reference
@@ -1886,10 +1826,7 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
self.setGeometry(
100,
100,
*list(
int(size)
for size in QApplication.instance().MainWidgetWindowSize.split(',')
),
*list(int(size) for size in APP().MainWidgetWindowSize.split(',')),
)
except Exception:
# Any non-exit exceptions
@@ -1905,7 +1842,7 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
if syncStorage:
# Sync it
Storage.sync()
ServerStorage.sync()
row = self.normalServerWidget.rowCount()
@@ -1914,21 +1851,21 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
self.normalServerWidget.flushRow(row, server)
item = QTableWidgetItem(str(row + 1))
item.setFont(QFont(QApplication.instance().customFontName))
item.setFont(QFont(APP().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():
if not APP().tray.ConnectAction.isConnected():
# Activate automatically
self.normalServerWidget.activateItemByIndex(0)
def showTabAndSpacesIfNecessary(self):
textOption = QTextOption()
if QApplication.instance().ShowTabAndSpacesInEditor == Switch.ON_:
if APP().ShowTabAndSpacesInEditor == Switch.ON_:
textOption.setFlags(QTextOption.Flag.ShowTabsAndSpaces)
self.plainTextEdit.document().setDefaultTextOption(textOption)
@@ -2009,17 +1946,15 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
self.questionSave()
def syncSettings(self):
QApplication.instance().MainWidgetWindowSize = (
APP().MainWidgetWindowSize = (
f'{self.geometry().width()},{self.geometry().height()}'
)
QApplication.instance().ServerWidgetSectionSizeTable = ujson.dumps(
APP().ServerWidgetSectionSizeTable = ujson.dumps(
self.normalServerWidget.sectionSizeTable,
ensure_ascii=False,
escape_forward_slashes=False,
)
QApplication.instance().EditorWidgetPointSize = str(
self.plainTextEdit.font().pointSize()
)
APP().EditorWidgetPointSize = str(self.plainTextEdit.font().pointSize())
def connectedCallback(self):
self.setWindowIcon(bootstrapIcon('rocket-takeoff-connected-dark.svg'))
File diff suppressed because it is too large Load Diff
+12 -6
View File
@@ -1,5 +1,6 @@
from Furious.Gui.Action import Action, Seperator
from Furious.Widget.Widget import Menu, MessageBox, ZoomableTextBrowser
from Furious.Utility.Constants import APP
from Furious.Utility.Utility import (
StateContext,
SupportConnectedCallback,
@@ -9,7 +10,11 @@ from Furious.Utility.Translator import Translatable, gettext as _
from Furious.Utility.Theme import DraculaTheme
from PySide6 import QtCore
from PySide6.QtWidgets import QApplication, QFileDialog, QMainWindow, QTextBrowser
from PySide6.QtWidgets import QFileDialog, QMainWindow, QTextBrowser
import logging
logger = logging.getLogger(__name__)
class SaveErrorBox(MessageBox):
@@ -141,14 +146,14 @@ class LogViewerWidget(Translatable, SupportConnectedCallback, QMainWindow):
self.textBrowser.setStyleSheet(
DraculaTheme.getStyleSheet(
widgetName='QTextBrowser',
fontFamily=QApplication.instance().customFontName,
fontFamily=APP().customFontName,
)
)
try:
# Restore point size
font = self.textBrowser.font()
font.setPointSize(int(QApplication.instance().ViewerWidgetPointSize))
font.setPointSize(int(APP().ViewerWidgetPointSize))
self.textBrowser.setFont(font)
except Exception:
@@ -178,6 +183,9 @@ class LogViewerWidget(Translatable, SupportConnectedCallback, QMainWindow):
for menu in (fileMenuActions, editMenuActions, viewMenuActions):
for action in menu:
if isinstance(action, Action):
if hasattr(self, f'{action}'):
logger.warning(f'{self} already has action {action}')
setattr(self, f'{action}', action)
self._fileMenu = Menu(*fileMenuActions, title=_('File'), parent=self)
@@ -192,9 +200,7 @@ class LogViewerWidget(Translatable, SupportConnectedCallback, QMainWindow):
return self.textBrowser.toPlainText()
def syncSettings(self):
QApplication.instance().ViewerWidgetPointSize = str(
self.textBrowser.font().pointSize()
)
APP().ViewerWidgetPointSize = str(self.textBrowser.font().pointSize())
def closeEvent(self, event):
event.ignore()
+17 -5
View File
@@ -9,14 +9,23 @@ from Furious.Action import (
)
from Furious.Gui.Action import Action, Seperator
from Furious.Widget.Widget import Menu
from Furious.Utility.Constants import APPLICATION_NAME, APPLICATION_VERSION, PLATFORM
from Furious.Utility.Constants import (
APP,
APPLICATION_NAME,
APPLICATION_VERSION,
PLATFORM,
)
from Furious.Utility.Utility import bootstrapIcon, StateContext, Switch
from Furious.Utility.Translator import Translatable, gettext as _
from Furious.Utility.Proxy import Proxy
from Furious.Utility.StartupOnBoot import StartupOnBoot
from PySide6 import QtCore
from PySide6.QtWidgets import QApplication, QSystemTrayIcon
from PySide6.QtWidgets import QSystemTrayIcon
import logging
logger = logging.getLogger(__name__)
class SystemTrayIcon(Translatable, QSystemTrayIcon):
@@ -41,6 +50,9 @@ class SystemTrayIcon(Translatable, QSystemTrayIcon):
for action in actions:
if isinstance(action, Action):
if hasattr(self, f'{action}'):
logger.warning(f'{self} already has action {action}')
setattr(self, f'{action}', action)
self._menu = Menu(*actions)
@@ -48,11 +60,11 @@ class SystemTrayIcon(Translatable, QSystemTrayIcon):
self.setContextMenu(self._menu)
def bootstrap(self):
if QApplication.instance().StartupOnBoot == Switch.ON_:
if APP().StartupOnBoot == Switch.ON_:
# Rrefresh startup application location
StartupOnBoot.on_()
if QApplication.instance().Connect == Switch.ON_:
if APP().Connect == Switch.ON_:
# Trigger connect action
self.ConnectAction.trigger()
@@ -77,7 +89,7 @@ class SystemTrayIcon(Translatable, QSystemTrayIcon):
@QtCore.Slot(QSystemTrayIcon.ActivationReason)
def handleActivated(self, reason):
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
QApplication.instance().MainWidget.show()
APP().MainWidget.show()
def setApplicationToolTip(self):
self.setToolTip(f'{_(APPLICATION_NAME)} {APPLICATION_VERSION}')
+132 -2
View File
@@ -1,4 +1,5 @@
from Furious.Gui.Action import Action, Seperator
from Furious.Utility.Constants import APP
from Furious.Utility.Utility import (
StateContext,
SupportConnectedCallback,
@@ -8,16 +9,53 @@ from Furious.Utility.Utility import (
from Furious.Utility.Translator import Translatable, gettext as _
from PySide6 import QtCore
from PySide6.QtGui import QBrush, QColor, QFont
from PySide6.QtWidgets import (
QHeaderView,
QInputDialog,
QLineEdit,
QListWidget,
QMenu,
QMessageBox,
QPlainTextEdit,
QPushButton,
QStyledItemDelegate,
QTableWidget,
QTabWidget,
QTextBrowser,
)
class HeaderView(SupportConnectedCallback, QHeaderView):
def __init__(self, *args, **kwargs):
super().__init__(*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 ListWidget(QListWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@property
def selectedIndex(self):
return sorted(list(set(index.row() for index in self.selectedIndexes())))
class Menu(Translatable, QMenu):
def __init__(self, *actions, **kwargs):
super().__init__(**kwargs)
@@ -99,19 +137,111 @@ class PushButton(Translatable, QPushButton):
self.setText(_(self.text()))
class StyledItemDelegate(QStyledItemDelegate):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def createEditor(self, parent, option, index):
editor = QLineEdit(parent)
editor.setFont(QFont(APP().customFontName))
return editor
class TableWidget(QTableWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@property
def selectedIndex(self):
return sorted(list(set(index.row() for index in self.selectedIndexes())))
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()):
item = self.item(int(index), column)
if item is None:
# Do nothing
continue
font = item.font()
font.setBold(True)
item.setFont(font)
if APP().tray is None:
# Initializing
item.setForeground(QColor('#008AE1'))
else:
if APP().tray.ConnectAction.isConnected():
item.setForeground(QColor('#EB212E'))
else:
item.setForeground(QColor('#008AE1'))
else:
for column in range(self.columnCount()):
item = self.item(int(index), column)
if item is None:
# Do nothing
continue
font = item.font()
font.setBold(False)
item.setFont(font)
item.setForeground(QBrush())
class TabWidget(Translatable, QTabWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def retranslate(self):
for i in range(self.tabBar().count()):
self.tabBar().setTabText(i, _(self.tabBar().tabText(i)))
for index in range(self.count()):
self.setTabText(index, _(self.tabText(index)))
class ZoomablePlainTextEdit(QPlainTextEdit):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@staticmethod
def getIndent(line):
indent = ''
for char in line:
if char.isspace():
indent += char
else:
break
return indent
def smartIndent(self, event):
cursor = self.textCursor()
indent = self.getIndent(cursor.block().text())
# Do newline action
super().keyPressEvent(event)
# Add last line indent
cursor.insertText(indent)
self.setTextCursor(cursor)
def keyPressEvent(self, event):
if (
event.key() == QtCore.Qt.Key.Key_Return
or event.key() == QtCore.Qt.Key.Key_Enter
):
self.smartIndent(event)
else:
super().keyPressEvent(event)
def wheelEvent(self, event):
if event.modifiers() == QtCore.Qt.KeyboardModifier.ControlModifier:
delta = event.angleDelta().y()