Toggling between Perspectives involving Multi Docking open file editors

Hi Beni,

Thank you for sharing such an excellent framework, together with the documentation, tutorials, support, and your energy to bring Docking Frames to us. Thanks for making this happen. :slight_smile:

I am in the process of creating a tool that uses Docking Frames and I am currently stuck with a multiple docking /working area /perspective scenario exception error:

java.lang.IllegalStateException: cannot unbind, counter is already 0.

After studying the examples in the DF documentation, including some of solutions mentioned in this blog, I feel that I must be missing something and was hoping to learn from you where I went wrong.

Requirement: To enable the user to navigate between different views /perspectives using buttons /links, while preserving the location of any docking, including any open editor files. (This functionality is similar to toggling between Java and Debug perspectives using the buttons at top right in Eclipse).

I am attaching enough code to enable you to zoom in on the problem area and run the code to reproduce the exception mentioned above. The code is a stripped down version as the original consists of many larger modules.

Scenario to reproduce the exception:
1- Launch the app; it displays a single Welcome docking view. (This view is rendered by the Welcome perspective).
2- Click Editor button for the first time to display the Editor perspective that consists of West and East dockings (both are SingleCDocking), and a WorkingArea docking in the middle – all grey dockings.
3- Click File > New File. This creates and displays a new editor file in the WorkingArea.
4- Click Welcome button for the first time to display the Welcome view again (courtesy of the Welcome perspective).
5- Click Editor button for the second time. The open editor file shows up fine – well, almost fine since now the Closable function on the open editor file has disappeared).
6- Now, click Welcome button for the second time, and you should see the IllegalStateException error as below (top 5 lines shown):
Exception in thread „AWT-EventQueue-0“ java.lang.IllegalStateException: cannot unbind, counter is already 0
at bibliothek.gui.dock.util.extension.ExtensionManager$1.unbind(ExtensionManager.java:165)
at bibliothek.gui.dock.displayer.DisplayerRequest.setController(DisplayerRequest.java:84)
at bibliothek.gui.dock.station.DisplayerCollection$Handle.setController(DisplayerCollection.java:228)
at bibliothek.gui.dock.station.DisplayerCollection.setController(DisplayerCollection.java:208)
at bibliothek.gui.dock.SplitDockStation.setController(SplitDockStation.java:629)

7- Finally, click Editor button a third time, then File > New File, does not create a new second editor file. Instead, you should see the following (which I assume is a symptom of the earlier exception):
Exception in thread „AWT-EventQueue-0“ java.lang.IllegalStateException: A dockable that wants to be on a CWorkingArea can’t be made visible unless the CWorkingArea is visible.

Other behaviour noticed:
a- In step 5 above, the Closable feature on the editor file vanished.
b- In step 3, the WorkingArea docking shrank in size by 2 pixels.

I am using Common, 1.1.1p6c, JRE JavaSE-1.6, on Windows Vista.

Any help would be much appreciated. Thank you. :slight_smile:

/Adi

Thanks for the test case. It looks like a bug in the framework, I’ll have a closer look at this in the evening.

A lot is going on in this application, in fact this application hit more than one bug at the same time. The main issue is, that the “editor layout” includes a CWorkingArea, while the “welcome layout” does not. It’s one of the ideas I would never have had (personally I would have made the “welcome” layout just with a maximized welcome-dockable, such that it can be minimized like in Eclipse).

When the layout changed, the CWorkingArea lost its root station property and hence its visibility state. This was caused by a missing check whether the CWorkingArea was a root station in the first place. Root stations should never lose their root state automatically.

To make things worse: any event telling that a Dockable was removed from its parent triggered “unregister” events for all the children. But when the CWorkingArea was removed from its parent, it automatically became a root station, hence its children were not “unregistered”, and the event caused the internal data structures to be invalid.

Both bugs together then caused the MultipleCDockable to be automatically removed from the CControl (see also “DefaultMultipleCDockable.setRemoveOnClose”), which in return explains why the “close” button was missing. Neither the root property nor the visibility state of the CWorkingArea should ever have changed.

I did not find out how the exception were created, but they were clearly caused by the invalid data structures. After I fixed the other bugs, they did no longer appear. I’m going to upload version 1.1.1p6d within the next few hours, please tell me if you have any more issues.

Anyway: thanks a lot for the exact bug report and the patience, it was really easy to find the bug with your help.

This is awesome, Beni. Thanks for the fixes – and thanks also for your super fast response!

There is one minor visual inconsistency (more like a cosmetic defect) which is not important (since it appears during the creation of the first editor docking and then fixes itself), but I thought I mention it for the sake of completeness. Besides, I have the beginnings of a workaround (actually a hack) for it, but a proper fix would be preferred and will surely benefit the DF community.

Defect scenario 1: (Working area retains focus on creation of first editor)
1- Launch the app.
2- Click Editor button.
3- (Notice the central working area docking gains focus). Now, click File > New File. You should see the focus marker surrounding the outer boundary of the working area docking (with the unsightly upper corners).
4- Now tap West (or East) docking to gain focus – which it does; however, you should also see the working area docking still displaying its initial focus border marking.
5- If you now click the Editor button, the visual defect goes away. Similarly, if instead you click the Welcome button followed by the Editor button, the defect also goes away.

Defect scenario 2: (Working area shrinks in size on creation of the first editor)
1- As 1 above.
2- As 2 above.
3- Here, tap West (or East) docking to give it focus; now, click File > New File. You should be able to see the working area docking shrank in size by two pixels. The focus marker is working as expected.
4- As 5 above.

Workaround (hack!):
Since clicking on either (Welcome+Editor combo) or Editor button resolves the issue, all these do are to re-load the Editor perspective. So,

in class “InitializeMultipleCDock”, and within method “createEditorLayout()”, insert the line: dockControl.load(“editorPerspective”);

Code looks like this:

	public static EditorDockable createEditorLayout() {
		editorDock = initializeMultipleDock();
		
		dockControl.load("editorPerspective"); //Warning: code is a workaround to fix working area visual defect in editor perspective.
		
		return editorDock;
}```

The only problem is that the newly created editor file loses focus – which is no big deal – but the behaviour is inconsistent.  Naturally, I can add additional code to fix this but then it would not be an elegant or a clean solution.

Thanks again for all your help, and for sharing your exciting DF project with us. :)

/Adi

I did see the same issue, but it was late and I did not want to continue working… so I’ll fix that another time. It’s not that an important bug, so it can wait a few days :slight_smile:

Thanks for that, Beni.

On a separate issue, one of the requirements for my tool is to track and preserve any changes to perspective layouts the user makes when navigating between perspectives. The idea is that if the user customizes the default perspective, navigates away to a secondary perspective, then revisits the first one, the tool should be able to render the first perspective as the user left it on the last visit. This functionality is similar in Eclipse, for example, when toggling between Java and Debug perspectives. So, is this currently possibly with DF? I noticed that the DF 1.1.0 Common Manual, section 9.2, mentions that this functionality is currently under consideration.

As this requirement is essential for my tool, I am looking through the DF API’s to figure out if I could develop a solution using existing DF code. This way it would be simpler than writing lines of new routines, less prone to bugs, and would not break with future DF upgrades.

The good news is that I have written a solution that achieves this (thanks for the clean API’s by the way – it simplifies my work and is fun!), and I am in the process of completing my tests. So far, it appears to work well and I have not managed to break it. The performance looks good too – but then again DF is excellent with response times as it is. One of the things I am not sure about is the number of objects that are created and if memory is being used up rapidly as DF keeps storing perspective settings. Although I am using class methods and static variables where possible, it would be good to gain some understanding relating to perspectives and memory management.

I hope to post within few days my findings together with the solution on this forum for general feedback when my initial tests are complete.

Regards from a very cold London, UK. Hope the winter weather is manageable in Switzerland.:o)

/Adi

[QUOTE=adi]Thanks for that, Beni.

On a separate issue, one of the requirements for my tool is to track and preserve any changes to perspective layouts the user makes when navigating between perspectives. The idea is that if the user customizes the default perspective, navigates away to a secondary perspective, then revisits the first one, the tool should be able to render the first perspective as the user left it on the last visit. This functionality is similar in Eclipse, for example, when toggling between Java and Debug perspectives. So, is this currently possibly with DF? I noticed that the DF 1.1.0 Common Manual, section 9.2, mentions that this functionality is currently under consideration.

As this requirement is essential for my tool, I am looking through the DF API’s to figure out if I could develop a solution using existing DF code. This way it would be simpler than writing lines of new routines, less prone to bugs, and would not break with future DF upgrades.
[/quote]
There is a bit a mix here between „perspective - the API“ and „perspective - what you see on the screen“, I usually call the later „layout“. For layouts (the position of actual Dockables) you have to call the CControl.save/DockFrontend.save method before calling „load“, and then you have the described behavior. I deliberately do not call „save“ automatically, because some clients might not want to save a modified layout.

For perspectives (the API that allows to set the position of Dockables before they exist), no such mechanism exists. But as I understand you, you would not need it anyway. What I wrote in the guide would only affect clients that load additional Dockables (e.g. because of a new plugin) after some layouts already had been altered.

The good news is that I have written a solution that achieves this (thanks for the clean API’s by the way – it simplifies my work and is fun!), and I am in the process of completing my tests. So far, it appears to work well and I have not managed to break it. The performance looks good too – but then again DF is excellent with response times as it is. One of the things I am not sure about is the number of objects that are created and if memory is being used up rapidly as DF keeps storing perspective settings. Although I am using class methods and static variables where possible, it would be good to gain some understanding relating to perspectives and memory management.

I hope to post within few days my findings together with the solution on this forum for general feedback when my initial tests are complete.

Regards from a very cold London, UK. Hope the winter weather is manageable in Switzerland.:o)

/Adi

Honestly, I treat memory as in „there is always enough“. I try not to create memory leaks nor store the same thing twice, but I did not optimize the framework for memory efficiency. I think a good measurement is to write the current layout into a byte-file (see CControl.write…), the size of the file should be close to the used memory.

As for the number of objects: many, but most of them are very small. For a stored layout, each DockStation/Dockable gets a „layout object“ which is wrapped into several layers, containing maps describing the position of the children… maybe if you have a look at the xml file (see CControl.writeXML…) you get a feeling on the size. While not exactly true, the formula „one tag = one object“ could be used.

P.S. -11°C, but then I live in one of the warmer spots, hardly any snow :frowning:

Thanks for the clarification and notes.

As promised, attached you will find a zip with my solution I mentioned earlier. To my surprise, it turned out simpler than I first thought, mainly due to the fact that all of the code was already available out there – I just needed to find it! And yes – saving the current perspective is exactly as you described it (DockFrontend.save()). In addition, I even managed to add an extra functionality (“Reset” default perspective) that is accessible from a menu – similar to the reset default perspective feature in Eclipse. So, in short, whilst there are a couple of tiny bits I would like to tweak (we can discuss these if time allows), I am really pleased with the outcome so far. Naturally, I will wait for your and the community’s feedback to assess the merits of this solution.

Solution:

Keep in mind the solution consists of two parts:

a- Saves the current perspective, including any layout changes, closing, and additions of dockables. It does this when switching between one perspective and another, and keeps on doing it automatically without user intervention (i.e. no dialogue prompts).

b- Resets the current perspective back to its “Default” setting. This is done via the user menu system.

Installation and Set-up:

A working example of the complete solution is attached in this posting, which contain detailed code annotations to help describe the solution.

1- Class module(s): At minimum, one class is required; PerspectiveManager. This defines routine calls for both parts of the solution.

Optionally, a second class, PerspectiveSnapshot, can be used if the developer wishes to override (or add) methods in one of DF’s classes (namely, the super class FrontendSettingsMenuPiece). Since I plan to deploy a different dialogue than the one currently defined in FrontendSettingsMenuPiece, I have opted to implement PerspectiveSnapshot as a flexible utility class for future customisation to my tool. Clearly, the developer will need to reference FrontendSettingsMenuPiece instead of PerspectiveSnapshot within PerspectiveManager.

**PerspectiveManager **looks like this:


import java.awt.Component;
import java.awt.event.ActionEvent;

import com.df.DFWelcome;
import com.df.util.PerspectiveSnapshot;

import bibliothek.gui.DockFrontend;
import bibliothek.gui.dock.common.CControl;

public class PerspectiveManager {
	
	//Define your preferred way to declare CControl.
	private static CControl dockControl = DFWelcome.getDockControl();
	private static PerspectiveSnapshot perspectiveSnapshot = null;
	
	//Optional: Useful for associating directly with a component such as a JMenuItem or JButton, among others.
	public static void updatePerspectiveSnapshot(Component owner) {
		initializePerspectiveSnapshot();
		perspectiveSnapshot.save(owner);
	}
	
	//Mandatory: In almost all cases, you will use this method called by the appropriate listener to save a snapshot of the
	//current perspective (i.e. the perspective you are on) immediately before the user switches to another perspective.
	//This method saves any changes to the perspective the user may have made, and should look the same on the next visit.
	//Note: changes to MultipleCDockables, such as editor files, behave differently than SingleCDockables, i.e. any editor
	//files created after the previous snapshot will be carried over to the previous snapshot. This behaviour is in keeping
	//with Eclipse.
	public static void updatePerspectiveSnapshot(ActionEvent evIn) {
		initializePerspectiveSnapshot();
		perspectiveSnapshot.save((Component) evIn.getSource());
	}
	
	//Optional: Reset the current perspective back to its Default setting. Note: you will need to add the extra "Reset" perspective
	//setting (one line of code) when defining the original layout perspective for this functionality to work. See InitializeSingleCDock
	//and InitializeMultipleCDock classes in this app example. 
	public static void resetPerspectiveSnapshot(ActionEvent evIn, DockFrontend dockFrontendIn) {
		initializePerspectiveSnapshot();
		
		//List the perspectives that should participate in Reset functionality.
		if (("welcomePerspective").equals(dockFrontendIn.getCurrentSetting())) {
			dockControl.load("welcomePerspectiveReset");
			perspectiveSnapshot.getFrontend().setSetting("welcomePerspective",
														 perspectiveSnapshot.getFrontend().getSetting("welcomePerspectiveReset"));
			dockControl.load("welcomePerspective");
		}
		else if (("editorPerspective").equals(dockFrontendIn.getCurrentSetting())) {
			dockControl.load("editorPerspectiveReset");
			perspectiveSnapshot.getFrontend().setSetting("editorPerspective",
														 perspectiveSnapshot.getFrontend().getSetting("editorPerspectiveReset"));
			dockControl.load("editorPerspective");
		}
		
		//Define any additional perspectives using the above templates... 
	}
	
	//Grabs all perspectives out there that are known by DockFrontend.
	public static DockFrontend getPerspectives() {
		initializePerspectiveSnapshot();
		return perspectiveSnapshot.getFrontend();
	}
	
	//Creates PerspectiveSnapshot object if it is missing. There should only be one at any given one time.
	private static void initializePerspectiveSnapshot() {
		if (perspectiveSnapshot == null) {
			perspectiveSnapshot = new PerspectiveSnapshot(dockControl, false);
		}
	}
}

2- Set-up: This is for setting-up part ‘b’ of the solution. A line of code is all that is required to set-up the ‘Reset’ functionality for each layout perspective definition. It is probably best to define it at same time as the original layout (aka. ‘Default’) perspective. In the attached app, the single line looks like this (refer to InitializeSingleCDock and InitializeMultipleCDock classes):

perspectives.setPerspective(“welcomePerspectiveReset”, welcomePerspective);

A perspective definition may look like this:

		perspectives = dockControl.getPerspectives();
		
		CPerspective welcomePerspective = perspectives.createEmptyPerspective();
		
		CGridPerspective gridPerspective = welcomePerspective.getContentArea().getCenter();
		
		gridPerspective.gridAdd(0,0,6,10, new SingleCDockablePerspective("welcomeDockID"));
		gridPerspective.gridAdd(6,0,6,10, new SingleCDockablePerspective("newsDockID"));
		
		//Set your perspective (i.e. initial layout) in the usual way. Call this the 'Default' perspective.
		perspectives.setPerspective("welcomePerspective", welcomePerspective);
		
		//Optional: For each Default perspective definition, set a secondary unique perspective if you wish to enable the user to reset
		//back to the default perspective at any point during runtime. Call this the 'Reset' perspective.
		//If you do not define this line, you will deactivate the ability to reset the default perspective, but Docking Frames will still
		//continue to track and save the latest changes made to the perspective. Essentially, the Reset setting is a copy of the Default
		//setting, i.e. each layout perspective has a pair of saved perspectives in the framework. It may help if you append 'Reset' in
		//the name.
		perspectives.setPerspective("welcomePerspectiveReset", welcomePerspective);
		
		//Do not load perspective yet. The load order is controlled in DFMain initially, and later in PerspectiveManager during runtime.
		//dockControl.load("welcomePerspective");
}

3- Implementation:
To save the updated current perspective, insert the following two lines within a listener /event handler:

PerspectiveManager.updatePerspectiveSnapshot(evIn);
dockControl.load(“welcomePerspective”);

My app implements the ‘Welcome’ perspective like this:

			btnWelcome = new JButton("Welcome");
			btnWelcome.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent evIn) {
					
					//Call to save a snapshot of the current perspective (i.e. saves any changes to the perspective the user made).
					PerspectiveManager.updatePerspectiveSnapshot(evIn);
					dockControl.load("welcomePerspective");
				}
			});
			toolBar.add(btnWelcome);```

And for the ‘Editor’ perspective like this:

```			//JButton: "Editor"
			btnEditor = new JButton("Editor");
			btnEditor.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent evIn) {
					
					//Call to save a snapshot of the current perspective (i.e. saves any changes to the perspective the user made).
					PerspectiveManager.updatePerspectiveSnapshot(evIn);
					dockControl.load("editorPerspective");
				}
			});
			toolBar.add(btnEditor);```

To reset the current perspective back to the default, insert the following line within the appropriate listerner /event handler:

**PerspectiveManager.resetPerspectiveSnapshot(evIn, PerspectiveManager.getPerspectives());**

In my app, it looks like this:

```			//JMenuItem: "Reset Perspective...".
			//Optional: Create menu item in order to reset back to the Default perspective. Note: For this to work, you will need to include
			//the 'Reset' perspective line at the point of defining your Default layout perspective (refer to InitializeSingleCDock and
			//InitializeMultipleCDock classes in this app example).
			final JMenuItem mntmResetPerspective = new JMenuItem("Reset Perspective...");
			mntmResetPerspective.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent evIn) {
					
					//We pass all known perspectives by calling PerspectiveManager.getPerspectives(). Hopefully, our current
					//perspective is included in the list (within DockFrontend).
					PerspectiveManager.resetPerspectiveSnapshot(evIn, PerspectiveManager.getPerspectives());
				}
			});```

**4-	All done.**


**Test Scenario: Saving current perspective (when toggling between perspectives)**
1-	Launch the app.
2-	Welcome perspective displays, showing Welcome and News dockables. Changes the position or layout of the dockables.
3-	Click File > Open Help. You should see the newly created ‘Help 1’ dockable display within the current perspective (the position will depend on the changes you made to the dockables earlier). Feel free to continue making changes, include creating additional new Help dockables.
4-	Now click Editor menu button. This displays the Editor perspective, showing dockables for West, East and an empty Workingarea in the middle.
5-	Make any position or layout changes you wish. Next, click File > New File. This displays the newly created editor file. You can even create Help dockables if so desired. Continue to make additional changes as desired, even creating more editor files.
6-	Now click the Welcome menu button. You should see the Welcome perspective display in the same state as you left in your last visit.
7-	Click the Editor menu button. You should see the Editor perspective display also in the same state as you left in your previous visit. One note: Editor files (i.e. MultipleCDockables) dockables behave differently than West, East (and Help) dockables (i.e. SingleCDockables) in that the editor files are user-specific and the framework should keep them at all times. This makes sense since a user would expect his /her open editor files to be maintained. This behaviour is the same in Eclipse.
8-	Continue this cycle of switching between perspectives and making changes as you go along. You should (hopefully) see the app keeping up with updating the state of the perspective.


**Test Scenario: Resetting back to Default perspective**
1-	Follow same 1 – 6 steps above (you should be in the Welcome perspective).
2-	Click Perspective main menu button > Reset Perspective. You should see the Default Welcome perspective display showing the original layout.
3-	Now click the Editor menu button. In the Editor perspective, click Perspective > Reset Perspective. You should see the Default Editor perspective showing, with the exception of any open editor files. Specifically, the layout of West, East, and Workingarea dockables should mach their Default definitions. Again, as noted earlier, any open user-specific editor files should always display as this would be the expected behaviour.


Hope this helps others looking for a similar solution.
Any feedback, comments, etc, are welcome.

/Adi

Looks good, and should work.

Instead of “getFrontend().setSetting(name,…)” you could have used “CControlPerspective.setPerspective(name,…)”, it might have made the code easier to read. But its a minor detail that should not affect the outcome.

Loading the reset-perspectives before reading them should not be necessary. Saves one call to “load” and makes the code a big more efficient.

Beni, Thanks. I have included your changes in my code.

Quick question – when I create my initial layout perspectives (for Home and Editor), it appears that my Location (e.g. CLocation.base().minimalSouth()) definition is overridden. So when I Minimize a dockable, DF places it North instead of South (I am guessing that the layout perspective is set to save the minimize location North by default). This behaviour is also noticed when I reset the perspectives. What is the easiest way to fix this? Or what are my options to correct this?

Thanks again.

/Adi

The perspective can indeed override such settings, or just delete them such that they fall back to their default behavior.

Open the project “docking-frames-demo-tutorial” and search for a class named “PerspectivesHistory”. This is a small example on how the history of positions can be changed, exactly what you need.

Basically there are two solutions:

  1. First add the dockable to the south CMinimicePerspective and then call “perspective.storeLocations”. Afterwards you add the dockable to its final position in the center. The perspective will remember that the dockable once was minimized at the south. This is the easier solution, and I would recommend using it.
  2. Use “CDockablePerspective.getLocationHistory” to access the history, and “add” a new Location for the minimized position.

Some codes from the example:

For solution 1:

private static void setUpMinimized( CPerspective perspective, Map<String, CDockablePerspective> dockables ){
/* In the beginning we access different minimize-perspectives and just drop our dockables
* onto them. */
CMinimizePerspective west = perspective.getContentArea().getWest();
west.add( dockables.get( "Red" ) );
west.add( dockables.get( "Green" ) );
west.add( dockables.get( "Blue" ) );

CMinimizePerspective east = perspective.getContentArea().getEast();
east.add( dockables.get( "Yellow" ) );
east.add( dockables.get( "White" ) );
east.add( dockables.get( "Black" ) );

CMinimizePerspective south = perspective.getContentArea().getSouth();
for( int i = 0; i < 5; i++ ){
south.add( dockables.get( "m" + i ) );
}

/* And then we instruct the framework that the current location of the dockables should
* be stored as history information.
* The dockables remain at their current location, but we can just re-arrange them later. */
perspective.storeLocations();
}```

For solution 2:
```/* It is possible to access and modify history information directly. In this case
* modify the history of the "White" dockable such that it thinks it was minimized
* on the "north" minimize-area. */
private static void modifyHistoryDirectly( CPerspective perspective, Map<String, CDockablePerspective> dockables ){
CMinimizePerspective north = perspective.getContentArea().getNorth();
CDockablePerspective white = dockables.get( "White" );

/* We first inform the north minimize-area that "white" was a child by
* inserting a placeholder for "white" */
north.addPlaceholder( white );

/* Now we build up location information first by specifying the exact location of "white" */
DockableProperty property = new FlapDockProperty( 0, false, 100, white.intern().asDockable().getPlaceholder() );
/* We pack additional information like the mode and the root-station that was the parent of
* "white" together. */
Location location = new Location( ExtendedMode.MINIMIZED.getModeIdentifier(), north.getUniqueId(), property );
/* And finally we add the new location information to the history. */
white.getLocationHistory().add( ExtendedMode.MINIMIZED, location );
}```

Thanks. Solution 1 worked fine for my SingleCDockables. However, neither solutions worked for my WorkingArea – but that’s alright since I managed to resolve the issue by other means (namely, moving its setLocation() statement below setVisible()). Not sure why this works but it does! Anyway, I have DF working as I want it now so I am very pleased with the results. Now I turn my attention to developing the content inside the dockables.
Thanks for all your great help.

Regards,

/Adi