44from app_config import AppConfig , PatternConfig
55from app_pattern import VibrationPattern
66
7- WINDOW_NAME = "Haptic Pancake Bridge v0.8.0-beta.2 "
7+ WINDOW_NAME = "Haptic Pancake Bridge v0.8.0-beta.3 "
88WINDOW_ICON = b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpVIrDhYVcchQnezgB+JYqlgEC6Wt0KqDyaVf0KQhSXFxFFwLDn4sVh1cnHV1cBUEwQ8QZwcnRRcp8X9NoUWMB8f9eHfvcfcOEOplpppdEUDVLCMZi4qZ7Kroe0UvBjEEPyYlZurx1GIaruPrHh6+3oV5lvu5P0efkjMZ4BGJI0w3LOIN4tlNS+e8TxxkRUkhPieeMOiCxI9clx1+41xossAzg0Y6OU8cJBYLHSx3MCsaKvEMcUhRNcoXMg4rnLc4q+Uqa92TvzCQ01ZSXKc5ihiWEEcCImRUUUIZFsK0aqSYSNJ+1MU/0vQnyCWTqwRGjgVUoEJq+sH/4He3Zn56ykkKRIHuF9v+GAN8u0CjZtvfx7bdOAG8z8CV1vZX6sDcJ+m1thY6Avq3gYvrtibvAZc7wPCTLhlSU/LSFPJ54P2MvikLDNwC/jWnt9Y+Th+ANHW1fAMcHALjBcped3l3T2dv/55p9fcD3S9y0apk9h0AAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfoCxYXCzDoJVaPAAACuElEQVQ4y2WTTW8bdRDGfzO767f1xnGcJiSNVNoKqMoJgRBCwifuIHFFuSDRTwDi2CNfgC/gGxfElV6ockEISAJBiAJ5KVnqxk7idbx+ie39DwcngaqH0Wj0HJ756ZmRjz7940Ym0nCe1DMVnArZRbn/d+85bcMJ6744GqrUzYFimIAamMF5P8GCAC1GqAMDlFk3qItKQ9WsrmaoGd32LjYeMeg0edj4mOP9Hzj4/ku2v77PJO3MtPZj1GYmYlb31c1ch2ct5uZW6XZikpN9Rr02v377BenRNsuvvkfa2sUP5wlKFbJhDy1FmAm+GpiD6XkfRFGDfD7infc/p9XcobTwGbmoRpq2sKBI8VqNXjemUCijAr6YXa2kCL74RHOrEORYufkW5vk4haJlOAQFyDLUAAeqzhBn5IOQwMsxGfUo5MqMBwm++IjLmPQTovk1PFFGyVPCygpqhphdIBiUS4t0zg5ZWrqL84RcWL2KraTgVPEWb5KmLQIvT+bsWYThIKF1+BO9XpPDg+9YufU2raPfqCy/zOnJHvlyFcmVyMhYq65h/yGAOGA64cmjBxzHW/Tbf5Ec/c5Z6xFh+RphtESvfUAn3mFh+Q5yEbuYoZc3oAbVxZc4T2KiuRcYnP7N3dc/JCzVZjpGLiiyv/kVl6bqDPnk3o51ksc0n25SrFwn85TxdIRXiBi7Mc5Tpm5CYX6F5uGP5Mo1BoMON974AK8YzRCycUpn9yHd5i9M0xO6/2xzGm9ynsSEpRrZ8Ixee4+9b+5TrqzSP96buV8ieA5eefMeMh7Re/IzleqLyGSEOkcn3iKJt+i3/qR2612GpzGlcBFPfdSBL8bG/MLtulMhfO06mQoWBLMIRXCesnZn+sxnLtkUTzww2/AxWw+8fMOJ1NUv4Lzn39lXIVOu5kAFJ7rhnK3/C07bcJ2GHOyzAAAAAElFTkSuQmCC'
99
1010# If changing order, also change update_oscquery_state()
6262KEY_BTN_TEST = '-BTN-TEST-'
6363KEY_BTN_CALIBRATE = '-BTN-CALIBRATE-'
6464KEY_BTN_ADD_EXTERNAL = '-BTN-ADD-EXTERNAL-'
65+ KEY_BTN_DEBUG = '-BTN-DEBUG-'
6566KEY_BATTERY_THRESHOLD = '-BATTERY-'
6667KEY_START_WITH_STEAMVR = '-START-WITH-STEAMVR-'
6768KEY_AUTOSTART_STATUS_BAR = '-AUTOSTART-STATUS-BAR-'
8788# Resaving config
8889KEY_SAVE_TO_CONFIG = '-FAUX-SAVE-EVENT-'
8990
91+ # Debug window
92+ KEY_DEBUG_BTN_CLEAR = '-DEBUG-BTN-CLEAR-'
93+ KEY_DEBUG_BTN_COPY = '-DEBUG-BTN-COPY-'
94+ KEY_DEBUG_BTN_REFRESH = '-DEBUG-BTN-REFRESH-'
95+ KEY_DEBUG_CHK_AUTOREFRESH = '-DEBUG-CHK-AUTOREFRESH-'
96+ KEY_DEBUG_INPUT_FILTER = '-DEBUG-INPUT-FILTER-'
97+ KEY_DEBUG_TBL_ADDRESS = '-DEBUG-TBL-ADDRESS-'
98+ KEY_DEBUG_TIMER_REFRESH = '-DEBUG-TIMER-REFRESH-'
99+ DEBUG_TIMER_REFRESH_MS = 0.5 * 1000
100+
90101# If changing this, also update app_config.py!
91102DEFAULT_THEME = "DarkAmber"
92103
@@ -111,6 +122,7 @@ def __init__(self, app_config: AppConfig, tracker_test_event,
111122 self .config = app_config
112123 self .shutting_down = False
113124 self .window = None
125+ self .debug_popup = None
114126 self .layout_dirty = False
115127 self .trackers = []
116128
@@ -143,6 +155,8 @@ def build_layout(self):
143155 # Ideally these would be updated to maintain contrast
144156 self .theme_color_bad = "red"
145157 self .theme_color_good = "lime"
158+ # Help with alignment
159+ small_button_size = 6
146160
147161 self .layout = []
148162
@@ -173,7 +187,7 @@ def build_layout(self):
173187 # Replace with a simple Text string instead
174188 self .server_port_input = sg .InputText (self .config .server_port , key = KEY_REC_PORT , size = 13 )
175189 self .server_port_auto = sg .Text ("[automatic]" , size = 13 , visible = False )
176- self .server_apply_btn = sg .Button ("Apply" , key = KEY_BTN_APPLY , tooltip = "Apply and restart server." , disabled = self .cache_server_apply_disabled )
190+ self .server_apply_btn = sg .Button ("Apply" , key = KEY_BTN_APPLY , size = small_button_size , tooltip = "Apply and restart server." , disabled = self .cache_server_apply_disabled )
177191
178192 self .no_data_timeout = sg .Spin (
179193 [num for num in range (1 , 601 )], self .config .no_data_timeout , size = 4 , pad = ((0 ,3 ),(0 ,0 )),
@@ -185,8 +199,10 @@ def build_layout(self):
185199 [sg .Checkbox ("Start minimized" , default = self .config .start_minimized , key = KEY_START_MINIMIZED , enable_events = True )],
186200 [sg .Text ("Theme:" , justification = 'right' , size = 7 ),
187201 sg .InputCombo (LIST_THEME , self .config .theme , key = KEY_THEME , readonly = True , enable_events = True , tooltip = "Change app theme.\n CAUTION: Some themes are hard to read. Be ready to reset." ),
188- sg .Button ("Reset" , key = KEY_BTN_THEME_RESET , tooltip = f"Reset theme to default ({ DEFAULT_THEME } )" )],
189- [sg .Text ('Server settings:' , font = '_ 14' )],
202+ sg .Button ("Reset" , key = KEY_BTN_THEME_RESET , size = small_button_size , tooltip = f"Reset theme to default ({ DEFAULT_THEME } )" )],
203+ [sg .Text ('Server settings:' , font = '_ 14' ),
204+ sg .Push (),
205+ sg .Button ("Debug" , key = KEY_BTN_DEBUG , size = small_button_size )],
190206 [sg .Text ("Type:" , justification = 'right' , size = 7 ),
191207 sg .InputCombo (LIST_SERVER_TYPE , LIST_SERVER_TYPE [self .config .server_type ], key = KEY_SERVER_TYPE , readonly = True , enable_events = True ),
192208 sg .pin (self .server_oscquery_chkbox )],
@@ -205,7 +221,10 @@ def build_layout(self):
205221 sg .Text ("(seconds)" )],
206222 # Padding from "Stop stuck haptics" checkbox removes the need for this:
207223 #[self.small_vertical_space()],
208- [sg .Text ('Devices:' , font = '_ 14' ), self .tracker_status_bar , sg .Push (), sg .Button ("Refresh" , key = KEY_BTN_REFRESH )],
224+ [sg .Text ('Devices:' , font = '_ 14' ),
225+ self .tracker_status_bar ,
226+ sg .Push (),
227+ sg .Button ("Refresh" , key = KEY_BTN_REFRESH , size = small_button_size )],
209228 # add_external_button],
210229 [self .tracker_frame ],
211230 [sg .HSep ()],
@@ -517,6 +536,8 @@ def run(self):
517536 self .setup_tracker_preset (event [1 ])
518537 elif event == KEY_BTN_ADD_EXTERNAL :
519538 self .add_external_event (values [KEY_BTN_ADD_EXTERNAL ])
539+ elif event == KEY_BTN_DEBUG :
540+ self .popup_debug_params ()
520541 elif event == KEY_BTN_APPLY :
521542 self .restart_osc_event ()
522543 elif event == KEY_BTN_THEME_RESET :
@@ -607,6 +628,22 @@ def popup_pick_address_preset(self, tracker_serial):
607628 window .close ()
608629 return None
609630
631+ def popup_debug_params (self ):
632+ """GUI to pick a tracker location preset"""
633+ if self .debug_popup :
634+ # Don't try to open twice
635+ return
636+
637+ self .debug_popup = DebugAddressPopup ()
638+ # Separate object creation from event loop so object gets assigned
639+ # This is needed to pass parameters along
640+ self .debug_popup .show_popup ()
641+ self .debug_popup = None
642+
643+ def debug_params_received (self , address , value ):
644+ if self .debug_popup :
645+ self .debug_popup .params_received (address , value )
646+
610647 def setup_tracker_preset (self , tracker_serial ):
611648 # Find value if possible
612649 address_widget = (KEY_OSC_ADDRESS , tracker_serial )
@@ -694,3 +731,164 @@ def update_pattern_config(self, values, index: int, key: str):
694731 except ValueError :
695732 pass
696733 self .config .pattern_config_list [index ].speed = int (values [key + KEY_VIB_SPEED ])
734+
735+ class DebugAddressPopup :
736+ def __init__ (self ):
737+ """GUI to show parameter addresses and values"""
738+ button_size = 6
739+ self .timer_active = False
740+ self .param_list = {}
741+ self .debug_table = sg .Table ([], headings = ["Address" , "Value" ], justification = 'left' , key = KEY_DEBUG_TBL_ADDRESS , enable_events = True , expand_x = True , expand_y = True , col_widths = [25 , 5 ], auto_size_columns = False , select_mode = sg .TABLE_SELECT_MODE_BROWSE )
742+ # Set a specific column width to show more of address
743+
744+ self .debug_autorefresh_chkbox = sg .Checkbox ("Auto-refresh" ,
745+ default = True , key = KEY_DEBUG_CHK_AUTOREFRESH , enable_events = True ,
746+ tooltip = "Automatically refresh list of addresses and values." , pad = 0 )
747+ self .debug_filter_input = sg .InputText ('haptic' , key = KEY_DEBUG_INPUT_FILTER , size = 25 , expand_x = True )
748+
749+ self .debug_stats = sg .Text ('Refresh to see addresses' )
750+
751+ self .debug_copy_button = sg .Button ("Copy Row" , key = KEY_DEBUG_BTN_COPY , tooltip = "Copy selected Address and Value to clipboard." , disabled = True )
752+
753+ self .debug_layout = [
754+ [sg .Text ("Filter:" ),
755+ self .debug_filter_input ,
756+ sg .Button ("Refresh" , key = KEY_DEBUG_BTN_REFRESH , size = button_size , tooltip = "Refresh list of addresses and values." )],
757+ [self .debug_autorefresh_chkbox ,
758+ sg .Push (),
759+ self .debug_copy_button ,
760+ sg .Button ("Clear" , key = KEY_DEBUG_BTN_CLEAR , size = button_size , tooltip = "Clear list of known addresses." )],
761+ [self .debug_table ],
762+ [sg .HSep ()],
763+ [self .debug_stats ,
764+ sg .Push (),
765+ sg .Sizegrip ()],
766+ ]
767+
768+ @property
769+ def autorefresh_active (self ):
770+ return self .debug_autorefresh_chkbox .get ()
771+
772+ def clear (self ):
773+ self .param_list = {}
774+ # Preemptively clear selection too
775+ self .debug_table .update (select_rows = [])
776+ self .refresh ()
777+
778+ def copy_row (self ):
779+ selected_rows = self .debug_table .get ()
780+ if not len (selected_rows ):
781+ return
782+
783+ # Get selected row text (assume first row if multiple are selected)
784+ selected_row = self .debug_table .Values [selected_rows [0 ]]
785+ print (f"[DebugAddress] Copying row to clipboard: { selected_row } " )
786+ # NOTE: On Linux, Tkinter requires the app to stay running to keep text
787+ # Modern desktop environments resolve this with clipboard managers.
788+ sg .clipboard_set (selected_row )
789+
790+ def params_received (self , address , value ):
791+ self .param_list [address ] = value
792+
793+ def refresh (self ):
794+ ## Intentionally out of order to test sorting
795+ #self.param_list["/avatar/parameter/TestIndex"] = 3
796+ #self.param_list["/avatar/parameter/HapticChest"] = 0.3
797+ #self.param_list["/avatar/parameter/HapticHips"] = 0.3567465434
798+ #self.param_list["/avatar/parameter/WillGiveYouUp"] = False
799+ #self.param_list["/avatar/parameter/WillLetYouDown"] = False
800+ #self.param_list["/avatar/parameter/WillRunAroundAndDesertYou"] = False
801+ #self.param_list["/avatar/parameter/PineappleOnPizza"] = True
802+
803+ filter_text = self .debug_filter_input .get ().casefold ()
804+ # Find addresses by matching substring
805+ param_list_filtered = [(k , v ) for k , v in self .param_list .items () if filter_text in k .casefold ()]
806+ # A-Z
807+ param_list_filtered .sort ()
808+
809+ # Get selected rows
810+ selected_rows = self .debug_table .get ()
811+ selected_addr_text = None
812+ if len (selected_rows ):
813+ # Check first selected row
814+ selected_addr_text = self .debug_table .Values [selected_rows [0 ]][0 ]
815+
816+ # Update
817+ self .debug_table .update (param_list_filtered )
818+ self .debug_stats .update (f"{ len (param_list_filtered )} matching addresses, { len (self .param_list )} total" )
819+
820+ # Restore selected rows
821+ if selected_addr_text :
822+ # Find new index
823+ try :
824+ # Find index of first matching address
825+ new_row_index = [row [0 ] for row in self .debug_table .Values ].index (selected_addr_text )
826+ # Wrap in list (only a single item)
827+ self .debug_table .update (select_rows = [new_row_index ])
828+ except ValueError :
829+ # No value found
830+ pass
831+
832+ # Refresh again if enabled
833+ self .schedule_autorefresh ()
834+
835+ def run_loop (self ):
836+ while True :
837+ # Wait for event
838+ event , values = self .window .read ()
839+ if event == KEY_DEBUG_BTN_CLEAR :
840+ self .clear ()
841+ self .refresh ()
842+ elif event == KEY_DEBUG_BTN_COPY :
843+ self .copy_row ()
844+ elif event == KEY_DEBUG_CHK_AUTOREFRESH :
845+ if self .autorefresh_active :
846+ self .refresh ()
847+ elif event == KEY_DEBUG_BTN_REFRESH :
848+ self .refresh ()
849+ elif event == KEY_DEBUG_TBL_ADDRESS :
850+ self .update_select_status ()
851+ elif event == KEY_DEBUG_TIMER_REFRESH :
852+ self .timer_active = False
853+ if self .autorefresh_active :
854+ self .refresh ()
855+ else :
856+ self .window .close ()
857+ return None
858+
859+ def schedule_autorefresh (self ):
860+ if self .autorefresh_active and not self .timer_active :
861+ self .timer_active = True
862+ self .window .timer_start (DEBUG_TIMER_REFRESH_MS , key = KEY_DEBUG_TIMER_REFRESH , repeating = False )
863+
864+ def show_popup (self ):
865+ self .window = sg .Window ("Debug server input" , self .debug_layout , keep_on_top = True , finalize = True , modal = True , icon = WINDOW_ICON , use_default_focus = False )
866+ # Py/FreeSimpleGUI doesn't have real modal windows, so set keep_on_top
867+ # to help avoid losing the dialog box.
868+
869+ # Center window on mouse cursor
870+ loc_mid_x = self .window .size [0 ] / 2
871+ loc_mid_y = self .window .size [1 ] / 2
872+ mouse_loc = self .window .mouse_location ()
873+ self .window .move (int (mouse_loc [0 ] - loc_mid_x ), int (mouse_loc [1 ] - loc_mid_y ))
874+ # Allow resizing
875+ self .window .set_resizable (True , True )
876+ # Lock in current window size as minimum
877+ self .window .set_min_size (self .window .size )
878+ # Expand from minimum size in X and Y direction
879+ self .window .size = (self .window .size [0 ] + 100 , self .window .size [1 ] + 100 )
880+
881+ # Make it easy to clear default filter
882+ self .debug_filter_input .update (select = True )
883+ self .debug_filter_input .set_focus ()
884+
885+ # Close on Escape key
886+ self .window .bind ("<Escape>" , "-ESCAPE-" )
887+
888+ self .schedule_autorefresh ()
889+
890+ self .run_loop ()
891+
892+ def update_select_status (self ):
893+ """Update status of Copy Row button based on row selection"""
894+ self .debug_copy_button .update (disabled = not len (self .debug_table .get ()))
0 commit comments