Rearranging all windows

I’ve tried to solve two problems for several months now, and am now desparate for anyone’s help!

  1. I have an app with 3 window sections, starting as
    [left-top][entire right side]
    [left-bot]
    “entire right side” is where the text-editor gets opened, but I want to allow the user to rearrange the left-top and left-bot to the top, right, etc., without restrictions, but be able to open more documents.

When a new document is opened, I want to dock it somewhere reasonable, currently I dock it wherever any other text-editor window is open. This is OK so long as there is SOME text-editor window open, but when the user closes all of them and then wants to create a new one, I want to use the dead space for it.

I looked into using CWorkingArea, but that restricts where the windows can be docked and rearranged.

  1. I add several DefaultSingleCDockable items to a Control, the user can rearrange them, but can make a mess of them all. I would like to add a button that rearranges them all into 3 areas (depending on each window’s attribute).
    My “rearrange” code looks like this…
    int dc = wsControl.getCDockableCount();
    CLocation loc = CLocation.base();
    for (int i = 0; i < dc; ++i) {
    bibliothek.gui.dock.common.intern.CDockable win = wsControl.getCDockable(i);
    if (win instanceof DefaultSingleCDockable) {
    DefaultSingleCDockable wSingle = ((DefaultSingleCDockable) win);
    utMisc.debugOutput(“Win #” + i + “=id:” + wSingle.getUniqueId() + “, Title=” + wSingle.getTitleText());
    int windowType = xxxx; // let’s say 0, 1, or 2
    put into one of three regions based on windowType

I don’t know how to set the locations!

Let’s start with 2, I think that is the easier question.

There is a method CDockable.setLocation, where you can use arguments like e.g. “CLocation.base().north( 0.25 )” (points to the top 25% of your application). But in your case I think it would be very hard to use this method because you can only move one Dockable at a time - which results in a really big puzzle to find out in which order to move the Dockabes.

So instead I would use the CGrid class. With the CGrid you can lay out all the Dockables at the same time. Beware however that the method “CContentArea.deploy” will remove any Dockables before applying “grid”. So any Dockable that is not added to “grid” will just disappear. In pseudo-code it would look somewhat like this:

CControl control = ...

// that is the helper-object we are going to fill up
CGrid grid = new CGrid();

int dc = wsControl.getCDockableCount();
for (int i = 0; i < dc; ++i) {
  CDockable win = wsControl.getCDockable(i);
  if (win instanceof DefaultSingleCDockable) {
    DefaultSingleCDockable wSingle = ((DefaultSingleCDockable) win);
    utMisc.debugOutput("Win #" + i + "=id:" + wSingle.getUniqueId() + ", Title=" + wSingle.getTitleText());
    int windowType = xxxx; // let's say 0, 1, or 2
    if( windowType = yyyy ){
      // e.g. top. Assume "grid" has a size of 100/100, then we take up 75% horizontal, and 50% vertical space
      grid.add( 0, 0, 75, 50, win );
    }
    else{
      ... // other location(s)
      grid.add( ... );
    }
  }
  else {
    // don't forget to add other Dockables as well
    grid.add( win, <appropriate location> );
  }

  // now replace any content of the default CContentArea with "grid".
  control.getContentArea().deploy( grid );

… or as an application that really works. You certainly have to play a bit with the correct positions, but I think you understand what I suggest as solution?


import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

import bibliothek.gui.dock.common.CControl;
import bibliothek.gui.dock.common.CGrid;
import bibliothek.gui.dock.common.CLocation;
import bibliothek.gui.dock.common.DefaultSingleCDockable;
import bibliothek.gui.dock.common.SingleCDockable;
import bibliothek.gui.dock.common.location.CMaximalExternalizedLocation;
import bibliothek.gui.dock.common.mode.ExtendedMode;
import bibliothek.gui.dock.layout.DockableProperty;
import bibliothek.gui.dock.station.split.SplitDockPathProperty;
import bibliothek.gui.dock.station.split.SplitDockPlaceholderProperty;
import bibliothek.gui.dock.station.split.SplitDockProperty;

public class Dock29 {
    public static void main( String[] args ){
        JFrame frame = new JFrame();
        frame.setBounds( 20, 20, 500, 500 );
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        
        final CControl control = new CControl( frame );
        
        final SingleCDockable red = create( "red", Color.RED );
        final SingleCDockable green = create( "green", Color.GREEN );
        final SingleCDockable blue = create( "blue", Color.BLUE );
        
        JButton button = new JButton( "Reset" );
        button.addActionListener( new ActionListener(){
            public void actionPerformed( ActionEvent e ){
                CGrid grid = new CGrid();
                
                // put red and blue to the top left/bottom left
                grid.add( 0, 0, 75, 50, red );
                grid.add( 0, 50, 75, 50, blue );
                
                // let green stay where it currently is
                if( green.getExtendedMode() == ExtendedMode.MAXIMIZED ){
                    if( !(green.getBaseLocation() instanceof CMaximalExternalizedLocation) ){
                        // if in maximized mode and not free floating: demaximize
                        green.setExtendedMode( ExtendedMode.NORMALIZED );    
                    }
                }
                if( green.getExtendedMode() == ExtendedMode.NORMALIZED ){
                    // fall back to Core and assume that green is a child of a SplitDockStation (not necessarily true,
                    // it could also be a StackDockStation. If that happens "property.getSuccessor" is not null, and the
                    // code below may not work properly anymore). 
                    CLocation location = green.getBaseLocation();
                    DockableProperty property = location.findProperty();
                    
                    if( property instanceof SplitDockPlaceholderProperty ){
                        property = ((SplitDockPlaceholderProperty)property).getBackup();
                    }
                    if( property instanceof SplitDockPathProperty ){
                        property = ((SplitDockPathProperty)property).toLocation();
                    }
                    if( property instanceof SplitDockProperty ){
                        SplitDockProperty rectangle = (SplitDockProperty)property;
                        grid.add( 100*rectangle.getX(), 100*rectangle.getY(), 100*rectangle.getWidth(), 100*rectangle.getHeight(), green );
                    }
                    else{
                        // backup plan if we guessed wrong about the type of "property"
                        grid.add( 75, 0, 25, 100, green );
                    }
                }
                
                control.getContentArea().deploy( grid );
            }
        });
        
        CGrid grid = new CGrid( control );
        grid.add( 0, 0, 75, 50, red );
        grid.add( 0, 50, 75, 50, blue );
        grid.add( 75, 0, 25, 100, green );
        control.getContentArea().deploy( grid );
        
        frame.add( control.getContentArea() );
        frame.add( button, BorderLayout.SOUTH );
        
        frame.setVisible( true );
    }
    
    public static DefaultSingleCDockable create( String title, Color color ){
        JPanel panel = new JPanel();
        panel.setOpaque(true);
        panel.setBackground(color);

        DefaultSingleCDockable singleDockable = new DefaultSingleCDockable(title, title, panel);
        
        return singleDockable;
    }
}

Now question 1.

  • Since you tried CWorkingArea: you could try CGridArea instead. It behaves almost exactly the same way as CWorkingArea, but does not impose restrictions to what Dockables can be children or the destination where the user can drag children. But the user could very easily create a useless grey area by dragging a child away from the CGridArea, I don’t think that would be a good solution.

  • What about storing the position of the last open Document-Dockable and use that position for the next document? You could add a CVetoClosingListener to any Document-Dockable (using CDockable.addVetoClosingListener). The method “closing” is called just before the document is closed, that would be the appropriate place to ask and store the location. Like this:

        singleDockable.setCloseable( true );
        singleDockable.addVetoClosingListener( new CVetoClosingListener(){
            public void closing( CVetoClosingEvent event ){
                System.out.println( title + ": location=" + singleDockable.getBaseLocation() );
            }
            
            public void closed( CVetoClosingEvent event ){
                // ignore
            }
        });```
You would not need any gray areas. This solution will have some issues if the user heavily modifies the layout while no document is open (the location gets outdated), but I think in most cases it would work.

- With the method above you could also insert an uncloseable, title-less, unmoveable gray Dockable directly after "closed" was called at the location of the old document-Dockable. And the next time the user opens a document you would just replace that "placeholder" with a real document-dockable. I think that is what I would use.

Thanks so much for the help, Beni. I am working on #2 first and have a question…

So instead I would use the CGrid class.
I tried this earlier and had a problem, maybe you can help.

Each window may hold system resources (file handles, database connections, etc.) that need to be released when the window is closed. I could not find any „on close“ of the window, so I did the following:
dockable.setCloseable(true);
dockable.addCDockableStateListener(new bibliothek.gui.dock.common.event.CDockableAdapter() {

        @Override
        public void visibilityChanged(bibliothek.gui.dock.common.intern.CDockable d) {
            if (!d.isVisible()) {
                utDebug.debugOutput("Closing view's panel");
                (release resources here...)
            }
        }
    });

What happens with the grid method of rearranging everything: the windows get closed (they are probably being made invisible and then visible because they are being moved to the grid).

Is there a better way to catch a „window is closing“, so I don’t run into this problem?

The state-listener is the best way to catch the close (=invisible) event, using a CVetoClosingListener would be another way but with the same results.

In my example I don’t see any close events, but then it is a small example and your application may be a bit more complex… You can try and call the methods “DockController.freezeLayout” (CControl().intern().getController().freezeLayout()) before resetting the layout, and “meltLayout” afterwards. “freezeLayout” temporarily disables the event system, while “meltLayout” enables events again. It should not have any bad side effects.

Or just use some global boolean variable you set while resetting the layout, if the variable is set, no resources are to be released.

Thanks, again, Beni.

(CControl().intern().getController().freezeLayout()) before resetting the layout, and „meltLayout“ afterwards. „freezeLayout“ temporarily disables the event system, while „meltLayout“ enables events again. It should not have any bad side effects.

This didn’t help, so I just did the global variable „rearranging windows“ (to prevent „hidden means closed“ temporarily), and everything works well now.

Now question 1.

you could try CGridArea instead. It behaves almost exactly the same way as CWorkingArea, but does not impose restrictions to what Dockables can be children or the destination where the user can drag children. But the user could very easily create a useless grey area by dragging a child away from the CGridArea, I don’t think that would be a good solution.

This would be useful if the CGridArea had a handle/titlebar for moving it; then it could become „the workspace are“ where all the text editors would be dropped.

What about storing the position of the last open Document-Dockable and use that position for the next document?

You’re right, this can get ugly

With the method above you could also insert an uncloseable, title-less, unmoveable gray Dockable directly after „closed“ was called at the location of the old document-Dockable. And the next time the user opens a document you would just replace that „placeholder“ with a real document-dockable. I think that is what I would use.

I created an uncloseable window called the „Documents“ window, it’s where new windows will be dropped. It also gives the user a visual cue where new document windows will open.

By the way, I found when the document window was „hidden“ to be „closed“, it belonged to the control (when I enumerated all the control’s windows during the „rearrange layout“), so I added this line in my „on-close“ function:
dockable.getControl().getOwner().remove(dockable);

Thanks again for all your help.

Yes, being registered at a CControl and being visible are two completely different properties, each of them can change without affecting the other. The reason behind this is, that you may reuse a „closed“ Dockable and just open it again, or that you want to work with the Core API (I admit, that does not happen often).