mirror of
https://github.com/infiniteCable2/openpilot.git
synced 2026-07-25 19:32:03 +08:00
raylib: multilang (#36195)
* fix multilang dialog height
* split to file
* stash
* Revert "stash"
This reverts commit deb4239fe69f0260420fad03f2350e622e31542f.
* add updater
* add files
* stuff
* try
rev
* stash
* works!
* works!
* this should be the flow?
* cursor wrapping -- it missed entire sections, changed formatting, and didn't use trn properly!!!!!!!!!!!!!!!!!
* update translations
* learned my lesson
* this should be the one thing it's good at
* update trans
* onroad wrap
* spanish
* rename
* clean up
* load all
* Revert "load all"
This reverts commit 6f2a45861c914ffb9d40a5edd15751afd798d614.
* jp translations
* try jp
* Revert "try jp"
This reverts commit d0524b10110104baafcdc1ec385c3d57bc5ef901.
* remove languages we can't add rn
* tr
* pt and fr
* ai cannot be trusted
* ai cannot be trusted
* missing trans
* add fonts
* Revert "remove languages we can't add rn"
This reverts commit 73dc75fae2b9e347d867b6636dab6e2b5fe59da7.
* painfully slow to startup
* only load what we need
* Reapply "remove languages we can't add rn"
This reverts commit 52cb48f3b838520a421f9b90e5ea4409c27d4bd0.
* add system
* that's sick that this just works (dynamic)
* fix description falling back to first str + support callable titles in list items
* device is now live!
* make firehose live
* developer
* network live
* software live
* and that
* toggles live
* regen
* start to clean up gpt
* revert op sans
* bruh
* update translations
* rm old script
* add noops for descriptions to fix translating away from non-english after startup
* missing de
* do filtering in multilang.py
* clean up
clean up
* codespell: ignore po
* fix update
* should not depend
* more live
* sidebar and offroad alert panel live
* fix issues with offroad alerts
* fix firehose live
* fix weird tr("") behavior
* sh key live bugfix
* setup.py live
* update
* update
* no fuzzy matching -- breaks dynamic translations
* rm this
* fix calib desc live trans
* change onroad
* rm dfonts
* clean up device
* missing live
* update
* op lint
* not true
* add to gitignore
* speed up startup by reducing chars by ~half
* fix scons
* fix crash going from qt
* preserve original lang
* cancel kb live translate
* no preserve
* fix lint
This commit is contained in:
@@ -37,6 +37,7 @@ a.out
|
||||
*.pyxbldc
|
||||
*.vcd
|
||||
*.qm
|
||||
*.mo
|
||||
*_pyx.cpp
|
||||
config.json
|
||||
clcache
|
||||
|
||||
+1
-1
@@ -177,7 +177,7 @@ quiet-level = 3
|
||||
# if you've got a short variable name that's getting flagged, add it here
|
||||
ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints,whit,indexIn,ws,uint,grey,deque,stdio,amin,BA,LITE,atEnd,UIs,errorString,arange,FocusIn,od,tim,relA,hist,copyable,jupyter,thead,TGE,abl,lite"
|
||||
builtin = "clear,rare,informal,code,names,en-GB_to_en-US"
|
||||
skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.ts, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*"
|
||||
skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.ts, *.po, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*"
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.11"
|
||||
|
||||
@@ -3,5 +3,4 @@ SConscript(['controls/lib/lateral_mpc_lib/SConscript'])
|
||||
SConscript(['controls/lib/longitudinal_mpc_lib/SConscript'])
|
||||
SConscript(['locationd/SConscript'])
|
||||
SConscript(['modeld/SConscript'])
|
||||
if GetOption('extras'):
|
||||
SConscript(['ui/SConscript'])
|
||||
SConscript(['ui/SConscript'])
|
||||
|
||||
+98
-85
@@ -1,98 +1,111 @@
|
||||
import os
|
||||
import json
|
||||
Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'transformations')
|
||||
|
||||
base_libs = [common, messaging, visionipc, transformations,
|
||||
'm', 'OpenCL', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"]
|
||||
|
||||
if arch == 'larch64':
|
||||
base_libs.append('EGL')
|
||||
|
||||
if arch == "Darwin":
|
||||
del base_libs[base_libs.index('OpenCL')]
|
||||
qt_env['FRAMEWORKS'] += ['OpenCL']
|
||||
|
||||
# FIXME: remove this once we're on 5.15 (24.04)
|
||||
qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"]
|
||||
|
||||
qt_util = qt_env.Library("qt_util", ["#selfdrive/ui/qt/api.cc", "#selfdrive/ui/qt/util.cc"], LIBS=base_libs)
|
||||
widgets_src = ["qt/widgets/input.cc", "qt/widgets/wifi.cc", "qt/prime_state.cc",
|
||||
"qt/widgets/ssh_keys.cc", "qt/widgets/toggle.cc", "qt/widgets/controls.cc",
|
||||
"qt/widgets/offroad_alerts.cc", "qt/widgets/prime.cc", "qt/widgets/keyboard.cc",
|
||||
"qt/widgets/scrollview.cc", "qt/widgets/cameraview.cc", "#third_party/qrcode/QrCode.cc",
|
||||
"qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc"]
|
||||
|
||||
widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs)
|
||||
Export('widgets')
|
||||
qt_libs = [widgets, qt_util] + base_libs
|
||||
|
||||
qt_src = ["main.cc", "ui.cc", "qt/sidebar.cc", "qt/body.cc",
|
||||
"qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc",
|
||||
"qt/offroad/software_settings.cc", "qt/offroad/developer_panel.cc", "qt/offroad/onboarding.cc",
|
||||
"qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc", "qt/offroad/firehose.cc",
|
||||
"qt/onroad/onroad_home.cc", "qt/onroad/annotated_camera.cc", "qt/onroad/model.cc",
|
||||
"qt/onroad/buttons.cc", "qt/onroad/alerts.cc", "qt/onroad/driver_monitoring.cc", "qt/onroad/hud.cc"]
|
||||
|
||||
# build translation files
|
||||
# compile gettext .po -> .mo translations
|
||||
with open(File("translations/languages.json").abspath) as f:
|
||||
languages = json.loads(f.read())
|
||||
translation_sources = [f"#selfdrive/ui/translations/{l}.ts" for l in languages.values()]
|
||||
translation_targets = [src.replace(".ts", ".qm") for src in translation_sources]
|
||||
lrelease_bin = 'third_party/qt5/larch64/bin/lrelease' if arch == 'larch64' else 'lrelease'
|
||||
|
||||
lrelease = qt_env.Command(translation_targets, translation_sources, f"{lrelease_bin} $SOURCES")
|
||||
qt_env.NoClean(translation_sources)
|
||||
qt_env.Precious(translation_sources)
|
||||
po_sources = [f"#selfdrive/ui/translations/app_{l}.po" for l in languages.values()]
|
||||
po_sources = [src for src in po_sources if os.path.exists(File(src).abspath)]
|
||||
mo_targets = [src.replace(".po", ".mo") for src in po_sources]
|
||||
mo_build = []
|
||||
for src, tgt in zip(po_sources, mo_targets):
|
||||
mo_build.append(qt_env.Command(tgt, src, "msgfmt -o $TARGET $SOURCE"))
|
||||
mo_alias = qt_env.Alias('mo', mo_build)
|
||||
qt_env.AlwaysBuild(mo_alias)
|
||||
|
||||
# create qrc file for compiled translations to include with assets
|
||||
translations_assets_src = "#selfdrive/assets/translations_assets.qrc"
|
||||
with open(File(translations_assets_src).abspath, 'w') as f:
|
||||
f.write('<!DOCTYPE RCC><RCC version="1.0">\n<qresource>\n')
|
||||
f.write('\n'.join([f'<file alias="{l}">../ui/translations/{l}.qm</file>' for l in languages.values()]))
|
||||
f.write('\n</qresource>\n</RCC>')
|
||||
|
||||
# build assets
|
||||
assets = "#selfdrive/assets/assets.cc"
|
||||
assets_src = "#selfdrive/assets/assets.qrc"
|
||||
qt_env.Command(assets, [assets_src, translations_assets_src], f"rcc $SOURCES -o $TARGET")
|
||||
qt_env.Depends(assets, Glob('#selfdrive/assets/*', exclude=[assets, assets_src, translations_assets_src, "#selfdrive/assets/assets.o"]) + [lrelease])
|
||||
asset_obj = qt_env.Object("assets", assets)
|
||||
|
||||
# build main UI
|
||||
qt_env.Program("ui", qt_src + [asset_obj], LIBS=qt_libs)
|
||||
if GetOption('extras'):
|
||||
qt_src.remove("main.cc") # replaced by test_runner
|
||||
qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs)
|
||||
base_libs = [common, messaging, visionipc, transformations,
|
||||
'm', 'OpenCL', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"]
|
||||
|
||||
# build installers
|
||||
if arch != "Darwin":
|
||||
raylib_env = env.Clone()
|
||||
raylib_env['LIBPATH'] += [f'#third_party/raylib/{arch}/']
|
||||
raylib_env['LINKFLAGS'].append('-Wl,-strip-debug')
|
||||
if arch == 'larch64':
|
||||
base_libs.append('EGL')
|
||||
|
||||
raylib_libs = common + ["raylib"]
|
||||
if arch == "larch64":
|
||||
raylib_libs += ["GLESv2", "EGL", "gbm", "drm"]
|
||||
else:
|
||||
raylib_libs += ["GL"]
|
||||
if arch == "Darwin":
|
||||
del base_libs[base_libs.index('OpenCL')]
|
||||
qt_env['FRAMEWORKS'] += ['OpenCL']
|
||||
|
||||
release = "release3"
|
||||
installers = [
|
||||
("openpilot", release),
|
||||
("openpilot_test", f"{release}-staging"),
|
||||
("openpilot_nightly", "nightly"),
|
||||
("openpilot_internal", "nightly-dev"),
|
||||
]
|
||||
# FIXME: remove this once we're on 5.15 (24.04)
|
||||
qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"]
|
||||
|
||||
cont = raylib_env.Command("installer/continue_openpilot.o", "installer/continue_openpilot.sh",
|
||||
"ld -r -b binary -o $TARGET $SOURCE")
|
||||
inter = raylib_env.Command("installer/inter_ttf.o", "installer/inter-ascii.ttf",
|
||||
"ld -r -b binary -o $TARGET $SOURCE")
|
||||
for name, branch in installers:
|
||||
d = {'BRANCH': f"'\"{branch}\"'"}
|
||||
if "internal" in name:
|
||||
d['INTERNAL'] = "1"
|
||||
qt_util = qt_env.Library("qt_util", ["#selfdrive/ui/qt/api.cc", "#selfdrive/ui/qt/util.cc"], LIBS=base_libs)
|
||||
widgets_src = ["qt/widgets/input.cc", "qt/widgets/wifi.cc", "qt/prime_state.cc",
|
||||
"qt/widgets/ssh_keys.cc", "qt/widgets/toggle.cc", "qt/widgets/controls.cc",
|
||||
"qt/widgets/offroad_alerts.cc", "qt/widgets/prime.cc", "qt/widgets/keyboard.cc",
|
||||
"qt/widgets/scrollview.cc", "qt/widgets/cameraview.cc", "#third_party/qrcode/QrCode.cc",
|
||||
"qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc"]
|
||||
|
||||
obj = raylib_env.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d)
|
||||
f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter], LIBS=raylib_libs)
|
||||
# keep installers small
|
||||
assert f[0].get_size() < 1900*1e3, f[0].get_size()
|
||||
widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs)
|
||||
Export('widgets')
|
||||
qt_libs = [widgets, qt_util] + base_libs
|
||||
|
||||
qt_src = ["main.cc", "ui.cc", "qt/sidebar.cc", "qt/body.cc",
|
||||
"qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc",
|
||||
"qt/offroad/software_settings.cc", "qt/offroad/developer_panel.cc", "qt/offroad/onboarding.cc",
|
||||
"qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc", "qt/offroad/firehose.cc",
|
||||
"qt/onroad/onroad_home.cc", "qt/onroad/annotated_camera.cc", "qt/onroad/model.cc",
|
||||
"qt/onroad/buttons.cc", "qt/onroad/alerts.cc", "qt/onroad/driver_monitoring.cc", "qt/onroad/hud.cc"]
|
||||
|
||||
# build translation files
|
||||
translation_sources = [f"#selfdrive/ui/translations/{l}.ts" for l in languages.values()]
|
||||
translation_targets = [src.replace(".ts", ".qm") for src in translation_sources]
|
||||
lrelease_bin = 'third_party/qt5/larch64/bin/lrelease' if arch == 'larch64' else 'lrelease'
|
||||
|
||||
lrelease = qt_env.Command(translation_targets, translation_sources, f"{lrelease_bin} $SOURCES")
|
||||
qt_env.NoClean(translation_sources)
|
||||
qt_env.Precious(translation_sources)
|
||||
|
||||
# create qrc file for compiled translations to include with assets
|
||||
translations_assets_src = "#selfdrive/assets/translations_assets.qrc"
|
||||
with open(File(translations_assets_src).abspath, 'w') as f:
|
||||
f.write('<!DOCTYPE RCC><RCC version="1.0">\n<qresource>\n')
|
||||
f.write('\n'.join([f'<file alias="{l}">../ui/translations/{l}.qm</file>' for l in languages.values()]))
|
||||
f.write('\n</qresource>\n</RCC>')
|
||||
|
||||
# build assets
|
||||
assets = "#selfdrive/assets/assets.cc"
|
||||
assets_src = "#selfdrive/assets/assets.qrc"
|
||||
qt_env.Command(assets, [assets_src, translations_assets_src], f"rcc $SOURCES -o $TARGET")
|
||||
qt_env.Depends(assets, Glob('#selfdrive/assets/*', exclude=[assets, assets_src, translations_assets_src, "#selfdrive/assets/assets.o"]) + [lrelease])
|
||||
asset_obj = qt_env.Object("assets", assets)
|
||||
|
||||
# build main UI
|
||||
qt_env.Program("ui", qt_src + [asset_obj], LIBS=qt_libs)
|
||||
if GetOption('extras'):
|
||||
qt_src.remove("main.cc") # replaced by test_runner
|
||||
qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs)
|
||||
|
||||
# build installers
|
||||
if arch != "Darwin":
|
||||
raylib_env = env.Clone()
|
||||
raylib_env['LIBPATH'] += [f'#third_party/raylib/{arch}/']
|
||||
raylib_env['LINKFLAGS'].append('-Wl,-strip-debug')
|
||||
|
||||
raylib_libs = common + ["raylib"]
|
||||
if arch == "larch64":
|
||||
raylib_libs += ["GLESv2", "EGL", "gbm", "drm"]
|
||||
else:
|
||||
raylib_libs += ["GL"]
|
||||
|
||||
release = "release3"
|
||||
installers = [
|
||||
("openpilot", release),
|
||||
("openpilot_test", f"{release}-staging"),
|
||||
("openpilot_nightly", "nightly"),
|
||||
("openpilot_internal", "nightly-dev"),
|
||||
]
|
||||
|
||||
cont = raylib_env.Command("installer/continue_openpilot.o", "installer/continue_openpilot.sh",
|
||||
"ld -r -b binary -o $TARGET $SOURCE")
|
||||
inter = raylib_env.Command("installer/inter_ttf.o", "installer/inter-ascii.ttf",
|
||||
"ld -r -b binary -o $TARGET $SOURCE")
|
||||
for name, branch in installers:
|
||||
d = {'BRANCH': f"'\"{branch}\"'"}
|
||||
if "internal" in name:
|
||||
d['INTERNAL'] = "1"
|
||||
|
||||
obj = raylib_env.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d)
|
||||
f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter], LIBS=raylib_libs)
|
||||
# keep installers small
|
||||
assert f[0].get_size() < 1900*1e3, f[0].get_size()
|
||||
|
||||
@@ -6,23 +6,24 @@ from openpilot.system.ui.widgets.list_view import toggle_item
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
'enable_adb': tr(
|
||||
'enable_adb': tr_noop(
|
||||
"ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. " +
|
||||
"See https://docs.comma.ai/how-to/connect-to-comma for more info."
|
||||
),
|
||||
'ssh_key': tr(
|
||||
'ssh_key': tr_noop(
|
||||
"Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " +
|
||||
"other than your own. A comma employee will NEVER ask you to add their GitHub username."
|
||||
),
|
||||
'alpha_longitudinal': tr(
|
||||
'alpha_longitudinal': tr_noop(
|
||||
"<b>WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).</b><br><br>" +
|
||||
"On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. " +
|
||||
"Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha."
|
||||
"Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. " +
|
||||
"Changing this setting will restart openpilot if the car is powered on."
|
||||
),
|
||||
}
|
||||
|
||||
@@ -35,8 +36,8 @@ class DeveloperLayout(Widget):
|
||||
|
||||
# Build items and keep references for callbacks/state updates
|
||||
self._adb_toggle = toggle_item(
|
||||
tr("Enable ADB"),
|
||||
description=DESCRIPTIONS["enable_adb"],
|
||||
lambda: tr("Enable ADB"),
|
||||
description=lambda: DESCRIPTIONS["enable_adb"],
|
||||
initial_state=self._params.get_bool("AdbEnabled"),
|
||||
callback=self._on_enable_adb,
|
||||
enabled=ui_state.is_offroad,
|
||||
@@ -44,15 +45,15 @@ class DeveloperLayout(Widget):
|
||||
|
||||
# SSH enable toggle + SSH key management
|
||||
self._ssh_toggle = toggle_item(
|
||||
tr("Enable SSH"),
|
||||
lambda: tr("Enable SSH"),
|
||||
description="",
|
||||
initial_state=self._params.get_bool("SshEnabled"),
|
||||
callback=self._on_enable_ssh,
|
||||
)
|
||||
self._ssh_keys = ssh_key_item("SSH Keys", description=DESCRIPTIONS["ssh_key"])
|
||||
self._ssh_keys = ssh_key_item(lambda: tr("SSH Keys"), description=lambda: DESCRIPTIONS["ssh_key"])
|
||||
|
||||
self._joystick_toggle = toggle_item(
|
||||
tr("Joystick Debug Mode"),
|
||||
lambda: tr("Joystick Debug Mode"),
|
||||
description="",
|
||||
initial_state=self._params.get_bool("JoystickDebugMode"),
|
||||
callback=self._on_joystick_debug_mode,
|
||||
@@ -60,22 +61,20 @@ class DeveloperLayout(Widget):
|
||||
)
|
||||
|
||||
self._long_maneuver_toggle = toggle_item(
|
||||
tr("Longitudinal Maneuver Mode"),
|
||||
lambda: tr("Longitudinal Maneuver Mode"),
|
||||
description="",
|
||||
initial_state=self._params.get_bool("LongitudinalManeuverMode"),
|
||||
callback=self._on_long_maneuver_mode,
|
||||
)
|
||||
|
||||
self._alpha_long_toggle = toggle_item(
|
||||
tr("openpilot Longitudinal Control (Alpha)"),
|
||||
description=DESCRIPTIONS["alpha_longitudinal"],
|
||||
lambda: tr("openpilot Longitudinal Control (Alpha)"),
|
||||
description=lambda: DESCRIPTIONS["alpha_longitudinal"],
|
||||
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
|
||||
callback=self._on_alpha_long_enabled,
|
||||
enabled=lambda: not ui_state.engaged,
|
||||
)
|
||||
|
||||
self._alpha_long_toggle.set_description(self._alpha_long_toggle.description + " Changing this setting will restart openpilot if the car is powered on.")
|
||||
|
||||
items = [
|
||||
self._adb_toggle,
|
||||
self._ssh_toggle,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
import json
|
||||
import math
|
||||
|
||||
from cereal import messaging, log
|
||||
@@ -12,7 +11,7 @@ from openpilot.selfdrive.ui.layouts.onboarding import TrainingGuide
|
||||
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
|
||||
from openpilot.system.hardware import TICI
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.multilang import multilang, tr, tr_noop
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
|
||||
from openpilot.system.ui.widgets.html_render import HtmlModal
|
||||
@@ -22,10 +21,10 @@ from openpilot.system.ui.widgets.scroller import Scroller
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
'pair_device': tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer."),
|
||||
'driver_camera': tr("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)"),
|
||||
'reset_calibration': tr("openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."),
|
||||
'review_guide': tr("Review the rules, features, and limitations of openpilot"),
|
||||
'pair_device': tr_noop("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer."),
|
||||
'driver_camera': tr_noop("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)"),
|
||||
'reset_calibration': tr_noop("openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."),
|
||||
'review_guide': tr_noop("Review the rules, features, and limitations of openpilot"),
|
||||
}
|
||||
|
||||
|
||||
@@ -46,27 +45,27 @@ class DeviceLayout(Widget):
|
||||
ui_state.add_offroad_transition_callback(self._offroad_transition)
|
||||
|
||||
def _initialize_items(self):
|
||||
dongle_id = self._params.get("DongleId") or tr("N/A")
|
||||
serial = self._params.get("HardwareSerial") or tr("N/A")
|
||||
|
||||
self._pair_device_btn = button_item(tr("Pair Device"), tr("PAIR"), DESCRIPTIONS['pair_device'], callback=self._pair_device)
|
||||
self._pair_device_btn = button_item(lambda: tr("Pair Device"), lambda: tr("PAIR"), lambda: tr(DESCRIPTIONS['pair_device']), callback=self._pair_device)
|
||||
self._pair_device_btn.set_visible(lambda: not ui_state.prime_state.is_paired())
|
||||
|
||||
self._reset_calib_btn = button_item(tr("Reset Calibration"), tr("RESET"), DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt)
|
||||
self._reset_calib_btn = button_item(lambda: tr("Reset Calibration"), lambda: tr("RESET"), lambda: tr(DESCRIPTIONS['reset_calibration']),
|
||||
callback=self._reset_calibration_prompt)
|
||||
self._reset_calib_btn.set_description_opened_callback(self._update_calib_description)
|
||||
|
||||
self._power_off_btn = dual_button_item(tr("Reboot"), tr("Power Off"), left_callback=self._reboot_prompt, right_callback=self._power_off_prompt)
|
||||
self._power_off_btn = dual_button_item(lambda: tr("Reboot"), lambda: tr("Power Off"),
|
||||
left_callback=self._reboot_prompt, right_callback=self._power_off_prompt)
|
||||
|
||||
items = [
|
||||
text_item(tr("Dongle ID"), dongle_id),
|
||||
text_item(tr("Serial"), serial),
|
||||
text_item(lambda: tr("Dongle ID"), self._params.get("DongleId") or (lambda: tr("N/A"))),
|
||||
text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))),
|
||||
self._pair_device_btn,
|
||||
button_item(tr("Driver Camera"), tr("PREVIEW"), DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad),
|
||||
button_item(lambda: tr("Driver Camera"), lambda: tr("PREVIEW"), lambda: tr(DESCRIPTIONS['driver_camera']),
|
||||
callback=self._show_driver_camera, enabled=ui_state.is_offroad),
|
||||
self._reset_calib_btn,
|
||||
button_item(tr("Review Training Guide"), tr("REVIEW"), DESCRIPTIONS['review_guide'], self._on_review_training_guide, enabled=ui_state.is_offroad),
|
||||
regulatory_btn := button_item(tr("Regulatory"), tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad),
|
||||
# TODO: implement multilang
|
||||
# button_item(tr("Change Language"), tr("CHANGE"), callback=self._show_language_selection, enabled=ui_state.is_offroad),
|
||||
button_item(lambda: tr("Review Training Guide"), lambda: tr("REVIEW"), lambda: tr(DESCRIPTIONS['review_guide']),
|
||||
self._on_review_training_guide, enabled=ui_state.is_offroad),
|
||||
regulatory_btn := button_item(lambda: tr("Regulatory"), lambda: tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad),
|
||||
button_item(lambda: tr("Change Language"), lambda: tr("CHANGE"), callback=self._show_language_dialog),
|
||||
self._power_off_btn,
|
||||
]
|
||||
regulatory_btn.set_visible(TICI)
|
||||
@@ -81,23 +80,16 @@ class DeviceLayout(Widget):
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _show_language_selection(self):
|
||||
try:
|
||||
languages_file = os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json")
|
||||
with open(languages_file, encoding='utf-8') as f:
|
||||
languages = json.load(f)
|
||||
def _show_language_dialog(self):
|
||||
def handle_language_selection(result: int):
|
||||
if result == 1 and self._select_language_dialog:
|
||||
selected_language = multilang.languages[self._select_language_dialog.selection]
|
||||
multilang.change_language(selected_language)
|
||||
self._update_calib_description()
|
||||
self._select_language_dialog = None
|
||||
|
||||
self._select_language_dialog = MultiOptionDialog("Select a language", languages)
|
||||
gui_app.set_modal_overlay(self._select_language_dialog, callback=self._handle_language_selection)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def _handle_language_selection(self, result: int):
|
||||
if result == 1 and self._select_language_dialog:
|
||||
selected_language = self._select_language_dialog.selection
|
||||
self._params.put("LanguageSetting", selected_language)
|
||||
|
||||
self._select_language_dialog = None
|
||||
self._select_language_dialog = MultiOptionDialog(tr("Select a language"), multilang.languages, multilang.codes[multilang.language])
|
||||
gui_app.set_modal_overlay(self._select_language_dialog, callback=handle_language_selection)
|
||||
|
||||
def _show_driver_camera(self):
|
||||
if not self._driver_camera:
|
||||
@@ -127,7 +119,7 @@ class DeviceLayout(Widget):
|
||||
gui_app.set_modal_overlay(dialog, callback=reset_calibration)
|
||||
|
||||
def _update_calib_description(self):
|
||||
desc = DESCRIPTIONS['reset_calibration']
|
||||
desc = tr(DESCRIPTIONS['reset_calibration'])
|
||||
|
||||
calib_bytes = self._params.get("CalibrationParams")
|
||||
if calib_bytes:
|
||||
|
||||
@@ -8,20 +8,20 @@ from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||
from openpilot.system.ui.lib.multilang import tr, trn
|
||||
from openpilot.system.ui.lib.multilang import tr, trn, tr_noop
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.lib.api_helpers import get_token
|
||||
|
||||
TITLE = tr("Firehose Mode")
|
||||
DESCRIPTION = tr(
|
||||
TITLE = tr_noop("Firehose Mode")
|
||||
DESCRIPTION = tr_noop(
|
||||
"openpilot learns to drive by watching humans, like you, drive.\n\n"
|
||||
+ "Firehose Mode allows you to maximize your training data uploads to improve "
|
||||
+ "openpilot's driving models. More data means bigger models, which means better Experimental Mode."
|
||||
)
|
||||
INSTRUCTIONS = tr(
|
||||
INSTRUCTIONS = tr_noop(
|
||||
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n"
|
||||
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n\n"
|
||||
+ "Frequently Asked Questions\n\n"
|
||||
@@ -86,14 +86,15 @@ class FirehoseLayout(Widget):
|
||||
w = int(rect.width - 80)
|
||||
|
||||
# Title (centered)
|
||||
title_text = tr(TITLE) # live translate
|
||||
title_font = gui_app.font(FontWeight.MEDIUM)
|
||||
text_width = measure_text_cached(title_font, TITLE, 100).x
|
||||
text_width = measure_text_cached(title_font, title_text, 100).x
|
||||
title_x = rect.x + (rect.width - text_width) / 2
|
||||
rl.draw_text_ex(title_font, TITLE, rl.Vector2(title_x, y), 100, 0, rl.WHITE)
|
||||
rl.draw_text_ex(title_font, title_text, rl.Vector2(title_x, y), 100, 0, rl.WHITE)
|
||||
y += 200
|
||||
|
||||
# Description
|
||||
y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
|
||||
y = self._draw_wrapped_text(x, y, w, tr(DESCRIPTION), gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
|
||||
y += 40 + 20
|
||||
|
||||
# Separator
|
||||
@@ -117,7 +118,7 @@ class FirehoseLayout(Widget):
|
||||
y += 30 + 20
|
||||
|
||||
# Instructions
|
||||
y = self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
|
||||
y = self._draw_wrapped_text(x, y, w, tr(INSTRUCTIONS), gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
|
||||
|
||||
# bottom margin + remove effect of scroll offset
|
||||
return int(round(y - self.scroll_panel.offset + 40))
|
||||
|
||||
@@ -8,7 +8,7 @@ from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -59,12 +59,12 @@ class SettingsLayout(Widget):
|
||||
wifi_manager.set_active(False)
|
||||
|
||||
self._panels = {
|
||||
PanelType.DEVICE: PanelInfo(tr("Device"), DeviceLayout()),
|
||||
PanelType.NETWORK: PanelInfo(tr("Network"), NetworkUI(wifi_manager)),
|
||||
PanelType.TOGGLES: PanelInfo(tr("Toggles"), TogglesLayout()),
|
||||
PanelType.SOFTWARE: PanelInfo(tr("Software"), SoftwareLayout()),
|
||||
PanelType.FIREHOSE: PanelInfo(tr("Firehose"), FirehoseLayout()),
|
||||
PanelType.DEVELOPER: PanelInfo(tr("Developer"), DeveloperLayout()),
|
||||
PanelType.DEVICE: PanelInfo(tr_noop("Device"), DeviceLayout()),
|
||||
PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUI(wifi_manager)),
|
||||
PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout()),
|
||||
PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout()),
|
||||
PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout()),
|
||||
PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout()),
|
||||
}
|
||||
|
||||
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
||||
@@ -116,11 +116,12 @@ class SettingsLayout(Widget):
|
||||
is_selected = panel_type == self._current_panel
|
||||
text_color = TEXT_SELECTED if is_selected else TEXT_NORMAL
|
||||
# Draw button text (right-aligned)
|
||||
text_size = measure_text_cached(self._font_medium, panel_info.name, 65)
|
||||
panel_name = tr(panel_info.name)
|
||||
text_size = measure_text_cached(self._font_medium, panel_name, 65)
|
||||
text_pos = rl.Vector2(
|
||||
button_rect.x + button_rect.width - text_size.x, button_rect.y + (button_rect.height - text_size.y) / 2
|
||||
)
|
||||
rl.draw_text_ex(self._font_medium, panel_info.name, text_pos, 65, 0, text_color)
|
||||
rl.draw_text_ex(self._font_medium, panel_name, text_pos, 65, 0, text_color)
|
||||
|
||||
# Store button rect for click detection
|
||||
panel_info.button_rect = button_rect
|
||||
|
||||
@@ -45,12 +45,12 @@ class SoftwareLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._onroad_label = ListItem(title=tr("Updates are only downloaded while the car is off."))
|
||||
self._version_item = text_item(tr("Current Version"), ui_state.params.get("UpdaterCurrentDescription") or "")
|
||||
self._download_btn = button_item(tr("Download"), tr("CHECK"), callback=self._on_download_update)
|
||||
self._onroad_label = ListItem(lambda: tr("Updates are only downloaded while the car is off."))
|
||||
self._version_item = text_item(lambda: tr("Current Version"), ui_state.params.get("UpdaterCurrentDescription") or "")
|
||||
self._download_btn = button_item(lambda: tr("Download"), lambda: tr("CHECK"), callback=self._on_download_update)
|
||||
|
||||
# Install button is initially hidden
|
||||
self._install_btn = button_item(tr("Install Update"), tr("INSTALL"), callback=self._on_install_update)
|
||||
self._install_btn = button_item(lambda: tr("Install Update"), lambda: tr("INSTALL"), callback=self._on_install_update)
|
||||
self._install_btn.set_visible(False)
|
||||
|
||||
self._select_branch_dialog: MultiOptionDialog | None = None
|
||||
@@ -70,7 +70,7 @@ class SoftwareLayout(Widget):
|
||||
self._download_btn,
|
||||
self._install_btn,
|
||||
self._target_branch_btn,
|
||||
button_item("Uninstall", tr("UNINSTALL"), callback=self._on_uninstall),
|
||||
button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall),
|
||||
]
|
||||
return items
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_i
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
@@ -13,24 +13,24 @@ PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
"OpenpilotEnabledToggle": tr(
|
||||
"OpenpilotEnabledToggle": tr_noop(
|
||||
"Use the openpilot system for adaptive cruise control and lane keep driver assistance. " +
|
||||
"Your attention is required at all times to use this feature."
|
||||
),
|
||||
"DisengageOnAccelerator": tr("When enabled, pressing the accelerator pedal will disengage openpilot."),
|
||||
"LongitudinalPersonality": tr(
|
||||
"DisengageOnAccelerator": tr_noop("When enabled, pressing the accelerator pedal will disengage openpilot."),
|
||||
"LongitudinalPersonality": tr_noop(
|
||||
"Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " +
|
||||
"In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
|
||||
"your steering wheel distance button."
|
||||
),
|
||||
"IsLdwEnabled": tr(
|
||||
"IsLdwEnabled": tr_noop(
|
||||
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
|
||||
"without a turn signal activated while driving over 31 mph (50 km/h)."
|
||||
),
|
||||
"AlwaysOnDM": tr("Enable driver monitoring even when openpilot is not engaged."),
|
||||
'RecordFront': tr("Upload data from the driver facing camera and help improve the driver monitoring algorithm."),
|
||||
"IsMetric": tr("Display speed in km/h instead of mph."),
|
||||
"RecordAudio": tr("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."),
|
||||
"AlwaysOnDM": tr_noop("Enable driver monitoring even when openpilot is not engaged."),
|
||||
'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."),
|
||||
"IsMetric": tr_noop("Display speed in km/h instead of mph."),
|
||||
"RecordAudio": tr_noop("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."),
|
||||
}
|
||||
|
||||
|
||||
@@ -43,49 +43,49 @@ class TogglesLayout(Widget):
|
||||
# param, title, desc, icon, needs_restart
|
||||
self._toggle_defs = {
|
||||
"OpenpilotEnabledToggle": (
|
||||
tr("Enable openpilot"),
|
||||
lambda: tr("Enable openpilot"),
|
||||
DESCRIPTIONS["OpenpilotEnabledToggle"],
|
||||
"chffr_wheel.png",
|
||||
True,
|
||||
),
|
||||
"ExperimentalMode": (
|
||||
tr("Experimental Mode"),
|
||||
lambda: tr("Experimental Mode"),
|
||||
"",
|
||||
"experimental_white.png",
|
||||
False,
|
||||
),
|
||||
"DisengageOnAccelerator": (
|
||||
tr("Disengage on Accelerator Pedal"),
|
||||
lambda: tr("Disengage on Accelerator Pedal"),
|
||||
DESCRIPTIONS["DisengageOnAccelerator"],
|
||||
"disengage_on_accelerator.png",
|
||||
False,
|
||||
),
|
||||
"IsLdwEnabled": (
|
||||
tr("Enable Lane Departure Warnings"),
|
||||
lambda: tr("Enable Lane Departure Warnings"),
|
||||
DESCRIPTIONS["IsLdwEnabled"],
|
||||
"warning.png",
|
||||
False,
|
||||
),
|
||||
"AlwaysOnDM": (
|
||||
tr("Always-On Driver Monitoring"),
|
||||
lambda: tr("Always-On Driver Monitoring"),
|
||||
DESCRIPTIONS["AlwaysOnDM"],
|
||||
"monitoring.png",
|
||||
False,
|
||||
),
|
||||
"RecordFront": (
|
||||
tr("Record and Upload Driver Camera"),
|
||||
lambda: tr("Record and Upload Driver Camera"),
|
||||
DESCRIPTIONS["RecordFront"],
|
||||
"monitoring.png",
|
||||
True,
|
||||
),
|
||||
"RecordAudio": (
|
||||
tr("Record and Upload Microphone Audio"),
|
||||
lambda: tr("Record and Upload Microphone Audio"),
|
||||
DESCRIPTIONS["RecordAudio"],
|
||||
"microphone.png",
|
||||
True,
|
||||
),
|
||||
"IsMetric": (
|
||||
tr("Use Metric System"),
|
||||
lambda: tr("Use Metric System"),
|
||||
DESCRIPTIONS["IsMetric"],
|
||||
"metric.png",
|
||||
False,
|
||||
@@ -93,9 +93,9 @@ class TogglesLayout(Widget):
|
||||
}
|
||||
|
||||
self._long_personality_setting = multiple_button_item(
|
||||
tr("Driving Personality"),
|
||||
lambda: tr("Driving Personality"),
|
||||
DESCRIPTIONS["LongitudinalPersonality"],
|
||||
buttons=[tr("Aggressive"), tr("Standard"), tr("Relaxed")],
|
||||
buttons=[lambda: tr("Aggressive"), lambda: tr("Standard"), lambda: tr("Relaxed")],
|
||||
button_width=255,
|
||||
callback=self._set_longitudinal_personality,
|
||||
selected_index=self._params.get("LongitudinalPersonality", return_default=True),
|
||||
@@ -119,8 +119,11 @@ class TogglesLayout(Widget):
|
||||
locked = False
|
||||
toggle.action_item.set_enabled(not locked)
|
||||
|
||||
# Make description callable for live translation
|
||||
additional_desc = ""
|
||||
if needs_restart and not locked:
|
||||
toggle.set_description(toggle.description + tr(" Changing this setting will restart openpilot if the car is powered on."))
|
||||
additional_desc = tr("Changing this setting will restart openpilot if the car is powered on.")
|
||||
toggle.set_description(lambda og_desc=toggle.description, add_desc=additional_desc: tr(og_desc) + (" " + tr(add_desc) if add_desc else ""))
|
||||
|
||||
# track for engaged state updates
|
||||
if locked:
|
||||
|
||||
@@ -5,7 +5,7 @@ from collections.abc import Callable
|
||||
from cereal import log
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, FONT_SCALE
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
@@ -40,13 +40,13 @@ class Colors:
|
||||
|
||||
|
||||
NETWORK_TYPES = {
|
||||
NetworkType.none: tr("--"),
|
||||
NetworkType.wifi: tr("Wi-Fi"),
|
||||
NetworkType.ethernet: tr("ETH"),
|
||||
NetworkType.cell2G: tr("2G"),
|
||||
NetworkType.cell3G: tr("3G"),
|
||||
NetworkType.cell4G: tr("LTE"),
|
||||
NetworkType.cell5G: tr("5G"),
|
||||
NetworkType.none: tr_noop("--"),
|
||||
NetworkType.wifi: tr_noop("Wi-Fi"),
|
||||
NetworkType.ethernet: tr_noop("ETH"),
|
||||
NetworkType.cell2G: tr_noop("2G"),
|
||||
NetworkType.cell3G: tr_noop("3G"),
|
||||
NetworkType.cell4G: tr_noop("LTE"),
|
||||
NetworkType.cell5G: tr_noop("5G"),
|
||||
}
|
||||
|
||||
|
||||
@@ -68,9 +68,9 @@ class Sidebar(Widget):
|
||||
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
||||
self._net_strength = 0
|
||||
|
||||
self._temp_status = MetricData(tr("TEMP"), tr("GOOD"), Colors.GOOD)
|
||||
self._panda_status = MetricData(tr("VEHICLE"), tr("ONLINE"), Colors.GOOD)
|
||||
self._connect_status = MetricData(tr("CONNECT"), tr("OFFLINE"), Colors.WARNING)
|
||||
self._temp_status = MetricData(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD)
|
||||
self._panda_status = MetricData(tr_noop("VEHICLE"), tr_noop("ONLINE"), Colors.GOOD)
|
||||
self._connect_status = MetricData(tr_noop("CONNECT"), tr_noop("OFFLINE"), Colors.WARNING)
|
||||
self._recording_audio = False
|
||||
|
||||
self._home_img = gui_app.texture("images/button_home.png", HOME_BTN.width, HOME_BTN.height)
|
||||
@@ -114,7 +114,7 @@ class Sidebar(Widget):
|
||||
self._update_panda_status()
|
||||
|
||||
def _update_network_status(self, device_state):
|
||||
self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, tr("Unknown"))
|
||||
self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, tr_noop("Unknown"))
|
||||
strength = device_state.networkStrength
|
||||
self._net_strength = max(0, min(5, strength.raw + 1)) if strength > 0 else 0
|
||||
|
||||
@@ -122,26 +122,26 @@ class Sidebar(Widget):
|
||||
thermal_status = device_state.thermalStatus
|
||||
|
||||
if thermal_status == ThermalStatus.green:
|
||||
self._temp_status.update(tr("TEMP"), tr("GOOD"), Colors.GOOD)
|
||||
self._temp_status.update(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD)
|
||||
elif thermal_status == ThermalStatus.yellow:
|
||||
self._temp_status.update(tr("TEMP"), tr("OK"), Colors.WARNING)
|
||||
self._temp_status.update(tr_noop("TEMP"), tr_noop("OK"), Colors.WARNING)
|
||||
else:
|
||||
self._temp_status.update(tr("TEMP"), tr("HIGH"), Colors.DANGER)
|
||||
self._temp_status.update(tr_noop("TEMP"), tr_noop("HIGH"), Colors.DANGER)
|
||||
|
||||
def _update_connection_status(self, device_state):
|
||||
last_ping = device_state.lastAthenaPingTime
|
||||
if last_ping == 0:
|
||||
self._connect_status.update(tr("CONNECT"), tr("OFFLINE"), Colors.WARNING)
|
||||
self._connect_status.update(tr_noop("CONNECT"), tr_noop("OFFLINE"), Colors.WARNING)
|
||||
elif time.monotonic_ns() - last_ping < 80_000_000_000: # 80 seconds in nanoseconds
|
||||
self._connect_status.update(tr("CONNECT"), tr("ONLINE"), Colors.GOOD)
|
||||
self._connect_status.update(tr_noop("CONNECT"), tr_noop("ONLINE"), Colors.GOOD)
|
||||
else:
|
||||
self._connect_status.update(tr("CONNECT"), tr("ERROR"), Colors.DANGER)
|
||||
self._connect_status.update(tr_noop("CONNECT"), tr_noop("ERROR"), Colors.DANGER)
|
||||
|
||||
def _update_panda_status(self):
|
||||
if ui_state.panda_type == log.PandaState.PandaType.unknown:
|
||||
self._panda_status.update(tr("NO"), tr("PANDA"), Colors.DANGER)
|
||||
self._panda_status.update(tr_noop("NO"), tr_noop("PANDA"), Colors.DANGER)
|
||||
else:
|
||||
self._panda_status.update(tr("VEHICLE"), tr("ONLINE"), Colors.GOOD)
|
||||
self._panda_status.update(tr_noop("VEHICLE"), tr_noop("ONLINE"), Colors.GOOD)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN):
|
||||
@@ -197,7 +197,7 @@ class Sidebar(Widget):
|
||||
# Network type text
|
||||
text_y = rect.y + 247
|
||||
text_pos = rl.Vector2(rect.x + 58, text_y)
|
||||
rl.draw_text_ex(self._font_regular, self._net_type, text_pos, FONT_SIZE, 0, Colors.WHITE)
|
||||
rl.draw_text_ex(self._font_regular, tr(self._net_type), text_pos, FONT_SIZE, 0, Colors.WHITE)
|
||||
|
||||
def _draw_metrics(self, rect: rl.Rectangle):
|
||||
metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)]
|
||||
@@ -217,7 +217,7 @@ class Sidebar(Widget):
|
||||
rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.3, 10, 2, Colors.METRIC_BORDER)
|
||||
|
||||
# Draw label and value
|
||||
labels = [metric.label, metric.value]
|
||||
labels = [tr(metric.label), tr(metric.value)]
|
||||
text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE * FONT_SCALE)
|
||||
for text in labels:
|
||||
text_size = measure_text_cached(self._font_bold, text, FONT_SIZE)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
from itertools import chain
|
||||
import os
|
||||
from openpilot.system.ui.lib.multilang import SYSTEM_UI_DIR, UI_DIR, TRANSLATIONS_DIR, multilang
|
||||
|
||||
|
||||
def update_translations():
|
||||
files = []
|
||||
for root, _, filenames in chain(os.walk(SYSTEM_UI_DIR),
|
||||
os.walk(os.path.join(UI_DIR, "widgets")),
|
||||
os.walk(os.path.join(UI_DIR, "layouts")),
|
||||
os.walk(os.path.join(UI_DIR, "onroad"))):
|
||||
for filename in filenames:
|
||||
if filename.endswith(".py"):
|
||||
files.append(os.path.join(root, filename))
|
||||
|
||||
# Create main translation file
|
||||
cmd = ("xgettext -L Python --keyword=tr --keyword=trn:1,2 --keyword=tr_noop --from-code=UTF-8 " +
|
||||
"--flag=tr:1:python-brace-format --flag=trn:1:python-brace-format --flag=trn:2:python-brace-format " +
|
||||
"-o translations/app.pot {}").format(" ".join(files))
|
||||
|
||||
ret = os.system(cmd)
|
||||
assert ret == 0
|
||||
|
||||
# Generate/update translation files for each language
|
||||
for name in multilang.languages.values():
|
||||
if os.path.exists(os.path.join(TRANSLATIONS_DIR, f"app_{name}.po")):
|
||||
cmd = f"msgmerge --update --no-fuzzy-matching --backup=none --sort-output translations/app_{name}.po translations/app.pot"
|
||||
ret = os.system(cmd)
|
||||
assert ret == 0
|
||||
else:
|
||||
cmd = f"msginit -l {name} --no-translator --input translations/app.pot --output-file translations/app_{name}.po"
|
||||
ret = os.system(cmd)
|
||||
assert ret == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
update_translations()
|
||||
@@ -15,9 +15,6 @@ from openpilot.system.ui.widgets.html_render import HtmlRenderer
|
||||
from openpilot.selfdrive.selfdrived.alertmanager import OFFROAD_ALERTS
|
||||
|
||||
|
||||
NO_RELEASE_NOTES = tr("<h2>No release notes available.</h2>")
|
||||
|
||||
|
||||
class AlertColors:
|
||||
HIGH_SEVERITY = rl.Color(226, 44, 44, 255)
|
||||
LOW_SEVERITY = rl.Color(41, 41, 41, 255)
|
||||
@@ -56,21 +53,23 @@ class ButtonStyle(IntEnum):
|
||||
|
||||
|
||||
class ActionButton(Widget):
|
||||
def __init__(self, text: str, style: ButtonStyle = ButtonStyle.LIGHT,
|
||||
def __init__(self, text: str | Callable[[], str], style: ButtonStyle = ButtonStyle.LIGHT,
|
||||
min_width: int = AlertConstants.MIN_BUTTON_WIDTH):
|
||||
super().__init__()
|
||||
self._text = text
|
||||
self._style = style
|
||||
self._min_width = min_width
|
||||
self._font = gui_app.font(FontWeight.MEDIUM)
|
||||
self.set_text(text)
|
||||
|
||||
def set_text(self, text: str):
|
||||
self._text = text
|
||||
self._text_size = measure_text_cached(gui_app.font(FontWeight.MEDIUM), self._text, AlertConstants.FONT_SIZE)
|
||||
self._rect.width = max(self._text_size.x + 60 * 2, self._min_width)
|
||||
self._rect.height = AlertConstants.BUTTON_HEIGHT
|
||||
@property
|
||||
def text(self) -> str:
|
||||
return self._text() if callable(self._text) else self._text
|
||||
|
||||
def _render(self, _):
|
||||
text_size = measure_text_cached(gui_app.font(FontWeight.MEDIUM), self.text, AlertConstants.FONT_SIZE)
|
||||
self._rect.width = max(text_size.x + 60 * 2, self._min_width)
|
||||
self._rect.height = AlertConstants.BUTTON_HEIGHT
|
||||
|
||||
roundness = AlertConstants.BORDER_RADIUS / self._rect.height
|
||||
bg_color = AlertColors.BUTTON if self._style == ButtonStyle.LIGHT else AlertColors.SNOOZE_BG
|
||||
if self.is_pressed:
|
||||
@@ -80,9 +79,9 @@ class ActionButton(Widget):
|
||||
|
||||
# center text
|
||||
color = rl.WHITE if self._style == ButtonStyle.DARK else rl.BLACK
|
||||
text_x = int(self._rect.x + (self._rect.width - self._text_size.x) // 2)
|
||||
text_y = int(self._rect.y + (self._rect.height - self._text_size.y) // 2)
|
||||
rl.draw_text_ex(self._font, self._text, rl.Vector2(text_x, text_y), AlertConstants.FONT_SIZE, 0, color)
|
||||
text_x = int(self._rect.x + (self._rect.width - text_size.x) // 2)
|
||||
text_y = int(self._rect.y + (self._rect.height - text_size.y) // 2)
|
||||
rl.draw_text_ex(self._font, self.text, rl.Vector2(text_x, text_y), AlertConstants.FONT_SIZE, 0, color)
|
||||
|
||||
|
||||
class AbstractAlert(Widget, ABC):
|
||||
@@ -102,15 +101,15 @@ class AbstractAlert(Widget, ABC):
|
||||
if self.dismiss_callback:
|
||||
self.dismiss_callback()
|
||||
|
||||
self.dismiss_btn = ActionButton(tr("Close"))
|
||||
self.dismiss_btn = ActionButton(lambda: tr("Close"))
|
||||
|
||||
self.snooze_btn = ActionButton(tr("Snooze Update"), style=ButtonStyle.DARK)
|
||||
self.snooze_btn = ActionButton(lambda: tr("Snooze Update"), style=ButtonStyle.DARK)
|
||||
self.snooze_btn.set_click_callback(snooze_callback)
|
||||
|
||||
self.excessive_actuation_btn = ActionButton(tr("Acknowledge Excessive Actuation"), style=ButtonStyle.DARK, min_width=800)
|
||||
self.excessive_actuation_btn = ActionButton(lambda: tr("Acknowledge Excessive Actuation"), style=ButtonStyle.DARK, min_width=800)
|
||||
self.excessive_actuation_btn.set_click_callback(excessive_actuation_callback)
|
||||
|
||||
self.reboot_btn = ActionButton(tr("Reboot and Update"), min_width=600)
|
||||
self.reboot_btn = ActionButton(lambda: tr("Reboot and Update"), min_width=600)
|
||||
self.reboot_btn.set_click_callback(lambda: HARDWARE.reboot())
|
||||
|
||||
# TODO: just use a Scroller?
|
||||
@@ -318,12 +317,14 @@ class UpdateAlert(AbstractAlert):
|
||||
|
||||
def refresh(self) -> bool:
|
||||
update_available: bool = self.params.get_bool("UpdateAvailable")
|
||||
no_release_notes = "<h2>" + tr("No release notes available.") + "</h2>"
|
||||
|
||||
if update_available:
|
||||
self.release_notes = (self.params.get("UpdaterNewReleaseNotes") or b"").decode("utf8").strip()
|
||||
self._html_renderer.parse_html_content(self.release_notes or NO_RELEASE_NOTES)
|
||||
self._html_renderer.parse_html_content(self.release_notes or no_release_notes)
|
||||
self._cached_content_height = 0
|
||||
else:
|
||||
self._html_renderer.parse_html_content(NO_RELEASE_NOTES)
|
||||
self._html_renderer.parse_html_content(no_release_notes)
|
||||
|
||||
return update_available
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ class SetupWidget(Widget):
|
||||
super().__init__()
|
||||
self._open_settings_callback = None
|
||||
self._pairing_dialog: PairingDialog | None = None
|
||||
self._pair_device_btn = Button(tr("Pair device"), self._show_pairing, button_style=ButtonStyle.PRIMARY)
|
||||
self._open_settings_btn = Button(tr("Open"), lambda: self._open_settings_callback() if self._open_settings_callback else None,
|
||||
self._pair_device_btn = Button(lambda: tr("Pair device"), self._show_pairing, button_style=ButtonStyle.PRIMARY)
|
||||
self._open_settings_btn = Button(lambda: tr("Open"), lambda: self._open_settings_callback() if self._open_settings_callback else None,
|
||||
button_style=ButtonStyle.PRIMARY)
|
||||
self._firehose_label = Label(tr("🔥 Firehose Mode 🔥"), font_weight=FontWeight.MEDIUM, font_size=64)
|
||||
self._firehose_label = Label(lambda: tr("🔥 Firehose Mode 🔥"), font_weight=FontWeight.MEDIUM, font_size=64)
|
||||
|
||||
def set_open_settings_callback(self, callback):
|
||||
self._open_settings_callback = callback
|
||||
|
||||
@@ -7,7 +7,7 @@ from enum import Enum
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
@@ -26,9 +26,9 @@ VALUE_FONT_SIZE = 48
|
||||
|
||||
|
||||
class SshKeyActionState(Enum):
|
||||
LOADING = tr("LOADING")
|
||||
ADD = tr("ADD")
|
||||
REMOVE = tr("REMOVE")
|
||||
LOADING = tr_noop("LOADING")
|
||||
ADD = tr_noop("ADD")
|
||||
REMOVE = tr_noop("REMOVE")
|
||||
|
||||
|
||||
class SshKeyAction(ItemAction):
|
||||
@@ -78,7 +78,7 @@ class SshKeyAction(ItemAction):
|
||||
# Draw button
|
||||
button_rect = rl.Rectangle(rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT)
|
||||
self._button.set_rect(button_rect)
|
||||
self._button.set_text(self._state.value)
|
||||
self._button.set_text(tr(self._state.value))
|
||||
self._button.set_enabled(self._state != SshKeyActionState.LOADING)
|
||||
self._button.render(button_rect)
|
||||
return False
|
||||
@@ -127,5 +127,5 @@ class SshKeyAction(ItemAction):
|
||||
self._state = SshKeyActionState.ADD
|
||||
|
||||
|
||||
def ssh_key_item(title: str, description: str):
|
||||
def ssh_key_item(title: str | Callable[[], str], description: str | Callable[[], str]) -> ListItem:
|
||||
return ListItem(title=title, description=description, action_item=SshKeyAction())
|
||||
|
||||
@@ -14,6 +14,7 @@ from typing import NamedTuple
|
||||
from importlib.resources import as_file, files
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware import HARDWARE, PC, TICI
|
||||
from openpilot.system.ui.lib.multilang import TRANSLATIONS_DIR, multilang
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
|
||||
_DEFAULT_FPS = int(os.getenv("FPS", 20 if TICI else 60))
|
||||
@@ -352,8 +353,17 @@ class GuiApplication:
|
||||
all_chars = set()
|
||||
for layout in KEYBOARD_LAYOUTS.values():
|
||||
all_chars.update(key for row in layout for key in row)
|
||||
all_chars |= set("–‑✓×°§•")
|
||||
|
||||
# Load only the characters used in translations
|
||||
for language in multilang.codes:
|
||||
try:
|
||||
with open(os.path.join(TRANSLATIONS_DIR, f"app_{language}.po")) as f:
|
||||
all_chars |= set(f.read())
|
||||
except FileNotFoundError:
|
||||
cloudlog.warning(f"Translation file for language '{language}' not found when loading fonts.")
|
||||
|
||||
all_chars = "".join(all_chars)
|
||||
all_chars += "–✓×°§•"
|
||||
|
||||
codepoint_count = rl.ffi.new("int *", 1)
|
||||
codepoints = rl.load_codepoints(all_chars, codepoint_count)
|
||||
|
||||
@@ -1,10 +1,76 @@
|
||||
import os
|
||||
import json
|
||||
import gettext
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
|
||||
SYSTEM_UI_DIR = os.path.join(BASEDIR, "system", "ui")
|
||||
UI_DIR = os.path.join(BASEDIR, "selfdrive", "ui")
|
||||
TRANSLATIONS_DIR = os.path.join(UI_DIR, "translations")
|
||||
LANGUAGES_FILE = os.path.join(TRANSLATIONS_DIR, "languages.json")
|
||||
|
||||
tr = gettext.gettext
|
||||
trn = gettext.ngettext
|
||||
SUPPORTED_LANGUAGES = [
|
||||
"en",
|
||||
"de",
|
||||
"fr",
|
||||
"pt-BR",
|
||||
"es",
|
||||
"tr",
|
||||
]
|
||||
|
||||
|
||||
class Multilang:
|
||||
def __init__(self):
|
||||
self._params = Params()
|
||||
self.languages = {}
|
||||
self.codes = {}
|
||||
self._translation: gettext.NullTranslations | gettext.GNUTranslations = gettext.NullTranslations()
|
||||
self._load_languages()
|
||||
|
||||
@property
|
||||
def language(self) -> str:
|
||||
lang = str(self._params.get("LanguageSetting")).strip("main_")
|
||||
if lang not in SUPPORTED_LANGUAGES:
|
||||
lang = "en"
|
||||
return lang
|
||||
|
||||
def setup(self):
|
||||
language = self.language
|
||||
try:
|
||||
with open(os.path.join(TRANSLATIONS_DIR, f'app_{language}.mo'), 'rb') as fh:
|
||||
translation = gettext.GNUTranslations(fh)
|
||||
translation.install()
|
||||
self._translation = translation
|
||||
print(f"Loaded translations for language: {language}")
|
||||
except FileNotFoundError:
|
||||
print(f"No translation file found for language: {language}, using default.")
|
||||
gettext.install('app')
|
||||
self._translation = gettext.NullTranslations()
|
||||
return None
|
||||
|
||||
def change_language(self, language_code: str) -> None:
|
||||
# Reinstall gettext with the selected language
|
||||
self._params.put("LanguageSetting", language_code)
|
||||
self.setup()
|
||||
|
||||
def tr(self, text: str) -> str:
|
||||
return self._translation.gettext(text)
|
||||
|
||||
def trn(self, singular: str, plural: str, n: int) -> str:
|
||||
return self._translation.ngettext(singular, plural, n)
|
||||
|
||||
def _load_languages(self):
|
||||
with open(LANGUAGES_FILE, encoding='utf-8') as f:
|
||||
self.languages = {k: v for k, v in json.load(f).items() if v in SUPPORTED_LANGUAGES}
|
||||
self.codes = {v: k for k, v in self.languages.items() if v in SUPPORTED_LANGUAGES}
|
||||
|
||||
|
||||
multilang = Multilang()
|
||||
multilang.setup()
|
||||
|
||||
tr, trn = multilang.tr, multilang.trn
|
||||
|
||||
|
||||
# no-op marker for static strings translated later
|
||||
def tr_noop(s: str) -> str:
|
||||
return s
|
||||
|
||||
@@ -79,7 +79,7 @@ BUTTON_DISABLED_BACKGROUND_COLORS = {
|
||||
|
||||
class Button(Widget):
|
||||
def __init__(self,
|
||||
text: str,
|
||||
text: str | Callable[[], str],
|
||||
click_callback: Callable[[], None] | None = None,
|
||||
font_size: int = DEFAULT_BUTTON_FONT_SIZE,
|
||||
font_weight: FontWeight = FontWeight.MEDIUM,
|
||||
|
||||
@@ -78,7 +78,7 @@ class Keyboard(Widget):
|
||||
self._backspace_last_repeat: float = 0.0
|
||||
|
||||
self._render_return_status = -1
|
||||
self._cancel_button = Button(tr("Cancel"), self._cancel_button_callback)
|
||||
self._cancel_button = Button(lambda: tr("Cancel"), self._cancel_button_callback)
|
||||
|
||||
self._eye_button = Button("", self._eye_button_callback, button_style=ButtonStyle.TRANSPARENT)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from collections.abc import Callable
|
||||
from itertools import zip_longest
|
||||
from typing import Union
|
||||
import pyray as rl
|
||||
@@ -12,6 +13,13 @@ from openpilot.system.ui.widgets import Widget
|
||||
ICON_PADDING = 15
|
||||
|
||||
|
||||
# TODO: make this common
|
||||
def _resolve_value(value, default=""):
|
||||
if callable(value):
|
||||
return value()
|
||||
return value if value is not None else default
|
||||
|
||||
|
||||
# TODO: This should be a Widget class
|
||||
def gui_label(
|
||||
rect: rl.Rectangle,
|
||||
@@ -90,7 +98,7 @@ def gui_text_box(
|
||||
# Non-interactive text area. Can render emojis and an optional specified icon.
|
||||
class Label(Widget):
|
||||
def __init__(self,
|
||||
text: str,
|
||||
text: str | Callable[[], str],
|
||||
font_size: int = DEFAULT_TEXT_SIZE,
|
||||
font_weight: FontWeight = FontWeight.NORMAL,
|
||||
text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
@@ -130,7 +138,7 @@ class Label(Widget):
|
||||
def _update_text(self, text):
|
||||
self._emojis = []
|
||||
self._text_size = []
|
||||
self._text_wrapped = wrap_text(self._font, text, self._font_size, round(self._rect.width - (self._text_padding * 2)))
|
||||
self._text_wrapped = wrap_text(self._font, _resolve_value(text), self._font_size, round(self._rect.width - (self._text_padding * 2)))
|
||||
for t in self._text_wrapped:
|
||||
self._emojis.append(find_emoji(t))
|
||||
self._text_size.append(measure_text_cached(self._font, t, self._font_size))
|
||||
|
||||
@@ -171,11 +171,9 @@ class TextAction(ItemAction):
|
||||
|
||||
|
||||
class DualButtonAction(ItemAction):
|
||||
def __init__(self, left_text: str, right_text: str, left_callback: Callable = None,
|
||||
def __init__(self, left_text: str | Callable[[], str], right_text: str | Callable[[], str], left_callback: Callable = None,
|
||||
right_callback: Callable = None, enabled: bool | Callable[[], bool] = True):
|
||||
super().__init__(width=0, enabled=enabled) # Width 0 means use full width
|
||||
self.left_text, self.right_text = left_text, right_text
|
||||
|
||||
self.left_button = Button(left_text, click_callback=left_callback, button_style=ButtonStyle.NORMAL, text_padding=0)
|
||||
self.right_button = Button(right_text, click_callback=right_callback, button_style=ButtonStyle.DANGER, text_padding=0)
|
||||
|
||||
@@ -206,7 +204,7 @@ class DualButtonAction(ItemAction):
|
||||
|
||||
|
||||
class MultipleButtonAction(ItemAction):
|
||||
def __init__(self, buttons: list[str], button_width: int, selected_index: int = 0, callback: Callable = None):
|
||||
def __init__(self, buttons: list[str | Callable[[], str]], button_width: int, selected_index: int = 0, callback: Callable = None):
|
||||
super().__init__(width=len(buttons) * button_width + (len(buttons) - 1) * RIGHT_ITEM_PADDING, enabled=True)
|
||||
self.buttons = buttons
|
||||
self.button_width = button_width
|
||||
@@ -225,7 +223,7 @@ class MultipleButtonAction(ItemAction):
|
||||
spacing = RIGHT_ITEM_PADDING
|
||||
button_y = rect.y + (rect.height - BUTTON_HEIGHT) / 2
|
||||
|
||||
for i, text in enumerate(self.buttons):
|
||||
for i, _text in enumerate(self.buttons):
|
||||
button_x = rect.x + i * (self.button_width + spacing)
|
||||
button_rect = rl.Rectangle(button_x, button_y, self.button_width, BUTTON_HEIGHT)
|
||||
|
||||
@@ -249,6 +247,7 @@ class MultipleButtonAction(ItemAction):
|
||||
rl.draw_rectangle_rounded(button_rect, 1.0, 20, bg_color)
|
||||
|
||||
# Draw text
|
||||
text = _resolve_value(_text, "")
|
||||
text_size = measure_text_cached(self._font, text, 40)
|
||||
text_x = button_x + (self.button_width - text_size.x) / 2
|
||||
text_y = button_y + (BUTTON_HEIGHT - text_size.y) / 2
|
||||
@@ -258,7 +257,7 @@ class MultipleButtonAction(ItemAction):
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
spacing = RIGHT_ITEM_PADDING
|
||||
button_y = self._rect.y + (self._rect.height - BUTTON_HEIGHT) / 2
|
||||
for i, _text in enumerate(self.buttons):
|
||||
for i, _ in enumerate(self.buttons):
|
||||
button_x = self._rect.x + i * (self.button_width + spacing)
|
||||
button_rect = rl.Rectangle(button_x, button_y, self.button_width, BUTTON_HEIGHT)
|
||||
if rl.check_collision_point_rec(mouse_pos, button_rect):
|
||||
@@ -268,11 +267,11 @@ class MultipleButtonAction(ItemAction):
|
||||
|
||||
|
||||
class ListItem(Widget):
|
||||
def __init__(self, title: str = "", icon: str | None = None, description: str | Callable[[], str] | None = None,
|
||||
def __init__(self, title: str | Callable[[], str] = "", icon: str | None = None, description: str | Callable[[], str] | None = None,
|
||||
description_visible: bool = False, callback: Callable | None = None,
|
||||
action_item: ItemAction | None = None):
|
||||
super().__init__()
|
||||
self.title = title
|
||||
self._title = title
|
||||
self.set_icon(icon)
|
||||
self._description = description
|
||||
self.description_visible = description_visible
|
||||
@@ -285,7 +284,7 @@ class ListItem(Widget):
|
||||
|
||||
self._html_renderer = HtmlRenderer(text="", text_size={ElementType.P: ITEM_DESC_FONT_SIZE},
|
||||
text_color=ITEM_DESC_TEXT_COLOR)
|
||||
self.set_description(self.description)
|
||||
self._parse_description(self.description)
|
||||
|
||||
# Cached properties for performance
|
||||
self._prev_description: str | None = self.description
|
||||
@@ -332,7 +331,7 @@ class ListItem(Widget):
|
||||
# Detect changes if description is callback
|
||||
new_description = self.description
|
||||
if new_description != self._prev_description:
|
||||
self.set_description(new_description)
|
||||
self._parse_description(new_description)
|
||||
|
||||
def _render(self, _):
|
||||
if not self.is_visible:
|
||||
@@ -385,10 +384,15 @@ class ListItem(Widget):
|
||||
|
||||
def set_description(self, description: str | Callable[[], str] | None):
|
||||
self._description = description
|
||||
new_desc = self.description
|
||||
|
||||
def _parse_description(self, new_desc):
|
||||
self._html_renderer.parse_html_content(new_desc)
|
||||
self._prev_description = new_desc
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return _resolve_value(self._title, "")
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return _resolve_value(self._description, "")
|
||||
@@ -423,35 +427,35 @@ class ListItem(Widget):
|
||||
|
||||
|
||||
# Factory functions
|
||||
def simple_item(title: str, callback: Callable | None = None) -> ListItem:
|
||||
def simple_item(title: str | Callable[[], str], callback: Callable | None = None) -> ListItem:
|
||||
return ListItem(title=title, callback=callback)
|
||||
|
||||
|
||||
def toggle_item(title: str, description: str | Callable[[], str] | None = None, initial_state: bool = False,
|
||||
def toggle_item(title: str | Callable[[], str], description: str | Callable[[], str] | None = None, initial_state: bool = False,
|
||||
callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True) -> ListItem:
|
||||
action = ToggleAction(initial_state=initial_state, enabled=enabled, callback=callback)
|
||||
return ListItem(title=title, description=description, action_item=action, icon=icon)
|
||||
|
||||
|
||||
def button_item(title: str, button_text: str | Callable[[], str], description: str | Callable[[], str] | None = None,
|
||||
def button_item(title: str | Callable[[], str], button_text: str | Callable[[], str], description: str | Callable[[], str] | None = None,
|
||||
callback: Callable | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem:
|
||||
action = ButtonAction(text=button_text, enabled=enabled)
|
||||
return ListItem(title=title, description=description, action_item=action, callback=callback)
|
||||
|
||||
|
||||
def text_item(title: str, value: str | Callable[[], str], description: str | Callable[[], str] | None = None,
|
||||
def text_item(title: str | Callable[[], str], value: str | Callable[[], str], description: str | Callable[[], str] | None = None,
|
||||
callback: Callable | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem:
|
||||
action = TextAction(text=value, color=ITEM_TEXT_VALUE_COLOR, enabled=enabled)
|
||||
return ListItem(title=title, description=description, action_item=action, callback=callback)
|
||||
|
||||
|
||||
def dual_button_item(left_text: str, right_text: str, left_callback: Callable = None, right_callback: Callable = None,
|
||||
def dual_button_item(left_text: str | Callable[[], str], right_text: str | Callable[[], str], left_callback: Callable = None, right_callback: Callable = None,
|
||||
description: str | Callable[[], str] | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem:
|
||||
action = DualButtonAction(left_text, right_text, left_callback, right_callback, enabled)
|
||||
return ListItem(title="", description=description, action_item=action)
|
||||
|
||||
|
||||
def multiple_button_item(title: str, description: str, buttons: list[str], selected_index: int,
|
||||
def multiple_button_item(title: str | Callable[[], str], description: str | Callable[[], str], buttons: list[str | Callable[[], str]], selected_index: int,
|
||||
button_width: int = BUTTON_WIDTH, callback: Callable = None, icon: str = ""):
|
||||
action = MultipleButtonAction(buttons, button_width, selected_index, callback=callback)
|
||||
return ListItem(title=title, description=description, icon=icon, action_item=action)
|
||||
|
||||
@@ -117,40 +117,42 @@ class AdvancedNetworkSettings(Widget):
|
||||
|
||||
# Tethering
|
||||
self._tethering_action = ToggleAction(initial_state=False)
|
||||
tethering_btn = ListItem(title=tr("Enable Tethering"), action_item=self._tethering_action, callback=self._toggle_tethering)
|
||||
tethering_btn = ListItem(lambda: tr("Enable Tethering"), action_item=self._tethering_action, callback=self._toggle_tethering)
|
||||
|
||||
# Edit tethering password
|
||||
self._tethering_password_action = ButtonAction(text=tr("EDIT"))
|
||||
tethering_password_btn = ListItem(title=tr("Tethering Password"), action_item=self._tethering_password_action, callback=self._edit_tethering_password)
|
||||
self._tethering_password_action = ButtonAction(lambda: tr("EDIT"))
|
||||
tethering_password_btn = ListItem(lambda: tr("Tethering Password"), action_item=self._tethering_password_action, callback=self._edit_tethering_password)
|
||||
|
||||
# Roaming toggle
|
||||
roaming_enabled = self._params.get_bool("GsmRoaming")
|
||||
self._roaming_action = ToggleAction(initial_state=roaming_enabled)
|
||||
self._roaming_btn = ListItem(title=tr("Enable Roaming"), action_item=self._roaming_action, callback=self._toggle_roaming)
|
||||
self._roaming_btn = ListItem(lambda: tr("Enable Roaming"), action_item=self._roaming_action, callback=self._toggle_roaming)
|
||||
|
||||
# Cellular metered toggle
|
||||
cellular_metered = self._params.get_bool("GsmMetered")
|
||||
self._cellular_metered_action = ToggleAction(initial_state=cellular_metered)
|
||||
self._cellular_metered_btn = ListItem(title=tr("Cellular Metered"), description=tr("Prevent large data uploads when on a metered cellular connection"),
|
||||
self._cellular_metered_btn = ListItem(lambda: tr("Cellular Metered"),
|
||||
description=lambda: tr("Prevent large data uploads when on a metered cellular connection"),
|
||||
action_item=self._cellular_metered_action, callback=self._toggle_cellular_metered)
|
||||
|
||||
# APN setting
|
||||
self._apn_btn = button_item(tr("APN Setting"), tr("EDIT"), callback=self._edit_apn)
|
||||
self._apn_btn = button_item(lambda: tr("APN Setting"), lambda: tr("EDIT"), callback=self._edit_apn)
|
||||
|
||||
# Wi-Fi metered toggle
|
||||
self._wifi_metered_action = MultipleButtonAction([tr("default"), tr("metered"), tr("unmetered")], 255, 0, callback=self._toggle_wifi_metered)
|
||||
wifi_metered_btn = ListItem(title=tr("Wi-Fi Network Metered"), description=tr("Prevent large data uploads when on a metered Wi-Fi connection"),
|
||||
self._wifi_metered_action = MultipleButtonAction([lambda: tr("default"), lambda: tr("metered"), lambda: tr("unmetered")], 255, 0,
|
||||
callback=self._toggle_wifi_metered)
|
||||
wifi_metered_btn = ListItem(lambda: tr("Wi-Fi Network Metered"), description=lambda: tr("Prevent large data uploads when on a metered Wi-Fi connection"),
|
||||
action_item=self._wifi_metered_action)
|
||||
|
||||
items: list[Widget] = [
|
||||
tethering_btn,
|
||||
tethering_password_btn,
|
||||
text_item(tr("IP Address"), lambda: self._wifi_manager.ipv4_address),
|
||||
text_item(lambda: tr("IP Address"), lambda: self._wifi_manager.ipv4_address),
|
||||
self._roaming_btn,
|
||||
self._apn_btn,
|
||||
self._cellular_metered_btn,
|
||||
wifi_metered_btn,
|
||||
button_item(tr("Hidden Network"), tr("CONNECT"), callback=self._connect_to_hidden_network),
|
||||
button_item(lambda: tr("Hidden Network"), lambda: tr("CONNECT"), callback=self._connect_to_hidden_network),
|
||||
]
|
||||
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
@@ -282,7 +284,6 @@ class WifiManagerUI(Widget):
|
||||
self._networks: list[Network] = []
|
||||
self._networks_buttons: dict[str, Button] = {}
|
||||
self._forget_networks_buttons: dict[str, Button] = {}
|
||||
self._confirm_dialog = ConfirmDialog("", tr("Forget"), tr("Cancel"))
|
||||
|
||||
self._wifi_manager.set_callbacks(need_auth=self._on_need_auth,
|
||||
activated=self._on_activated,
|
||||
@@ -314,9 +315,10 @@ class WifiManagerUI(Widget):
|
||||
self.keyboard.reset(min_text_size=MIN_PASSWORD_LENGTH)
|
||||
gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(Network, self._state_network), result))
|
||||
elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network:
|
||||
self._confirm_dialog.set_text(tr("Forget Wi-Fi Network \"{}\"?").format(self._state_network.ssid))
|
||||
self._confirm_dialog.reset()
|
||||
gui_app.set_modal_overlay(self._confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result))
|
||||
confirm_dialog = ConfirmDialog("", tr("Forget"), tr("Cancel"))
|
||||
confirm_dialog.set_text(tr("Forget Wi-Fi Network \"{}\"?").format(self._state_network.ssid))
|
||||
confirm_dialog.reset()
|
||||
gui_app.set_modal_overlay(confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result))
|
||||
else:
|
||||
self._draw_network_list(rect)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user