1

Goal: Implement a standard "Settings" GUI window. Categories in a ListView on the left and the corresponding options in a Pane on the right. enter image description here (please ignore the obvious bug with repeated categories; still working on it)

I have a main window for the overall Settings window that contains a ListView with all the categories of settings. The right side of the window has an AnchorPane which is used to load separate FXML files for each category when one is selected from the list.

When a user selects a category, I need them to be able to edit the settings on the right, switch to another category and make more changes. Yet, if they return to the first category, the changes made there persist.

My obvious issue is that each time a user changes categories, the FXMLLoader reloads the FXML file and controller, resetting all controls within to their default values.

So is it possible to reuse an FXML file that has already been loaded and altered?

Research:

The only answer I found that seems to address the issue is How to swich javafx application controller without reloading FXML file?. That mentions using a Singleton for the FXML controller, but does not address the issue with the FXML file itself being reloaded each time.

I would be happy if someone could point to a basic example of this type of Settings menu.

Community
  • 1
  • 1
Zephyr
  • 9,885
  • 4
  • 28
  • 63
  • When you load the FXML file, you should pass the data model to the controller. The controller will set values in the UI from the model, and update the model when the user makes changes. You keep a single model instance with data from all the "screens" (i.e. all the settings). That way it doesn't matter if you reload the FXML file. – James_D Mar 03 '17 at 01:58
  • Ah! I already have a model for my overall Settings but I didn't want to update that until the user clicks the "Save Changes" button. I hadn't thought to use a temporary model until then. Thank you. – Zephyr Mar 03 '17 at 02:01
  • I have tried to copy/clone my `Settings` singleton (by implementing `Cloneable` and overriding the `clone()` method) but find the clone doesn't behave as a `Settings` object; none of the methods are accessible. Is cloning the wrong way to copy this? – Zephyr Mar 03 '17 at 02:25
  • Not sure what you are trying to do there. You just need a single instance of the model. There are other ways to do this too. Will try to post an answer next time I am at my computer. – James_D Mar 03 '17 at 02:37
  • Maybe I'm going about this all wrong. I don't want to update the permanent instance of my `Settings` model until the user clicks "Save Changes" (allowing them to discard all changes). So I would need a copy of the model to "play" with and then permanently save when the user chooses to. – Zephyr Mar 03 '17 at 02:42
  • If you want to be able to create a copy of an object, define it with a [copy constructor](http://stackoverflow.com/questions/5749218/building-a-copy-constructor-in-java) - that's kind of a different question entirely from the one you are asking here. – James_D Mar 03 '17 at 14:37

3 Answers3

4

There are basically three ways I see to do this:

  1. Define a model representing the data (Settings), and create a single instance of it. Reload the FXML files each time, and pass the single instance to the controller. Bind the data in the UI with the data in the model. That way, when you reload the FXML, it will be updated with the same data. (This is my preferred option.)
  2. Create the controllers once. Reload the FXML files each time, setting the same controller each time. Have the initialize() method update the UI from either locally-stored fields, or a model. The @FXML-annotated fields will be replaced when you reload the FXML file and the initialize() method will be called, updating the new controls with the existing data. (This feels a little artificial. Morally speaking, any method called initialize() should only be executed once. However, this is perfectly workable.)
  3. Load each FXML file once and cache the UI (and probably the controller). Then when the user selects something in the list view, just display the already-loaded view. This is probably the simplest, but costs a little more in memory as you are keeping all views in memory at all times.

Suppose you have a model, which might look like this:

public class Settings {

    private final UserInfo userInfo ;
    private final Preferences prefs ;
    private final Appearance appearance ;

    public Settings(UserInfo userInfo, Preferences prefs, Appearance appearance) {
        this.userInfo = userInfo ;
        this.prefs = prefs ;
        this.appearance = appearance ;
    }

    public Settings() {
        this(new UserInfo(), new Preferences(), new Appearance());
    }

    public UserInfo getUserInfo() {
        return userInfo ;
    }

    public Preferences getPreferences() {
        return prefs ;
    }

    public Appearance getAppearance() {
       return appearance ;
    }
}

and

public class UserInfo {

    private final StringProperty name = new SimpleStringProperty() ;
    private final StringProperty department = new SimpleStringProperty() ;
    // etc...

    public StringProperty nameProperty() {
        return name ;
    }

    public final String getName() {
        return nameProperty().get();
    }

    public final void setName(String name) {
        nameProperty().set(name);
    }

    // etc...
}

(and similarly for Preferences, Appearance, etc.)

Now you define controllers for you individual screens that use a model, e.g.

public class UserInfoController {

    private final UserInfo userInfo ;

    @FXML
    private TextField name ;
    @FXML
    private ComboBox<String> department ;

    public UserInfoController(UserInfo userInfo) {
        this.userInfo = userInfo ;
    }

    public void initialize() {
        name.textProperty().bindBidirectional(userInfo.nameProperty());
        department.valueProperty().bindBidirectional(userInfo.departmentProperty());
    }
}

and then you main controller looks like:

public class MainController {

    @FXML
    private BorderPane root ;
    @FXML
    private ListView<String> selector ;

    private Settings settings = new Settings() ; // or pass in from somewhere else..

    public void initialize() {
        selector.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> {
            if ("User Information".equals(newSelection)) {
                loadScreen("UserInfo.fxml", new UserInfoController(settings.getUserInfo()));
            } else if ("Preferences".equals(newSelection)) {
                loadScreen("Preferences.fxml", new PreferencesController(settings.getPreferences()));
            } else if ("Appearance".equals(newSelection)) {
                loadScreen("Appearance.fxml", new AppearanceController(settings.getAppearance()));
            } else {
                root.setCenter(null);
            }
    }

    private void loadScreen(String resource, Object controller) {
        try {
            FXMLLoader loader = new FXMLLoader(getClass().getResource(resource));
            loader.setController(controller);
            root.setCenter(loader.load());
        } catch (IOException exc) {
            exc.printStackTrace();
            root.setCenter(null);
        }
    }
}

(Obviously you can make the handler for the list view cleaner by defining a simple view class encapsulating the resource name, display name, and a factory for the controller, and populating the list view with it, instead of switching on strings.)

Note that since you are setting the controller on the FXMLLoader in code, UserInfo.fxml, Preferences.fxml and Appearance.fxml should not have a fx:controller attribute defined.


The second option is just a mild refactoring of this. Create the controllers once and keep a reference to them. Note that if you wanted, you could get rid of the model in this version, as the controllers have the data, so you can just refer back to them. So this might look like

public class UserInfoController {

    @FXML
    private TextField name ;
    @FXML
    private ComboBox<String> department ;

    private final StringProperty nameProp = new SimpleStringProperty();
    private final ObjectProperty<String> departmentProp = new SimpleObjectProperty();

    public StringProperty nameProperty() {
        return nameProp;
    }

    public final String getName() {
        return nameProperty().get();
    }

    public final void setName(String name) {
        nameProperty().set(name);
    }

    public ObjectProperty<String> departmentProperty() {
        return departmentProp ;
    }

    public final String getDepartment() {
        return departmentProperty().get();
    }

    public final void setDepartment(String department) {
        departmentProperty().set(department);
    }

    public void initialize() {
        // initialize controls with data currently in properties, 
        // and ensure changes to controls are written back to properties:
        name.textProperty().bindBidirectional(nameProp);
        department.valueProperty().bindBidirectional(departmentProp);
    }
}

and then

public class MainController {

    @FXML
    private BorderPane root ;
    @FXML
    private ListView<String> selector ;

    private UserInfoController userInfoController = new UserInfoController();
    private PreferencesController preferencesController = new PreferencesController();
    private AppearanceController appearanceController = new AppearanceController();

    public void initialize() {
        // initialize controllers with data if necessary...

        selector.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> {
        selector.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> {
            if ("User Information".equals(newSelection)) {
                loadScreen("UserInfo.fxml", userInfoController);
            } else if ("Preferences".equals(newSelection)) {
                loadScreen("Preferences.fxml", preferencesController);
            } else if ("Appearance".equals(newSelection)) {
                loadScreen("Appearance.fxml", appearanceController);
            } else {
                root.setCenter(null);
            }
        }
    }

    private void loadScreen(String resource, Object controller) {
        // as before...
    }
}

This works because you don't create new controllers when the FXML files are reloaded, and the initialize methods in the controllers update the controls with the data already there. (Note which way round the bindBidirectional methods are invoked.)


The third option can be implemented either in the main controller, or in the main fxml file. To implement it in the controller, you basically do

public class MainController {

    @FXML
    private BorderPane root ;
    @FXML
    private ListView<String> selector ;

    private Parent userInfo ;
    private Parent prefs;
    private Parent appearance;

    // need controllers to get data later...

    private UserInfoController userInfoController ;
    private PreferencesController prefsController ;
    private AppearanceController appearanceController ;

    public void initialize() throws IOException {

        FXMLLoader userInfoLoader = new FXMLLoader(getClass().getResource("userInfo.fxml));
        userInfo = userInfoLoader.load();
        userInfoController = userInfoLoader.getController();

        FXMLLoader prefsLoader = new FXMLLoader(getClass().getResource("preferences.fxml));
        prefs = prefsLoader.load();
        prefsController = prefsLoader.getController();

        FXMLLoader appearanceLoader = new FXMLLoader(getClass().getResource("appearance.fxml));
        appearance = appearanceLoader.load();
        appearanceController = appearanceLoader.getController();

        // configure controllers with data if needed...

        selector.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> {
            if ("User Information".equals(newSelection)) {
                root.setCenter(userInfo);
            } else if ("Preferences".equals(newSelection)) {
                root.setCenter(prefs); 
            } else if ("Appearance".equals(newSelection)) {
                root.setCenter(prefs);
            } else {
                root.setCenter(null);
            }
        }
    }
}

Note here you would revert to the usual fx:controller attribute in your FXML files.

This will work as you are only loading the FXML files once, so the view simply persists with all its state.

If you want to define the views in FXML in this approach, you can:

Main fxml file:

<!-- imports etc omitted -->
<BorderPane xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1"
    fx:controller="com.example.MainController">

    <left>
        <ListView fx:id="selector" />
    </left>

    <fx:define>
        <fx:include fx:id="userInfo" source="UserInfo.fxml" >
    </fx:define>

    <fx:define>
        <fx:include fx:id="prefs" source="Preferences.fxml" >
    </fx:define>

    <fx:define>
        <fx:include fx:id="appearance" source="Appearance.fxml" >
    </fx:define>

</BorderPane>

The rule for FXML-injection for <fx:include> is that the root of the included FMXL file is injected with the specified fx:id (e.g. userInfo) and the controllers for those included files ("nested controllers") are injected to the field with the name give when "Controller" is appended to the fx:id (e.g. to userInfoController). So the main controller for this would now look like

public class MainController {

    @FXML
    private BorderPane root ;
    @FXML
    private ListView<String> selector ;

    @FXML
    private Parent userInfo ;
    @FXML
    private Parent prefs;
    @FXML
    private Parent appearance;

    // need controllers to get data later...

    @FXML
    private UserInfoController userInfoController ;
    @FXML
    private PreferencesController prefsController ;
    @FXML
    private AppearanceController appearanceController ;

    public void initialize() {

        // configure controllers with data if needed...

        selector.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> {
            if ("User Information".equals(newSelection)) {
                root.setCenter(userInfo);
            } else if ("Preferences".equals(newSelection)) {
                root.setCenter(prefs); 
            } else if ("Appearance".equals(newSelection)) {
                root.setCenter(prefs);
            } else {
                root.setCenter(null);
            }
        }
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thank you, @James_D. I ended up going with creating controllers for each "Tab" and passing the same instance when loading the FXML. I did use property binding to keep changes persistent then just wrote them back to my `Settings` singleton when the user chooses to save. This worked beautifully. – Zephyr Mar 05 '17 at 14:07
1

Here is a completely different approach to creating a "navigation pane" like the one you show, inspired partially by Hypnic Jerk's answer. The key observation here is that the functionality you want is essentially the same as a TabPane: you have a series of nodes, displayed one at a time, with a mechanism for selecting which one is displayed (usually tabs, but here you have a ListView). So this approach simply makes a tab pane display the "selector" using a ListView instead of the usual tabs. It does this by creating a new Skin for the tab pane.

Here's the basic app:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.Tab?>

<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.MainController">
    <center>
        <TabPane  fx:id="navigationPane" id="navigationPane">
            <tabs>
                <Tab text="User Information">
                    <content>
                        <fx:include fx:id="userInfo" source="UserInfo.fxml"/>
                    </content>
                </Tab>
                <Tab text="Preferences">
                    <content>
                        <fx:include fx:id="prefs" source="Preferences.fxml"/>
                    </content>
                </Tab>
                <Tab text="Appearance">
                    <content>
                        <fx:include fx:id="appearance" source="Appearance.fxml"/>
                    </content>
                </Tab>
            </tabs>
        </TabPane>
    </center>
</BorderPane>

The controller for this test doesn't do anything:

package application;

import javafx.fxml.FXML;
import javafx.scene.control.TabPane;

public class MainController {

    @FXML
    private TabPane navigationPane ;

    public void initialize() {

    }
}

and the individual panes are just placeholders:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>

<VBox xmlns:fx="http://javafx.com/fxml/1" minWidth="600" minHeight="400" alignment="CENTER">
    <Label text="User Info Pane"/>
    <TextField  />
</VBox>

The application class just loads the FXML, and, crucially, sets a style sheet:

package application;

import java.io.IOException;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class NavTabPaneTest extends Application {

    @Override
    public void start(Stage primaryStage) throws IOException {

        Parent root = FXMLLoader.load(getClass().getResource("NavPaneTest.fxml"));

        Scene scene = new Scene(root);
        scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

and the style sheet specifies the skin:

#navigationPane {
    -fx-skin: "application.skin.NavigationSkin" ;
}

And finally the part that does the work: the skin:

package application.skin;

import java.util.function.ToDoubleFunction;

import javafx.beans.binding.Bindings;
import javafx.collections.ListChangeListener.Change;
import javafx.scene.Node;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.SkinBase;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;

public class NavigationSkin extends SkinBase<TabPane> {


    private final ListView<Tab> navigator ;

    public NavigationSkin(TabPane control) {
        super(control);

        navigator = new ListView<Tab>();

        navigator.setCellFactory(lv -> {
            ListCell<Tab> cell = new ListCell<>();
            cell.itemProperty().addListener((obs, oldTab, newTab) -> {
                cell.textProperty().unbind();
                cell.graphicProperty().unbind();
                if (newTab == null) {
                    cell.setText(null);
                    cell.setGraphic(null);
                } else {
                    cell.textProperty().bind(newTab.textProperty());
                    cell.graphicProperty().bind(newTab.graphicProperty());
                }
            });
            return cell ;
        });

        navigator.setItems(control.getTabs());  

        navigator.getSelectionModel().selectedItemProperty().addListener(
                (obs, oldItem, newItem) -> control.getSelectionModel().select(newItem));

        navigator.getSelectionModel().select(control.getSelectionModel().getSelectedItem());

        control.getSelectionModel().selectedItemProperty().addListener((obs, oldItem, newItem) -> {
            for (Tab t : control.getTabs()) {
                t.getContent().setVisible(t == control.getSelectionModel().getSelectedItem());
            }
            navigator.getSelectionModel().select(newItem);
        });

        getChildren().add(navigator);
        for (Tab t : control.getTabs()) {
            getChildren().add(t.getContent());
            t.getContent().setVisible(t == control.getSelectionModel().getSelectedItem());
        }


        control.getTabs().addListener((Change<? extends Tab> c) -> {
            while (c.next()) {
                if (c.wasRemoved()) {
                    getChildren().subList(c.getFrom()+1, c.getFrom()+c.getRemovedSize()+1).clear();
                }
                if (c.wasAdded()) {
                    for (int i = 0 ; i < c.getAddedSize() ; i++) {
                        getChildren().add(c.getFrom() + i + 1, c.getAddedSubList().get(i).getContent());
                    }
                }
            }
            getSkinnable().requestLayout();
        });
    }


    @Override
    protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
        double navPrefWidth = navigator.prefWidth(-1);
        navigator.resizeRelocate(contentX, contentY, navPrefWidth, contentHeight);
        for (Tab t : getSkinnable().getTabs()) {
            t.getContent().resizeRelocate(navPrefWidth, 0, contentWidth - navPrefWidth, contentHeight);
        }
    }

    @Override
    protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
        return computeHeight(n -> n.maxHeight(width - leftInset - rightInset));
    }

    @Override
    protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
        return computeWidth(n -> n.maxWidth(height - topInset - bottomInset)) ;
    }

    @Override
    protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
        return computeHeight(n -> n.minHeight(-1));
    }

    @Override
    protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
        return computeWidth(n -> n.minWidth(-1)) ;
    }   

    @Override
    protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
        return computeHeight(n -> n.prefHeight(-1));
    }

    @Override
    protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
        return computeWidth(n -> n.prefWidth(height - topInset - bottomInset)) ;
    }


    private double computeWidth(ToDoubleFunction<Node> width) {
        double navWidth = width.applyAsDouble(navigator);
        double max = 0 ;
        for (Tab tab : getSkinnable().getTabs()) {
            double tabWidth = width.applyAsDouble(tab.getContent());
            max = Math.max(max, tabWidth);
        }
        return navWidth + max ;
    }

    private double computeHeight(ToDoubleFunction<Node> height) {
        double max = height.applyAsDouble(navigator) ;
        for (Tab tab : getSkinnable().getTabs()) {
            max = Math.max(max, height.applyAsDouble(tab.getContent()));
        }
        return max ;
    }
}

This creates a ListView and does a little magic with listeners and bindings to make sure it always has the same content as the list of tabs in the tab pane, and that the selected item in the list view is the selected tab. (You need to make sure the list view updates if the selected tab is changed programmatically, as well as make sure the selected tab changes if the user changes the selected item in the list view.) The rest just overrides the layoutChildren() method and the various methods for computing min/max/pref sizes.

The result is a traditional "navigation pane":

enter image description here

Of course, because all the tab contents are loaded once, and just switched in and out of view (by changing their visibility), the issue of losing data when reverting to a previous view disappears.

James_D
  • 201,275
  • 16
  • 291
  • 322
0

Okay, so I did a little test and I potentially figured out a way to do this.

First explanation of my thinking process, and then code.

It seems like you basically want a TabPane, without the Tabs. i.e., Click on ListView, switch to a certain FXML file. Well, I did a little experiment to see what I could do.

First off, I used a SplitPane, ListView on the left, and two BorderPanes. Inside the nested BorderPane, I put a TabPane, this is where you will add the fxml files. I used fx:include, to save time/code. Plus, since this will be standard, an addition or removal of a Settings item, is a few lines added/removed.

So, when you select an item from the ListView, it change to the appropriate tab with the FXML file from that has the same index as the selection (there is a caveat). This can be edited to further your needs, but since this is proof-of-concept, I'm not going too far in to it.

This should allow you to keep "soft saves" of your users changes before you hit the save Button.

Code is below:

Main.java

public class Main extends Application {
    public static void main(String[] args) {
        launch(args);
    }
    @Override
    public void start(Stage primaryStage)throws Exception{
        FXMLLoader loader = new FXMLLoader(getClass().getResource("root.fxml"));

        Scene scene = new Scene(loader.load());
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

MainController.java

public class MainController {
    @FXML
    private ListView listView;
    @FXML
    private TabPane tabPane;

    public void initialize() {
        ObservableList<String> list = FXCollections.observableArrayList();
        list.add("Settings 1");
        list.add("Settings 2");
        list.add("Settings 3");
        list.add("Settings 4");
        listView.setItems(list);

        listView.getSelectionModel().selectedItemProperty().addListener(listener -> {
            tabPane.getSelectionModel().select(listView.getSelectionModel().getSelectedIndex());    
        });
    }
}

root.fxml

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" stylesheets="@root.css" xmlns="http://javafx.com/javafx/8.0.91" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.MainController">
   <left>
      <ListView fx:id="listView" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER" />
   </left>
   <center>
      <BorderPane prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
         <bottom>
            <HBox prefHeight="100.0" prefWidth="200.0" BorderPane.alignment="CENTER">
               <children>
                  <Button mnemonicParsing="false" text="Cancel" />
                  <Button mnemonicParsing="false" text="Save Changes" />
               </children>
            </HBox>
         </bottom>
         <center>
            <TabPane fx:id="tabPane" prefHeight="200.0" prefWidth="200.0" tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
              <tabs>
                <Tab>
                     <content>
                        <fx:include source="settings1.fxml" />
                     </content>
                </Tab>
                  <Tab>
                      <content>
                          <fx:include source="settings2.fxml" />
                      </content>
                  </Tab>
                  <Tab>
                      <content>
                          <fx:include source="settings3.fxml" />
                      </content>
                  </Tab>
                  <Tab>
                      <content>
                          <fx:include source="settings4.fxml" />
                      </content>
                  </Tab>
              </tabs>
            </TabPane>
         </center>
      </BorderPane>
   </center>
</BorderPane>

settings1.fxml, settings2.fxml, settings3.fxml, settings4.fxml. Only thing different is the Label is changed to reflect the FXML file.

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.91" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <Label layoutX="168.0" layoutY="14.0" text="Settings 1" />
      <TextField layoutX="127.0" layoutY="39.0" />
   </children>
</AnchorPane>

root.css. Located How to hide the TabBar in TabPane?

.tab-pane {
    -fx-tab-max-height: 0 ;
} 
.tab-pane .tab-header-area {
    visibility: hidden ;
}
Community
  • 1
  • 1
Hypnic Jerk
  • 1,192
  • 3
  • 14
  • 32
  • This works because you only load the individual views once, instead of reloading them each time the user selects something. You don't really need the hidden tab pane, though, you can just keep references to the individual views and change what's shown directly. Or, OTOH, perhaps you could just make the whole thing a `TabPane`, call `setSide(Side.LEFT)`, and style the tabs so that it looks like a "navigation pane". – James_D Mar 03 '17 at 13:44
  • Which gave me the idea that you could use a tab pane and use a custom skin, so that it displayed a list view instead of the tabs... – James_D Mar 03 '17 at 19:56
  • @James_D Now that is interesting. Never thought of doing it like that. – Hypnic Jerk Mar 03 '17 at 19:58