diff --git a/EyeTrackApp/algo_settings_widget.py b/EyeTrackApp/algo_settings_widget.py index 973665b..f5ad574 100644 --- a/EyeTrackApp/algo_settings_widget.py +++ b/EyeTrackApp/algo_settings_widget.py @@ -1,6 +1,6 @@ import PySimpleGUI as sg -from config import EyeTrackSettingsConfig +from config import EyeTrackConfig from osc import EyeId from queue import Queue from threading import Event @@ -8,7 +8,7 @@ from threading import Event class AlgoSettingsWidget: def __init__( - self, widget_id: EyeId, main_config: EyeTrackSettingsConfig, osc_queue: Queue + self, widget_id: EyeId, main_config: EyeTrackConfig, osc_queue: Queue ): self.gui_flip_x_axis_left = f"-FLIPXAXISLEFT{widget_id}-" @@ -522,5 +522,4 @@ class AlgoSettingsWidget: if changed: self.main_config.save() - # print(self.main_config) self.osc_queue.put(EyeId.ALGOSETTINGS) diff --git a/EyeTrackApp/config.py b/EyeTrackApp/config.py index 37e4a72..7963cca 100644 --- a/EyeTrackApp/config.py +++ b/EyeTrackApp/config.py @@ -88,8 +88,8 @@ class EyeTrackConfig(BaseModel): right_eye: EyeTrackCameraConfig = EyeTrackCameraConfig() left_eye: EyeTrackCameraConfig = EyeTrackCameraConfig() settings: EyeTrackSettingsConfig = EyeTrackSettingsConfig() - # algo_settings: EyeTrackSettingsConfig = EyeTrackSettingsConfig() eye_display_id: EyeId = EyeId.RIGHT + __listeners = [] @staticmethod def load(): @@ -114,6 +114,13 @@ class EyeTrackConfig(BaseModel): load_config = EyeTrackConfig() return load_config + def update(self, data, save=False): + for field, value in data.items(): + setattr(self.settings, field, value) + self.__notify_listeners() + if save: + self.save() + def save(self): # make sure this is only called if there is a change if os.path.exists(CONFIG_FILE_NAME): @@ -129,3 +136,11 @@ class EyeTrackConfig(BaseModel): with open(CONFIG_FILE_NAME, "w") as settings_file: json.dump(obj=self.dict(), fp=settings_file) print(f"\033[92m[INFO] Config Saved Successfully\033[0m") + + def register_listener_callback(self, callback): + print(f"[DEBUG] Registering listener {callback}") + self.__listeners.append(callback) + + def __notify_listeners(self): + for listener in self.__listeners: + listener() diff --git a/EyeTrackApp/eyetrackapp.py b/EyeTrackApp/eyetrackapp.py index bffe820..c8c96db 100644 --- a/EyeTrackApp/eyetrackapp.py +++ b/EyeTrackApp/eyetrackapp.py @@ -90,7 +90,7 @@ def main(): ] settings = [ - SettingsWidget(EyeId.SETTINGS, config, osc_queue), + SettingsWidget(EyeId.SETTINGS, config), AlgoSettingsWidget(EyeId.ALGOSETTINGS, config, osc_queue), ] @@ -282,6 +282,5 @@ def main(): setting.render(window, event, values) - if __name__ == "__main__": main() diff --git a/EyeTrackApp/general_settings_widget.py b/EyeTrackApp/general_settings_widget.py index e667e0a..4003af6 100644 --- a/EyeTrackApp/general_settings_widget.py +++ b/EyeTrackApp/general_settings_widget.py @@ -1,201 +1,54 @@ import PySimpleGUI as sg -from config import EyeTrackSettingsConfig +from config import EyeTrackConfig, EyeTrackSettingsConfig from osc import EyeId -from queue import Queue from threading import Event -class SettingsWidget: - def __init__(self, widget_id: EyeId, main_config: EyeTrackSettingsConfig, osc_queue: Queue): +from settings.modules.GeneralSettingsModule import GeneralSettingsModule +from settings.modules.OneEuroSettingsModule import OneEuroSettingsModule +from settings.modules.OSCSettingsModule import OSCSettingsModule - self.gui_flip_x_axis_left = f"-FLIPXAXISLEFT{widget_id}-" - self.gui_flip_x_axis_right = f"-FLIPXAXISRIGHT{widget_id}-" - self.gui_flip_y_axis = f"-FLIPYAXIS{widget_id}-" - self.gui_general_settings_layout = f"-GENERALSETTINGSLAYOUT{widget_id}-" - self.gui_osc_address = f"-OSCADDRESS{widget_id}-" - self.gui_osc_port = f"-OSCPORT{widget_id}-" - self.gui_osc_receiver_port = f"OSCRECEIVERPORT{widget_id}-" - self.gui_osc_recenter_address = f"OSCRECENTERADDRESS{widget_id}-" - self.gui_osc_recalibrate_address = f"OSCRECALIBRATEADDRESS{widget_id}-" - self.gui_BLOB = f"-BLOBFALLBACK{widget_id}-" - self.gui_HSF = f"-HSF{widget_id}-" - self.gui_DADDY = f"-DADDY{widget_id}-" - self.gui_DADDYP = f"-DADDYP{widget_id}-" - self.gui_RANSAC3D = f"-RANSAC3D{widget_id}-" - self.gui_BLINK = f"-BLINK{widget_id}-" - self.gui_IBO = f"-IBO{widget_id}-" - self.gui_HSRAC = f"-HSRAC{widget_id}-" - self.gui_HSF_radius = f"-HSFRADIUS{widget_id}-" - self.gui_blob_maxsize = f"-BLOBMAXSIZE{widget_id}-" - self.gui_blob_minsize = f"-BLOBMINSIZE{widget_id}-" - self.gui_speed_coefficient = f"-SPEEDCOEFFICIENT{widget_id}-" - self.gui_min_cutoff = f"-MINCUTOFF{widget_id}-" - self.gui_eye_falloff = f"-EYEFALLOFF{widget_id}-" - self.gui_skip_autoradius = f"-SKIPAUTORADIUS{widget_id}-" - self.gui_HSRACP = f"-HSRACP{widget_id}-" - self.gui_RANSAC3DP = f"-RANSAC3DP{widget_id}-" - self.gui_HSFP = f"-HSFP{widget_id}-" - self.gui_BLOBP = f"-BLOBP{widget_id}-" - self.gui_thresh_add = f"-THRESHADD{widget_id}-" - self.gui_ROSC = f"-ROSC{widget_id}-" - self.gui_vrc_native = f"-VRCNATIVE{widget_id}-" - self.gui_circular_crop_left = f"-CIRCLECROPLEFT{widget_id}-" - self.gui_circular_crop_right = f"-CIRCLECROPRIGHT{widget_id}-" - self.gui_update_check = f"-UPDATECHECK{widget_id}-" - self.gui_threshold_slider = f"-BLOBTHRESHOLD{widget_id}-" + +class SettingsWidget: + def __init__(self, widget_id: EyeId, main_config: EyeTrackConfig): + self.is_saving = False self.main_config = main_config self.config = main_config.settings - self.osc_queue = osc_queue + + settings_modules = [ + GeneralSettingsModule, + OneEuroSettingsModule, + OSCSettingsModule, + ] + + self.initialized_modules = self._initialize_modules( + settings_modules=settings_modules, widget_id=widget_id + ) + self.gui_general_settings_layout = f"-GENERALSETTINGSLAYOUT{widget_id}-" # Define the window's contents - self.general_settings_layout = [ - - [ - sg.Checkbox( - "Flip Left Eye X Axis", - default=self.config.gui_flip_x_axis_left, - key=self.gui_flip_x_axis_left, - background_color='#424042', - tooltip = "Flips the left eye's X axis.", - ), - sg.Checkbox( - "Flip Right Eye X Axis", - default=self.config.gui_flip_x_axis_right, - key=self.gui_flip_x_axis_right, - background_color='#424042', - tooltip = "Flips the right eye's X axis.", - ), + self.general_settings_layout = [] - # ], - #[ - sg.Checkbox( - "Flip Y Axis", - default=self.config.gui_flip_y_axis, - key=self.gui_flip_y_axis, - background_color='#424042', - tooltip = "Flips the eye's Y axis.", - ), - ], - [sg.Checkbox( - "VRC Native Eyetracking", - default=self.config.gui_vrc_native, - key=self.gui_vrc_native, - background_color='#424042', - tooltip = "Toggle VRCFT output or VRC native", - ), - sg.Checkbox( - "Dual Eye Falloff", - default=self.config.gui_eye_falloff, - key=self.gui_eye_falloff, - background_color='#424042', - tooltip = "If one eye stops tracking, we send tracking data from your other eye.", - ), - ], - [sg.Checkbox( - "Check For Updates", - default=self.config.gui_update_check, - key=self.gui_update_check, - background_color='#424042', - tooltip = "Toggle update check on launch.", - ), - ], - [ - sg.Text("One Euro Filter Paramaters:", background_color='#242224'), - ], - [ - - sg.Text("Min Frequency Cutoff", background_color='#424042'), - sg.InputText( - self.config.gui_min_cutoff, - key=self.gui_min_cutoff, - size=(0,10), - ), - #], - #[ - sg.Text("Speed Coefficient", background_color='#424042'), - sg.InputText( - self.config.gui_speed_coefficient, - key=self.gui_speed_coefficient, - size=(0,10), - ), - ], - [ - sg.Text("OSC Settings:", background_color='#242224'), - ], - [ - sg.Text("Address:", background_color='#424042'), - sg.InputText( - self.config.gui_osc_address, - key=self.gui_osc_address, - size=(0,20), - tooltip = "IP address we send OSC data to.", - ), - - # ], - # [ - sg.Text("Port:", background_color='#424042'), - sg.InputText( - self.config.gui_osc_port, - key=self.gui_osc_port, - size=(0,10), - tooltip = "OSC port we send data to.", - ), - ], - [ - sg.Text("Receive functions", background_color='#424042'), - sg.Checkbox( - "", - default=self.config.gui_ROSC, - key=self.gui_ROSC, - background_color='#424042', - size=(0,10), - tooltip = "Toggle OSC receive functions.", - ), - ], - [ - sg.Text("Receiver Port:", background_color='#424042'), - sg.InputText( - self.config.gui_osc_receiver_port, - key=self.gui_osc_receiver_port, - size=(0,10), - tooltip = "Port we receive OSC data from (used to recalibrate or recenter app from within VRChat.", - ), - #], - # [ - sg.Text("Recenter Address:", background_color='#424042'), - sg.InputText( - self.config.gui_osc_recenter_address, - key=self.gui_osc_recenter_address, - size=(0,10), - tooltip = "OSC Address used for recentering your eye.", - ), - ], - [ - sg.Text("Recalibrate Address:", background_color='#424042'), - sg.InputText( - self.config.gui_osc_recalibrate_address, - key=self.gui_osc_recalibrate_address, - size=(0,10), - tooltip = "OSC address we use for recalibrating your eye", - ), - ] + for module in self.initialized_modules: + self.general_settings_layout.extend(module.get_layout()) - ] - - self.widget_layout = [ - [ - sg.Text("General Settings:", background_color='#242224'), + [ + sg.Text("General Settings:", background_color="#242224"), ], [ - sg.Column(self.general_settings_layout, key=self.gui_general_settings_layout, background_color='#424042' ), + sg.Column( + self.general_settings_layout, + key=self.gui_general_settings_layout, + background_color="#424042", + ), ], ] - self.cancellation_event = Event() # Set the event until start is called, otherwise we can block if shutdown is called. + self.cancellation_event = ( + Event() + ) # Set the event until start is called, otherwise we can block if shutdown is called. self.cancellation_event.set() - self.image_queue = Queue() - def started(self): return not self.cancellation_event.is_set() @@ -212,81 +65,38 @@ class SettingsWidget: return self.cancellation_event.set() + def _update_and_save_config(self, validated_data: dict): + self.main_config.update(validated_data, save=True) + self.is_saving = False + + def _handle_errors(self, errors): + print(errors) + def render(self, window, event, values): - # If anything has changed in our configuration settings, change/update those. - changed = False + validated_data, errors = {}, [] + # we might want to think about event driven architecture here eventually, validate only + # if anything changes instead of checking for changes + for module in self.initialized_modules: + module_validated_data = module.validate(values) + if module_validated_data.changes: + validated_data.update(module_validated_data.changes) + if module_validated_data.errors: + errors.append(module_validated_data.errors) - if self.config.gui_osc_port != int(values[self.gui_osc_port]): - print(self.config.gui_osc_port, values[self.gui_osc_port]) - try: - int(values[self.gui_osc_port]) - if len(values[self.gui_osc_port]) <= 5: - self.config.gui_osc_port = int(values[self.gui_osc_port]) - changed = True - else: - print("\033[91m[ERROR] OSC port value must be an integer 0-65535\033[0m") - except: - print("\033[91m[ERROR] OSC port value must be an integer 0-65535\033[0m") + if not errors and validated_data and not self.is_saving: + self.is_saving = True + self._update_and_save_config(validated_data) - if self.config.gui_osc_receiver_port != int(values[self.gui_osc_receiver_port]): - try: - int(values[self.gui_osc_receiver_port]) - if len(values[self.gui_osc_receiver_port]) <= 5: - self.config.gui_osc_receiver_port = int(values[self.gui_osc_receiver_port]) - changed = True - else: - print("\033[91m[ERROR] OSC receive port value must be an integer 0-65535\033[0m") - except: - print("\033[91m[ERROR] OSC receive port value must be an integer 0-65535\033[0m") + if errors: + self._handle_errors(errors) - if self.config.gui_osc_address != values[self.gui_osc_address]: - self.config.gui_osc_address = values[self.gui_osc_address] - changed = True - - if self.config.gui_osc_recenter_address != values[self.gui_osc_recenter_address]: - self.config.gui_osc_recenter_address = values[self.gui_osc_recenter_address] - changed = True - - if self.config.gui_osc_recalibrate_address != values[self.gui_osc_recalibrate_address]: - self.config.gui_osc_recalibrate_address = values[self.gui_osc_recalibrate_address] - changed = True - - if self.config.gui_min_cutoff != values[self.gui_min_cutoff]: - self.config.gui_min_cutoff = values[self.gui_min_cutoff] - changed = True - - if self.config.gui_speed_coefficient != values[self.gui_speed_coefficient]: - self.config.gui_speed_coefficient = values[self.gui_speed_coefficient] - changed = True - - if self.config.gui_flip_x_axis_right != values[self.gui_flip_x_axis_right]: - self.config.gui_flip_x_axis_right = values[self.gui_flip_x_axis_right] - changed = True - - if self.config.gui_flip_x_axis_left != values[self.gui_flip_x_axis_left]: - self.config.gui_flip_x_axis_left = values[self.gui_flip_x_axis_left] - changed = True - - if self.config.gui_vrc_native != values[self.gui_vrc_native]: - self.config.gui_vrc_native = values[self.gui_vrc_native] - changed = True - - if self.config.gui_update_check != values[self.gui_update_check]: - self.config.gui_update_check = values[self.gui_update_check] - changed = True - - if self.config.gui_flip_y_axis != values[self.gui_flip_y_axis]: - self.config.gui_flip_y_axis = values[self.gui_flip_y_axis] - changed = True - - if self.config.gui_eye_falloff != values[self.gui_eye_falloff]: - self.config.gui_eye_falloff = values[self.gui_eye_falloff] - changed = True - - if self.config.gui_ROSC != values[self.gui_ROSC]: - self.config.gui_ROSC = values[self.gui_ROSC] - changed = True - - if changed: - self.main_config.save() - self.osc_queue.put((EyeId.SETTINGS)) + def _initialize_modules(self, settings_modules, widget_id): + return [ + module( + config=self.config, + settings=self.main_config, + settings_base_class=EyeTrackSettingsConfig, + widget_id=widget_id, + ) + for module in settings_modules + ] diff --git a/EyeTrackApp/settings/constants.py b/EyeTrackApp/settings/constants.py new file mode 100644 index 0000000..a6882b5 --- /dev/null +++ b/EyeTrackApp/settings/constants.py @@ -0,0 +1 @@ +BACKGROUND_COLOR = "#424042" \ No newline at end of file diff --git a/EyeTrackApp/settings/modules/BaseModule.py b/EyeTrackApp/settings/modules/BaseModule.py new file mode 100644 index 0000000..b796aab --- /dev/null +++ b/EyeTrackApp/settings/modules/BaseModule.py @@ -0,0 +1,73 @@ +from typing import Iterable, Optional, NamedTuple, Any + +import pydantic +from pydantic import BaseModel + +from config import EyeTrackSettingsConfig + + +class MissingValidationModelException(Exception): + pass + + +class MissingValidationConfigException(Exception): + pass + + +class BaseValidationModel(BaseModel): + pass + + +class ValidationResult(NamedTuple): + changes: Optional[dict[str, str]] + errors: list[Any] # ErrorDict but we cannot import it, it's not exposed + + +class BaseSettingsModule: + def __init__(self, config: EyeTrackSettingsConfig, settings_base_class, widget_id, **kwargs): + self.validation_model: BaseValidationModel = BaseValidationModel # noqa + self.config = config + self.settings_base_class = settings_base_class + self.widget_id = widget_id + + def get_validation_model(self): + """Return validation model, can be overridden for custom behaviour""" + return self.validation_model + + def initialize_validation_model(self, values): + validation_model = self.get_validation_model() + if not validation_model: + raise MissingValidationModelException() + + field_mapping = {} + for field in self.validation_model.schema().get("properties"): + field_mapping[field] = values[getattr(self, field)] + + return validation_model(**field_mapping) + + def validate(self, values, raise_exception=False) -> Optional[ValidationResult]: + """Return """ + if not self.config: + raise MissingValidationConfigException() + try: + changes = {} + validated_model = self.initialize_validation_model(values) + # this is empty + for field, value in validated_model.dict().items(): + if getattr(self.config, field) != value: + changes[field] = value + + return ValidationResult(changes, []) + except pydantic.ValidationError as e: + if not raise_exception: + return ValidationResult(None, e.errors()) + raise + + def get_layout(self) -> Iterable: + raise NotImplementedError + + def get_defaults(self) -> dict: + defaults = {} + for field in self.validation_model.schema().keys(): + defaults[field] = getattr(self.settings_base_class, field) + return defaults diff --git a/EyeTrackApp/settings/modules/GeneralSettingsModule.py b/EyeTrackApp/settings/modules/GeneralSettingsModule.py new file mode 100644 index 0000000..7968299 --- /dev/null +++ b/EyeTrackApp/settings/modules/GeneralSettingsModule.py @@ -0,0 +1,76 @@ +from config import EyeTrackSettingsConfig +from settings.modules.BaseModule import BaseSettingsModule, BaseValidationModel +import PySimpleGUI as sg + + +class GeneralSettingsValidationModel(BaseValidationModel): + gui_flip_x_axis_left: bool + gui_flip_x_axis_right: bool + gui_flip_y_axis: bool + gui_vrc_native: bool + gui_eye_falloff: bool + gui_update_check: bool + + +class GeneralSettingsModule(BaseSettingsModule): + def __init__(self, config, widget_id, settings_base_class, **kwargs): + super().__init__(config, widget_id, settings_base_class, **kwargs) + self.validation_model = GeneralSettingsValidationModel + self.gui_flip_x_axis_left = f"-FLIPXAXISLEFT{widget_id}-" + self.gui_flip_x_axis_right = f"-FLIPXAXISRIGHT{widget_id}-" + self.gui_flip_y_axis = f"-FLIPYAXIS{widget_id}-" + self.gui_eye_falloff = f"-EYEFALLOFF{widget_id}-" + self.gui_vrc_native = f"-VRCNATIVE{widget_id}-" + self.gui_update_check = f"-UPDATECHECK{widget_id}-" + + def get_layout(self): + return [ + [ + sg.Checkbox( + "Flip Left Eye X Axis", + default=self.config.gui_flip_x_axis_left, + key=self.gui_flip_x_axis_left, + background_color="#424042", + tooltip="Flips the left eye's X axis.", + ), + sg.Checkbox( + "Flip Right Eye X Axis", + default=self.config.gui_flip_x_axis_right, + key=self.gui_flip_x_axis_right, + background_color="#424042", + tooltip="Flips the right eye's X axis.", + ), + sg.Checkbox( + "Flip Y Axis", + default=self.config.gui_flip_y_axis, + key=self.gui_flip_y_axis, + background_color="#424042", + tooltip="Flips the eye's Y axis.", + ), + ], + [ + sg.Checkbox( + "VRC Native Eyetracking", + default=self.config.gui_vrc_native, + key=self.gui_vrc_native, + background_color="#424042", + tooltip="Toggle VRCFT output or VRC native", + ), + sg.Checkbox( + "Dual Eye Falloff", + default=self.config.gui_eye_falloff, + key=self.gui_eye_falloff, + background_color="#424042", + tooltip="If one eye stops tracking, we send tracking data from your other eye.", + ), + ], + [ + sg.Checkbox( + "Check For Updates", + default=self.config.gui_update_check, + key=self.gui_update_check, + background_color="#424042", + tooltip="Toggle update check on launch.", + ), + ], + ] \ No newline at end of file diff --git a/EyeTrackApp/settings/modules/OSCSettingsModule.py b/EyeTrackApp/settings/modules/OSCSettingsModule.py new file mode 100644 index 0000000..c50c16b --- /dev/null +++ b/EyeTrackApp/settings/modules/OSCSettingsModule.py @@ -0,0 +1,83 @@ +from settings.modules.BaseModule import BaseSettingsModule, BaseValidationModel +from settings.constants import BACKGROUND_COLOR +import PySimpleGUI as sg + + +class OSCValidationModel(BaseValidationModel): + gui_osc_port: int + gui_osc_address: str + gui_ROSC: bool + gui_osc_receiver_port: int + gui_osc_recenter_address: str + gui_osc_recalibrate_address: str + + +class OSCSettingsModule(BaseSettingsModule): + def __init__(self, config, widget_id, settings_base_class, **kwargs): + super().__init__(config, widget_id, settings_base_class, **kwargs) + self.validation_model = OSCValidationModel + self.gui_osc_address = f"-OSCADDRESS{widget_id}-" + self.gui_osc_port = f"-OSCPORT{widget_id}-" + self.gui_ROSC = f"-ROSC{widget_id}-" + self.gui_osc_receiver_port = f"OSCRECEIVERPORT{widget_id}-" + self.gui_osc_recenter_address = f"OSCRECENTERADDRESS{widget_id}-" + self.gui_osc_recalibrate_address = f"OSCRECALIBRATEADDRESS{widget_id}-" + + def get_layout(self): + return [ + [ + sg.Text("OSC Settings:", background_color="#242224"), + ], + [ + sg.Text("Address:", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_osc_address, + key=self.gui_osc_address, + size=(0, 20), + tooltip="IP address we send OSC data to.", + ), + sg.Text("Port:", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_osc_port, + key=self.gui_osc_port, + size=(0, 10), + tooltip="OSC port we send data to.", + ), + ], + [ + sg.Text("Receive functions", background_color=BACKGROUND_COLOR), + sg.Checkbox( + "", + default=self.config.gui_ROSC, + key=self.gui_ROSC, + background_color=BACKGROUND_COLOR, + size=(0, 10), + tooltip="Toggle OSC receive functions.", + ), + ], + [ + sg.Text("Receiver Port:", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_osc_receiver_port, + key=self.gui_osc_receiver_port, + size=(0, 10), + tooltip="Port we receive OSC data from (used to recalibrate or recenter app from within VRChat.", + ), + sg.Text("Recenter Address:", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_osc_recenter_address, + key=self.gui_osc_recenter_address, + size=(0, 10), + tooltip="OSC Address used for recentering your eye.", + ), + ], + [ + sg.Text("Recalibrate Address:", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_osc_recalibrate_address, + key=self.gui_osc_recalibrate_address, + size=(0, 10), + tooltip="OSC address we use for recalibrating your eye", + ), + ], + ] diff --git a/EyeTrackApp/settings/modules/OneEuroSettingsModule.py b/EyeTrackApp/settings/modules/OneEuroSettingsModule.py new file mode 100644 index 0000000..b20f39c --- /dev/null +++ b/EyeTrackApp/settings/modules/OneEuroSettingsModule.py @@ -0,0 +1,37 @@ +from settings.modules.BaseModule import BaseSettingsModule, BaseValidationModel +from settings.constants import BACKGROUND_COLOR +import PySimpleGUI as sg + + +class OneEuroFilterValidationModel(BaseValidationModel): + gui_speed_coefficient: str # GUI lib does not support doubles ;-; + gui_min_cutoff: str # or floats ;-; + + +class OneEuroSettingsModule(BaseSettingsModule): + def __init__(self, config, widget_id, settings_base_class, **kwargs): + super().__init__(config, widget_id, settings_base_class, **kwargs) + self.gui_speed_coefficient = f"-SPEEDCOEFFICIENT{widget_id}-" + self.gui_min_cutoff = f"-MINCUTOFF{widget_id}-" + self.validation_model = OneEuroFilterValidationModel + + def get_layout(self): + return [ + [ + sg.Text("One Euro Filter Paramaters:", background_color='#242224'), + ], + [ + sg.Text("Min Frequency Cutoff", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_min_cutoff, + key=self.gui_min_cutoff, + size=(0, 10), + ), + sg.Text("Speed Coefficient", background_color=BACKGROUND_COLOR), + sg.InputText( + self.config.gui_speed_coefficient, + key=self.gui_speed_coefficient, + size=(0, 10), + ), + ], + ] \ No newline at end of file