@@ -36,40 +36,42 @@ def __init__(self):
3636 self .running = False
3737 settings_editor .set_gui_instance (self )
3838
39- self .project_dir : pathlib .Path = None
40- self .study_config : config .Study = None
41- self .plane_configs : dict [str ,gt_plane .Plane | None ] = {} # None is plane config has errors
42- self ._dict_type_rec : dict [str , typing .Type ]| None = None
43-
44- self .sessions : dict [str , session .Session ] = {}
45- self .session_config_overrides : dict [str , config .StudyOverride ] = {}
46- self ._session_dict_type_rec : dict [str , dict [str , typing .Type ]] = {}
47- self ._sessions_lock : threading .Lock = threading .Lock ()
48- self ._selected_sessions : dict [str , bool ] = {}
49- self ._session_lister = session_lister .List (self .sessions , self ._sessions_lock , self ._selected_sessions , info_callback = self ._open_session_detail , draw_action_status_callback = self ._session_action_status , item_context_callback = self ._session_context_menu )
50- self ._et_widget_drawer = gt_gui .recording_table .EyeTrackerName ()
51-
52- self .recording_config_overrides : dict [str , dict [str , config .StudyOverride ]] = {}
53- self ._recording_dict_type_rec : dict [str , dict [str , dict [str , typing .Type ]]] = {}
54- self ._recording_listers : dict [str , gt_gui .recording_table .RecordingTable ] = {}
55- self ._selected_recordings : dict [str , dict [str , bool ]] = {}
56-
57- self ._possible_value_getters : dict [str ] = {}
58-
59- self .need_setup_recordings = True
60- self .need_setup_plane = True
61- self .need_setup_episode = True
62- self .need_setup_individual_markers = True
63- self .can_accept_sessions = False
64- self ._session_actions : set [process .Action ] = set ()
39+ self .project_dir : pathlib .Path = None
40+ self .study_config : config .Study = None
41+ self .plane_configs : dict [str ,gt_plane .Plane | None ] = {} # None is plane config has errors
42+ self ._dict_type_rec : dict [str , typing .Type ]| None = None
43+
44+ self .sessions : dict [str , session .Session ] = {}
45+ self .session_config_overrides : dict [str , config .StudyOverride ] = {}
46+ self ._session_dict_type_rec : dict [str , dict [str , typing .Type ]] = {}
47+ self ._sessions_lock : threading .Lock = threading .Lock ()
48+ self ._selected_sessions : dict [str , bool ] = {}
49+ self ._session_lister = session_lister .List (self .sessions , self ._sessions_lock , self ._selected_sessions , info_callback = self ._open_session_detail , draw_action_status_callback = self ._session_action_status , item_context_callback = self ._session_context_menu )
50+ self ._et_widget_drawer = gt_gui .recording_table .EyeTrackerName ()
51+
52+ self .recording_config_overrides : dict [str , dict [str , config .StudyOverride ]] = {}
53+ self ._recording_dict_type_rec : dict [str , dict [str , dict [str , typing .Type ]]] = {}
54+ self ._recording_listers : dict [str , gt_gui .recording_table .RecordingTable ] = {}
55+ self ._selected_recordings : dict [str , dict [str , bool ]] = {}
56+
57+ self ._possible_value_getters : dict [str ] = {}
58+
59+ self .need_setup_recordings = True
60+ self .need_setup_plane = True
61+ self .need_setup_episode = True
62+ self .need_setup_individual_markers = True
63+ self .can_accept_sessions = False
64+ self ._session_actions : set [process .Action ] = set ()
65+ self ._problems_cache : type_utils .ProblemDict = {}
6566
6667 self ._session_watcher : concurrent .futures .Future = None
6768 self ._session_watcher_stop_event : asyncio .Event = None
6869 self ._config_watcher : concurrent .futures .Future = None
6970 self ._config_watcher_stop_event : asyncio .Event = None
7071
71- self .process_pool = process_pool .ProcessPool ()
72- self .job_scheduler = process_pool .JobScheduler [utils .JobInfo ](self .process_pool , self ._check_job_valid )
72+ self .process_pool = process_pool .ProcessPool ()
73+ self .job_scheduler = process_pool .JobScheduler [utils .JobInfo ](self .process_pool , self ._check_job_valid )
74+ self ._active_jobs_cache : dict [utils .JobInfo , int ] = {}
7375
7476 self ._window_list : list [hello_imgui .DockableWindow ] = []
7577 self ._to_dock = []
@@ -78,15 +80,14 @@ def __init__(self):
7880 self ._need_set_window_title = False
7981 self ._main_dock_node_id = None
8082
81- self ._sessions_pane : hello_imgui .DockableWindow = None
82- self ._project_settings_pane : hello_imgui .DockableWindow = None
83- self ._action_list_pane : hello_imgui .DockableWindow = None
84- self ._show_demo_window = False
83+ self ._sessions_pane : hello_imgui .DockableWindow = None
84+ self ._project_settings_pane : hello_imgui .DockableWindow = None
85+ self ._action_list_pane : hello_imgui .DockableWindow = None
86+ self ._show_demo_window = False
8587
86- self ._icon_font : imgui .ImFont = None
87- self ._big_font : imgui .ImFont = None
88+ self ._icon_font : imgui .ImFont = None
89+ self ._big_font : imgui .ImFont = None
8890
89- self ._problems_cache : type_utils .ProblemDict = {}
9091 self ._marker_preview_cache : dict [tuple [int ,int ,int ], image_helper .ImageHelper ]= {}
9192 self ._plane_preview_cache : dict [str , image_helper .ImageHelper ]= {}
9293
@@ -283,8 +284,8 @@ def _show_app_menu_items(self):
283284 def _show_menu_gui (self ):
284285 # this is always called, so we handle popups and other state here
285286 self ._check_project_setup_state ()
286- gt_gui .utils .handle_popup_stack (self .popup_stack )
287287 self ._update_jobs_and_process_pool ()
288+ gt_gui .utils .handle_popup_stack (self .popup_stack )
288289 # also handle showing of debug windows
289290 if self ._show_demo_window :
290291 self ._show_demo_window = imgui .show_demo_window (self ._show_demo_window )
@@ -446,7 +447,7 @@ def status_file_reloader(loader: Callable[[bool, bool], None]):
446447 def launch_task (self , sess : str , recording : str | None , action : process .Action , ** kwargs ):
447448 # NB: this is run under lock, so sess and recording are valid
448449 job = utils .JobInfo (action , sess , recording )
449- if job in self ._get_pending_running_job_list () :
450+ if job in self ._active_jobs_cache :
450451 # already scheduled, nothing to do
451452 return
452453 if action == process .Action .IMPORT :
@@ -467,7 +468,8 @@ def launch_task(self, sess: str, recording: str|None, action: process.Action, **
467468
468469 # add to scheduler
469470 payload = process_pool .JobPayload (func , args , kwargs )
470- self .job_scheduler .add_job (job , payload , self ._action_done_callback , exclusive_id = exclusive_id , priority = priority )
471+ progress_indicator = self .job_scheduler .get_progress_indicator () if action .has_progress_indication else None
472+ self .job_scheduler .add_job (job , payload , self ._action_done_callback , exclusive_id = exclusive_id , priority = priority , progress_indicator = progress_indicator )
471473
472474 def _update_jobs_and_process_pool (self ):
473475 with self ._sessions_lock :
@@ -479,6 +481,12 @@ def _update_jobs_and_process_pool(self):
479481 job_state = self .job_scheduler .jobs [job_id ].get_state ()
480482 self ._update_job_states_impl (self .job_scheduler .jobs [job_id ].user_data , job_state )
481483
484+ # build cache for easy access to running jobs
485+ self ._active_jobs_cache .clear ()
486+ for job_id in self .job_scheduler .jobs :
487+ if self .job_scheduler .jobs [job_id ].get_state () in [process_pool .State .Pending , process_pool .State .Running ]:
488+ self ._active_jobs_cache [self .job_scheduler .jobs [job_id ].user_data ] = job_id
489+
482490 # if there are no jobs left, clean up process pool
483491 self .process_pool .cleanup_if_no_jobs ()
484492
@@ -490,6 +498,13 @@ def _check_job_valid(self, job: utils.JobInfo) -> bool:
490498 return False
491499 return True
492500
501+ def _get_active_job (self , action : process .Action , session_name : str , recording_name : str = None ):
502+ j = utils .JobInfo (action , session_name , recording_name )
503+ if (job_id := self ._active_jobs_cache .get (j , None )) is not None :
504+ return job_id , self .job_scheduler .jobs [job_id ]
505+ else :
506+ return None , None
507+
493508 def _update_job_states_impl (self , job : utils .JobInfo , job_state : process_pool .State ):
494509 sess = self .sessions .get (job .session ,None )
495510 if sess is None :
@@ -742,7 +757,10 @@ def _recording_lister_set_actions_to_show(self, lister: gt_gui.recording_table.R
742757 actions = process .get_actions_for_config (cfg , exclude_session_level = True )
743758 def _draw_status (action : process .Action , item : session .Recording ):
744759 if process .is_action_possible_for_recording (item .definition .name , item .definition .type , action , cfg ):
745- gt_gui .utils .draw_process_state (item .state [action ])
760+ progress = None
761+ if item .state [action ]== process_pool .State .Running and (job_desc := self ._get_active_job (action , item .info .working_directory .parent .name , item .definition .name )[1 ]):
762+ progress = job_desc .progress .get_progress () if job_desc .progress is not None else None
763+ gt_gui .utils .draw_process_state (item .state [action ], progress = progress )
746764 else :
747765 imgui .text ('-' )
748766 if action == process .Action .AUTO_CODE_EPISODES and cfg .sync_ref_recording and item .definition .name != cfg .sync_ref_recording :
@@ -860,6 +878,8 @@ def _finish_unload_project(self):
860878 self ._selected_recordings .clear ()
861879 self ._plane_preview_cache .clear ()
862880 self ._marker_preview_cache .clear ()
881+ self ._problems_cache .clear ()
882+ self ._active_jobs_cache .clear ()
863883
864884
865885 def _sessions_pane_drawer (self ):
@@ -1054,7 +1074,11 @@ def _action_list_pane_drawer(self):
10541074 imgui .text (f'{ job_id } ' )
10551075 case 1 :
10561076 # Status
1057- gt_gui .utils .draw_process_state ((job_state := jobs [job_id ].get_state ()))
1077+ job_state = jobs [job_id ].get_state ()
1078+ progress = None
1079+ if job_state == process_pool .State .Running :
1080+ progress = jobs [job_id ].progress .get_progress () if jobs [job_id ].progress is not None else None
1081+ gt_gui .utils .draw_process_state (job_state , progress = progress )
10581082 if jobs [job_id ].error :
10591083 gt_gui .utils .draw_hover_text (jobs [job_id ].error , text = '' )
10601084 imgui .same_line ()
@@ -1475,32 +1499,37 @@ def _add_marker_popup():
14751499 }
14761500 gt_gui .utils .push_popup (self , lambda : gt_gui .utils .popup ("Add marker" , _add_marker_popup , buttons = buttons , button_keymap = {0 :imgui .Key .enter }, outside = False ))
14771501
1478- def _get_pending_running_job_list (self ) -> dict [utils .JobInfo , int ]:
1479- active_jobs : dict [utils .JobInfo , int ] = {}
1480- for job_id in self .job_scheduler .jobs :
1481- if self .job_scheduler .jobs [job_id ].get_state () in [process_pool .State .Pending , process_pool .State .Running ]:
1482- active_jobs [self .job_scheduler .jobs [job_id ].user_data ] = job_id
1483- return active_jobs
1484-
14851502 def _session_action_status (self , item : session .Session , action : process .Action ):
14861503 if not item .has_all_recordings ():
14871504 imgui .text_colored (colors .error , '-' )
14881505 else :
14891506 if process .is_session_level_action (action ):
1490- gt_gui .utils .draw_process_state (item .state [action ])
1507+ progress = None
1508+ if item .state [action ]== process_pool .State .Running and (job_desc := self ._get_active_job (action , item .name )[1 ]):
1509+ progress = job_desc .progress .get_progress () if job_desc .progress is not None else None
1510+ gt_gui .utils .draw_process_state (item .state [action ], progress = progress )
14911511 else :
14921512 cfg = self .session_config_overrides [item .name ].apply (self .study_config , strict_check = False ) if item .name in self .session_config_overrides else self .study_config
14931513 states = {r :item .recordings [r ].state [action ] for r in item .recordings if process .is_action_possible_for_recording (r , item .definition .get_recording_def (r ).type , action , cfg )}
14941514 not_completed = [r for r in states if states [r ]!= process_pool .State .Completed ]
1495- if any (st := [s for r in states if (s := states [r ]) in [process_pool .State .Pending , process_pool .State .Running ]]):
1515+ progress : dict [str ,tuple [float ,str ]] = {}
1516+ if any (st := {s for r in states if (s := states [r ]) in [process_pool .State .Pending , process_pool .State .Running ]}):
14961517 # progress marker
1497- gt_gui .utils .draw_process_state (process_pool .State .Running if process_pool .State .Running in st else process_pool .State .Pending , have_hover_popup = False )
1518+ if process_pool .State .Running in st :
1519+ # figure out progress, if available
1520+ for r in states :
1521+ if (job_desc := self ._get_active_job (action , item .name , r )[1 ]) and job_desc .progress is not None :
1522+ progress [r ] = job_desc .progress .get_progress ()
1523+ gt_gui .utils .draw_process_state (process_pool .State .Running if process_pool .State .Running in st else process_pool .State .Pending , have_hover_popup = False , progress = (sum ((progress [r ][0 ] for r in progress ))/ len (progress ),'' ) if progress else None )
14981524 else :
14991525 n_rec = len (states )
15001526 clr = colors .error if not_completed else colors .ok
15011527 imgui .text_colored (clr , f'{ n_rec - len (not_completed )} /{ n_rec } ' )
15021528 if not_completed :
1503- rec_strs = [f'{ r } ({ states [r ].displayable_name } )' for r in not_completed ]
1529+ rec_strs : list [str ] = []
1530+ for r in not_completed :
1531+ extra = (', ' + progress [r ][1 ]) if r in progress else ''
1532+ rec_strs .append (f'{ r } ({ states [r ].displayable_name } { extra } )' )
15041533 gt_gui .utils .draw_hover_text ('not completed for recordings:\n ' + '\n ' .join (rec_strs ),'' )
15051534
15061535 def _session_context_menu (self , session_name : str ) -> bool :
@@ -1679,40 +1708,33 @@ def _filter_session_context_menu_actions(self, session_name: str, rec_name: str|
16791708 return {}, {}
16801709
16811710 # first get list of scheduled actions for this session/recordings
1682- active_jobs = self ._get_pending_running_job_list ()
16831711 actions_running : dict [process .Action ,int | dict [str ,int ]] = {}
16841712 for a in process .Action :
16851713 if process .is_session_level_action (a ):
1686- if (j := utils .JobInfo (a , session_name )) in active_jobs :
1687- actions_running [a ] = active_jobs [j ]
1714+ recs = None
1715+ elif rec_name :
1716+ recs = rec_name
16881717 else :
1689- if rec_name :
1690- if (j := utils .JobInfo (a , session_name , rec_name )) in active_jobs :
1691- actions_running [a ] = active_jobs [j ]
1692- else :
1693- # check each recording
1694- recs = {r .name :active_jobs [j ] for r in self .study_config .session_def .recordings if (j := utils .JobInfo (a , session_name , r .name )) in active_jobs }
1695- if recs :
1696- actions_running [a ] = recs
1718+ recs = [r .name for r in self .study_config .session_def .recordings ]
1719+
1720+ if isinstance (recs ,list ):
1721+ recs = {r :job_id for r in recs if (job_id := self ._get_active_job (a , session_name , r )[0 ]) is not None }
1722+ if recs :
1723+ actions_running [a ] = recs
1724+ else :
1725+ if (job_id := self ._get_active_job (a , session_name , recs )[0 ]):
1726+ actions_running [a ] = job_id
16971727
16981728 # filter out running actions from possible actions
16991729 actions_possible : dict [process .Action ,bool | list [str ]] = {}
17001730 for a in actions :
1701- if process .is_session_level_action (a ):
1702- if a not in actions_running :
1703- actions_possible [a ] = actions [a ]
1704- else :
1705- if rec_name :
1706- if a not in actions_running :
1707- actions_possible [a ] = actions [a ]
1708- else :
1709- # check each recording
1710- if a not in actions_running :
1711- actions_possible [a ] = actions [a ]
1712- else :
1713- recs = [r for r in actions [a ][0 ] if r not in actions_running [a ]]
1714- if recs :
1715- actions_possible [a ] = recs
1731+ if a not in actions_running :
1732+ actions_possible [a ] = actions [a ]
1733+ elif not process .is_session_level_action (a ) and not rec_name :
1734+ # check each recording
1735+ recs = [r for r in actions [a ][0 ] if r not in actions_running [a ]]
1736+ if recs :
1737+ actions_possible [a ] = recs
17161738 return actions_possible , actions_running
17171739
17181740 def _open_session_detail (self , sess : session .Session ):
@@ -1763,7 +1785,10 @@ def _session_detail_GUI(self, sess: session.Session):
17631785 if session_level_actions and imgui .begin_table (f'##{ sess .name } _session_level' , 2 , imgui .TableFlags_ .sizing_fixed_fit ):
17641786 for a in session_level_actions :
17651787 imgui .table_next_column ()
1766- gt_gui .utils .draw_process_state (sess .state [a ])
1788+ progress = None
1789+ if sess .state [a ]== process_pool .State .Running and (job_desc := self ._get_active_job (a , sess .name )[1 ]):
1790+ progress = job_desc .progress .get_progress () if job_desc .progress is not None else None
1791+ gt_gui .utils .draw_process_state (sess .state [a ], progress = progress )
17671792 imgui .table_next_column ()
17681793 imgui .selectable (a .displayable_name , False , imgui .SelectableFlags_ .span_all_columns | imgui .SelectableFlags_ .allow_overlap )
17691794 if (a in menu_actions or a in menu_actions_running ) and imgui .begin_popup_context_item (f"##{ sess .name } _{ a } _context" ):
0 commit comments