Techsenger ShellFX is a platform for building JavaFX applications, where an application is structured as a tree of MVVM components, each of which has its own lifecycle, config, etc. The platform provides abstract classes for creating the main types of components: window, tab, area, page, dialog, and popup.
It also includes ready-to-use implementations of containers (including a docking layout) and dialogs (including a universal file chooser). In addition, the platform provides powerful devtools that allow you to inspect both the MVVM component tree and the underlying JavaFX scene graph. These tools make it easy to understand how the platform works and are invaluable during development.
ShellFX is built around two core subsystems: the dynamic main menu and the workspace. The main menu is assembled
at runtime and automatically adapts to the currently focused component. The workspace is everything below the main
menu in the Shell — it provides the structural foundation of the application and defines how components are
arranged and interact visually. The platform supports different types of workspace models.
ShellFX is built according to the KISS principle. We aimed to keep it as simple as possible — with no magic and
no overly complex solutions. For example, the platform is based on a slightly extended classic MVVM pattern and provides
components for the core parts of an application, such as windows, tabs, dialogs, and others. The main idea was to
allow developers to start working with the platform within a single day, and we believe this goal has been achieved.
ShellFX is built on top of the PatternFX framework.
- Demo
- Features
- When to Use?
- Modules
- Component Overview
- Core Components
- Layout Components
- Shared Components
- Dialog Components
- DevTools Components
- Component Config
- Extension Registries
- Managed Controls
- Naming Convention
- Quick Start
- Requirements
- Dependencies
- Code Building
- Running Demo
- License
- Contributing
- Support Us
Key features of ShellFX include:
- Dynamically configurable menu.
- Support for different types of workspace.
- Abstract classes to simplify component development.
- A set of ready-made components that can be used out of the box.
- Support for different layouts, including a docking layout.
- Set of devtools for inspecting the application at both the component layer and the JavaFX scene graph layer.
- Ability to preserve component config.
- Support for inline popups and dialogs with two scopes — window and tab.
- Window styling that matches the theme.
- Support for 7 themes (4 dark and 3 light).
- API for working with all colors in the palettes of all themes
- Styling with CSS.
ShellFX is well suited for medium to large JavaFX applications that require a structured UI architecture and flexible workspace management.
It is particularly effective for projects that:
- Rely on a component-based MVVM architecture.
- Contain multiple tabs or require complex workspace layouts (including docking-based layouts).
- Need dynamic menus, theming support, and centralized shell-level infrastructure.
- Benefit from built-in DevTools for inspecting both the component tree and the JavaFX scene graph.
ShellFX provides a scalable foundation for applications where UI complexity grows over time and clear structural boundaries are essential.
Typical application types include:
- Enterprise systems managing different data entities.
- Code editors and lightweight IDEs.
- Database and query tools.
- File managers and content browsers.
- Monitoring and analytics dashboards.
- Tools that require parallel workflows within multiple tabs or panels.
The tab-based approach allows users to maintain workflow context while switching between different tasks, making complex applications more intuitive and productive.
The platform consists of the following modules:
- Material — provides UI elements (menus, text areas, etc.) and supporting classes.
- Core — includes the shell itself, base classes for component development, settings, and core utility classes.
- Layout — offers abstract components for creating tabs with various layouts.
- Shared — includes components that are used by other components from different modules.
- Icons — contains the Material Design Icons font and module-specific stylesheets that utilize these icons. To use custom icons instead, simply create your own stylesheets and add them to Shell.
- Storage — provides abstractions for working with file systems. The module includes a default implementation for the local file system. Additional storage providers (for Google Drive, Dropbox, FTP, and similar) can be implemented separately.
- Dialogs — provides ready-to-use dialogs: alert, file chooser, confirmation etc.
- DevTools — contains tools for exploring component tree and JavaFX scene graph.
- Demo — showcases ShellFX's core functionality, provides examples for building custom components, and presents ready-made components.
The following diagram shows the basic components, containers, and their implementations in the core and layout
modules:
classDiagram
class Parent {
<<interface>>
}
class Child {
<<interface>>
}
class PageContainer {
<<interface>>
}
class TreePageContainer {
<<interface>>
}
class PopupContainer {
<<interface>>
}
class WindowContainer {
<<interface>>
}
class TabContainer {
<<interface>>
}
class AbstractParent
class AbstractChild
class AbstractArea
class AbstractPage
class AbstractPopup
class AbstractPageHost
class PageHost
class TreePageHost
class TabHost
class TabDock
class AbstractWindow
class AbstractDialog
class AbstractHostWindow
class DefaultShell
class AbstractTab
class AbstractHostTab
Parent <|-- Child
Child <|-- PageContainer
Child <|-- TreePageContainer
Child <|-- PopupContainer
Child <|-- TabContainer
PopupContainer <|-- WindowContainer
Parent <|.. AbstractParent
AbstractParent <|-- AbstractChild
AbstractChild <|-- AbstractArea
AbstractChild <|-- AbstractWindow
AbstractChild <|-- AbstractTab
AbstractArea <|-- AbstractPage
AbstractArea <|-- AbstractPopup
AbstractArea <|-- AbstractPageHost
AbstractArea <|-- TabHost
AbstractPageHost <|-- PageHost
AbstractPageHost <|-- TreePageHost
TabHost <|-- TabDock
AbstractWindow <|-- AbstractDialog
AbstractWindow <|-- AbstractHostWindow
AbstractHostWindow <|-- DefaultShell
AbstractTab <|-- AbstractHostTab
PageContainer <|.. PageHost
TreePageContainer <|.. TreePageHost
TabContainer <|.. TabHost
WindowContainer <|.. AbstractHostWindow
WindowContainer <|.. AbstractHostTab
%% composition: 0..N TabDock inside DockHost
DockHost "1" o-- "0..*" TabDock
AbstractArea <|-- DockHost
These components form the architectural foundation of the platform, and all higher-level platform components are built upon them.
ShellFX is built on top of the PatternFX platform, which supports working both with and without a component tree.
In ShellFX, all components form a tree structure, and multiple trees may exist depending on the number of Windows.
For this reason, all ShellFX core components inherit from the Parent and Child components provided by PatternFX.
Each component is defined by an interface accompanied by a base implementation. This approach ensures loose coupling
while still providing default implementations out of the box. It also allows developers to replace or extend the default
behavior with custom implementations when required. For instance, the platform consistently references Shell
through the ShellView interface rather than a concrete class.
When working with components, there are several important points to keep in mind:
-
The developer must control the component lifecycle. Component initialization is performed either manually or in the
open*orshow*methods ofComposer, which may delegate this logic tocreate*methods (usingcreate*methods makes it easy to replace the component being created). Component deinitialization is performed either manually or in theclose*orhide*methods ofComposer. See Naming Convention for details. -
Working with components involves maintaining two hierarchies — the component tree and the JavaFX scene graph. Therefore, any addition or removal of a component must be reflected in both structures. For example, removing a component from the node tree without removing it from the component tree will result in a memory leak. DevTools provide the ability to inspect and monitor both hierarchies.
Shell is the main and top-level component. It extends the HostWindow component and inherits its responsibilities for
managing the JavaFX Stage and window-level infrastructure. In addition, Shell defines the primary application
structure and user experience layer.
It is responsible for the following tasks:
- Dynamic menu management.
- Workspace management.
- Context management.
The Shell core does not contain any business logic. It is only a shell for other components that contain logic.
Working with the main menu of the Shell is carried out in two directions:
- Configuring menu elements
- Managing the state of elements and responding to user actions
The configuration of menu elements is performed dynamically and in any order, with the final result being unknown in advance. This feature is crucial in cases where plugins/extensions are used, as they can be added/removed dynamically by the user. Each plugin may introduce its own menu items and interact with existing menus. Therefore, it is impossible to predict the final structure of the menu that the user will work with.
The implementation is built on two extension registries (see Extension Registries). The
SlotRegistry holds the structure of the menu: the menu bar, its menus and the groups of menu items are slots, put
into each other at positions. The ControlRegistry holds the factories that create the controls: the menu bar, the
menus, the groups and the menu items. Both registries can be changed and unregistered from at any time, so a plugin
can add its own menu or put its items into an existing one. When the menu needs to be updated, Shell has ManagedControlBuilder
read both registries and build a new menu bar from them. How the built controls behave at runtime is described in
Managed Controls.
A menu consists of groups separated by separators. Items are added to groups, and empty groups are ignored. Each menu
and group is identified by its slot. The MenuBarManager is responsible for managing the state
of menu elements and responding to their actions. It interacts with a component that provides a port implementing the
MenuAwarePort interface.
The algorithm works as follows. First, the component that has focus is determined. The Shell tracks changes to
the focused node using Scene#focusOwnerProperty(). When this property changes, the component that owns the node is
identified, and the result is stored in ShellView#focusedProperty(). Note that if a component should become focused
when the user clicks on an empty area of that component (for example, a Pane), you must explicitly call
pane.requestFocus().
At the same time, the focused component may not participate in menu formation (for example, it could be just a toolbar).
Therefore, after the focused component changes, Shell searches from the focused component up to the root of the
tree — the Shell — for the first component whose port implements MenuAwarePort. Note that Shell can also form
the main menu, but this is usually done only when the workspace is empty. See also ShellView#menuAwareProperty().
It is also important to remember that the MenuBarManager also interacts with MenuAwarePort when the user uses
accelerators.
To gain a complete understanding of working with the menu, it is recommended to familiarize yourself with the
MenuAwarePort interface, experiment with the menu in the demo, and pay attention to log messages at the debug level.
The second key part of ShellFX is the workspace, which represents one of the available layouts. ShellFX supports different types of workspace:
- Browser-like. This workspace is created using the
ProminentTabHostcomponent. Additionally, the tabs added to thisProminentTabHostcan contain a docking layout created with theDockHostcomponent. - IDE-like. This workspace is a straightforward docking layout created with the
DockHostcomponent.
Window is one of the core components of the platform and is available in two variants: WindowType#NESTED and
WindowType#TOP_LEVEL.
NESTED windows are internal windows managed by WindowManager. WindowManager allows an unlimited number of
windows to be opened simultaneously, tracks the active window, manages window state, and provides various window
arrangement operations such as cascade, tile, and others.
The platform provides two implementations of window hosts: HostWindow and HostTab. This allows nested windows
to be displayed either inside another window or inside a tab. The latter approach is particularly useful for
tab-based applications, where each tab can maintain its own set of dialogs and auxiliary windows, similar to
how modern web browsers isolate dialogs and popups per tab.
TOP_LEVEL windows are created in a separate Stage and are integrated with the operating system's windowing environment.
Despite their different implementations, both NESTED and TOP_LEVEL windows are accessed through the same API.
As a result, components built on top of Window (such as dialogs, wizards, or utility windows) can be displayed
either inside the application or in separate system windows without any changes to application code. This allows
window-based components to be implemented once and reused with any window type.
Tab is an abstract component used for creating custom tab implementations. In ShellFX, Tab is one of the central
platform components, since the primary application functionality is delivered through tabs.
Tab can be added to any component that implements the TabContainer interface. The platform
provides two components that implement this interface: TabHost and TabDock, where TabDock extends TabHost.
Page is a component that represents a titled, selectable element. A key feature of this component is its lazy
initialization. For example, if a container displays one of N Pages, only the Page that the user actually chooses to
view will be initialized.
Page can be added to any component that implements the PageContainer interface. The default implementation of
this interface is PageHost.
Dialog inherits Window and is a specialized component designed for user interaction and result acquisition.
Since Dialog inherits Window, it can be displayed in any environment that supports windows and uses the same
API as regular windows.
All dialogs in ShellFX are asynchronous. Opening a dialog does not block the application's execution flow or freeze the user interface. Instead, user responses are delivered through callbacks, events, observable properties, or other asynchronous mechanisms. This approach keeps the UI responsive and simplifies background processing.
Dialogs can be displayed either as NESTED or TOP_LEVEL windows. Nested dialogs are rendered within a
WindowContainer and appear as an integral part of the application's user interface. Top-level dialogs are displayed
in a separate Stage and are integrated with the operating system's windowing environment.
Nested dialogs can be displayed in any implementation of WindowContainer. The platform provides two implementations:
HostWindow and HostTab. This allows dialogs to be associated either with a window or with a specific tab.
For example, in a browser-like application, each tab can maintain its own set of dialogs and auxiliary windows,
isolated from all other tabs.
All Popups in ShellFX are inline and have a scope that affects what will be blocked when the Popup is open.
Inline Popups are components that appear embedded within the current application window, typically overlaid on top
of the existing content. They are contextually tied to a specific section (e.g., a Shell or Tab) and do not
create a separate OS-level window. In contrast, modal window Popups (or native Popups) open as standalone
OS-managed windows with their own frames and system controls, completely independent of the parent UI.
There are two types of scope: Window and Tab. Popups in the Tab scope are bound to a specific tab and are visible
only while that tab is open. Popups in the Window scope are global to the Window and remain visible even when all
tabs are closed.
Popup can be added to any component that implements the PopupContainer interface. The platform provides two
components that implement this interface: Tab and Window.
Area is an abstract base component that represents a rectangular region. Naturally, AreaView#getNode() returns a
Region.
Layout components are responsible for arranging Tab, Page, and, in some cases, Area components and their derivatives.
TabHost is the primary component that can contain Tab components; therefore, it implements the TabContainer interface.
This component provides all the necessary APIs for working with tabs — adding, selecting, removing, transferring
tab ports, and more.
ProminentTabHost extends TabHost and is the primary TabHost used for building browser-like applications,
where the application shell consists of a main menu and this single component as its workspace. It is named after
the prominent style class (see StyleClasses#PROMINENT) and visually stands out from a regular TabHost through
larger tabs and a more prominent, saturated tab header.
DockHost is the main component of the docking layout and one of the most complex components in the platform.
Before describing how it works, let’s examine its child components.
TabDock extends TabHost, meaning it can contain tabs. In addition, it introduces docking-specific functionality
such as dragging an entire TabDock from one layout position to another, collapsing it into a SideBar, and
similar behaviors.
SideBar is a component that displays collapsed TabDock instances. It is important to note that a SideBar can
be shown even when it contains no collapsed TabDock components, using SideBarPolicy. This is useful when the
SideBar is intended to host additional UI elements besides collapsed TabDocks.
When a TabDock is minimized to a SideBar, any of its minimized tabs can be previewed in a TabPopup component,
which allows its width to be resized.
In addition to defining the layout structure, a DockHost can have a main component. The main component is intended
to host the primary application content, while TabDocks typically contain auxiliary tools and supporting
components. The main component can be any Area-based component and is specified in the model using
ModelNodeBuilder#mainArea(...). The defining characteristic of the main component is that it remains part of the
docking layout for the entire lifetime of the DockHost and cannot be minimized to a SideBar. When a main
component is present, it is used as the reference for determining the target SideBar when minimizing a TabDock.
If no main component is defined, DockHost falls back to determining the target side based on the position of the
TabDock within the docking layout.
Now that the components are introduced, let’s outline how everything works together. DockHost provides two
complementary APIs for working with docking layouts.
The first is the whole-tree API, which is intended for complete layout construction, restoration, and serialization.
A docking layout is described as a ModelNode tree. Each node represents either a group or a leaf component. Group
nodes (GroupNode) define the layout orientation (HORIZONTAL or VERTICAL) and their children, while leaf nodes
(AreaNode) contain Area-based components displayed in the workspace. An immutable model tree is created using
ModelNodeBuilder and applied to DockHost using Composer#applyModel(GroupNode). The current layout can be
captured as such an immutable tree using Composer#captureModel(). This approach is recommended when initializing a
workspace, restoring a previously saved layout, or persisting the current layout state.
The second is the partial-tree API, which is intended for incremental runtime modifications. Instead of rebuilding
the entire layout, it performs targeted operations relative to an existing anchor component. This API is used for
operations such as adding a new area next to an existing area, replacing a component, removing a component, or
performing docking operations initiated by the user. Anchors are addressed the same way as in the whole-tree API —
via ModelNode — but obtained live from the current layout using Composer#getModelNode(AreaView) rather than
built by hand. A node obtained this way is not a snapshot: navigating it via ModelNode#getParent() or
GroupNode#getChildren() always reflects the layout's actual current state, which lets an anchor be resolved to any
ancestor group regardless of nesting depth.
PageHost is a simple component that displays Page components and performs their lazy initialization.
It can be used to display navigable pages with a flat menu-like structure in diffent components - tabs, dialogs etc.
TreePageHost is almost identical to PageHost, except for the menu structure: PageHost uses a flat menu
(ListView), whereas TreePageHost uses a hierarchical menu (TreeView).
Shared components are auxiliary components built on top of Core components and used by components from other modules.
Find is an abstract base search component that contains the entire search view implementation, including both
submit search and instant search functionality. Since child components may be of different types (toolbar, panel, etc.),
this component includes only minimal CSS styling. It owns the full logic of a generic search UI — which trigger
mode is in effect (submit vs. instant), debounce timing for both search and adding the find text to the earlier
find texts, and match-count display formatting (including an optional, domain-agnostic
FindResult contract a component can implement over its own result type to get that formatting for free). The
only thing it does not know is what a match actually is or how to find one; that stays entirely up to the concrete
component: onFind() returns a CompletableFuture with the outcome, so a component can either run its own search
synchronously or delegate to something external and report back asynchronously — either way, Find clears the
displayed result before a new search starts and discards a stale outcome on its own if a newer search has since
superseded it, so no implementation has to worry about that itself.
NavigableFind extends Find with the ability to move between individual matches one at a time. It adds
previous/next buttons, disabling them when there is nothing to navigate to, and can display the current match
position alongside the total count (e.g. 1 / 10) instead of a total-only count. It works with
NavigableFindResult, a FindResult that also knows how to move to its next/previous match.
FindPanel is an abstract class for find panels that are placed at the bottom of other components, built on top
of NavigableFind. Besides the generic search orchestration described above, it also adds whole word, regular
expression, and highlight-all toggles, plus a close button — none of which Find/NavigableFind know about.
In this section, the dialogs from the dialogs module are described. This module contains implementations of the most
commonly used dialogs.
AlertDialog is a dialog for common user notification scenarios such as informational messages, warnings, errors,
and confirmation requests.
FileChooserDialog is a dialog for selecting a file when opening or saving. The dialog type is defined using the
FileChooserType enumeration.
It is important to note that this dialog works with files provided by classes from the storage module. This makes
it possible to use the dialog with virtually any file storage implementation, provided that an appropriate FileStorage
implementation is supplied.
NameValueDialog is a simple dialog for displaying name–value pairs. The parameter name is shown in a TextField,
while the value is displayed in a TextArea.
ProgressDialog is a dialog for reporting the progress of a long-running operation, with an optional message and
an optional step counter (e.g. 3 / 10) shown alongside the progress bar.
DevTools components are tools for inspecting and analyzing the application at two levels: the component tree and the JavaFX scene graph. They are primarily intended for developers building components on top of the platform.
This component is a container for Tab components and provides shared tab management mechanisms. It can be added to
any layout, whether a simple layout or a docking layout.
This component allows exploring the tree of active components and inspecting their properties. In addition, it provides information about the class hierarchy of the selected component.
NodeTab is an interactive inspector for the JavaFX scene graph. It allows developers to explore the node hierarchy, inspect JavaFX properties, and modify supported property values at runtime without restarting the application. The component also enables opening reference documentation (Javadoc) for both classes and their properties.
When working with JavaFX properties, the right panel provides the ability to inspect property values and edit them
using dedicated dialogs, which can be opened by right-clicking a Property in the panel.
ViewDialog displays the full value of a property. This is particularly useful when the value is too long to fit
within the property table.
TextEditorDialog, EnumEditorDialog, and InsetsEditorDialog allow editing different types of properties.
TextEditorDialog supports properties whose values can be represented as text (such as Number, Boolean, String,
and similar types), EnumEditorDialog is intended for properties backed by enumerations, and InsetsEditorDialog
is used for properties of type Insets.
Being able to modify property values without restarting the application greatly simplifies debugging and makes it much easier to experiment with and determine the appropriate values for JavaFX node properties.
This component allows recording node events. It can operate with or without filters. Events can be filtered by selected component, message, event type, and other criteria.
This component allows inspecting which stylesheets are applied to nodes within the scene.
This component provides access to platform settings, system properties, and environment variables.
A component often has state that is worth keeping: the size of a dialog, the layout of the columns of a table, the recent queries of a search field. ShellFX keeps such state in a config. A config is stored outside the component, so it serves two purposes:
- Saving between sessions. The config is written to a file when the application stops and read again at the next start, so the component comes back the way the user left it.
- Synchronization between components. Components of the same kind may share one config, and a change made by one of them is seen by the others. For example, two search fields that share a config show the same list of recent queries: a query submitted in one field appears in the drop-down list of the other one immediately.
A config is optional. A component that has nothing to keep, or whose state must not be shared, simply works without it.
A config is a plain serializable object that extends AbstractConfig and contains no logic, only fields with
getters and setters. Configs follow the same naming as the rest of the component (FileChooserDialogConfig,
DockHostConfig). A config may contain other configs: for example, DockHostConfig holds the configs of its side
bars, and the parent component passes the corresponding part to each child.
The config is passed to the component as the first argument of its Params. It is either required (the Params
checks it in validate()) or optional (the caller passes null and the component just does not keep its state).
The default values are set in the constructor of the config, so a new config already describes a sensible
component. Configs are read from a file with Java serialization, which does not call constructors, therefore keep
in mind that a field added after a file was written has the Java zero value when that file is read, and that every
class of a config must declare its own serialVersionUID.
The base ViewModel classes of the three main components — Window, Tab and Area — hold the config and give
a ViewModel two hooks that are called from postInitialize(), only if the config is set:
loadConfigToState()copies the values of the config into the state of theViewModel.observeStateForConfig()adds listeners to the state of theViewModelthat write every change into the config and callnotifyListeners().
The second hook is what makes a config different from history. History is a snapshot: it is filled with the state of
a component when the component is closed, and it belongs to that one component. A config is updated all the time while
the component works, because it is changed together with the properties of the ViewModel. This is why a config can
be shared: other components can listen to it with a ConfigListener and update themselves when it changes. A listener
is told that the config has changed and may get a hint saying what exactly happened, so it can react only to the
changes it is interested in. A component that registers a listener must remove it when it is deinitialized, because a
config lives as long as the application.
ConfigManager owns the configs of the application. A config is stored either by its class, and then it is shared
by all components of that kind, or by the UUID of a component instance, and then it belongs to that instance only.
For each of the two ways the manager can get a config, create it with a factory if it does not exist yet, put a new
one and remove it.
The platform provides two implementations:
FileConfigManagerkeeps the configs in a file (seeConfigFile) and writes them back whensave()is called. The application decides when to do it, usually when it stops. The file is replaced as a whole, so a failure never leaves a half-written one. The classes of the configs are loaded with the given class loader, which allows to read back the configs of plugins. Before saving the manager warns about every listener that is still registered on a config, because by that time the components are gone and such a listener is a leak.InMemoryConfigManagerkeeps the configs only in memory. It is meant for tests and demos.
The manager is available to components through ShellViewModelContext, and the code that creates a component takes
the config from it and passes it to the Params.
An extension registry provides a runtime mechanism for components, plugins, and modules to contribute functionality without creating direct compile-time dependencies between them. Extensions can be registered and removed at any time, and registrations do not depend on the order in which components or plugins are loaded.
Registrations are associated with the component class of the slot. When resolving registrations for an actual
component, the registry considers its class, superclasses, and interfaces. Therefore, a registration made for a base
view type such as TabHostView also applies to its subtypes.
The resolved result is cached by Class#getName() rather than by the Class object itself. This avoids retaining the
ClassLoader of an unloaded plugin through the cache. The cache is invalidated whenever the registrations change.
Registries are not tied to a particular layer and can be used by both views and view models. In the shell, registries
are used only on the view side, so they are exposed through ShellViewContext, which is available to views.
ShellViewModelContext does not expose them. DefaultShellContext implements both contexts, allowing an application
to create a single context object while exposing each layer only the API intended for it.
A slot is an extension point in a component tree. It represents a place where components and plugins can contribute content, such as a menu bar, menu, menu group, or tool bar group.
The type of a slot defines what it can contain: MenuBarSlot, MenuSlot, ContextMenuSlot, ToolBarSlot, or
GroupSlot<V, C>. A slot is declared once, typically as a constant in a public catalog owned by the module that
defines the extension point. The slot itself does not know which contributions will be added to it.
SlotRegistry stores the structure of the extension tree: which slot belongs to which parent slot and at what
position. A plugin can declare its own slots and insert them into existing slots. Another plugin can then contribute
to those slots without knowing anything about the plugin that declared them.
The slot registry contains only the structure of the tree. It does not create or store controls. Menu items, buttons, and other controls are contributed through the Control Registry.
Types. Generic parameters make invalid slot relationships compile-time errors:
Vis the view type of the component the slot belongs to, for exampleShellView<?>. Two slots used in the same registration must have compatibleVtypes.Valso determines the type of component instance passed to a control factory. When a control is created, the factory receives the component instance into which the control may be placed.Cis the control type accepted by aGroupSlot<V, C>. A factory registered for the group in theControlRegistrymust create a compatible control.
Only compatible slots can be connected. A menu bar contains menus. A menu or context menu contains groups of menu
items (GroupSlot<V, ? extends MenuItem>). A tool bar contains groups of controls (GroupSlot<V, ? extends Control>).
A group of menu items can also contain nested menus.
ControlRegistry stores factories that create controls for registered slots.
A factory can be registered in three ways:
- For a control slot. A menu bar, menu, context menu, or tool bar is itself a control and can have one factory.
- For a group. A group is a
ControlGroup<C>and has one factory too, so a custom subclass can, for example, track the number of its items. A group without a factory is left out by the builders. - For a group item. A factory can be registered at a specific position within a
GroupSlot<V, C>. It creates a control belonging to that group, such as a menu item. The generic type of the group ensures that the factory creates a compatible control.
The builder fills the ControlGroup created by the group's factory with the controls of the group and decides how the
group is laid out.
Registering a factory does not invoke it. Factories are called only when a builder creates the corresponding controls.
Neither registry creates the final control hierarchy. Builders combine the two registries: they resolve the slots and factories for a component, walk the tree from a root slot, create the controls, and order them by their registered positions.
ControlBuilder builds tool bars. A tool bar slot produces the ToolBar created by its factory, filled with the
controls of its groups in the order of their positions; groups are separated by separators and empty groups are
omitted. For custom layouts it can also return the groups of a tool bar slot as ControlGroups.
ManagedControlBuilder builds complete menus. A menu bar slot produces a MenuBar containing its menus; a menu
produces its groups and nested menus; groups are separated by separators; and empty menus and groups are omitted.
See Managed Controls.
Managed controls are regular JavaFX controls such as Menu, MenuItem, CheckMenuItem, and RadioMenuItem.
Their platform-specific behavior is kept in a Handler stored in the control's properties rather than in the control
itself.
There are two reasons for this design. First, JavaFX already defines a fixed inheritance hierarchy for menu controls,
leaving no suitable common base class for adding platform behavior. Second, JavaFX exposes control behavior through
properties such as onShowing, onHiding, and onAction. Using those properties directly for platform handlers
would allow a call such as setOnAction to silently replace the platform behavior.
The menu classes are defined in the material module. They do not depend on ControlRegistry or the builders.
-
Menu. All controls remain ordinary JavaFX classes.
ContextMenuhas novisibleproperty, soContextMenuHandlerstores this state in the menu's properties, allowing the handler to hide the entire popup. -
Handler.
HandlerdefinesonUpdate,onShowing, andonHiding.MenuItemHandleradditionally definesonAction.MenuHandlerandContextMenuHandlerare attached toMenuandContextMenu, respectively, and determine their visibility independently of their contents. This is necessary because a menu can be assembled from contributions made by independent plugins and therefore cannot know which plugins contributed its items.AbstractHandler,AbstractMenuHandler,AbstractMenuItemHandler, andAbstractContextMenuHandlerprovide empty default implementations. -
Manager.
MenuBarManageris created for a builtMenuBarand manages its runtime behavior. It invokes item actions, resolves menu visibility when a menu is shown, and hides separators around empty sections. It also distinguishes mouse clicks from keyboard accelerators, because the same key combination can invoke an item whether or not its menu is currently open.The manager does not modify the menu structure. The builder creates the structure once; the manager only controls the state and behavior of the controls that already exist.
ContextMenuManagerprovides the same functionality for aContextMenu, without accelerator handling, which is not needed for a popup menu.
Managed controls are independent of registries and builders. A Menu tree can be constructed manually and passed to
a MenuBarManager, and handlers will work in exactly the same way. Registries and builders solve a different problem:
they assemble such a control tree from contributions made by independent plugins.
Menu visibility is resolved in one of two ways:
- The menu has a handler. The handler is called and its result determines the visibility. The menu items are not traversed.
- The menu has no handler. The manager recursively checks the menu items and shows the menu if at least one item is visible.
A handler therefore provides an explicit visibility decision without traversing the menu tree. It is also the only way to show or hide a menu independently of its items. Once the menu is shown, its items are resolved in both cases.
ShellFX is built on top of PatternFX and fully conforms to its patterns. Because of this, the naming of classes and interfaces for components follows a consistent scheme:
- A unique name (may be omitted for brevity) —
Alert,File,Info, etc. - The component role —
Tab,Window,Popup,Area,Panel,ToolBar, etc. - The component element —
View,ViewModel,Params,Port,Configetc.
Examples: AlertDialogView, EditorTabViewModel, InfoPopupParams, ToolBarPort
This approach is justified by the following reasons. When a complex component is split into multiple components
(due to complexity, reuse of components, or use of a docking layout), there may be several components with the same
unique name but different roles — such as FooTab and FooArea. Another reason is that the role of a component
immediately makes it clear how to work with it and where to place it.
When working with Composer methods, there are two categories of methods:
-
Methods that create/destroy a component and compose/decompose it. It is important to note that these methods manage the component lifecycle, meaning they are responsible for component initialization and deinitialization. Such methods include:
open*,close*,show*, andhide*. -
Methods that only compose/decompose a component. These methods are responsible solely for structural component composition and do not manage the component lifecycle. Such methods include:
add*,remove*, andreplace*.
Examples of Composer methods using open* and close*:
| Component | Create + Add | Remove + Destroy | Add Only | Remove Only |
|---|---|---|---|---|
| Window | openWindow(params) |
closeWindow(window) |
addWindow(window) |
removeWindow(window) |
| Tab | openTab(params) |
closeTab(tab) |
addTab(tab) |
removeTab(tab) |
| Dialog | openDialog(params) |
closeDialog(dialog) |
addDialog(dialog) |
removeDialog(dialog) |
| Popup | openPopup(params) |
closePopup(popup) |
addPopup(popup) |
removePopup(popup) |
| Page | openPage(params) |
closePage(page) |
addPage(page) |
removePage(page) |
| Area | openArea(params) |
closeArea(area) |
addArea(area) |
removeArea(area) |
View methods also follow a naming convention that distinguishes two kinds of methods:
To get started with ShellFX, it is recommended to follow these steps:
- Familiarize yourself with the PatternFX framework, the MVVM template, and its demo.
- Explore and run the demo. See Running Demo for details.
The library requires Java 25 and JavaFX 25.
This project is available on Maven Central. Minimal set of required dependencies:
<dependency>
<groupId>com.techsenger.shellfx</groupId>
<artifactId>shellfx-material</artifactId>
<version>${shellfx.version}</version>
</dependency>
<dependency>
<groupId>com.techsenger.shellfx</groupId>
<artifactId>shellfx-core</artifactId>
<version>${shellfx.version}</version>
</dependency>
<dependency>
<groupId>com.techsenger.shellfx</groupId>
<artifactId>shellfx-layout</artifactId>
<version>${shellfx.version}</version>
</dependency>
<dependency>
<groupId>com.techsenger.shellfx</groupId>
<artifactId>shellfx-icons</artifactId>
<version>${shellfx.version}</version>
</dependency>
To build the library use standard Git and Maven commands:
git clone https://github.com/techsenger/shellfx
cd shellfx
mvn clean install
To run the demo, execute the following commands in the project root:
cd shellfx-demo
mvn javafx:run
Please note, that debugger settings are in pom.xml file.
Techsenger ShellFX is licensed under the Apache License, Version 2.0.
We welcome all contributions. You can help by reporting bugs, suggesting improvements, or submitting pull requests with fixes and new features. If you have any questions, feel free to reach out — we’ll be happy to assist you.
You can support our open-source work through GitHub Sponsors. Your contribution helps us maintain projects, develop new features, and provide ongoing improvements. Multiple sponsorship tiers are available, each offering different levels of recognition and benefits.






