Skip to content

Commit c7af0e0

Browse files
authored
Add FullScreenResizeHandler to resize GUI views (#18)
1 parent 86961a8 commit c7af0e0

3 files changed

Lines changed: 82 additions & 14 deletions

File tree

Dockerfile

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,13 @@ RUN wget "https://www.wavpack.com/wavpack-${WAVPACK_VERSION}.tar.bz2" && \
2323
RUN pip install wavpack-numcodecs
2424

2525
# Install spikeinterface from source
26-
RUN git clone https://github.com/SpikeInterface/spikeinterface.git && \
27-
cd spikeinterface && \
28-
git checkout f732780fd88f5802033b57c9bb9b06229ec7de30 && \
29-
pip install . && cd ..
26+
RUN pip install spikeinterface==0.104.1
3027

3128
# Force scikit-learn to 1.6.1 to avoid issues with newer versions
3229
RUN pip install scikit-learn==1.6.1
3330

3431
# Install spikeinterface-gui from source
35-
RUN git clone https://github.com/alejoe91/spikeinterface-gui.git && \
36-
cd spikeinterface-gui && \
37-
git checkout 47999372e405f5d7a435072ca3015a9fd1b9812c && \
38-
pip install . && cd ..
32+
RUN pip install spikeinterface-gui==0.13.1
3933

4034

4135
ENV PYTHONUNBUFFERED=1

src/aind_ephys_portal/panel/ephys_gui.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from spikeinterface.curation import validate_curation_dict
1919

2020
from aind_ephys_portal.panel.logging import setup_logging, local_log_context
21-
from aind_ephys_portal.panel.utils import PostMessageListener
21+
from aind_ephys_portal.panel.utils import PostMessageListener, FullscreenResizeHandler
2222

2323

2424
displayed_unit_properties = [
@@ -111,6 +111,9 @@ def delayed_init():
111111
sizing_mode="stretch_both",
112112
)
113113

114+
def create_fullscreen_resize_listener(self):
115+
return FullscreenResizeHandler()
116+
114117
def create_post_message_listener(self):
115118
if self.identifier is not None:
116119
listener = PostMessageListener()
@@ -212,13 +215,15 @@ def _initialize(self):
212215
# Add custom curation callback to send data to parent window
213216
self.submit_trigger = self.create_submit_trigger()
214217
# Add postMessage listener to receive data from parent window
215-
self.listener = self.create_post_message_listener()
218+
self.curation_listener = self.create_post_message_listener()
219+
self.fullscreen_listener = self.create_fullscreen_resize_listener()
216220

217221
self.win_layout = self._create_main_window()
218222
self.layout[0] = self.win_layout
219223
if self.identifier is not None:
220224
self.layout.append(self.submit_trigger)
221-
self.layout.append(self.listener)
225+
self.layout.append(self.curation_listener)
226+
self.layout.append(self.fullscreen_listener)
222227

223228
print("\nEphys GUI initialized successfully!")
224229
t_stop = time.perf_counter()
@@ -231,7 +236,9 @@ def _initialize(self):
231236

232237
if error is not None:
233238
print(f"Error during initialization: {error}")
234-
self.layout[0] = pn.pane.Markdown(f"⚠️ Error during initialization: {error}", sizing_mode="stretch_both")
239+
self.layout = pn.Column(
240+
pn.pane.Markdown(f"⚠️ Error during initialization: {error}", sizing_mode="stretch_both")
241+
)
235242
else:
236243
final_mem = psutil.virtual_memory()
237244
final_ram_usage = final_mem.used / (1024**3)
@@ -320,8 +327,9 @@ def cleanup(self):
320327
current_ram_usage = initial_mem.used / (1024**3)
321328
print(f"\nRAM Usage before cleanup: {current_ram_usage:.2f} / {total_ram:.2f} GB\n")
322329

323-
# 1) Clear postMessage listener and submit trigger (they hold bound-method back-refs to self)
324-
self.listener = None
330+
# 1) Clear postMessage listeners and submit trigger (they hold bound-method back-refs to self)
331+
self.curation_listener = None
332+
self.fullscreen_listener = None
325333
self.submit_trigger = None
326334

327335
# 2) Release GUI controller and all its data

src/aind_ephys_portal/panel/utils.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,72 @@ def format_css_background():
5959

6060

6161

62+
class FullscreenResizeHandler(ReactComponent):
63+
"""
64+
Pure-JS component that listens for 'fullscreen-resize' postMessages and
65+
forces Bokeh to re-measure canvas sizes without any Python layout rebuild.
66+
67+
Bokeh 3.x uses ResizeObserver (not window.resize events) to detect size
68+
changes. We trigger it by briefly collapsing the Bokeh root element to 1px
69+
then removing the override — ResizeObserver fires on the size delta, Bokeh
70+
re-renders at the new (fullscreen) container dimensions.
71+
72+
This avoids the destructive Python layout swap (remove + re-add) which
73+
loses Bokeh event handler routing (e.g. selectiongeometry) and resets
74+
toolbar.active_drag, breaking the lasso selection tool.
75+
"""
76+
77+
_esm = """
78+
export function render({ model }) {
79+
React.useEffect(() => {
80+
function triggerResize() {
81+
// 1) BokehJS API — invalidate layout on all registered views.
82+
// Bokeh.index is a plain object in Bokeh 3.x ({id: view, ...}).
83+
try {
84+
if (window.Bokeh && window.Bokeh.index) {
85+
Object.values(window.Bokeh.index).forEach(view => {
86+
if (view) {
87+
view.invalidate_layout?.();
88+
view.invalidate_render?.();
89+
}
90+
});
91+
}
92+
} catch(e) {
93+
console.warn("[FullscreenResizeHandler] BokehJS API error:", e);
94+
}
95+
96+
// 2) Force ResizeObserver to fire by briefly collapsing the Bokeh
97+
// root element then removing the override (one animation frame)
98+
const roots = document.querySelectorAll("[data-root-id]");
99+
roots.forEach(el => {
100+
el.style.setProperty("width", "1px", "important");
101+
el.style.setProperty("height", "1px", "important");
102+
});
103+
requestAnimationFrame(() => {
104+
roots.forEach(el => {
105+
el.style.removeProperty("width");
106+
el.style.removeProperty("height");
107+
});
108+
// 3) window.resize fallback for older Bokeh / Panel versions
109+
window.dispatchEvent(new Event("resize"));
110+
});
111+
}
112+
113+
function onMessage(event) {
114+
const data = event.data;
115+
if (!data || data.type !== "fullscreen-resize") return;
116+
triggerResize();
117+
setTimeout(triggerResize, 300);
118+
}
119+
120+
window.addEventListener("message", onMessage);
121+
return () => window.removeEventListener("message", onMessage);
122+
}, []);
123+
return <></>;
124+
}
125+
"""
126+
127+
62128
class PostMessageListener(ReactComponent):
63129
"""
64130
Listen to window.postMessage events and forward them to Python via on_msg().

0 commit comments

Comments
 (0)