Skip to content

Commit 0023a76

Browse files
committed
gui:
- add support for showing progress from tasks that have a progress indicator - now cache active jobs once per frame, and have function to get an active job - simplified some code - cosmetics
1 parent d25ed14 commit 0023a76

File tree

1 file changed

+103
-78
lines changed
  • src/gazeMapper/GUI/_impl

1 file changed

+103
-78
lines changed

src/gazeMapper/GUI/_impl/gui.py

Lines changed: 103 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)