Handlin new non-closable dockables

In our application we store the settings in an xml file. This works fine, but when ever a new SingleCDockable is added, it wont appear on screen. The reason for that new dockables and invisible dockables are handled the same way. Non-closable dockable arn’t in the ClosableList and can’t be made visible by the user.

My solution is to extend the xml and store the id’s of all SingleCDockables. When restoring the settings, I go throug the list of known dockables and make all new dockables visible:

public class ExampleApp extends JFrame {
  private static final long serialVersionUID = -8284759244434428348L;

  public static class SingleDockable extends DefaultSingleCDockable {
    
    public SingleDockable(int id, boolean closeable) {
      super(SingleDockable.class.getSimpleName() + String.valueOf(id), SingleDockable.class.getSimpleName() + " - " + id);
      add(new JLabel(SingleDockable.class.getSimpleName() + " - " + id), BorderLayout.CENTER);
      setCloseable(closeable);
    }
    
  }
  
  private final CControl fControl;
  
  public ExampleApp() {
    fControl = new CControl(this);
    
    CGrid grid = new CGrid(fControl);
    grid.add(0, 0, 1, 1, new SingleDockable(1, true));
    grid.add(1, 0, 1, 1, new SingleDockable(2, true));
    grid.add(0, 1, 1, 1, new SingleDockable(3, false));
    grid.add(1, 1, 1, 1, new SingleDockable(4, false));
    
    //grid.add(0, 2, 2, 1, new SingleDockable(5, false)); //NEW DOCKABLE
    
    fControl.getContentArea().deploy(grid);
    
    getContentPane().add(fControl.getContentArea());
    
    JMenuBar menuBar = new JMenuBar();
    RootMenuPiece rootPiece = new RootMenuPiece("Closables", true, new SingleCDockableListMenuPiece(fControl));
    menuBar.add(rootPiece.getMenu());
    setJMenuBar(menuBar);
    
    setBounds(0, 0, 800, 600);
  }
  
  public void loadSettings(File file) {
    try {
      BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));
      try {
        XElement element = XIO.readUTF(in);
        fControl.readXML(element.getElement("controlSettings"));
//        handleNewDockables(element.getElement("knownDockables"));
      } catch (IOException e) {
        in.close();
      }
    } catch (RuntimeException e) {
      //ignore save new file
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }
  
  private void handleNewDockables(XElement element) {
    Set<String> knownIds = new HashSet<String>();
    for (XElement dockable: element.getElements("dockable")) {
      XAttribute dockableId = dockable.getAttribute("id");
      knownIds.add(dockableId.getValue());
    }
    
    for (int i = 0; i < fControl.getCDockableCount(); i++) {
      CDockable cDockable = fControl.getCDockable(i);
      if (cDockable instanceof SingleCDockable) {
        SingleCDockable singleDockable = (SingleCDockable)cDockable;
        if (knownIds.contains(singleDockable.getUniqueId())) continue;
        handleNewDockable(singleDockable);
      }
    }
  }
  
  private void handleNewDockable(SingleCDockable singleDockable) {
    singleDockable.setVisible(true);
  }
  
  public void saveSettings(File file) {
    XElement root = new XElement("root");
    fControl.writeXML(root.addElement("controlSettings"));
    writeKnownDockables(root.addElement("knownDockables"));
    try {
      BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
      try {
        XIO.writeUTF(root, out);
      } catch (IOException e) {
        out.close();
      }
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }
  
  private void writeKnownDockables(XElement element) {
    for (int i = 0; i < fControl.getCDockableCount(); i++) {
      CDockable cDockable = fControl.getCDockable(i);
      if (cDockable instanceof SingleCDockable) {
        SingleCDockable singleDockable = (SingleCDockable)cDockable;
        element.addElement("dockable").addString("id", singleDockable.getUniqueId());
      }
    }
  }
  
  public static void main(String[] args) {
    final File settingsFile = new File("exampleSettings.xml");
    
    SwingUtilities.invokeLater(new Runnable() {
      
      @Override
      public void run() {
        final ExampleApp exampleApp = new ExampleApp();
        exampleApp.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
        exampleApp.setVisible(true);
        if (settingsFile.exists()) exampleApp.loadSettings(settingsFile);
        
        exampleApp.addWindowListener(new WindowAdapter() {
          
          @Override
          public void windowClosing(WindowEvent e) {
            exampleApp.saveSettings(settingsFile);
          }
          
        });
      }
    });
  }
}

Is there better way?

Best Regards
Heiko

You already have a correct solution, but you might want to hear what other options there are and why the framework behaves like this.

First I may need to clarify that “closeable” only influences whether there is a close-button shown on the Dockable or not. The framework assumes that the client application wont remove the Dockable directly or indirectly - or that the client makes the Dockable invisible on purpose (kind of “if you know what you do, then you are allowed to do it”).

What happens in your case is: when you load the file and thus the layout, the new layout overrides the location and visibility of all Dockables. The client application indirectly removes the Dockable, because a new layout is always stronger than any previous setting. It would be impossible for the framework to guess a good position for all the missing, non-closeable Dockables (it could just open the new items somewhere, but you would certainly not like the effect).

Basically the framework was designed with “either you know all Dockables in advance, or you can make them invisible/visible any time”. Unfortunately your use case does not match.

If you use more than one layout (see CControl.load, CControl.save), then the Dockable might appear some time, but when applying a new layout it may disappear again. To prevent that effect you would need to edit the raw data of the layout, you can do this with perspectives. I’ve added an example at the end of this post, but it will only work with the next release (currently there is a bug preventing it from working - sorry, this API is very new and still has some issues).

Some more solutions to consider:
[ul]
[li]Just make new Dockable closeable. I always see a big question mark when I hear someone talk of a “non” word like “non-hideable”, non-moveable", “non-resizeable”… would it really hurt the application if the user had more freedom to set up his GUI?
[/li][li]Add new Dockables after loading the layout. In this case you have to use “CControl.addDockable”, “CDockable.setLocation” and “CDockable.setVisible(true)”. You would need some mechanism that tells you, which Dockables are new and which not (but you already have implemented that in the example).
[/li][li]Reset/delete the layout(s) if there are new dockables. Kind of harsh but if does not happen often it would be a pragmatic solution.
[/li][/ul]

Example as basis when using more than one layout:


import java.awt.BorderLayout;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenuBar;
import javax.swing.SwingUtilities;

import bibliothek.gui.dock.SplitDockStation.Orientation;
import bibliothek.gui.dock.common.CControl;
import bibliothek.gui.dock.common.CGrid;
import bibliothek.gui.dock.common.DefaultSingleCDockable;
import bibliothek.gui.dock.common.menu.SingleCDockableListMenuPiece;
import bibliothek.gui.dock.common.perspective.CControlPerspective;
import bibliothek.gui.dock.common.perspective.CGridPerspective;
import bibliothek.gui.dock.common.perspective.CPerspective;
import bibliothek.gui.dock.common.perspective.CStackPerspective;
import bibliothek.gui.dock.common.perspective.SingleCDockablePerspective;
import bibliothek.gui.dock.facile.menu.RootMenuPiece;
import bibliothek.gui.dock.perspective.PerspectiveDockable;
import bibliothek.gui.dock.station.split.SplitDockPerspective.Leaf;
import bibliothek.gui.dock.station.split.SplitDockPerspective.Node;
import bibliothek.gui.dock.station.split.SplitDockPerspective.Root;
import bibliothek.gui.dock.station.stack.StackDockPerspective;

public class ExampleApp extends JFrame {
    private static final long serialVersionUID = -8284759244434428348L;

    public static class SingleDockable extends DefaultSingleCDockable {

        public SingleDockable( int id, boolean closeable ){
            super( SingleDockable.class.getSimpleName() + String.valueOf( id ), SingleDockable.class.getSimpleName() + " - " + id );
            add( new JLabel( SingleDockable.class.getSimpleName() + " - " + id ), BorderLayout.CENTER );
            setCloseable( closeable );
        }

    }

    private final CControl fControl;

    public ExampleApp(){
        fControl = new CControl( this );

        CGrid grid = new CGrid( fControl );
        grid.add( 0, 0, 1, 1, new SingleDockable( 1, true ) );
        grid.add( 1, 0, 1, 1, new SingleDockable( 2, true ) );
        grid.add( 0, 1, 1, 1, new SingleDockable( 3, false ) );
        grid.add( 1, 1, 1, 1, new SingleDockable( 4, false ) );

        fControl.getContentArea().deploy( grid );

        getContentPane().add( fControl.getContentArea() );

        JMenuBar menuBar = new JMenuBar();
        RootMenuPiece rootPiece = new RootMenuPiece( "Closables", true, new SingleCDockableListMenuPiece( fControl ) );
        menuBar.add( rootPiece.getMenu() );
        setJMenuBar( menuBar );

        setBounds( 0, 0, 800, 600 );
    }

    // here we add a new dockable
    public void addNewDockable(){
        SingleDockable dockable = new SingleDockable( 5, false );
        fControl.addDockable( dockable );
        sneakInNewDockables( dockable.getUniqueId() );
    }

    // insert one or many dockables on all layouts
    private void sneakInNewDockables( String... ids ){
        CControlPerspective perspectives = fControl.getPerspectives();

        // iterate over all existing layouts and modify them
        for( String name : perspectives.getNames() ) {
            CPerspective perspective = perspectives.getPerspective( name );
            sneakIn( perspective, ids );
            perspectives.setPerspective( name, perspective );
        }

        // modify the current layout and re-apply it
        CPerspective perspective = perspectives.getPerspective( false );
        sneakIn( perspective, ids );
        perspectives.setPerspective( perspective, false );
    }

    private void sneakIn( CPerspective perspective, String... ids ){
        // access the center of the CContentArea
        CGridPerspective center = perspective.getContentArea().getCenter();

        // set up the a handle for the new dockable(s)
        PerspectiveDockable dockable;
        if( ids.length == 1 ) {
            dockable = new SingleCDockablePerspective( ids[0] ).intern().asDockable();
        }
        else {
            CStackPerspective stack = new CStackPerspective();
            for( String id : ids ) {
                stack.add( new SingleCDockablePerspective( id ) );
            }
            dockable = stack;
        }

        // just add the new dockable at the bottom using up 25% of all the available space
        center.grid().addDockable( 0, 100, 100, 25, dockable );
    }

    private void loadSettings( File file ){
        try {
            fControl.readXML( file );
        }
        catch( IOException e ) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    private void saveSettings( File file ){
        try {
            fControl.writeXML( file );
        }
        catch( IOException e ) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public static void main( String[] args ){
        final boolean testing = true;
        final File settingsFile = new File( "exampleSettings.xml" );

        SwingUtilities.invokeLater( new Runnable(){
            @Override
            public void run(){
                final ExampleApp exampleApp = new ExampleApp();
                exampleApp.setDefaultCloseOperation( DISPOSE_ON_CLOSE );
                exampleApp.setVisible( true );
                if( settingsFile.exists() )
                    exampleApp.loadSettings( settingsFile );

                if( testing ) {
                    exampleApp.addNewDockable(); // NEW dockables
                }
                else {
                    exampleApp.addWindowListener( new WindowAdapter(){
                        @Override
                        public void windowClosing( WindowEvent e ){
                            exampleApp.saveSettings( settingsFile );
                        }
                    } );
                }
            }
        } );
    }
}

Many thanks for your answer.
You are right, it may make no sense to hide the close button. I also think my solution is only needed in rare cases, we just don’t want to delete the user settings when ever a new dockable was added.