Dockable location persistence

I am working on a platform that other developers use to develop GUI applications. The existing framework supplies a GUI where there a few split panes, and developers can load views (panels) into tabbed panes. Users would like more flexibility in the GUI, so I am trying to migrate the platform to use DockingFrames on the back end.

I would like the application to behave roughly like Eclipse, in the way there are several dock stations, users can generally drag tabs in and out of any station, and when an un-shown tab is shown, it goes to its previous location or to a default location if it has never been shown before. (To be clear, I don’t need the Eclipse theme, though I do like how that theme looks. I am concerned more about application behavior.)

More specifically, I would like the following conditions to be met:
(a) The views that are loaded and where they are loaded must persist between shutdown and launch of the application (that is, between application sessions).
(b) Within an application session, when a view is closed, it should remember where it was for the next time it is opened.
© Different types of views should have different default locations, so that if a user has never opened a particular view before, it should appear in a reasonable location.
(d) When a view is closed, no references to it should be maintained, in order to conserve memory.

After testing out DockingFrames for a while, it seems to support all these conditions, but I am running into some problems with the implementation. I have written the example below to demonstrate the problems I am having. It is roughly a combination of the tutorial examples PlaceholderExample, PersistentLayoutExample, and MultipleDockables. The example was tested against DockingFrames version 1.1.0p7.

import bibliothek.gui.Dockable;
import bibliothek.gui.dock.action.DockActionSource;
import bibliothek.gui.dock.common.intern.DefaultCommonDockable;
import bibliothek.gui.dock.common.*;
import bibliothek.gui.dock.common.intern.CPlaceholderStrategy;
import bibliothek.util.Path;
import bibliothek.util.xml.XElement;
import java.awt.Color;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.ActionEvent;
import java.awt.event.WindowEvent;
import java.io.*;
import java.io.File;
import java.util.*;
import javax.swing.*;
import tutorial.support.ColorIcon;

public class LikeEclipseExample extends JFrame {

    private static final String[] COLOR_NAMES = { "red", "green", "yellow", 
            "blue", "white", "black", "magenta", "cyan" };
    private final CControl control;
    private final MyFactory factory;
    private Map<String, CLocation> locationMap;
    
    public LikeEclipseExample() {
        super("Eclipse-like Example");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        locationMap = new HashMap<String, CLocation>();
        control = new CControl(this);
        control.putProperty(CPlaceholderStrategy.PLACEHOLDER_STRATEGY, 
                new MyPlaceholderStrategy(control));
        
        add(control.getContentArea());
        factory = new MyFactory();
        control.addMultipleDockableFactory("MyFactory", factory);
        
        File layoutFile = new File(LikeEclipseExample.class.getSimpleName() + ".xml");
        if (layoutFile.exists()) {
            try {
                control.readXML(layoutFile);
            } catch (IOException ex) {
                ex.printStackTrace(System.err);
            }
        } else {
            control.readXML(createLayout(factory));
        }
        
        JMenuBar menubar = new JMenuBar();
        menubar.add(createDockableMenu());
        setJMenuBar(menubar);
        
        addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                try {
                    File f = new File(LikeEclipseExample.class.getSimpleName() + ".xml");
                    control.writeXML(f);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                control.destroy();
            }
        });

        setBounds(20, 20, 640, 480);
    }
    
    private XElement createLayout(MyFactory factory) {
        CControl aControl = new CControl();
        factory = new MyFactory();
        aControl.addMultipleDockableFactory("MyFactory", factory);
        
        MyDockable yellow = factory.read(new MyLayout("yellow")); 
        MyDockable green = factory.read(new MyLayout("green"));
        MyDockable red = factory.read(new MyLayout("red"));
        MyDockable blue = factory.read(new MyLayout("blue"));
        MyDockable white = factory.read(new MyLayout("white"));
        
        CGrid grid = new CGrid(aControl);
        grid.add(0.25, 0.75, 0.75, 0.25, green);
        grid.add(0, 0, 0.25, 1, red);
        grid.add(0.75, 0, 0.25, 0.75, blue);
        grid.add(0.25, 0, 0.5, 0.75, yellow);
        grid.add(0.25, 0, 0.5, 0.75, white);
        aControl.getContentArea().deploy(grid);
        
        XElement root = new XElement("root");
        aControl.writeXML(root);
        aControl.destroy();
        return root;
    }
    
    private JMenu createDockableMenu() {
        JMenu menu = new JMenu("Dockables");
        List<MultipleCDockable> mdlist = control.getRegister().listMultipleDockables(factory);
        Map<String, MyDockable> dmap = new HashMap<String, MyDockable>();
        for (int i = 0; i < mdlist.size(); i++) {
            MyDockable dockable = (MyDockable) mdlist.get(i);
            dmap.put(dockable.getName(), dockable);
        }
        
        for (String name : COLOR_NAMES) {
            JMenuItem m = createDockableMenuItem(name, dmap.get(name));
            menu.add(m);
        }
        
        return menu;
    }
    
    private JMenuItem createDockableMenuItem(final String name, MyDockable dockable) {
        JCheckBoxMenuItem m = new JCheckBoxMenuItem(name);
        m.setSelected(dockable != null && dockable.isVisible());
        m.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                JCheckBoxMenuItem m = (JCheckBoxMenuItem) e.getSource();
                if (m.isSelected()) {
                    MyDockable dockable = factory.read(new MyLayout(name));
                    doOpen(dockable);
                } else {
                    MyDockable dockable = getMyDockableByName(name);
                    if (dockable != null) doClose(dockable);
                }
            }
        });
        return m;
    }
    
    private MyDockable getMyDockableByName(String name) {
        List<MultipleCDockable> mlist = control.getRegister().listMultipleDockables(factory);
        for (int i = 0; i < mlist.size(); i++) {
            MyDockable m = (MyDockable)mlist.get(i);
            if (m.getName().equals(name)) {
                return m;
            }
        }
        return null;
    }

    private void doOpen(MyDockable dockable) {
        CLocation location = locationMap.get(dockable.getName());
        dockable.setLocation(location);
        control.addDockable(dockable);
        dockable.setVisible(true);
    }

    private void doClose(MyDockable dockable) {
        saveDockableLocation(dockable);
        dockable.setVisible(false);
    }

    /**
     * Saves the location to be used when {@link #doOpen} is called.
     */
    private void saveDockableLocation(MyDockable dockable) {
        locationMap.put(dockable.getName(), dockable.getBaseLocation());
    }
    
    /**
     * Gets a color from {@link Color}'s static constants.
     */
    private static Color getColorByName(String colorName) {
        try {
            return (Color) Color.class.getDeclaredField(colorName).get(null);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        } 
    }
 
    private static class MyCommonDockable extends DefaultCommonDockable {
    
        private Path placeholder;
        
        public MyCommonDockable(MyDockable dockable, DockActionSource...sources) {
            super(dockable, sources);
            placeholder = new Path(dockable.getName());
        }
        
        public Path getPlaceholder() {
            return placeholder;
        }
    }
    
    private static class MyDockable extends DefaultMultipleCDockable {
        
        private final String name;
        
        public MyDockable(MyFactory factory, MyLayout layout) {
            super(factory);
            name = layout.getName();
            setTitleText(name);
            Color color = getColorByName(name);
            setTitleIcon(new ColorIcon(color));
            JPanel panel = new JPanel();
            panel.setBackground(color);
            add(panel);
        }

        @Override
        protected DefaultCommonDockable createCommonDockable() {
            return new MyCommonDockable(this, getClose());
        }
        
        public String getName() {
            return name;
        }
    }
    
    private static class MyLayout implements MultipleCDockableLayout {

        private String name;
        
        public MyLayout() {
        }

        public MyLayout(String name) {
            this.name = name;
        }

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

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

        @Override
        public void writeXML(XElement element) {
            element.addElement("name").setString(name);
                    
        }

        @Override
        public void readXML(XElement element) {
            setName(element.getElement("name").getString());
        }
        
        public String getName() {
            return name;
        }

        private void setName(String name) {
            this.name = name;
        }
        
    }

    private static class MyFactory implements MultipleCDockableFactory<MyDockable, MyLayout> {

        @Override
        public MyLayout create() {
            return new MyLayout();
        }

        @Override
        public boolean match(MyDockable dockable, MyLayout layout) {
            return false;
        }

        @Override
        public MyDockable read(MyLayout layout) {
            MyDockable dockable = new MyDockable(this, layout);
            return dockable;
        }

        @Override
        public MyLayout write(MyDockable dockable) {
            return new MyLayout(dockable.getName());
        }
        

    }

    private static class MyPlaceholderStrategy extends CPlaceholderStrategy {

        public MyPlaceholderStrategy(CControl control) {
            super(control);
        }

        @Override
        public Path getPlaceholderFor(Dockable dockable) {
            if (dockable instanceof MyCommonDockable) {
                MyCommonDockable m = (MyCommonDockable) dockable;
                return m.getPlaceholder();
            } else {
                return super.getPlaceholderFor(dockable);
            }
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new LikeEclipseExample().setVisible(true);
            }
        });
    }
}

The example uses MultipleCDockables, each with a unique “name”. It may seem like using SingleCDockables would be more appropriate, but to integrateDocking Frames with my existing system, I think MultipleCDockables are the easier choice. (I am open to re-thinking this.) When the program is launched, it loads its layout from a file on disk (LikeEclipseExample.xml in the working directory). A menu allows the user to open and close dockables. It stores its layout in that file when it is closed.

The problems are:
(1) Doesn’t remember location of dockables between sessions
If you close a stacked dockable during an application session, its location is remembered for when it is opened again using a map from the “name” to its CLocation. But if you close a dockable and then exit the application, on next launch, when you open the same dockable, it appears in a default location. I can’t figure out how to store the map of names-to-locations to disk on exit (so that it could be loaded on next launch). Is there a way to do that within the DockingFrames API? If not, is there a good way to write CLocation objects to disk?

Steps to reproduce:

  • Launch application with no existing LikeEclipseExample.xml file
  • Close white stacked dockable using the menu
  • Exit the application (the LikeEclipseExample.xml file is written to disk)
  • Launch application again (automatically loads LikeEclipseExample.xml)
  • Open white dockable using the menu: it does not re-appear in the stack

(2) Doesn’t remember location of non-stacked dockables within a session
If a dockable is in a stack, you can close it, move all the dockables around, and then re-open the dockable you closed, and it reappears in the same stack. This is good, expected behavior. But if you close a non-stacked dockable, then when it is re-opened, it does not appear in its old location. Note that the dockables are removed from the control when they are closed – they are MultipleCDockables whose removeOnClose flag is true. I have been able to re-create the PlaceholderExample tutorial with the common framework, using SingleCDockables, and they behave as expected, re-opening in the same spot they last were. In the example above, before a dockable is closed, its location is stored in a map, and when it is opened the next time, its old location is set using the setLocation method. But non-stacked dockables seem to ignore this. Screenshot are attached showing the issue.

Steps to reproduce:

  • Launch the application (with no existing LikeEclipseExample.xml file)
  • Close red dockable using menu
  • Open red dockable using menu: it appears in a different location

(3) Never-before-opened dockables have no default locations
I’m sure the framework provides a way to do this, but I don’t know how to implement it. I would to specify, for dockables, the user has never opened before, default locations and sizes. In the example code, any never-before-opened dockable appears at the top of the frame, with 100% width and 50% height. Where can I insert code to handle providing the default location and size?

Steps to reproduce:

  • Launch the application
  • From the menu, open a dockable that is not currently visible

Those three items are the main road blocks for me. Other questions I have about the framework and my example are:

(Q1) Am I using Placeholders correctly? Does my use of Placeholders even matter in this example? Does the default CPlaceholderStrategy already implement what I need?

(Q2) What’s the difference between using Placeholders and using Perspectives? I have no need for the user to be able to switch perspectives, so Placeholders seem like the simpler choice. Are there other advantages to using the Perspectives API?

Thanks to everybody in advance. I am open to discussing the design decisions I made – I’m just getting started with DockingFrames, so I would benefit a lot from high-level explanations of how to use the different parts of the API correctly.

Edit: this post is already obsolete, please scroll down to the next one.

Unfortunately persistent location support for MultipleCDockables is not yet that good. I’m currently working on that, since it would be very nice if your example would work. I may need a day or so to complete that work. If you do not want to wait, this is what would happen if you switched to SingleCDockable:


Currently the easier solution is to use SingleCDockables. This switch also makes the application much smaller because many of your issues are solved for free.

Basically you provide a factory that receives an identifier (String) of one of your Dockables, and the factory has somehow to figure out how to create the best matching dockable. The factory is associated with a “Filter”, and this filter tells the framework which identifiers are legal. The framework then automatically stores meta information about Dockables with these identifers - even if the Dockables do not really exist.

(Q1) Am I using Placeholders correctly? Does my use of Placeholders even matter in this example? Does the default CPlaceholderStrategy already implement what I need?

  • In the default implementation placeholders for MultipleCDockables do not get stored. And your implementation is missing the “isValidPlaceholder” method. With that method your example would behave a bit better. With SingleCDockables you do not neet to implement a new PlaceholderStrategy.

(Q2) What’s the difference between using Placeholders and using Perspectives? I have no need for the user to be able to switch perspectives, so Placeholders seem like the simpler choice. Are there other advantages to using the Perspectives API?

  • They are two separate concepts.
    Placeholders are little Strings that represent a Dockable and the framework treats them a little bit like Dockables. A placeholder can be switched with the real Dockable, that is an easy solution to store the location of a Dockable even while the user continues to drag and drop items around.
    Perspectives are building kind of an editor: they allow you to modify the layout of an application without the existence of the application. Basically perspectives are yet another representation of the xml-file you write and allow you to edit that file in a safe way.

(a) The views that are loaded and where they are loaded must persist between shutdown and launch of the application (that is, between application sessions).

  • With SingleCDockables you do no longer need your own location map, so there should be no problems here.

(b) Within an application session, when a view is closed, it should remember where it was for the next time it is opened.

  • Should also be solved automatically due to the filter that is associated with your factory.

(d) When a view is closed, no references to it should be maintained, in order to conserve memory.

  • You will have to manually call “CControl.remove” for SingleCDockables, but that will kill all references to the Dockable.

© Different types of views should have different default locations, so that if a user has never opened a particular view before, it should appear in a reasonable location.

  • The “setLocation” approach would not be bad, if you could find out whether the framework already knows the location of the dockable or not. Unfortunatelly that API is missing, I’ll add some method like “CControl.hasLocation(CDockable)” in the next version.

This is your example rewritten to use SingleCDockables:


import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import tutorial.support.ColorIcon;
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.MissingCDockableStrategy;
import bibliothek.gui.dock.common.SingleCDockable;
import bibliothek.gui.dock.common.SingleCDockableFactory;
import bibliothek.gui.dock.common.mode.ExtendedMode;
import bibliothek.util.filter.PresetFilter;
import bibliothek.util.xml.XElement;

public class LikeEclipseExample extends JFrame {

    private static final String[] COLOR_NAMES = { "red", "green", "yellow", 
            "blue", "white", "black", "magenta", "cyan" };
    private final CControl control;
    
    public LikeEclipseExample() {
        super("Eclipse-like Example");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        control = new CControl(this);
//        control.putProperty(CPlaceholderStrategy.PLACEHOLDER_STRATEGY, new MyPlaceholderStrategy(control));
        
        /* The filter defines which identifiers can be understood by MyFactory, and MyFactory creates new Dockables if necessary */
        control.addSingleDockableFactory(new PresetFilter<String>( COLOR_NAMES ), new MyFactory());
        
        add(control.getContentArea());
                
        File layoutFile = new File(LikeEclipseExample.class.getSimpleName() + ".xml");
        if (layoutFile.exists()) {
            try {
                control.readXML(layoutFile);
            } catch (IOException ex) {
                ex.printStackTrace(System.err);
            }
        } else {
            control.readXML(createLayout());
        }
        
        JMenuBar menubar = new JMenuBar();
        menubar.add(createDockableMenu());
        setJMenuBar(menubar);
        
        addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                try {
                    File f = new File(LikeEclipseExample.class.getSimpleName() + ".xml");
                    control.writeXML(f);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                control.destroy();
            }
        });

        setBounds(20, 20, 640, 480);
    }
    
    private XElement createLayout() {
        CControl aControl = new CControl();
        
        MyDockable yellow = new MyDockable("yellow"); 
        MyDockable green = new MyDockable("green");
        MyDockable red = new MyDockable("red");
        MyDockable blue = new MyDockable("blue");
        MyDockable white = new MyDockable("white");
        
        CGrid grid = new CGrid(aControl);
        grid.add(0.25, 0.75, 0.75, 0.25, green);
        grid.add(0, 0, 0.25, 1, red);
        grid.add(0.75, 0, 0.25, 0.75, blue);
        grid.add(0.25, 0, 0.5, 0.75, yellow);
        grid.add(0.25, 0, 0.5, 0.75, white);
        aControl.getContentArea().deploy(grid);
        
        XElement root = new XElement("root");
        aControl.writeXML(root);
        aControl.destroy();
        return root;
    }
    
    private JMenu createDockableMenu() {
        JMenu menu = new JMenu("Dockables");
        List<SingleCDockable> mdlist = control.getRegister().getSingleDockables();
        Map<String, MyDockable> dmap = new HashMap<String, MyDockable>();
        for (int i = 0; i < mdlist.size(); i++) {
            MyDockable dockable = (MyDockable) mdlist.get(i);
            dmap.put(dockable.getName(), dockable);
        }
        
        for (String name : COLOR_NAMES) {
            JMenuItem m = createDockableMenuItem(name, dmap.get(name));
            menu.add(m);
        }
        
        return menu;
    }
    
    private JMenuItem createDockableMenuItem(final String name, MyDockable dockable) {
        JCheckBoxMenuItem m = new JCheckBoxMenuItem(name);
        m.setSelected(dockable != null && dockable.isVisible());
        m.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                JCheckBoxMenuItem m = (JCheckBoxMenuItem) e.getSource();
                if (m.isSelected()) {
                    MyDockable dockable = new MyDockable(name);
                    doOpen(dockable);
                } else {
                    MyDockable dockable = getMyDockableByName(name);
                    if (dockable != null) doClose(dockable);
                }
            }
        });
        return m;
    }
    
    private MyDockable getMyDockableByName(String name) {
    	return (MyDockable) control.getSingleDockable( name );
    	
//        List<SingleCDockable> mlist = control.getRegister().getSingleDockables();
//        for (int i = 0; i < mlist.size(); i++) {
//            MyDockable m = (MyDockable)mlist.get(i);
//            if (m.getName().equals(name)) {
//                return m;
//            }
//        }
//        return null;
    }

    private void doOpen(MyDockable dockable) {
//        CLocation location = locationMap.get(dockable.getName());
//        dockable.setLocation(location);
    	
    	control.addDockable(dockable);
    	
    	/* This line does not yet work with 1.1.0, but it will work with the next version */
    	dockable.setDefaultLocation( ExtendedMode.NORMALIZED, CLocation.base().normalEast( 0.5 ) );
    	
        dockable.setVisible(true);
    }

    private void doClose(MyDockable dockable) {
//        saveDockableLocation(dockable);
        dockable.setVisible(false);
        control.remove( dockable );
    }

//    /**
//     * Saves the location to be used when {@link #doOpen} is called.
//     */
//    private void saveDockableLocation(MyDockable dockable) {
//        locationMap.put(dockable.getName(), dockable.getBaseLocation());
//    }
    
    /**
     * Gets a color from {@link Color}'s static constants.
     */
    private static Color getColorByName(String colorName) {
        try {
            return (Color) Color.class.getDeclaredField(colorName).get(null);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        } 
    }
 
//    private static class MyCommonDockable extends DefaultCommonDockable {
//    
//        private Path placeholder;
//        
//        public MyCommonDockable(MyDockable dockable, DockActionSource...sources) {
//            super(dockable, sources);
//            placeholder = new Path(dockable.getName());
//        }
//        
//        public Path getPlaceholder() {
//            return placeholder;
//        }
//    }
    
    private static class MyDockable extends DefaultSingleCDockable {
        
        private final String name;
        
        public MyDockable(String name) {
            super(name);
            this.name = name;
            setTitleText(name);
            Color color = getColorByName(name);
            setTitleIcon(new ColorIcon(color));
            JPanel panel = new JPanel();
            panel.setBackground(color);
            add(panel);
        }

//        @Override
//        protected DefaultCommonDockable createCommonDockable() {
//            return new MyCommonDockable(this, getClose());
//        }
        
        public String getName() {
            return name;
        }
    }
    
    private static class MyFactory implements SingleCDockableFactory{
    	@Override
    	public SingleCDockable createBackup( String id ){
	    	return new MyDockable( id );
    	}
    }
    
//    private static class MyLayout implements MultipleCDockableLayout {
//
//        private String name;
//        
//        public MyLayout() {
//        }
//
//        public MyLayout(String name) {
//            this.name = name;
//        }
//
//        
//        @Override
//        public void writeStream(DataOutputStream out) throws IOException {
//            out.writeUTF(name);
//        }
//
//        @Override
//        public void readStream(DataInputStream in) throws IOException {
//            setName(in.readUTF());
//        }
//
//        @Override
//        public void writeXML(XElement element) {
//            element.addElement("name").setString(name);
//                    
//        }
//
//        @Override
//        public void readXML(XElement element) {
//            setName(element.getElement("name").getString());
//        }
//        
//        public String getName() {
//            return name;
//        }
//
//        private void setName(String name) {
//            this.name = name;
//        }
//        
//    }
    
//    private static class MyPlaceholderStrategy extends CPlaceholderStrategy {
//
//        public MyPlaceholderStrategy(CControl control) {
//            super(control);
//        }
//
//        @Override
//        public Path getPlaceholderFor(Dockable dockable) {
//            if (dockable instanceof MyCommonDockable) {
//                MyCommonDockable m = (MyCommonDockable) dockable;
//                return m.getPlaceholder();
//            } else {
//                return super.getPlaceholderFor(dockable);
//            }
//        }
//    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new LikeEclipseExample().setVisible(true);
            }
        });
    }
}```

Ok, I did some updates in the framework, you find the new version as attachment on this post (you may need to scroll down a bit). The support for locations of MultipleCDockables is now much better.

I’ve also updated your example a bit:

  • Using “CControl.addDockable(String,MultipleCDockable)” I ensure that each MyDockable always gets the same unique identifier. This way the framework can store meta information about the Dockable.
  • I’ve removed the location map - this is now handled by the framework itself.
  • Instead of a PlaceholderStrategy I use a MissingCDockableStrategy, this strategy implicitly enables placeholders but also tells the framework to store meta information even if a MultipleCDockable is not registered
  • And I’ve added to “getAutoBaseLocation” method (see “doOpen”) to find out whether a Dockable has a default location or not.

Actually I like this solution better then the one in the first post, and it is closer to your original code. I hope that takes care of your problems, but feel free to ask if you encounter any more oddities.


import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import tutorial.support.ColorIcon;
import bibliothek.gui.dock.action.DockActionSource;
import bibliothek.gui.dock.common.CControl;
import bibliothek.gui.dock.common.CGrid;
import bibliothek.gui.dock.common.CLocation;
import bibliothek.gui.dock.common.DefaultMultipleCDockable;
import bibliothek.gui.dock.common.MissingCDockableStrategy;
import bibliothek.gui.dock.common.MultipleCDockable;
import bibliothek.gui.dock.common.MultipleCDockableFactory;
import bibliothek.gui.dock.common.MultipleCDockableLayout;
import bibliothek.gui.dock.common.intern.DefaultCommonDockable;
import bibliothek.util.Path;
import bibliothek.util.xml.XElement;

public class LikeEclipseExample extends JFrame {

    private static final String[] COLOR_NAMES = { "red", "green", "yellow", 
            "blue", "white", "black", "magenta", "cyan" };
    private final CControl control;
    private final MyFactory factory;
    
    public LikeEclipseExample() {
        super("Eclipse-like Example");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        control = new CControl(this);
//        control.putProperty(CPlaceholderStrategy.PLACEHOLDER_STRATEGY, new MyPlaceholderStrategy(control));
        control.setMissingStrategy( MissingCDockableStrategy.STORE );
        
        add(control.getContentArea());
        factory = new MyFactory();
        control.addMultipleDockableFactory("MyFactory", factory);
        
        File layoutFile = new File(LikeEclipseExample.class.getSimpleName() + ".xml");
        if (layoutFile.exists()) {
            try {
                control.readXML(layoutFile);
            } catch (IOException ex) {
                ex.printStackTrace(System.err);
            }
        } else {
            control.readXML(createLayout(factory));
        }
        
        JMenuBar menubar = new JMenuBar();
        menubar.add(createDockableMenu());
        setJMenuBar(menubar);
        
        addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                try {
                    File f = new File(LikeEclipseExample.class.getSimpleName() + ".xml");
                    control.writeXML(f);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                control.destroy();
            }
        });

        setBounds(20, 20, 640, 480);
    }
    
    private XElement createLayout(MyFactory factory) {
        CControl aControl = new CControl();
        factory = new MyFactory();
        aControl.setMissingStrategy( MissingCDockableStrategy.STORE );
        aControl.addMultipleDockableFactory("MyFactory", factory);
        
        MyDockable yellow = factory.read(new MyLayout("yellow")); 
        MyDockable green = factory.read(new MyLayout("green"));
        MyDockable red = factory.read(new MyLayout("red"));
        MyDockable blue = factory.read(new MyLayout("blue"));
        MyDockable white = factory.read(new MyLayout("white"));
        
        aControl.addDockable( yellow.getName(), yellow );
        aControl.addDockable( green.getName(), green );
        aControl.addDockable( red.getName(), red );
        aControl.addDockable( blue.getName(), blue );
        aControl.addDockable( white.getName(), white );
        
        CGrid grid = new CGrid(aControl);
        grid.add(0.25, 0.75, 0.75, 0.25, green);
        grid.add(0, 0, 0.25, 1, red);
        grid.add(0.75, 0, 0.25, 0.75, blue);
        grid.add(0.25, 0, 0.5, 0.75, yellow);
        grid.add(0.25, 0, 0.5, 0.75, white);
        aControl.getContentArea().deploy(grid);
        
        XElement root = new XElement("root");
        aControl.writeXML(root);
        aControl.destroy();
        return root;
    }
    
    private JMenu createDockableMenu() {
        JMenu menu = new JMenu("Dockables");
        List<MultipleCDockable> mdlist = control.getRegister().listMultipleDockables(factory);
        Map<String, MyDockable> dmap = new HashMap<String, MyDockable>();
        for (int i = 0; i < mdlist.size(); i++) {
            MyDockable dockable = (MyDockable) mdlist.get(i);
            dmap.put(dockable.getName(), dockable);
        }
        
        for (String name : COLOR_NAMES) {
            JMenuItem m = createDockableMenuItem(name, dmap.get(name));
            menu.add(m);
        }
        
        return menu;
    }
    
    private JMenuItem createDockableMenuItem(final String name, MyDockable dockable) {
        JCheckBoxMenuItem m = new JCheckBoxMenuItem(name);
        m.setSelected(dockable != null && dockable.isVisible());
        m.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                JCheckBoxMenuItem m = (JCheckBoxMenuItem) e.getSource();
                if (m.isSelected()) {
                    MyDockable dockable = factory.read(new MyLayout(name));
                    doOpen(dockable);
                } else {
                    MyDockable dockable = getMyDockableByName(name);
                    if (dockable != null) doClose(dockable);
                }
            }
        });
        return m;
    }
    
    private MyDockable getMyDockableByName(String name) {
        List<MultipleCDockable> mlist = control.getRegister().listMultipleDockables(factory);
        for (int i = 0; i < mlist.size(); i++) {
            MyDockable m = (MyDockable)mlist.get(i);
            if (m.getName().equals(name)) {
                return m;
            }
        }
        return null;
    }

    private void doOpen(MyDockable dockable) {
    	control.addDockable(dockable.getName(), dockable);
    	
    	CLocation location = dockable.getAutoBaseLocation( false );
    	if( location == null || location == control.getDefaultLocation() ){
    		dockable.setLocation( CLocation.base().normalSouth( 0.3333 ) );
    	}
    	
        dockable.setVisible(true);
    }

    private void doClose(MyDockable dockable) {
        dockable.setVisible(false);
        control.removeDockable( dockable );
    }

    /**
     * Gets a color from {@link Color}'s static constants.
     */
    private static Color getColorByName(String colorName) {
        try {
            return (Color) Color.class.getDeclaredField(colorName).get(null);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        } 
    }
 
    private static class MyCommonDockable extends DefaultCommonDockable {
    
        private Path placeholder;
        
        public MyCommonDockable(MyDockable dockable, DockActionSource...sources) {
            super(dockable, sources);
            placeholder = new Path(dockable.getName());
        }
        
        public Path getPlaceholder() {
            return placeholder;
        }
    }
    
    private static class MyDockable extends DefaultMultipleCDockable {
        
        private final String name;
        
        public MyDockable(MyFactory factory, MyLayout layout) {
            super(factory);
            name = layout.getName();
            setTitleText(name);
            Color color = getColorByName(name);
            setTitleIcon(new ColorIcon(color));
            JPanel panel = new JPanel();
            panel.setBackground(color);
            add(panel);
        }

        @Override
        protected DefaultCommonDockable createCommonDockable() {
            return new MyCommonDockable(this, getClose());
        }
        
        public String getName() {
            return name;
        }
    }
    
    private static class MyLayout implements MultipleCDockableLayout {

        private String name;
        
        public MyLayout() {
        }

        public MyLayout(String name) {
            this.name = name;
        }

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

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

        @Override
        public void writeXML(XElement element) {
            element.addElement("name").setString(name);
                    
        }

        @Override
        public void readXML(XElement element) {
            setName(element.getElement("name").getString());
        }
        
        public String getName() {
            return name;
        }

        private void setName(String name) {
            this.name = name;
        }
        
    }

    private static class MyFactory implements MultipleCDockableFactory<MyDockable, MyLayout> {

        @Override
        public MyLayout create() {
            return new MyLayout();
        }

        @Override
        public boolean match(MyDockable dockable, MyLayout layout) {
            return false;
        }

        @Override
        public MyDockable read(MyLayout layout) {
            MyDockable dockable = new MyDockable(this, layout);
            return dockable;
        }

        @Override
        public MyLayout write(MyDockable dockable) {
            return new MyLayout(dockable.getName());
        }
        

    }

//    private static class MyPlaceholderStrategy extends CPlaceholderStrategy {
//
//        public MyPlaceholderStrategy(CControl control) {
//            super(control);
//        }
//
//        @Override
//        public Path getPlaceholderFor(Dockable dockable) {
//            if (dockable instanceof MyCommonDockable) {
//                MyCommonDockable m = (MyCommonDockable) dockable;
//                return m.getPlaceholder();
//            } else {
//                return super.getPlaceholderFor(dockable);
//            }
//        }
//    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new LikeEclipseExample().setVisible(true);
            }
        });
    }
}```

Beni, i presume it will work if I have a mix of Single/Multi CDockables? Or I have to make two different types of factories? :slight_smile:

There are two different interfaces for factories, one factory producing MulitpleCDockables (MultipleCDockableFactory), the other SingleCDockables (SingleCDockableFactory). So you’ll need to create and manage two factories. But once you have the factories you can mix the Single/Multi CDockables freely, there are absolutely no restrictions (e.g. “a single- and a multi-dockable can be in the same stack”).

You may use my first post in this thread as basis, or ask if you have a more specific question.

Beni,

Thanks for your prompt and thorough assistance on this. I’m working on a mixed Single/Multiple dockable solution so far, using the revised library you attached. Much appreciated,

Mike