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.