MultipleCDockable and persistent layout

I am searching for an example, which shows how to use MultipleCDockable dockables and persistent layout with files.

I already tried to save a layout with some MultipleCDockable dockables with using

control.getResources().writeXML(element)

Unfortunately if I load the XML file on application startup with

control.getResources().readXML(element);

and create some MultipleCDockable dockables later with the same IDs used when saving the XML file, the dockables are still not placed, where they should be according to the XML layout file.

I also like to know, how I can find out, if the layout of a MultipleCDockable dockable is part of an XML layout file or not.

That is actually the intended behavior, because you could have an infinite amount of MultipleCDockables the framework does not store the location information forever. Otherwise the layout files would grow without stop.

Only the layout information of visible MultipleCDockables are stored, everything else is ignored.

You can change the behavior by replacing the “MissingeCDockableStrategy”, which can be set by calling “CControl.setMissingStrategy”. If you have a look at “MissingCDockableStrategy” you will find a few constants, for you “STORE” is interesting, as this is a strategy that does never throw away any location information. I warned you: the layout files may start to grow if you use this strategy.

For finding out whether a Dockable is part of a layout you can use the perspective API. Like in this example you need to traverse the tree, and use a lot of “instanceofs” to find the MultipleCDockables:


import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

import bibliothek.gui.dock.common.CControl;
import bibliothek.gui.dock.common.CStation;
import bibliothek.gui.dock.common.MultipleCDockableLayout;
import bibliothek.gui.dock.common.perspective.CDockablePerspective;
import bibliothek.gui.dock.common.perspective.CGridPerspective;
import bibliothek.gui.dock.common.perspective.CPerspective;
import bibliothek.gui.dock.common.perspective.CStationPerspective;
import bibliothek.gui.dock.common.perspective.CommonElementPerspective;
import bibliothek.gui.dock.common.perspective.MultipleCDockablePerspective;
import bibliothek.gui.dock.perspective.PerspectiveDockable;
import bibliothek.gui.dock.perspective.PerspectiveStation;
import bibliothek.util.xml.XElement;

public class FindTest {
	public static void main(String[] args) {
		CControl control = new CControl();
		
		// read one of the loaded perspectives
		// CPerspective perspective = control.getPerspectives().getPerspective("nameOfALayout");
		
		// or read the perspective directly from a part of a file. If you need this I'll have to think a little bit on
		// how to extract the "xmlElementOfAPerspective" easily from the xml file. I think there is some method missing
		// in the framework...
		// CPerspective perspective = control.getPerspectives().readXML(xmlElementOfAPerspective);
		
		// for the test we are going to use a new empty perspective and fill it with some items
		CPerspective perspective = control.getPerspectives().createEmptyPerspective();
		
		CGridPerspective grid = perspective.getContentArea().getCenter();
		grid.gridAdd(0, 1, 1, 1, new MultipleCDockablePerspective("someFactory", "idA", new StringLayout("helloA")));
		grid.gridAdd(0, 0, 1, 1, new MultipleCDockablePerspective("someFactory", "idB", new StringLayout("helloB")));
		grid.gridDeploy();
		
		for( String stationKey : perspective.getStationKeys() ){
			CStationPerspective station = perspective.getStation(stationKey);
			visit( station.intern().asStation() );
		}
	}
	
	private static void visit( PerspectiveStation parent ){
		for( int i = 0, n = parent.getDockableCount(); i<n; i++ ){
			PerspectiveDockable child = parent.getDockable(i);
			if( child.asStation() != null ){
				visit( child.asStation() );
			}
			checkMultiDockable( child );
		}
	}
	
	private static void checkMultiDockable( PerspectiveDockable dockable ){
		if( dockable instanceof CommonElementPerspective ){
			CDockablePerspective cdockable = ((CommonElementPerspective)dockable).getElement().asDockable();
			if( cdockable instanceof MultipleCDockablePerspective ){
				MultipleCDockablePerspective multi = (MultipleCDockablePerspective)cdockable;
				System.out.println( multi.getFactoryID() + " " + multi.getUniqueId() );
			}
		}
	}
	
	private static final class StringLayout implements MultipleCDockableLayout{
		private String content;
		
		public StringLayout( String content ){
			this.content = content;
		}

		@Override
		public void writeStream(DataOutputStream out) throws IOException {
			out.writeUTF(content);
		}

		@Override
		public void readStream(DataInputStream in) throws IOException {
			content = in.readUTF();
		}

		@Override
		public void writeXML(XElement element) {
			element.setString(content);
		}

		@Override
		public void readXML(XElement element) {
			content = element.getString();
		}
	}
}

Thank you very much.

Persistent layout now works.

The next problem with my code is, that I want to load layout on application startup, but some SingleCDockable objects and most of the MultipleCDockable should not be automacally created and displayed on startup.

I found out, that it I can filter out SingleCDockable dockables. But adding a filter to the MultipleCDockableFactory does not work. The method “match” is probably also not appropriate.

Do you have any tip for filtering MultipleCDockable objects in MultipleCDockableFactory?

Here is my example code (adopted from DockingFrames/dock/src/docking-frames-demo-tutorial/tutorial/common/guide/PerspectivesMulti.java):

import bibliothek.gui.dock.common.CControl;
import bibliothek.gui.dock.common.DefaultMultipleCDockable;
import bibliothek.gui.dock.common.MissingCDockableStrategy;
import bibliothek.gui.dock.common.MultipleCDockableFactory;
import bibliothek.gui.dock.common.MultipleCDockableLayout;
import bibliothek.gui.dock.common.SingleCDockable;
import bibliothek.gui.dock.common.SingleCDockableFactory;
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.CWorkingPerspective;
import bibliothek.gui.dock.common.perspective.MultipleCDockablePerspective;
import bibliothek.gui.dock.common.perspective.SingleCDockablePerspective;
import bibliothek.util.Filter;
import bibliothek.util.xml.XElement;
import bibliothek.util.xml.XIO;
import java.awt.BorderLayout;
import java.awt.Color;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.WindowConstants;
import tutorial.support.ColorSingleCDockable;

public class PerspectivesMulti {
    /* Perspectives support SingleCDockables and MultipleCDockables. This example sets up
     * a CWorkingArea with some dockables on it. */

    /* Since we are going to work with MultipleCDockables we will need some factory and 
     * an unique identifier for this factory. That would be this constant. */
    public static final String CUSTOM_MULTI_FACTORY_ID = "custom";

    private enum MODE {

        SAVING, LOADING
    };
    private static MODE mode = MODE.LOADING;

    public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException, IOException {
        JFrame frame = new JFrame();
        CControl control = new CControl(frame);
        control.setMissingStrategy(MissingCDockableStrategy.STORE);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        /* When working with perspectives it is always a good idea first to set up the 
         * CControl, then create the perspectives. */
        ColorFactory colorFactory = new ColorFactory();
        control.addSingleDockableFactory(colorFactory, colorFactory);
        control.addMultipleDockableFactory(CUSTOM_MULTI_FACTORY_ID, new CustomMultiFactory());

        /* By creating the root-stations now we automatically create counterparts in the 
         * perspective. Otherwise we would need to do some setting up with the perspectives as 
         * well. */
        frame.add(control.getContentArea(), BorderLayout.CENTER);

        /* Access to the perspective API */
        CControlPerspective perspectives = control.getPerspectives();

        control.createWorkingArea("work");

        if (mode == MODE.SAVING) {
            /* Creating a new, empty perspective */
            CPerspective perspective = perspectives.createEmptyPerspective();

            /* Now we just drop some random SingleCDockables onto the center area */
            CGridPerspective center = perspective.getContentArea().getCenter();

            center.gridAdd(0, 0, 50, 50, new SingleCDockablePerspective("Red"));
//            center.gridAdd(50, 0, 50, 50, new SingleCDockablePerspective("Green"));
//            center.gridAdd(50, 0, 50, 50, new SingleCDockablePerspective("Blue"));
            center.gridAdd(20, 0, 80, 50, new SingleCDockablePerspective("Green"));
            center.gridAdd(20, 0, 80, 50, new SingleCDockablePerspective("Blue"));

            /* because we called "control.createWorkingArea" we can now access the 
             * CWorkingPerspective with the same unique identifier. We could also just
             * create a new CWorkingPerspective and use "addRoot" to store it. */
            CWorkingPerspective work = (CWorkingPerspective) perspective.getStation("work");

            center.gridAdd(0, 50, 100, 100, work);

            for (int i = 0; i < 5; i++) {
                /* To add MultipleCDockales we only need the unique identifier of their factory,
                 * and their content. The content is an object of type MultipleCDockableLayout.
                 * 
                 * Btw. by using the same position and size for all dockables we can easily 
                 * stack them. */
                CustomMultiLayout layout = new CustomMultiLayout(new Color(20 * i, 50, 0));
                work.gridAdd(0, 0, 100, 100, new MultipleCDockablePerspective(CUSTOM_MULTI_FACTORY_ID, layout));
            }

            /* Finally we apply the perspective we just created */
            perspectives.setPerspective(perspective, true);
            frame.setSize(600, 300);
            frame.setVisible(true);

            File file = new File("/tmp/layout2.xml");
            DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)));

            XElement element = new XElement("docking-layout");
            //control.getResources().writeXML(element);
            control.getPerspectives().writeXML(element, perspective);

            OutputStreamWriter writer = new OutputStreamWriter(out, "UTF-8");
            XIO.write(element, writer);
            writer.close();

            out.flush();
            out.close();
        } else if (mode == MODE.LOADING) {
            File file = new File("/tmp/layout2.xml");
            DataInputStream in;
            in = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));

            InputStreamReader reader = new InputStreamReader(in, "UTF-8");
            XElement element = XIO.read(reader);
            CPerspective perspective = control.getPerspectives().readXML(element);
            //CPerspective perspective = control.getPerspectives().getPerspective("nameOfALayout");            
            //CPerspective perspective = control.getPerspectives().readXML(element);
            reader.close();

            in.close();

            // example: "Red" is added later, but used layout information from layout file
            control.addDockable(colorFactory.createBackup("Red"));

            perspectives.setPerspective(perspective, true);
            frame.setSize(600, 300);
            frame.setVisible(true);
        }
    }

    /* This factory and filter creates new SingleCDockables with some panel that has some
     * special color set as background. */
    private static class ColorFactory implements SingleCDockableFactory, Filter<String> {

        private Map<String, Color> colors = new HashMap<String, Color>();

        public ColorFactory() {
            colors.put("Red", Color.RED);
            colors.put("Green", Color.GREEN);
            colors.put("Blue", Color.BLUE);
            colors.put("Yellow", Color.YELLOW);
            colors.put("White", Color.WHITE);
            colors.put("Black", Color.BLACK);
        }

        @Override
        public boolean includes(String item) {
            // example: "Red" is not available on startup
            if ("Red".equals(item)) {
                return false;
            }
            return colors.containsKey(item);
        }

        @Override
        public SingleCDockable createBackup(String id) {
            return new ColorSingleCDockable(id, colors.get(id));
        }
    }

    /* This is the kind of MultipleCDockable we will use on our application. */
    private static class CustomMultiDockable extends DefaultMultipleCDockable {

        private Color color;

        public CustomMultiDockable(CustomMultiFactory factory, String title, Color color) {
            super(factory, title);
            this.color = color;
            JPanel panel = new JPanel();
            panel.setBackground(color);
            panel.setOpaque(true);
            add(panel, BorderLayout.CENTER);
            //setTitleIcon( new ColorIcon( color ) );
        }

        public Color getColor() {
            return color;
        }
    }

    /* This kind of MultipleCDockableLayout describes the content of a CustomMultiDockable. */
    private static class CustomMultiLayout implements MultipleCDockableLayout {

        private Color color;

        public CustomMultiLayout(Color color) {
            this.color = color;
        }

        public Color getColor() {
            return color;
        }

        @Override
        public void readStream(DataInputStream in) throws IOException {
            color = new Color(in.readInt());
        }

        @Override
        public void readXML(XElement element) {
            color = new Color(element.getInt());
        }

        @Override
        public void writeStream(DataOutputStream out) throws IOException {
            out.writeInt(color.getRGB());
        }

        @Override
        public void writeXML(XElement element) {
            element.setInt(color.getRGB());
        }
    }

    /* And this factory creates new CustomMultiDockables and new CustomMultiLayouts when 
     * the framework needs them. */
    private static class CustomMultiFactory implements MultipleCDockableFactory<CustomMultiDockable, CustomMultiLayout> {

        @Override
        public CustomMultiLayout create() {
            return new CustomMultiLayout(null);
        }

        @Override
        public boolean match(CustomMultiDockable dockable, CustomMultiLayout layout) {
            return dockable.getColor().equals(layout.getColor());
        }

        @Override
        public CustomMultiDockable read(CustomMultiLayout layout) {
            Color color = layout.getColor();
            String title = "R=" + color.getRed() + ", G=" + color.getGreen() + ", B=" + color.getBlue();
            return new CustomMultiDockable(this, title, color);
        }

        @Override
        public CustomMultiLayout write(CustomMultiDockable dockable) {
            return new CustomMultiLayout(dockable.getColor());
        }
    }
}```

The factories are allowed to return “null” as a new Dockable (methods “createBackup” and “read”). If a “null” Dockable is returned, the framework has no other option that to ignore it (for now at least).

Thanks. Returning “null” in the MultipleCDockableFactory works.

But I have still no luck with persistent layouts. My example program shows the problem:

  • At first a perspective with 5 multiple dockables is created and saved (set “mode = MODE.SAVING;”)
  • After loading (set mode = MODE.LOADING) the multiple dockables should be invisible at first
  • Adding only 3 multiple dockables later does work, but they do not use the stored layout position,
    they are located on top left

Of course I could work around the problem by using a working area for all multiple dockables. Unfortunately my users already have an program version, where they can freely move and arrange a fixed number of editor windows and they do not want to loose functionality. I moved from Core to Common, because I thought, that Common is better in dealing with an unknown number of editor windows and persitent layouts. But, I still do not know, how I can prepare different perspectives/layouts for an unknown number of editor windows.


import bibliothek.gui.dock.common.CControl;
import bibliothek.gui.dock.common.DefaultMultipleCDockable;
import bibliothek.gui.dock.common.MissingCDockableStrategy;
import bibliothek.gui.dock.common.MultipleCDockableFactory;
import bibliothek.gui.dock.common.MultipleCDockableLayout;
import bibliothek.gui.dock.common.SingleCDockable;
import bibliothek.gui.dock.common.SingleCDockableFactory;
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.CWorkingPerspective;
import bibliothek.gui.dock.common.perspective.MultipleCDockablePerspective;
import bibliothek.gui.dock.common.perspective.SingleCDockablePerspective;
import bibliothek.util.Filter;
import bibliothek.util.xml.XElement;
import bibliothek.util.xml.XIO;
import java.awt.BorderLayout;
import java.awt.Color;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.WindowConstants;
import tutorial.support.ColorSingleCDockable;

public class PerspectivesMulti {
    /* Perspectives support SingleCDockables and MultipleCDockables. This example sets up
     * a CWorkingArea with some dockables on it. */

    /* Since we are going to work with MultipleCDockables we will need some factory and 
     * an unique identifier for this factory. That would be this constant. */
    public static final String CUSTOM_MULTI_FACTORY_ID = "custom";

    private enum MODE {

        SAVING, LOADING
    };
    private static MODE mode = MODE.LOADING;

    public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException, IOException {
        JFrame frame = new JFrame();
        CControl control = new CControl(frame);
        control.setMissingStrategy(MissingCDockableStrategy.STORE);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        /* When working with perspectives it is always a good idea first to set up the 
         * CControl, then create the perspectives. */
        ColorFactory colorFactory = new ColorFactory();
        colorFactory.setFilterEnabled(false);
        control.addSingleDockableFactory(colorFactory, colorFactory);
        CustomMultiFactory customFactory = new CustomMultiFactory();
        customFactory.setFilterEnabled(false);
        control.addMultipleDockableFactory(CUSTOM_MULTI_FACTORY_ID, customFactory);

        /* By creating the root-stations now we automatically create counterparts in the 
         * perspective. Otherwise we would need to do some setting up with the perspectives as 
         * well. */
        frame.add(control.getContentArea(), BorderLayout.CENTER);

        /* Access to the perspective API */
        CControlPerspective perspectives = control.getPerspectives();

        control.createWorkingArea("work");

        if (mode == MODE.SAVING) {
            /* Creating a new, empty perspective */
            CPerspective perspective = perspectives.createEmptyPerspective();

            /* Now we just drop some random SingleCDockables onto the center area */
            CGridPerspective center = perspective.getContentArea().getCenter();

            center.gridAdd(0, 0, 50, 50, new SingleCDockablePerspective("Red"));
//            center.gridAdd(50, 0, 50, 50, new SingleCDockablePerspective("Green"));
//            center.gridAdd(50, 0, 50, 50, new SingleCDockablePerspective("Blue"));
            center.gridAdd(20, 0, 80, 50, new SingleCDockablePerspective("Green"));
            center.gridAdd(20, 0, 80, 50, new SingleCDockablePerspective("Blue"));

            /* because we called "control.createWorkingArea" we can now access the 
             * CWorkingPerspective with the same unique identifier. We could also just
             * create a new CWorkingPerspective and use "addRoot" to store it. */
            CWorkingPerspective work = (CWorkingPerspective) perspective.getStation("work");

            center.gridAdd(0, 50, 100, 100, work);

            for (int i = 0; i < 5; i++) {
                /* To add MultipleCDockales we only need the unique identifier of their factory,
                 * and their content. The content is an object of type MultipleCDockableLayout.
                 * 
                 * Btw. by using the same position and size for all dockables we can easily 
                 * stack them. */
                CustomMultiLayout layout = new CustomMultiLayout(new Color(20 * i, 50, 0));
                work.gridAdd(20 * i, 0, 20, 100, new MultipleCDockablePerspective(CUSTOM_MULTI_FACTORY_ID, layout));
            }

            /* Finally we apply the perspective we just created */
            perspectives.setPerspective(perspective, true);
            frame.setSize(600, 300);
            frame.setVisible(true);

            File file = new File("/tmp/layout-p1.xml");
            DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)));

            XElement element = new XElement("docking-layout");
            control.getPerspectives().writeXML(element, perspective);

            OutputStreamWriter writer = new OutputStreamWriter(out, "UTF-8");
            XIO.write(element, writer);
            writer.close();

            out.flush();
            out.close();


        } else if (mode == MODE.LOADING) {
            colorFactory.setFilterEnabled(true);
            customFactory.setFilterEnabled(true);
            File file = new File("/tmp/layout-p1.xml");
            DataInputStream in;
            in = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));

            InputStreamReader reader = new InputStreamReader(in, "UTF-8");
            XElement element = XIO.read(reader);
            CPerspective perspective = control.getPerspectives().readXML(element);
            reader.close();
            in.close();

            colorFactory.setFilterEnabled(false);
            customFactory.setFilterEnabled(false);

            // example: single CDockable "Red" is added later, but used layout information from layout file
            ColorSingleCDockable dockable2 = (ColorSingleCDockable) colorFactory.createBackup("Red");
            control.addDockable(dockable2);
            dockable2.setVisible(true);

            perspectives.setPerspective(perspective, true);

            // add all multiple dockables later dynamically
            // problem: the dockables do not use the stored layout position
            for (int i = 0; i < 3; i++) {
                CustomMultiDockable dockable3 = new CustomMultiDockable(customFactory, "D" + (i + 1), new Color(20 * i, 50, 0));
                control.addDockable(dockable3);
                dockable3.setVisible(true);
            }


            frame.setSize(600, 300);
            frame.setVisible(true);
        }
    }

    /* This factory and filter creates new SingleCDockables with some panel that has some
     * special color set as background. */
    private static class ColorFactory implements SingleCDockableFactory, Filter<String> {

        private boolean filterEnabled = true;
        private Map<String, Color> colors = new HashMap<String, Color>();

        public ColorFactory() {
            colors.put("Red", Color.RED);
            colors.put("Green", Color.GREEN);
            colors.put("Blue", Color.BLUE);
            colors.put("Yellow", Color.YELLOW);
            colors.put("White", Color.WHITE);
            colors.put("Black", Color.BLACK);
        }

        @Override
        public boolean includes(String item) {
            // example: "Red" is not available on startup
            if (filterEnabled && "Red".equals(item)) {
                return false;
            }
            return colors.containsKey(item);
        }

        @Override
        public SingleCDockable createBackup(String id) {
            return new ColorSingleCDockable(id, colors.get(id));
        }

        public void setFilterEnabled(boolean filterEnabled) {
            this.filterEnabled = filterEnabled;
        }
    }

    /* This is the kind of MultipleCDockable we will use on our application. */
    private static class CustomMultiDockable extends DefaultMultipleCDockable {

        private Color color;

        public CustomMultiDockable(CustomMultiFactory factory, String title, Color color) {
            super(factory, title);
            this.color = color;
            JPanel panel = new JPanel();
            panel.setBackground(color);
            panel.setOpaque(true);
            add(panel, BorderLayout.CENTER);
            //setTitleIcon( new ColorIcon( color ) );
        }

        public Color getColor() {
            return color;
        }
    }

    /* This kind of MultipleCDockableLayout describes the content of a CustomMultiDockable. */
    private static class CustomMultiLayout implements MultipleCDockableLayout {

        private Color color;

        public CustomMultiLayout(Color color) {
            this.color = color;
        }

        public Color getColor() {
            return color;
        }

        @Override
        public void readStream(DataInputStream in) throws IOException {
            color = new Color(in.readInt());
        }

        @Override
        public void readXML(XElement element) {
            color = new Color(element.getInt());
        }

        @Override
        public void writeStream(DataOutputStream out) throws IOException {
            out.writeInt(color.getRGB());
        }

        @Override
        public void writeXML(XElement element) {
            element.setInt(color.getRGB());
        }
    }

    /* And this factory creates new CustomMultiDockables and new CustomMultiLayouts when 
     * the framework needs them. */
    private static class CustomMultiFactory implements MultipleCDockableFactory<CustomMultiDockable, CustomMultiLayout> {

        private boolean filterEnabled = true;

        @Override
        public CustomMultiLayout create() {
            return new CustomMultiLayout(null);
        }

        @Override
        public boolean match(CustomMultiDockable dockable, CustomMultiLayout layout) {
            return dockable.getColor().equals(layout.getColor());
        }

        @Override
        public CustomMultiDockable read(CustomMultiLayout layout) {
            Color color = layout.getColor();
            String title = "R=" + color.getRed() + ", G=" + color.getGreen() + ", B=" + color.getBlue();

            if (filterEnabled) {
                return null;
            }

            return new CustomMultiDockable(this, title, color);
        }

        @Override
        public CustomMultiLayout write(CustomMultiDockable dockable) {
            return new CustomMultiLayout(dockable.getColor());
        }

        public void setFilterEnabled(boolean filterEnabled) {
            this.filterEnabled = filterEnabled;
        }
    }
}

import java.awt.Color;

import javax.swing.JPanel;

import bibliothek.gui.dock.common.DefaultSingleCDockable;

public class ColorSingleCDockable extends DefaultSingleCDockable{
	private JPanel panel = new JPanel();
	
	public ColorSingleCDockable( String title, Color color ){
		this( title, color, 1.0f );
	}
	
	public ColorSingleCDockable( String title, Color color, float brightness ){
		super( title );
		setTitleText( title );

		if( brightness != 1.0 ){
			float[] hsb = Color.RGBtoHSB( color.getRed(), color.getGreen(), color.getBlue(), null );
			
			hsb[1] = Math.min( 1.0f, hsb[1] / brightness );
			hsb[2] = Math.min( 1.0f, hsb[2] * brightness );
			
			color = Color.getHSBColor( hsb[0], hsb[1], hsb[2] );
		}

		setColor( color );
	}
	
	public void setColor( Color color ){
		panel = new JPanel();
		panel.setOpaque( true );
		panel.setBackground( color );
		add( panel );
		//setTitleIcon( new ColorIcon( color ) );
	}
	
	public Color getColor(){
		return panel.getBackground();
	}
}

I have to apologize, there was a bug in the framework preventing you from writing files and storing the location of invisible dockables. The good news: version 1.1.1p8f is online which solves this issue.

Your client needs some additional changes: the multiple-dockables need identifiers. When creating the perspective use something like this:

MultipleCDockablePerspective item = new MultipleCDockablePerspective( CUSTOM_MULTI_FACTORY_ID, "id" + i, layout);

And when making the dockable visible use something like this:

control.addDockable("id" + i, dockable3);

Further more you may now receive an exception if you try to register more than one Dockable with the same identifier. So enable your factory after you load the layout:


            customFactory.setFilterEnabled(false);```

Since you create the dockables yourself the framework does not configure the working area. So before making your dockables visible you need to set it yourself:
```dockable3.setWorkingArea( control.getStation( "work" ) );```

I think that's it, at least on my machine your example is now working as it should.

Thank you. It works.

I found that calling dockable.setVisible(false) removes a multiple dockable from internal registers such as control.getRegister().getMultipleDockables() or control.getRegister().getDockables(). This happens only for MultipleCDockable dockables, not for SingleCDockable dockables.

Calling dockable.setVisible(true) after dockable.setVisible(false) has the effect, that the multiple dockable moves to the top left position.

The background is, that I try to make a “Window” menu with all single and multiple dockables. The user can show and hide existing dockables in this menu. Using the RootMenuPiece class

    RootMenuPiece settings = new RootMenuPiece( "View", false );
    settings.add( new SingleCDockableListMenuPiece( control ));

does not fully solve my problem, because I miss a class for multiple dockables.

As a work-around, I minimize multiple dockables with dockable.setExtendedMode(ExtendedMode.MINIMIZED) instead of hiding them with dockable.setVisible(false).

Is it possible to hide multiple dockable without removing it from the internal register and without loosing the layout location of these dockables?

(If it’s helpful I can extend my example code above to show the problem.)

DefaultMultipleCDockable has a method called „setRemoveOnClose“, call it with an argument of „false“ :slight_smile:

There is no menu that shows MultipleCDockable, you’ll have to implement that yourself.

Thank you.

I still have to deal with a small persistent layout problem.

My first example program only saves the initial perspective.

But now I want to save snapshot perspectives: The user should arrange it’s editor windows (for instance in a stack or special sequence). At the end, the program should save the current layout in a autosave function (not used in example) and the user should be able to save layout with a menu button at any time.

The following works as expected:

  • start example
  • start two editors (with menu)
  • arrange editor windows in a sequence (1: left, 2: right)
  • save snapshot layout
  • restart program
  • load layout
  • start two editors

Unfortunately the snapshot perspective does not correctly save invisible multiple dockables. I find the invisible multiple dockables in XML file, but without positions.

Please look at the following example:

  • start example program
  • start two editors (with menu)
  • arrange editor windows in a sequence (1: left, 2: right)
  • close editor windows
  • save snapshot layout
  • restart program
  • load layout
  • start two editors

Program: The “load layout” action has no effect. The editors are arranged in a stack.

import bibliothek.gui.dock.common.CControl;
import bibliothek.gui.dock.common.DefaultMultipleCDockable;
import bibliothek.gui.dock.common.DefaultSingleCDockable;
import bibliothek.gui.dock.common.MissingCDockableStrategy;
import bibliothek.gui.dock.common.MultipleCDockableFactory;
import bibliothek.gui.dock.common.MultipleCDockableLayout;
import bibliothek.gui.dock.common.SingleCDockable;
import bibliothek.gui.dock.common.SingleCDockableFactory;
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.CWorkingPerspective;
import bibliothek.gui.dock.common.perspective.MultipleCDockablePerspective;
import bibliothek.gui.dock.common.perspective.SingleCDockablePerspective;
import bibliothek.util.Filter;
import bibliothek.util.xml.XElement;
import bibliothek.util.xml.XIO;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.WindowConstants;

public class PerspectivesMulti {
    /* Perspectives support SingleCDockables and MultipleCDockables. This example sets up
     * a CWorkingArea with some dockables on it. */

    /* Since we are going to work with MultipleCDockables we will need some factory and
     * an unique identifier for this factory. That would be this constant. */
    public static final String CUSTOM_MULTI_FACTORY_ID = "custom";
    private static CControl control;
    private static CPerspective initialPerspective;

    private enum MODE {

        SAVE_INITIAL_LAYOUT, SAVE_SNAPSHOT_LAYOUT, LOAD
    };
    private static MODE mode = MODE.LOAD;

    public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException, IOException {
        JFrame frame = new JFrame();
        control = new CControl(frame);
        control.setMissingStrategy(MissingCDockableStrategy.STORE);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        /* When working with perspectives it is always a good idea first to set up the
         * CControl, then create the perspectives. */
        ColorFactory colorFactory = new ColorFactory();
        colorFactory.setFilterEnabled(false);
        control.addSingleDockableFactory(colorFactory, colorFactory);
        CustomMultiFactory customFactory = new CustomMultiFactory();
        customFactory.setFilterEnabled(true);
        control.addMultipleDockableFactory(CUSTOM_MULTI_FACTORY_ID, customFactory);

        /* By creating the root-stations now we automatically create counterparts in the
         * perspective. Otherwise we would need to do some setting up with the perspectives as
         * well. */
        frame.add(control.getContentArea(), BorderLayout.CENTER);

        JMenuBar menuBar = new JMenuBar();

        JMenu layoutMenu = new JMenu("Layout");
        menuBar.add(layoutMenu);
        
        JMenuItem newEditorItem = new JMenuItem("New editor");
        layoutMenu.add(newEditorItem);

        JMenuItem saveMenuItem1 = new JMenuItem("Save initial layout");
        layoutMenu.add(saveMenuItem1);

        JMenuItem saveMenuItem2 = new JMenuItem("Save snapshot layout");
        layoutMenu.add(saveMenuItem2);

        JMenuItem loadMenuItem = new JMenuItem("Load layout");
        layoutMenu.add(loadMenuItem);

        JMenuItem resetMenuItem = new JMenuItem("Reset layout");
        layoutMenu.add(resetMenuItem);

        frame.add(menuBar, BorderLayout.NORTH);

                newEditorItem.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    createEditor();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });
                
        saveMenuItem1.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    saveLayout(MODE.SAVE_INITIAL_LAYOUT);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });

        saveMenuItem2.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    saveLayout(MODE.SAVE_SNAPSHOT_LAYOUT);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });

        loadMenuItem.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    loadLayout();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });
        
                resetMenuItem.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    resetLayout();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });

        /* Access to the perspective API */
        CControlPerspective perspectives = control.getPerspectives();

        control.createWorkingArea("work");

        initialPerspective = perspectives.createEmptyPerspective();

        /* Now we just drop some random SingleCDockables onto the center area */
        CGridPerspective center = initialPerspective.getContentArea().getCenter();

        center.gridAdd(0, 0, 50, 50, new SingleCDockablePerspective("Red"));
//            center.gridAdd(50, 0, 50, 50, new SingleCDockablePerspective("Green"));
//            center.gridAdd(50, 0, 50, 50, new SingleCDockablePerspective("Blue"));
        center.gridAdd(20, 0, 80, 50, new SingleCDockablePerspective("Green"));
        center.gridAdd(20, 0, 80, 50, new SingleCDockablePerspective("Blue"));

        /* because we called "control.createWorkingArea" we can now access the
         * CWorkingPerspective with the same unique identifier. We could also just
         * create a new CWorkingPerspective and use "addRoot" to store it. */
        CWorkingPerspective work = (CWorkingPerspective) initialPerspective.getStation("work");

        center.gridAdd(0, 50, 100, 100, work);

        MultipleCDockablePerspective[] cp = new MultipleCDockablePerspective[5];
        for (int i = 0; i < 5; i++) {
            /* To add MultipleCDockales we only need the unique identifier of their factory,
             * and their content. The content is an object of type MultipleCDockableLayout.
             *
             * Btw. by using the same position and size for all dockables we can easily
             * stack them. */
            CustomMultiLayout layout = new CustomMultiLayout(i);
            cp** = new MultipleCDockablePerspective(CUSTOM_MULTI_FACTORY_ID, "id" + i, layout);
            work.gridAdd(20 * i, 0, 20, 100, cp**);
        }

        /* Finally we apply the perspective we just created */
        perspectives.setPerspective(initialPerspective, true);
        frame.setSize(600, 300);
        frame.setVisible(true);
    }
    
    private static int editorNumber=0;
    
    private static void createEditor()
    {
        CustomMultiLayout layout=new CustomMultiLayout(editorNumber);
        CustomMultiFactory factory = (CustomMultiFactory) control.getMultipleDockableFactory(CUSTOM_MULTI_FACTORY_ID);
        CustomMultiDockable dock=new CustomMultiDockable(factory, "D" + (editorNumber+1), layout.getNumber());
        editorNumber++;      
        
        dock.setWorkingArea(control.getStation("work"));
        dock.setCloseable(true);
        dock.setRemoveOnClose(false);
        control.addDockable(dock);
        dock.setVisible(true);
    }
    
    private static void saveLayout(MODE mode) throws FileNotFoundException, UnsupportedEncodingException, IOException {
        CPerspective perspective;

        if (mode == MODE.SAVE_SNAPSHOT_LAYOUT) {
            // create a snapshot perspective
            perspective = control.getPerspectives().getPerspective(true);

            // try to add invisible dockable perspective for dockable 4
            //perspective.putDockable(cp[4]);
        } else {
            perspective = initialPerspective;
        }

        File file = new File("layout-p1.xml");
        DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)));

        XElement element = new XElement("docking-layout");
        control.getPerspectives().writeXML(element, perspective);

        OutputStreamWriter writer = new OutputStreamWriter(out, "UTF-8");
        XIO.write(element, writer);
        writer.close();

        out.flush();
        out.close();
    }

    private static void loadLayout() throws UnsupportedEncodingException, IOException {
        File file = new File("layout-p1.xml");
        DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));

        InputStreamReader reader = new InputStreamReader(in, "UTF-8");
        XElement element = XIO.read(reader);
        CPerspective perspective = control.getPerspectives().readXML(element);
        reader.close();
        in.close();

        control.getPerspectives().setPerspective(perspective, true);
    }
    
    private static void resetLayout()
    {
        control.getPerspectives().setPerspective(initialPerspective, true);
    }

    /* This factory and filter creates new SingleCDockables with some panel that has some
     * special color set as background. */
    private static class ColorFactory implements SingleCDockableFactory, Filter<String> {

        private boolean filterEnabled = true;
        private Map<String, Color> colors = new HashMap<String, Color>();

        public ColorFactory() {
            colors.put("Red", Color.RED);
            colors.put("Green", Color.GREEN);
            colors.put("Blue", Color.BLUE);
            colors.put("Yellow", Color.YELLOW);
            colors.put("White", Color.WHITE);
            colors.put("Black", Color.BLACK);
        }

        @Override
        public boolean includes(String item) {
            // example: "Red" is not available on startup
            if (filterEnabled && "Red".equals(item)) {
                return false;
            }
            return colors.containsKey(item);
        }

        @Override
        public SingleCDockable createBackup(String id) {
            return new ColorSingleCDockable(id, colors.get(id));
        }

        public void setFilterEnabled(boolean filterEnabled) {
            this.filterEnabled = filterEnabled;
        }
    }

    private static Color numberToColor(int number) {
        switch (number) {
            case 0:
                return Color.BLUE;
            case 1:
                return Color.CYAN;
            case 2:
                return Color.GRAY;
            case 3:
                return Color.GREEN;
            case 4:
                return Color.MAGENTA;
            case 5:
                return Color.ORANGE;
            default:
                return Color.WHITE;
        }
    }

    /* This is the kind of MultipleCDockable we will use on our application. */
    private static class CustomMultiDockable extends DefaultMultipleCDockable {

        private int number;

        public CustomMultiDockable(CustomMultiFactory factory, String title, int number) {
            super(factory, title);
            this.number = number;
            JPanel panel = new JPanel();
            panel.setBackground(numberToColor(number));
            panel.setOpaque(true);
            add(panel, BorderLayout.CENTER);
            //setTitleIcon( new ColorIcon( color ) );
        }

        public int getNumber() {
            return number;
        }
    }

    /* This kind of MultipleCDockableLayout describes the content of a CustomMultiDockable. */
    private static class CustomMultiLayout implements MultipleCDockableLayout {

        private int number;

        public CustomMultiLayout(int number) {
            this.number = number;
        }

        private int getNumber() {
            return number;
        }

        @Override
        public void readStream(DataInputStream in) throws IOException {
            number = in.readInt();
        }

        @Override
        public void readXML(XElement element) {
            number = element.getInt();
        }

        @Override
        public void writeStream(DataOutputStream out) throws IOException {
            out.writeInt(number);
        }

        @Override
        public void writeXML(XElement element) {
            element.setInt(number);
        }
    }

    /* And this factory creates new CustomMultiDockables and new CustomMultiLayouts when
     * the framework needs them. */
    private static class CustomMultiFactory implements MultipleCDockableFactory<CustomMultiDockable, CustomMultiLayout> {

        private boolean filterEnabled = true;

        @Override
        public CustomMultiLayout create() {
            return new CustomMultiLayout(-1);
        }

        @Override
        public boolean match(CustomMultiDockable dockable, CustomMultiLayout layout) {
            return dockable.getNumber() == layout.getNumber();
        }

        @Override
        public CustomMultiDockable read(CustomMultiLayout layout) {
            int number = layout.getNumber();
            String title = "D" + (layout.getNumber() + 1); //"R=" + color.getRed() + ", G=" + color.getGreen() + ", B=" + color.getBlue();

            if (filterEnabled) {
                return null;
            }
            CustomMultiDockable multi = new CustomMultiDockable(this, title, layout.getNumber());
            multi.setRemoveOnClose(false);
            multi.setCloseable(true);
            return multi;
        }

        @Override
        public CustomMultiLayout write(CustomMultiDockable dockable) {
            return new CustomMultiLayout(dockable.getNumber());
        }

        public void setFilterEnabled(boolean filterEnabled) {
            this.filterEnabled = filterEnabled;
        }
    }

    public static class ColorSingleCDockable extends DefaultSingleCDockable {

        private JPanel panel = new JPanel();

        public ColorSingleCDockable(String title, Color color) {
            this(title, color, 1.0f);
        }

        public ColorSingleCDockable(String title, Color color, float brightness) {
            super(title);
            setTitleText(title);

            if (brightness != 1.0) {
                float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null);

                hsb[1] = Math.min(1.0f, hsb[1] / brightness);
                hsb[2] = Math.min(1.0f, hsb[2] * brightness);

                color = Color.getHSBColor(hsb[0], hsb[1], hsb[2]);
            }

            setColor(color);
        }

        public void setColor(Color color) {
            panel = new JPanel();
            panel.setOpaque(true);
            panel.setBackground(color);
            add(panel);
            //setTitleIcon( new ColorIcon( color ) );
        }

        public Color getColor() {
            return panel.getBackground();
        }
    }
}

You are right. In one of the conversions the invisible dockables were just ignored. I’ve wrote a little bugfix, you can try it out with this version. At least your current example should work with it.

I better not promise this to be the last bug you encounter. Sorry, you have the bad luck of being the first one really trying to use the perspective API for persistence storage of layouts and making heavy use of invisible Dockables. Personally I would advice to just use CControl.writeXML/readXML when possible, because these methods should be more reliable.

(On the bright side, you have elected to test about the hardest possible case. So you just covered a lot of code, most issues should already have shown up by now.)

Working with invisible dockables is ok now. Thanks.

A remaining issue is, that single dockables which were invisible during saving, are not created, if I load the perspective from an XML file. May be, this is not a big issue, because I can check all single dockables after loading the perspective from a file and create the missing single dockables manually.

I tried a tip from you without success: How I can load a perspective from a XML file with the methods CControl.readXML and CControl.writeXML?

I changed my old code from:

XElement element = new XElement("docking-layout");
control.getPerspectives().writeXML(element, currentPerspective);
XIO.write(element, writer);
...
XElement element = XIO.read(reader);
CPerspective perspective2 = control.getPerspectives().readXML(element);
...```
to 
```...
XElement element = new XElement("docking-layout");
control.writeXML(element);
XIO.write(element, writer);
...
XElement element = XIO.read(reader);
control.readXML(element);
CPerspective perspective2 = control.getPerspectives().getPerspective("???");
...```
I do not know the name of the perspective to load and also the list of loaded perspectives seems to be empty (tested in debugger). Using the files directly with CControl.readXML(file) and CControl.writeXML(file) did not solve the problem for me.

To give the layouts/perspectives a name you have to call „CControl.save()“. And any layout you saved that way can be loaded again with „CControl.load()“. CControl.write/read will read/write all these layouts in one big file.

You get the perspective for these layouts with „getPerspective()“. The current layout does not have a name (it just „is“), but for that you have the method „getPerspective(boolean)“.

A remaining issue is, that single dockables which were invisible during saving, are not created

I’m not certain how you mean that. The framework won’t create Dockables if it does not actually use them (meaning: show them). Or is there still some information loss?

I think, I had an error in my application logic. After fixing this, the visibility problem gone away.

But I tried again to use CControl.readXML and CControl.writeXML with your hints. This works now in principle. But I have a problem with multiple dockables. They get invisible after loading a perspective with CControl.readXML.

I changed the example so, that both APIs can be compared.

First, let „useControlXMLAPI = true“ to use CControl.readXML and CControl.writeXML. The following does not work:

[ol]
[li]Click „New editor“ two times
[/li][li]Click „Save snapshot layout“
[/li][li]Click „Load layout“
[/li][li]Problem: The editors get invisible
[/li][/ol]

Now try the same with „useControlXMLAPI = false“ to use CControl.getPerspectives().readXML(element) and CControl.getPerspectives().writeXML. The editors stay visible, which is ok.

import bibliothek.gui.dock.common.DefaultMultipleCDockable;
import bibliothek.gui.dock.common.DefaultSingleCDockable;
import bibliothek.gui.dock.common.MissingCDockableStrategy;
import bibliothek.gui.dock.common.MultipleCDockableFactory;
import bibliothek.gui.dock.common.MultipleCDockableLayout;
import bibliothek.gui.dock.common.SingleCDockable;
import bibliothek.gui.dock.common.SingleCDockableFactory;
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.CWorkingPerspective;
import bibliothek.gui.dock.common.perspective.MultipleCDockablePerspective;
import bibliothek.gui.dock.common.perspective.SingleCDockablePerspective;
import bibliothek.util.Filter;
import bibliothek.util.xml.XElement;
import bibliothek.util.xml.XIO;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.WindowConstants;

public class PerspectivesMulti {
    /* Perspectives support SingleCDockables and MultipleCDockables. This example sets up
     * a CWorkingArea with some dockables on it. */

    /* Since we are going to work with MultipleCDockables we will need some factory and
     * an unique identifier for this factory. That would be this constant. */
    public static final String CUSTOM_MULTI_FACTORY_ID = "custom";
    private static CControl control;
    private static CPerspective initialPerspective;

    private enum MODE {

        SAVE_INITIAL_LAYOUT, SAVE_SNAPSHOT_LAYOUT, LOAD
    };
//    private static MODE mode = MODE.LOAD;
    private static final boolean useControlXMLAPI = true;

    public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException, IOException {
        JFrame frame = new JFrame();
        control = new CControl(frame);
        control.setMissingStrategy(MissingCDockableStrategy.STORE);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        /* When working with perspectives it is always a good idea first to set up the
         * CControl, then create the perspectives. */
        ColorFactory colorFactory = new ColorFactory();
        colorFactory.setFilterEnabled(false);
        control.addSingleDockableFactory(colorFactory, colorFactory);
        CustomMultiFactory customFactory = new CustomMultiFactory();
        customFactory.setFilterEnabled(true);
        control.addMultipleDockableFactory(CUSTOM_MULTI_FACTORY_ID, customFactory);

        /* By creating the root-stations now we automatically create counterparts in the
         * perspective. Otherwise we would need to do some setting up with the perspectives as
         * well. */
        frame.add(control.getContentArea(), BorderLayout.CENTER);

        JMenuBar menuBar = new JMenuBar();

        JMenu layoutMenu = new JMenu("Layout");
        menuBar.add(layoutMenu);

        JMenuItem newEditorItem = new JMenuItem("New editor");
        layoutMenu.add(newEditorItem);

        JMenuItem saveMenuItem1 = new JMenuItem("Save initial layout");
        layoutMenu.add(saveMenuItem1);

        JMenuItem saveMenuItem2 = new JMenuItem("Save snapshot layout");
        layoutMenu.add(saveMenuItem2);

        JMenuItem loadMenuItem = new JMenuItem("Load layout");
        layoutMenu.add(loadMenuItem);

        JMenuItem resetMenuItem = new JMenuItem("Reset layout");
        layoutMenu.add(resetMenuItem);

        frame.add(menuBar, BorderLayout.NORTH);

        newEditorItem.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    createEditor();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });

        saveMenuItem1.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    saveLayout(MODE.SAVE_INITIAL_LAYOUT);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });

        saveMenuItem2.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    saveLayout(MODE.SAVE_SNAPSHOT_LAYOUT);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });

        loadMenuItem.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    loadLayout();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });

        resetMenuItem.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    resetLayout();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });

        /* Access to the perspective API */
        CControlPerspective perspectives = control.getPerspectives();

        control.createWorkingArea("work");

        initialPerspective = perspectives.createEmptyPerspective();

        /* Now we just drop some random SingleCDockables onto the center area */
        CGridPerspective center = initialPerspective.getContentArea().getCenter();

        center.gridAdd(0, 0, 50, 50, new SingleCDockablePerspective("Red"));
//            center.gridAdd(50, 0, 50, 50, new SingleCDockablePerspective("Green"));
//            center.gridAdd(50, 0, 50, 50, new SingleCDockablePerspective("Blue"));
        center.gridAdd(20, 0, 80, 50, new SingleCDockablePerspective("Green"));
        center.gridAdd(20, 0, 80, 50, new SingleCDockablePerspective("Blue"));

        /* because we called "control.createWorkingArea" we can now access the
         * CWorkingPerspective with the same unique identifier. We could also just
         * create a new CWorkingPerspective and use "addRoot" to store it. */
        CWorkingPerspective work = (CWorkingPerspective) initialPerspective.getStation("work");

        center.gridAdd(0, 50, 100, 100, work);

        MultipleCDockablePerspective[] cp = new MultipleCDockablePerspective[5];
        for (int i = 0; i < 5; i++) {
            /* To add MultipleCDockales we only need the unique identifier of their factory,
             * and their content. The content is an object of type MultipleCDockableLayout.
             *
             * Btw. by using the same position and size for all dockables we can easily
             * stack them. */
            CustomMultiLayout layout = new CustomMultiLayout(i);
            cp** = new MultipleCDockablePerspective(CUSTOM_MULTI_FACTORY_ID, "id" + i, layout);
            work.gridAdd(20 * i, 0, 20, 100, cp**);
        }

        /* Finally we apply the perspective we just created */
        perspectives.setPerspective(initialPerspective, true);
        frame.setSize(600, 300);
        frame.setVisible(true);
    }
    private static int editorNumber = 0;

    private static void createEditor() {
        CustomMultiLayout layout = new CustomMultiLayout(editorNumber);
        CustomMultiFactory factory = (CustomMultiFactory) control.getMultipleDockableFactory(CUSTOM_MULTI_FACTORY_ID);
        CustomMultiDockable dock = new CustomMultiDockable(factory, "D" + (editorNumber + 1), layout.getNumber());
        editorNumber++;

        dock.setWorkingArea(control.getStation("work"));
        dock.setCloseable(true);
        dock.setRemoveOnClose(false);
        control.addDockable(dock);
        dock.setVisible(true);
    }

    private static void saveLayout(MODE mode) throws FileNotFoundException, UnsupportedEncodingException, IOException {
        CPerspective perspective;

        if (mode == MODE.SAVE_SNAPSHOT_LAYOUT) {
            // create a snapshot perspective
            perspective = control.getPerspectives().getPerspective(true);

            // try to add invisible dockable perspective for dockable 4
            //perspective.putDockable(cp[4]);
        } else {
            perspective = initialPerspective;
        }
        if (useControlXMLAPI) {
            control.save("mysettings");
            control.writeXML(new File("layout-p2.xml"));
        } else {
            File file = new File("layout-p1.xml");
            DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)));

            XElement element = new XElement("docking-layout");
            control.getPerspectives().writeXML(element, perspective);

            OutputStreamWriter writer = new OutputStreamWriter(out, "UTF-8");
            XIO.write(element, writer);
            writer.close();

            out.flush();
            out.close();
        }
    }

    private static void loadLayout() throws UnsupportedEncodingException, IOException {
        CPerspective perspective ;
        if (useControlXMLAPI) {
            control.readXML(new File("layout-p2.xml"));
            control.load("mysettings");
            perspective = control.getPerspectives().getPerspective("mysettings");
        } else {
            File file = new File("layout-p1.xml");
            DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));

            InputStreamReader reader = new InputStreamReader(in, "UTF-8");
            XElement element = XIO.read(reader);
            perspective = control.getPerspectives().readXML(element);
            reader.close();
            in.close();
        }

        control.getPerspectives().setPerspective(perspective, true);
    }

    private static void resetLayout() {
        control.getPerspectives().setPerspective(initialPerspective, true);
    }

    /* This factory and filter creates new SingleCDockables with some panel that has some
     * special color set as background. */
    private static class ColorFactory implements SingleCDockableFactory, Filter<String> {

        private boolean filterEnabled = true;
        private Map<String, Color> colors = new HashMap<String, Color>();

        public ColorFactory() {
            colors.put("Red", Color.RED);
            colors.put("Green", Color.GREEN);
            colors.put("Blue", Color.BLUE);
            colors.put("Yellow", Color.YELLOW);
            colors.put("White", Color.WHITE);
            colors.put("Black", Color.BLACK);
        }

        @Override
        public boolean includes(String item) {
            // example: "Red" is not available on startup
            if (filterEnabled && "Red".equals(item)) {
                return false;
            }
            return colors.containsKey(item);
        }

        @Override
        public SingleCDockable createBackup(String id) {
            return new ColorSingleCDockable(id, colors.get(id));
        }

        public void setFilterEnabled(boolean filterEnabled) {
            this.filterEnabled = filterEnabled;
        }
    }

    private static Color numberToColor(int number) {
        switch (number) {
            case 0:
                return Color.BLUE;
            case 1:
                return Color.CYAN;
            case 2:
                return Color.GRAY;
            case 3:
                return Color.GREEN;
            case 4:
                return Color.MAGENTA;
            case 5:
                return Color.ORANGE;
            default:
                return Color.WHITE;
        }
    }

    /* This is the kind of MultipleCDockable we will use on our application. */
    private static class CustomMultiDockable extends DefaultMultipleCDockable {

        private int number;

        public CustomMultiDockable(CustomMultiFactory factory, String title, int number) {
            super(factory, title);
            this.number = number;
            JPanel panel = new JPanel();
            panel.setBackground(numberToColor(number));
            panel.setOpaque(true);
            add(panel, BorderLayout.CENTER);
            //setTitleIcon( new ColorIcon( color ) );
        }

        public int getNumber() {
            return number;
        }
    }

    /* This kind of MultipleCDockableLayout describes the content of a CustomMultiDockable. */
    private static class CustomMultiLayout implements MultipleCDockableLayout {

        private int number;

        public CustomMultiLayout(int number) {
            this.number = number;
        }

        private int getNumber() {
            return number;
        }

        @Override
        public void readStream(DataInputStream in) throws IOException {
            number = in.readInt();
        }

        @Override
        public void readXML(XElement element) {
            number = element.getInt();
        }

        @Override
        public void writeStream(DataOutputStream out) throws IOException {
            out.writeInt(number);
        }

        @Override
        public void writeXML(XElement element) {
            element.setInt(number);
        }
    }

    /* And this factory creates new CustomMultiDockables and new CustomMultiLayouts when
     * the framework needs them. */
    private static class CustomMultiFactory implements MultipleCDockableFactory<CustomMultiDockable, CustomMultiLayout> {

        private boolean filterEnabled = true;

        @Override
        public CustomMultiLayout create() {
            return new CustomMultiLayout(-1);
        }

        @Override
        public boolean match(CustomMultiDockable dockable, CustomMultiLayout layout) {
            return dockable.getNumber() == layout.getNumber();
        }

        @Override
        public CustomMultiDockable read(CustomMultiLayout layout) {
            int number = layout.getNumber();
            String title = "D" + (layout.getNumber() + 1); //"R=" + color.getRed() + ", G=" + color.getGreen() + ", B=" + color.getBlue();

            if (filterEnabled) {
                return null;
            }
            CustomMultiDockable multi = new CustomMultiDockable(this, title, layout.getNumber());
            multi.setRemoveOnClose(false);
            multi.setCloseable(true);
            return multi;
        }

        @Override
        public CustomMultiLayout write(CustomMultiDockable dockable) {
            return new CustomMultiLayout(dockable.getNumber());
        }

        public void setFilterEnabled(boolean filterEnabled) {
            this.filterEnabled = filterEnabled;
        }
    }

    public static class ColorSingleCDockable extends DefaultSingleCDockable {

        private JPanel panel = new JPanel();

        public ColorSingleCDockable(String title, Color color) {
            this(title, color, 1.0f);
        }

        public ColorSingleCDockable(String title, Color color, float brightness) {
            super(title);
            setTitleText(title);

            if (brightness != 1.0) {
                float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null);

                hsb[1] = Math.min(1.0f, hsb[1] / brightness);
                hsb[2] = Math.min(1.0f, hsb[2] * brightness);

                color = Color.getHSBColor(hsb[0], hsb[1], hsb[2]);
            }

            setColor(color);
        }

        public void setColor(Color color) {
            panel = new JPanel();
            panel.setOpaque(true);
            panel.setBackground(color);
            add(panel);
            //setTitleIcon( new ColorIcon( color ) );
        }

        public Color getColor() {
            return panel.getBackground();
        }
    }
}```

Just looking at this piece of code:

    control.readXML(new File("layout-p2.xml"));
    control.load("mysettings");
    perspective = control.getPerspectives().getPerspective("mysettings");
    control.getPerspectives().setPerspective(perspective, true);

There are two issues here:
[ul]
[li]The calls on line 4 and 5 are not necessary. You already loaded a layout on line 2 and 3, no need to load it again. What you are doing here is a little bit like receiving an e-mail (lines 2-3), print it out (line 4) and then put it into a scanner in order to read it on the screen (line 5). Just delete the last two lines.
[/li][li]The other issue is much more hidden. If you follow my first advice you don’t need to fix this, so this is just some additional information. You call “setPerspective” with “includeWorkingAreas = true”, but ccontrol.save/load always works with “includeWorkingAreas = false”. So calling “setPerspective(…, false)” would also help. What happens is: because of how ccontrol.save works, the MultipleCDockables are not even stored in the layout because the are children of a working-area. But when calling setPerspective the working-area is loaded - and its children were not stored…
[/li][/ul]

I realized your line of calls produces still a little issue, this time it is not information loss but actually too much information transfered (that is much easier to fix). I’ll address this issue with the next release.
[Edit: no, that is not true. There is no bug. The strange behavior was caused by the very first call to “readXML” which - as is expected - does reset the layout.]