Giter VIP home page Giter VIP logo

Comments (14)

vidartf avatar vidartf commented on September 16, 2024

Would it be possible for the demo to be lumino only, without any of the jupyterlab packages?

from lumino.

vidartf avatar vidartf commented on September 16, 2024

The dockpanel example might be relevant: https://github.com/jupyterlab/lumino/tree/master/examples/example-dockpanel

from lumino.

jasongrout avatar jasongrout commented on September 16, 2024

Perhaps something is responding to the tab close request by disposing the document, which is cascading into disconnecting the rest of the signal handlers? If so, perhaps whatever is disposing the document should do so asynchronously so that the other signal handlers can run first?

from lumino.

ajbozarth avatar ajbozarth commented on September 16, 2024

@vidartf I didn't know that existed, I took some time working in that and managed to really whittle down my example. You can add the following chunk of code at the end of the main() function in https://github.com/jupyterlab/lumino/tree/master/examples/example-dockpanel and recreate the issue.

  let dockPanel = new DockPanel();
  dockPanel.addWidget(new ContentWidget('Black'), { mode: 'split-bottom' });

  let myTabBar = dockPanel.tabBars().next();

  if (myTabBar) {
    let connect = myTabBar.tabCloseRequested.connect((sender, args) => {
      console.log('tabClose callback run');
    });
    console.log('tabClose calBack connected: ' + connect);
  }

  Widget.attach(dockPanel, document.body);

By adding that code it will put a tab at the bottom of the page. By looking at the console you'll see that the callback function successfully connects to the tabCloseRequested signal on page load. But when you close the tab you will not see the console log that is inside the callback function (ie the callback was not run).

from lumino.

ajbozarth avatar ajbozarth commented on September 16, 2024

@jasongrout I'll look at the lumino code to see if I can track something like that down

from lumino.

ajbozarth avatar ajbozarth commented on September 16, 2024

I forgot to click send on this comment this afternoon, so I'll comment it as is:


Ok I have some update to this, as well as an even more streamlined example.

Add just to following to the end of main() instead and the callback will be added to the closing of any tab on the example.

  let myTabBar = dock.tabBars().next();

  if (myTabBar) {
    let connect = myTabBar.tabCloseRequested.connect((sender, args) => {
      console.log('tabClose callback run');
    });
    console.log('tabClose calBack connected: ' + connect);
  }

This more streamlined version has allowed me to more accurately trace the issue. Specifically the callback is successfully run as long as at least one tab in the section is still there, but if you close the last tab it doesn't run. This is because Widget.dispose() runs Signal.clearData()

from lumino.

ajbozarth avatar ajbozarth commented on September 16, 2024

So I've spent most of the day doing intricate stack tracing and logging to trace exactly what's happening here. Essentially this is a really specific edge case and annoying as it is it is behaving exactly as it should imho.

When a tab is closed on a tab bar a function called _removeWidget() in dock panel.ts is eventually run, in this function there is a check if the tab being closed is the last tab in the tab bar, if it's not it breaks and things work as I expected. But if it's the last tab in the tab bar it disposes of the tabbar itself, which then clears the signals related to it. This mean that the tabCloseRequested signal will be removed and further connections to it will never run when closing the final tab in a tab bar.

@jasongrout @vidartf if this makes sense to you and you agree this annoying edge case should be left as is feel free to close this issue.

from lumino.

jasongrout avatar jasongrout commented on September 16, 2024

But if it's the last tab in the tab bar it disposes of the tabbar itself, which then clears the signals related to it.

Ah, makes sense. Perhaps that tab bar disposal should be done asynchronously, like setTimeout(...,0) or maybe even better a promise resolution, to allow other tab close request handlers to run?

from lumino.

jasongrout avatar jasongrout commented on September 16, 2024

That would complicate the logic to introduce asynchronicity, but at the same time it would make this corner case maybe a bit better?

from lumino.

ajbozarth avatar ajbozarth commented on September 16, 2024

Now that I'm aware of this edge case I've discovered an easy work-around to it. Instead of connecting my callback to the tabCloseRequested signal I've instead attached it to the disposed signal. This work-around may be a little much if I wanted it to run on tab close for all tabs including the last one (since I'd have to attach it to both) but in my case the final tab was the one I wanted to catch to begin with.

As for the asynchronicity idea, I'm not sure it would be possible, or at least very difficult, since the line where the tabbar is disposed is deep in the call stack and much code after it requires it is disposed first.

from lumino.

jasongrout avatar jasongrout commented on September 16, 2024

As for the asynchronicity idea, I'm not sure it would be possible, or at least very difficult, since the line where the tabbar is disposed is deep in the call stack and much code after it requires it is disposed first.

I agree it's difficult to think through the corner cases. To be clear, my suggestion was to consider putting all of these lines in a setTimeout or promise resolution so they are executed after the js frame has finished:

tabNode.tabBar.dispose();
// Handle the case where the tab node is the root.
if (this._root === tabNode) {
this._root = null;
return;
}
// Otherwise, remove the tab node from its parent...
// Prevent widget resizing unless needed.
this._root.holdAllSizes();
// Clear the parent reference on the tab node.
let splitNode = tabNode.parent!;
tabNode.parent = null;
// Remove the tab node from its parent split node.
let i = ArrayExt.removeFirstOf(splitNode.children, tabNode);
let handle = ArrayExt.removeAt(splitNode.handles, i)!;
ArrayExt.removeAt(splitNode.sizers, i);
// Remove the handle from its parent DOM node.
if (handle.parentNode) {
handle.parentNode.removeChild(handle);
}
// If there are multiple children, just update the handles.
if (splitNode.children.length > 1) {
splitNode.syncHandles();
return;
}
// Otherwise, the split node also needs to be removed...
// Clear the parent reference on the split node.
let maybeParent = splitNode.parent;
splitNode.parent = null;
// Lookup the remaining child node and handle.
let childNode = splitNode.children[0];
let childHandle = splitNode.handles[0];
// Clear the split node data.
splitNode.children.length = 0;
splitNode.handles.length = 0;
splitNode.sizers.length = 0;
// Remove the child handle from its parent node.
if (childHandle.parentNode) {
childHandle.parentNode.removeChild(childHandle);
}
// Handle the case where the split node is the root.
if (this._root === splitNode) {
childNode.parent = null;
this._root = childNode;
return;
}
// Otherwise, move the child node to the parent node...
let parentNode = maybeParent!;
// Lookup the index of the split node.
let j = parentNode.children.indexOf(splitNode);
// Handle the case where the child node is a tab node.
if (childNode instanceof Private.TabLayoutNode) {
childNode.parent = parentNode;
parentNode.children[j] = childNode;
return;
}
// Remove the split data from the parent.
let splitHandle = ArrayExt.removeAt(parentNode.handles, j)!;
ArrayExt.removeAt(parentNode.children, j);
ArrayExt.removeAt(parentNode.sizers, j);
// Remove the handle from its parent node.
if (splitHandle.parentNode) {
splitHandle.parentNode.removeChild(splitHandle);
}
// The child node and the split parent node will have the same
// orientation. Merge the grand-children with the parent node.
for (let i = 0, n = childNode.children.length; i < n; ++i) {
let gChild = childNode.children[i];
let gHandle = childNode.handles[i];
let gSizer = childNode.sizers[i];
ArrayExt.insert(parentNode.children, j + i, gChild);
ArrayExt.insert(parentNode.handles, j + i, gHandle);
ArrayExt.insert(parentNode.sizers, j + i, gSizer);
gChild.parent = parentNode;
}
// Clear the child node.
childNode.children.length = 0;
childNode.handles.length = 0;
childNode.sizers.length = 0;
childNode.parent = null;
// Sync the handles on the parent node.
parentNode.syncHandles();

It's great that you have a workaround, and I think you're right to argue that the current behavior is also correct, and simpler to reason about as well since it is all synchronous.

from lumino.

jasongrout avatar jasongrout commented on September 16, 2024

@jasongrout @vidartf if this makes sense to you and you agree this annoying edge case should be left as is feel free to close this issue.

Sounds good.

from lumino.

vidartf avatar vidartf commented on September 16, 2024

If someone wants to tweak this, I see two possible ways:

  • Always fire all signals "in-flight" if an object is disposed when emitting, e.g. by copying the relevant data while in the emission process. This might have a long tail though, so it might not be straight forward.
  • Have the dispose call in docklayout (
    tabNode.tabBar.dispose();
    ) be called in the next animation frame / timeout of 0 (whatever is the current pattern in the repo). Likely to be ok, but some care would have to be taken that we do not create other corner cases due to the changes in timing.

from lumino.

vidartf avatar vidartf commented on September 16, 2024

(here is a pattern of delayed dispose)

setTimeout(() => {
// Do nothing if the drag has been aborted.
if (data.dragAborted) {
return;
}
// Clear the drag data reference.
this._dragData = null;
// Reset the positions of the tabs.
Private.resetTabPositions(this.contentNode.children, this._orientation);
// Clear the cursor grab.
data.override!.dispose();

from lumino.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.