NetBeans Window System - what happens during startup
Below is a blow-by-blow account of what actually goes on during NetBeans startup, put together by, well, reading the code. It’s here as much for the author’s edification (if you read through it and document it, you understand it) as yours.
The three and a half models model
The NetBeans window system is extremely defensively coded - one of the main goals of rewriting it for 3.6 was robustness in the face of components that throw exceptions, do evil things to other components, and so forth. The way the robustness of the current system is achieved is by very cleanly separating the model of how the UI should be, the model of how the UI actually is and the AWT component hierarchy, which is a model of sorts itself, but cannot be relied upon, because in an extensible application any component may do something evil. On top of this is the persistence model.
UIs are hierarchical, with components inside containers inside other containers - so each model we’ll describe is hierarchical to represent this. TopComponents (panels in tabbed container) get a little special handling because there’s a potential one-to-many relationship between TC’s and tabbed containers (an implication of winsys v1, where one component could live in more than one docking Mode (tabbed container) per workspace).
So, the models (names made up for the purpose of this document):
-
The Persistent Model -
org.netbeans.core.windows.persistence
- these classes are really record types - classes with public fields that contain data read out of persisted XML describing the window system and written back to it. These classes are instantiated at de-persistence and persistence time, and read and written, but are not used at runtime - they just provide the stored state of the system, and are used to construct the initial model of the window system at runtim -
The Runtime State Model - this is the model of how the window system and all its components are supposed to be, based on persisted state and any changes made by calling methods (i.e. the user maximizes or closes a component) at runtime
-
The Runtime View Model - this is another set of model objects -
org.netbeans.core.windows.view
, each of which represents (and can manipulate) a UI component. Each model object has a corresponding Accessor object which isa layer of indirection that connects it to the actual Component it models. -
Snapshots (this is the 1/2 model) - a snapshot is an object that immutably captures the state of the Runtime State Model at a particular moment in time. When a change has happened, a request to update the UI is enqueued. When that request runs, it synchronizes the UI model with the state described in the snapshot (what’s open, what’s closed, what’s maximized, splitter positions, etc…)
Reading the source to the window system can be a little complicated, because there are multiple models of the same thing all being synchronized, and just about everything follows a chain of single-method-call methods back to Central
or WindowManagerImpl
. Hence this document.
In its essence, though it’s simple: all changes in the window system simply mean synchronizing the two runtime models. It’s only the number of classes that requires that make it look complex.
The Window System Startup Sequence
Loading the window system is a distinct phase in NetBeans startup. A set of model objects representing the window manager (data like SDI vs. MDI, frame size/location/state), its Modes (docking containers - bounds, contents), and references to `TopComponent`s by ID (not the components themselves, but unique String ids for them).
Once all this is done, we’ll have a set of model objects representing all the persisted data. Note that these model objects are not the ones used by the model of the window system at runtime, there are different classes for that.
Here’s the load sequence:
WindowSystemImpl
-
copy all TopComponent settings files aggressively to the userdir (BUG? Probably needed this for the 3.5 project system, probably don’t now)
-
checks its on the dispatch thread, and calls PersistenceHandler.getDefault().load()
-
that calls PersistenceManager.getDefault().loadWindowSystem()
-
that gets an XML parser
-
the parser creates a WindowManagerConfig from data it finds in XML files. A
WindowManagerConfig
just has a lot of public fields from parsed data, that refers to other similar objects -
ModeConfig
- information about a docking container, placement, contents -
TcRefConfig
- references to a TopComponent by ID, no component there yet -
GroupConfig
- Refers to a Group of TopComponents (like form editor + its palettes) -
TcGroupConfig
- Reference to a TopComponent by a GroupConfig -
InternalConfig
- Just notes what version of the window system saved the date we loaded
Now we’re back out in PersistenceManager
. We:
-
Build a set of
TopComponent
ID’s in use (if a new component opens and it wants the same ID as an existing one, we append an integer to the ID string) -
Start listening on the folder for changes (i.e. a new module is installed and it has a persisted component that should be opened)
-
Clear the reference to the parser
-
Return the
WindowManagerConfig
toPersistenceHandler
Now we have a model for the contents of the window system as it was persisted…
Back in PersistenceHandler.load()
now, we build the runtime model of the window system (note that except for deserializing `TopComponent`s, we’re not creating any components yet, we’re just creating model objects that will be represented by GUI components in the UI):
-
Misc: get the large/small preferred toolbar icon size and store that on the WindowManagerImpl
-
Get the list of TopComponent ids found in deserializing the window system
-
Iterate the IDs
-
for each, deserialize the TopComponent in question
-
Set the "recent view list" on the WindowManager with the array of deserialized TopComponents. This is used for ordering the components visited when the user Ctrl-Tabs between TopComponents
-
Iterate all of the ModeConfigs
-
for each, create a ModeImpl (note this is a model object, not a tabbed container)
-
if the mode was active at shutdown last time, remember that fact in a local variable
-
if the mode was persisted as maximized, remember that fact in a local variable
-
Iterate all of the ModeImpls created
-
initialize each one from the ModeConfig it was created for
-
iterate all TcRefConfigs in the ModeConfig, extract some persisted data about the "previous mode" the TopComponent was in, and pass that data to the window system - this is so that sliding windows know where they should land if the user presses the "pin" button to put them in a tabbed container
-
set the id of the selected TopComponent on the ModeImpl
-
Iterate all the GroupConfigs loaded
-
For each group, create a TcGroup object
-
Add a mapping from GroupConfig.name to the tcGroup to a map held by PersistenceHandler (BUG? Why should this data be kept here? Nothing else keeps data in PersistenceManager - it means it can’t be collected - or I’m not seeing/understanding how it’s used)
-
Iterate all the TcGroupConfigs (BUG: bad name - these are wrappers for TopComponent IDs)
-
For each TcGroupConfig (PersistenceHanponent reference), add the ID into the list of IDs in the TcGroup
-
Check the boolean open flag for the TcGroupConfig. If true, it’s a component that, when opened, should open the entire group
-
Check the boolean flag whether the TopComponent was closed explicitly by the user. If true, when the group of components are all opened, leave that one closed
-
Check the boolean flag whether the TopComponent was reopened explicitly by the user, and if so, ignore the result of the closed flag - add it to the list of ids that should open
-
Add the TcGroup we created to the list held by the window manager
Note the group handling code is a little different than the rest in terms of the way it’s modelled - this should probably be corrected - it appears that for some reason, PersistenceHandler holds the data for that, there is no corresponding model object for TC’s in a group (not necessarily bad, but inconsistent), and the data is passed to the window manager before its initialized (harmless, but odd). On the other hand, it’s less complicated.
We’re not done yet.
-
Next is a hotfix for issues 37188 and 40237 (which like all good hotfixes, was never replaced by a proper fix) - this calls componentShowing() on the component before it’s even in the AWT hierarchy
-
Set the active (focused) Mode in the window manager from the field we saved earlier (BUG: this code seems to run earlier than it should, and the comment refers to the NetBeans 3.x project system, which persisted the entire window system out and loaded a new one in
-
Set the maximized mode, if any, in the window manager from the field we saved earlier
-
Compute the main window sizes for MDI and SDI mode, based on persisted data and current screen size, and set it on the window manager
-
Compute the editor area bounds and set it on the window manager
-
Set the id of the toolbar configuration that’s active, based on persisted data
We now have a singleton instance of WindowManagerImpl
, with its model fully initialized from persisted data (or a semi-sane default if de-persisting failed). It will be available from WindowManager.getDefault();
Showing/creating the UI
The next phase happens when setVisible(true) is called on the window system. A thing to know here if you read the code is that all requests to do anything in the window system are funneled through one class called Central
(yes, Central is the God Object anti-pattern). So pretty much any method that you look at in the model objects will call back through a method in Central, sometimes to itself, sometimes to some other object.
So…
-
WindowManager.show()
: -
asserts we’re on the EDT
-
installs the global KeyEventDispatcher on Swing’s KeyboardFocusManager to handle action bindings
-
calls WindowManagerImpl.getInstance().setVisible(true) - that in turn checks that its a state change and calls
-
Central.setVisible(true) which calls
DefaultModel.setVisible(true)
(this just stores the boolean value in a field) -
calls
ViewRequestor.scheduleRequest()
- enqueues a runnable that will set the window system’s visibility property to true, which -
* has a special check if it’s a visibility change request, and if so tries to run it immediately if on the EDT (semi-BUG: it will always be the EDT, unless the assertion is turned off)
Now we’re into the runtime behavior of the window system - this system of enqueuing requests is how code that will change window system state operates: A change is made to the model of the expected state of the window system, and the requested change is encoded in an object that will be processed in a subsequent event on the EQ. ViewRequestor keeps a list of all pending changes, and coalesces changes to the same value. When the request is processed, the state of the UI (open components, positions, splitter positions, everything) as described by the model is composed into a "snapshot", which is then used to set the necessary parameters on the UI components.
But right now, we’re still just showing the window system, period. Here’s what happens:
-
ViewRequestor.processRequest()
gets called when the request runs. It gets the array of all pending requests and clears the queue -
It iterates the
ViewRequests
that are enqueued -
For each, create a
ViewEvent
and add it to a list of events to be processed -
and passes that to
ViewRequestor.dispatchRequest
, which -
passes them to
DefaultView.changeGUI()
. View is an interface representing the UI state of the entire window system. It’s another set of model objects, this time modelling the state of the component. For each model object (ViewElement
,ModeContainer
,ModeView
are interfaces the winsys implements elsewhere…), there is also an "accessor" object, which is what actually talks to the UI component.
DefaultView.changeGUI
is what will actually modify the UI. A ViewEvent
is pretty much like a PropertyChangeEvent, with an old value and a new value, but with an integer type instead of a property name. What it does:
-
Diff the last known showing set of
TopComponent`s and find any newly shown TC’s and call `componentShowing()
on them -
Iterate all the
ViewEvents
and see if any one is a visibility change for the entire window system (BUG? There is already special handling to dispatch such requests ahead of the queue - this seems to duplicate the work). (BUG: Interestingly, this is the real source of the componentShowing() called twice bug that has the hotfix mentioned above - if the order of operations is switched here, that hack can be deleted. Note if the request is a visibility change request, method returns after calling WindowSystemVisibilityChanged()). -
Iterate all the ViewEvents passed:
-
for each, check the type, and for each type, cast the new value and old value to the proper types, and
-
call a setter on the UI-view-model object that in turn should call something on the actual UI component
But we’re getting ahead of ourselves here - as you may have noticed above, if it’s a window system visibility request, we actually exit before we’ve gotten to iterating all the `ViewEvent`s the second time, to change component state and so forth.
I should mention ViewHierarchy
here - it’s not a very exciting class, but it’s the root model for the UI model objects, so when you have one of those Accessor
objects for a Mode
or a component in a mode, it’s where you get the corresponding model object whose setters will actually call the real UI component.
So let’s go back to where we call windowSystemVisibilityChanged()
. What that does:
-
First, we call
hierarchy.getMainWindow()
(now we’re actually touching GUI - the main window is aJFrame
subclass,MainWindow
- so this is the first time we’re really creating components, except for the `TopComponent`s we deserialized. What that does: -
set the icon
-
add a
WindowListener
that will callLifecycleManager.exit()
onWindowClosing
, and close menus if the window is deactivated -
set the menu bar (this calls a whole bunch of code that generates the menu from folders of actions in the system filesystem - we won’t cover it here)
-
install the toolbar panel
-
Install the statusbar (and check the special constant for putting it in the menubar for screen real estate freaks)
-
Install a
JPanel
atBorderLayout.CENTER
, calleddesktopPanel
, which our window system will live in in MDI mode -
Install a hack listener on
MenuSelectionManager
to focus the main window if a menu is activated - this is SDI mode specific - you can invoke a menu by mnemonic but then the keyboard doesn’t work unless you send focus to the main window - see issue #38810
Now we’re back out in DefaultView.windowSystemVisibilityChanged()
. What we do now:
-
Set the toolbar configuration - this should actually cause the toolbars to be instantiated (there can be multiple toolbar configurations - it’s how the debugger changes the set of visible toolbars when you start it)
-
Next, we go back to
MainWindow
by callinghierarchy.getMainWindow().prepareWindow()
. What that does: -
Calls back to
WindowManagerImpl
and gets the main window bounds (different calls for MDI and SDI). We stored this value there when we loaded the window system data, fromWindowManagerConfig
-
If not empty, set those bounds on the main window
-
(BUG: there’s a bunch of weird consecutive log statements here that should be deleted)
-
Next we call
hierarchy.setSplitModesVisible()
, which gets the root split pane of the split desktop layout, and recursively callssetVisible(true)
on it and all its tabbed containers and `TopComponent`s -
Next we get the bitmask frame state we should have and call
Frame.setExtendedState()
with it on the main window -
Now we set the maximized
Mode
, if any, that we stored when de-persisting -
Then we call
hierarchy.updateDesktop()
. This takes the root split pane and adds it to the "desktop"JPanel
inside the main window -
Now we run some code to show all the frames for SDI mode windows, if any
-
Next we set up the editor area, using a dummy panel in TDI mode if there are no open editors
-
Update frame states for SDI windows, if any (BUG?: shouldn’t we do this before calling show() on them all? Harmless since
show()
is asynchronous, but I’m not sure all AWT impls have to make it asynch) MKLEINT: On some (maybe all) platforms setting of frames states before it’s shown has no effect. In other words, you cannot open an initially maximized frame. You need to show it and maximize then. -
Set the main window title - using
WindowSystemAccessor.getProjectName()
-
If the main window is maximized, run a hack that fakes the user resizing the window to its current size, passing that into the model (which will generate a synchronization
ViewRequest
and update splitters, etc. to proportional relative sizes) -
Get the activated mode, and tell it it’s activated (meaning focus should be sent to the
TopComponent
that should have focus, andcomponentActivated()
will be called, etc. -
InvokeLater adding listeners to the main window for resize and mode changes (invokeLater it so that when the frame state changes that we called to initialize the frame state on startup don’t get processed as the user resizing the window and cause a storm of window system updates just because frame state changes in AWT are processed asynchronously - so we want to start listening only after things have settled down - see issues 39238 and 37369 (the fix for 37369 caused 39238)
MKLEINT: again this is a hacky workaround to the fact that one cannot prepare a maximized version of the frame before it’s shown.
At this point we’ve got our main window up and ready to go.
Obvious questions for those unfamiliar with the winsys:
Q: So where do all the tabbed containers and split panes come from? You didn’t mention those.
A: The actual implementations of ViewElement
(things that own ViewEvent`s), like `org.netbeans.core.windows.view.ModeView
actually create the UI components they talk to in their constructors
Q: Why are TopComponents
treated so differently and what’s this reference stuff in the de-persisting process?
A: In the pre-3.5 window system, a component could be open in more than one tabbed container at the same time. What??? It is because of workspaces, which we got rid of. A workspace was a switchable window system configuration or set of windows. The interface is still there, but there is only ever one workspace in the post 3.5 winsys. So any given Mode, for legacy reasons, is not sole owner of a TopComponent
, it just has a handle for one.